summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE.md3
-rw-r--r--.github/ISSUE_TEMPLATE/bug-report.yml19
-rw-r--r--.github/ISSUE_TEMPLATE/config.yml20
-rw-r--r--.github/ISSUE_TEMPLATE/feature-request.md19
-rw-r--r--.github/ISSUE_TEMPLATE/feature-request.yml59
-rw-r--r--.github/ISSUE_TEMPLATE/~good-first-issue.md15
-rw-r--r--.github/lock.yml8
-rw-r--r--.github/workflows/ci.yml121
-rw-r--r--.github/workflows/label-merge-conflicts.yml19
-rw-r--r--.github/workflows/lock-threads.yml23
-rw-r--r--.github/workflows/news-file.yml25
-rw-r--r--.mailmap2
-rw-r--r--.pre-commit-config.yaml48
-rw-r--r--AUTHORS.txt63
-rw-r--r--LICENSE.txt2
-rw-r--r--MANIFEST.in3
-rw-r--r--NEWS.rst471
-rw-r--r--docs/html/cli/index.md1
-rw-r--r--docs/html/cli/pip.rst178
-rw-r--r--docs/html/cli/pip_download.rst4
-rw-r--r--docs/html/cli/pip_freeze.rst2
-rw-r--r--docs/html/cli/pip_inspect.rst33
-rw-r--r--docs/html/cli/pip_install.rst571
-rw-r--r--docs/html/cli/pip_list.rst170
-rw-r--r--docs/html/cli/pip_uninstall.rst4
-rw-r--r--docs/html/cli/pip_wheel.rst63
-rw-r--r--docs/html/conf.py1
-rw-r--r--docs/html/development/architecture/anatomy.rst24
-rw-r--r--docs/html/development/ci.rst68
-rw-r--r--docs/html/development/configuration.rst7
-rw-r--r--docs/html/development/contributing.rst11
-rw-r--r--docs/html/development/getting-started.rst68
-rw-r--r--docs/html/development/issue-triage.md346
-rw-r--r--docs/html/development/issue-triage.rst312
-rw-r--r--docs/html/development/release-process.rst8
-rw-r--r--docs/html/getting-started.md6
-rw-r--r--docs/html/index.md3
-rw-r--r--docs/html/installation.md47
-rw-r--r--docs/html/news.rst2
-rw-r--r--docs/html/reference/build-system/index.md127
-rw-r--r--docs/html/reference/build-system/pyproject-toml.md164
-rw-r--r--docs/html/reference/build-system/setup-py.md133
-rw-r--r--docs/html/reference/index.md14
-rw-r--r--docs/html/reference/index.rst11
-rw-r--r--docs/html/reference/inspect-report.md217
-rw-r--r--docs/html/reference/installation-report.md192
-rw-r--r--docs/html/reference/requirement-specifiers.md61
-rw-r--r--docs/html/reference/requirements-file-format.md185
-rw-r--r--docs/html/topics/caching.md65
-rw-r--r--docs/html/topics/configuration.md21
-rw-r--r--docs/html/topics/dependency-resolution.md211
-rw-r--r--docs/html/topics/https-certificates.md71
-rw-r--r--docs/html/topics/index.md4
-rw-r--r--docs/html/topics/local-project-installs.md67
-rw-r--r--docs/html/topics/python-option.md29
-rw-r--r--docs/html/topics/repeatable-installs.md3
-rw-r--r--docs/html/topics/secure-installs.md100
-rw-r--r--docs/html/topics/vcs-support.md51
-rw-r--r--docs/html/user_guide.rst518
-rw-r--r--docs/pip_sphinxext.py3
-rw-r--r--docs/requirements.txt2
-rw-r--r--news/10128.removal.rst1
-rw-r--r--news/10165.trivial.rst4
-rw-r--r--news/10233.bugfix.rst3
-rw-r--r--news/10716.feature.rst1
-rw-r--r--news/11111.feature.rst1
-rw-r--r--news/11250.feature.rst1
-rw-r--r--news/11254.trivial.rst (renamed from src/pip/_vendor/html5lib/filters/__init__.py)0
-rw-r--r--news/11276.bugfix.rst2
-rw-r--r--news/11309.bugfix.rst1
-rw-r--r--news/11320.feature.rst2
-rw-r--r--news/11352.bugfix.rst2
-rw-r--r--news/11357.doc.rst1
-rw-r--r--news/11459.feature.rst1
-rw-r--r--news/5580954E-E089-4CDB-857A-868BA1F7435D.trivial.rst0
-rw-r--r--news/8559.removal.rst2
-rw-r--r--noxfile.py53
-rw-r--r--pyproject.toml23
-rw-r--r--setup.cfg12
-rw-r--r--setup.py16
-rw-r--r--src/pip/__init__.py2
-rw-r--r--src/pip/__pip-runner__.py50
-rw-r--r--src/pip/_internal/build_env.py246
-rw-r--r--src/pip/_internal/cache.py142
-rw-r--r--src/pip/_internal/cli/autocompletion.py10
-rw-r--r--src/pip/_internal/cli/base_command.py125
-rw-r--r--src/pip/_internal/cli/cmdoptions.py115
-rw-r--r--src/pip/_internal/cli/command_context.py4
-rw-r--r--src/pip/_internal/cli/main_parser.py49
-rw-r--r--src/pip/_internal/cli/parser.py6
-rw-r--r--src/pip/_internal/cli/progress_bars.py298
-rw-r--r--src/pip/_internal/cli/req_command.py75
-rw-r--r--src/pip/_internal/cli/spinners.py14
-rw-r--r--src/pip/_internal/commands/__init__.py168
-rw-r--r--src/pip/_internal/commands/cache.py105
-rw-r--r--src/pip/_internal/commands/check.py14
-rw-r--r--src/pip/_internal/commands/completion.py79
-rw-r--r--src/pip/_internal/commands/configuration.py100
-rw-r--r--src/pip/_internal/commands/debug.py95
-rw-r--r--src/pip/_internal/commands/download.py27
-rw-r--r--src/pip/_internal/commands/freeze.py67
-rw-r--r--src/pip/_internal/commands/hash.py22
-rw-r--r--src/pip/_internal/commands/help.py2
-rw-r--r--src/pip/_internal/commands/index.py23
-rw-r--r--src/pip/_internal/commands/inspect.py97
-rw-r--r--src/pip/_internal/commands/install.py356
-rw-r--r--src/pip/_internal/commands/list.py175
-rw-r--r--src/pip/_internal/commands/search.py72
-rw-r--r--src/pip/_internal/commands/show.py115
-rw-r--r--src/pip/_internal/commands/uninstall.py48
-rw-r--r--src/pip/_internal/commands/wheel.py56
-rw-r--r--src/pip/_internal/configuration.py177
-rw-r--r--src/pip/_internal/distributions/base.py11
-rw-r--r--src/pip/_internal/distributions/installed.py13
-rw-r--r--src/pip/_internal/distributions/sdist.py123
-rw-r--r--src/pip/_internal/distributions/wheel.py30
-rw-r--r--src/pip/_internal/exceptions.py525
-rw-r--r--src/pip/_internal/index/collector.py407
-rw-r--r--src/pip/_internal/index/package_finder.py261
-rw-r--r--src/pip/_internal/locations/__init__.py286
-rw-r--r--src/pip/_internal/locations/_distutils.py41
-rw-r--r--src/pip/_internal/locations/_sysconfig.py58
-rw-r--r--src/pip/_internal/locations/base.py29
-rw-r--r--src/pip/_internal/main.py3
-rw-r--r--src/pip/_internal/metadata/__init__.py99
-rw-r--r--src/pip/_internal/metadata/_json.py84
-rw-r--r--src/pip/_internal/metadata/base.py488
-rw-r--r--src/pip/_internal/metadata/importlib/__init__.py4
-rw-r--r--src/pip/_internal/metadata/importlib/_compat.py55
-rw-r--r--src/pip/_internal/metadata/importlib/_dists.py224
-rw-r--r--src/pip/_internal/metadata/importlib/_envs.py188
-rw-r--r--src/pip/_internal/metadata/pkg_resources.py221
-rw-r--r--src/pip/_internal/models/candidate.py15
-rw-r--r--src/pip/_internal/models/direct_url.py22
-rw-r--r--src/pip/_internal/models/format_control.py44
-rw-r--r--src/pip/_internal/models/index.py16
-rw-r--r--src/pip/_internal/models/installation_report.py53
-rw-r--r--src/pip/_internal/models/link.py347
-rw-r--r--src/pip/_internal/models/scheme.py2
-rw-r--r--src/pip/_internal/models/search_scope.py41
-rw-r--r--src/pip/_internal/models/selection_prefs.py9
-rw-r--r--src/pip/_internal/models/target_python.py17
-rw-r--r--src/pip/_internal/models/wheel.py26
-rw-r--r--src/pip/_internal/network/auth.py19
-rw-r--r--src/pip/_internal/network/cache.py6
-rw-r--r--src/pip/_internal/network/download.py14
-rw-r--r--src/pip/_internal/network/lazy_wheel.py28
-rw-r--r--src/pip/_internal/network/session.py104
-rw-r--r--src/pip/_internal/network/utils.py4
-rw-r--r--src/pip/_internal/operations/build/build_tracker.py (renamed from src/pip/_internal/req/req_tracker.py)20
-rw-r--r--src/pip/_internal/operations/build/metadata.py22
-rw-r--r--src/pip/_internal/operations/build/metadata_editable.py41
-rw-r--r--src/pip/_internal/operations/build/metadata_legacy.py62
-rw-r--r--src/pip/_internal/operations/build/wheel.py17
-rw-r--r--src/pip/_internal/operations/build/wheel_editable.py46
-rw-r--r--src/pip/_internal/operations/build/wheel_legacy.py68
-rw-r--r--src/pip/_internal/operations/check.py52
-rw-r--r--src/pip/_internal/operations/freeze.py193
-rw-r--r--src/pip/_internal/operations/install/editable_legacy.py26
-rw-r--r--src/pip/_internal/operations/install/legacy.py68
-rw-r--r--src/pip/_internal/operations/install/wheel.py387
-rw-r--r--src/pip/_internal/operations/prepare.py462
-rw-r--r--src/pip/_internal/pyproject.py90
-rw-r--r--src/pip/_internal/req/__init__.py4
-rw-r--r--src/pip/_internal/req/constructors.py102
-rw-r--r--src/pip/_internal/req/req_file.py35
-rw-r--r--src/pip/_internal/req/req_install.py253
-rw-r--r--src/pip/_internal/req/req_set.py135
-rw-r--r--src/pip/_internal/req/req_uninstall.py275
-rw-r--r--src/pip/_internal/resolution/base.py6
-rw-r--r--src/pip/_internal/resolution/legacy/resolver.py193
-rw-r--r--src/pip/_internal/resolution/resolvelib/base.py5
-rw-r--r--src/pip/_internal/resolution/resolvelib/candidates.py115
-rw-r--r--src/pip/_internal/resolution/resolvelib/factory.py79
-rw-r--r--src/pip/_internal/resolution/resolvelib/found_candidates.py25
-rw-r--r--src/pip/_internal/resolution/resolvelib/provider.py99
-rw-r--r--src/pip/_internal/resolution/resolvelib/reporter.py5
-rw-r--r--src/pip/_internal/resolution/resolvelib/requirements.py4
-rw-r--r--src/pip/_internal/resolution/resolvelib/resolver.py98
-rw-r--r--src/pip/_internal/self_outdated_check.py240
-rw-r--r--src/pip/_internal/utils/appdirs.py39
-rw-r--r--src/pip/_internal/utils/compatibility_tags.py11
-rw-r--r--src/pip/_internal/utils/deprecation.py132
-rw-r--r--src/pip/_internal/utils/direct_url_helpers.py8
-rw-r--r--src/pip/_internal/utils/distutils_args.py51
-rw-r--r--src/pip/_internal/utils/egg_link.py75
-rw-r--r--src/pip/_internal/utils/encoding.py2
-rw-r--r--src/pip/_internal/utils/entrypoints.py57
-rw-r--r--src/pip/_internal/utils/filesystem.py33
-rw-r--r--src/pip/_internal/utils/filetypes.py13
-rw-r--r--src/pip/_internal/utils/glibc.py12
-rw-r--r--src/pip/_internal/utils/hashes.py49
-rw-r--r--src/pip/_internal/utils/inject_securetransport.py3
-rw-r--r--src/pip/_internal/utils/logging.py237
-rw-r--r--src/pip/_internal/utils/misc.py417
-rw-r--r--src/pip/_internal/utils/models.py24
-rw-r--r--src/pip/_internal/utils/packaging.py80
-rw-r--r--src/pip/_internal/utils/parallel.py101
-rw-r--r--src/pip/_internal/utils/pkg_resources.py40
-rw-r--r--src/pip/_internal/utils/setuptools_build.py136
-rw-r--r--src/pip/_internal/utils/subprocess.py143
-rw-r--r--src/pip/_internal/utils/temp_dir.py58
-rw-r--r--src/pip/_internal/utils/unpacking.py36
-rw-r--r--src/pip/_internal/utils/urls.py9
-rw-r--r--src/pip/_internal/utils/virtualenv.py21
-rw-r--r--src/pip/_internal/utils/wheel.py67
-rw-r--r--src/pip/_internal/vcs/bazaar.py73
-rw-r--r--src/pip/_internal/vcs/git.py230
-rw-r--r--src/pip/_internal/vcs/mercurial.py97
-rw-r--r--src/pip/_internal/vcs/subversion.py151
-rw-r--r--src/pip/_internal/vcs/versioncontrol.py337
-rw-r--r--src/pip/_internal/wheel_builder.py213
-rw-r--r--src/pip/_vendor/README.rst8
-rw-r--r--src/pip/_vendor/__init__.py13
-rw-r--r--src/pip/_vendor/appdirs.py633
-rw-r--r--src/pip/_vendor/cachecontrol/LICENSE.txt6
-rw-r--r--src/pip/_vendor/cachecontrol/__init__.py9
-rw-r--r--src/pip/_vendor/cachecontrol/_cmd.py4
-rw-r--r--src/pip/_vendor/cachecontrol/adapter.py10
-rw-r--r--src/pip/_vendor/cachecontrol/cache.py30
-rw-r--r--src/pip/_vendor/cachecontrol/caches/__init__.py11
-rw-r--r--src/pip/_vendor/cachecontrol/caches/file_cache.py58
-rw-r--r--src/pip/_vendor/cachecontrol/caches/redis_cache.py8
-rw-r--r--src/pip/_vendor/cachecontrol/compat.py5
-rw-r--r--src/pip/_vendor/cachecontrol/controller.py111
-rw-r--r--src/pip/_vendor/cachecontrol/filewrapper.py39
-rw-r--r--src/pip/_vendor/cachecontrol/heuristics.py4
-rw-r--r--src/pip/_vendor/cachecontrol/serialize.py52
-rw-r--r--src/pip/_vendor/cachecontrol/wrapper.py4
-rw-r--r--src/pip/_vendor/certifi.pyi1
-rw-r--r--src/pip/_vendor/certifi/__init__.py3
-rw-r--r--src/pip/_vendor/certifi/cacert.pem952
-rw-r--r--src/pip/_vendor/certifi/core.py22
-rw-r--r--src/pip/_vendor/certifi/py.typed0
-rw-r--r--src/pip/_vendor/chardet/LICENSE20
-rw-r--r--src/pip/_vendor/chardet/__init__.py72
-rw-r--r--src/pip/_vendor/chardet/big5freq.py6
-rw-r--r--src/pip/_vendor/chardet/big5prober.py6
-rw-r--r--src/pip/_vendor/chardet/chardistribution.py102
-rw-r--r--src/pip/_vendor/chardet/charsetgroupprober.py16
-rw-r--r--src/pip/_vendor/chardet/charsetprober.py47
-rw-r--r--src/pip/_vendor/chardet/cli/__init__.py1
-rw-r--r--src/pip/_vendor/chardet/cli/chardetect.py52
-rw-r--r--src/pip/_vendor/chardet/codingstatemachine.py16
-rw-r--r--src/pip/_vendor/chardet/cp949prober.py2
-rw-r--r--src/pip/_vendor/chardet/enums.py18
-rw-r--r--src/pip/_vendor/chardet/escprober.py19
-rw-r--r--src/pip/_vendor/chardet/escsm.py384
-rw-r--r--src/pip/_vendor/chardet/eucjpprober.py41
-rw-r--r--src/pip/_vendor/chardet/euckrfreq.py3
-rw-r--r--src/pip/_vendor/chardet/euckrprober.py6
-rw-r--r--src/pip/_vendor/chardet/euctwfreq.py677
-rw-r--r--src/pip/_vendor/chardet/euctwprober.py7
-rw-r--r--src/pip/_vendor/chardet/gb2312freq.py3
-rw-r--r--src/pip/_vendor/chardet/gb2312prober.py7
-rw-r--r--src/pip/_vendor/chardet/hebrewprober.py68
-rw-r--r--src/pip/_vendor/chardet/jisfreq.py4
-rw-r--r--src/pip/_vendor/chardet/johabfreq.py2382
-rw-r--r--src/pip/_vendor/chardet/johabprober.py (renamed from src/pip/_vendor/chardet/compat.py)41
-rw-r--r--src/pip/_vendor/chardet/jpcntx.py190
-rw-r--r--src/pip/_vendor/chardet/langbulgarianmodel.py1061
-rw-r--r--src/pip/_vendor/chardet/langgreekmodel.py1061
-rw-r--r--src/pip/_vendor/chardet/langhebrewmodel.py533
-rw-r--r--src/pip/_vendor/chardet/langhungarianmodel.py1061
-rw-r--r--src/pip/_vendor/chardet/langrussianmodel.py3173
-rw-r--r--src/pip/_vendor/chardet/langthaimodel.py533
-rw-r--r--src/pip/_vendor/chardet/langturkishmodel.py533
-rw-r--r--src/pip/_vendor/chardet/latin1prober.py26
-rw-r--r--src/pip/_vendor/chardet/mbcharsetprober.py32
-rw-r--r--src/pip/_vendor/chardet/mbcsgroupprober.py16
-rw-r--r--src/pip/_vendor/chardet/mbcssm.py812
-rw-r--r--src/pip/_vendor/chardet/metadata/languages.py571
-rw-r--r--src/pip/_vendor/chardet/sbcharsetprober.py73
-rw-r--r--src/pip/_vendor/chardet/sbcsgroupprober.py31
-rw-r--r--src/pip/_vendor/chardet/sjisprober.py46
-rw-r--r--src/pip/_vendor/chardet/universaldetector.py174
-rw-r--r--src/pip/_vendor/chardet/utf1632prober.py223
-rw-r--r--src/pip/_vendor/chardet/utf8prober.py16
-rw-r--r--src/pip/_vendor/chardet/version.py4
-rw-r--r--src/pip/_vendor/colorama/__init__.py2
-rw-r--r--src/pip/_vendor/colorama/ansitowin32.py10
-rw-r--r--src/pip/_vendor/distlib/__init__.py2
-rw-r--r--src/pip/_vendor/distlib/_backport/__init__.py6
-rw-r--r--src/pip/_vendor/distlib/_backport/misc.py41
-rw-r--r--src/pip/_vendor/distlib/_backport/shutil.py764
-rw-r--r--src/pip/_vendor/distlib/_backport/sysconfig.cfg84
-rw-r--r--src/pip/_vendor/distlib/_backport/sysconfig.py786
-rw-r--r--src/pip/_vendor/distlib/_backport/tarfile.py2607
-rw-r--r--src/pip/_vendor/distlib/compat.py52
-rw-r--r--src/pip/_vendor/distlib/database.py67
-rw-r--r--src/pip/_vendor/distlib/index.py13
-rw-r--r--src/pip/_vendor/distlib/locators.py6
-rw-r--r--src/pip/_vendor/distlib/markers.py26
-rw-r--r--src/pip/_vendor/distlib/metadata.py48
-rw-r--r--src/pip/_vendor/distlib/scripts.py20
-rw-r--r--src/pip/_vendor/distlib/t32.exebin96768 -> 97792 bytes
-rw-r--r--src/pip/_vendor/distlib/t64-arm.exebin0 -> 182784 bytes
-rw-r--r--src/pip/_vendor/distlib/t64.exebin105984 -> 107520 bytes
-rw-r--r--src/pip/_vendor/distlib/util.py85
-rw-r--r--src/pip/_vendor/distlib/version.py2
-rw-r--r--src/pip/_vendor/distlib/w32.exebin90112 -> 91648 bytes
-rw-r--r--src/pip/_vendor/distlib/w64-arm.exebin0 -> 168448 bytes
-rw-r--r--src/pip/_vendor/distlib/w64.exebin99840 -> 101888 bytes
-rw-r--r--src/pip/_vendor/distlib/wheel.py56
-rw-r--r--src/pip/_vendor/distro.pyi1
-rw-r--r--src/pip/_vendor/distro/LICENSE (renamed from src/pip/_vendor/distro.LICENSE)0
-rw-r--r--src/pip/_vendor/distro/__init__.py54
-rw-r--r--src/pip/_vendor/distro/__main__.py4
-rw-r--r--src/pip/_vendor/distro/distro.py (renamed from src/pip/_vendor/distro.py)686
-rw-r--r--src/pip/_vendor/distro/py.typed0
-rw-r--r--src/pip/_vendor/html5lib.pyi1
-rw-r--r--src/pip/_vendor/html5lib/LICENSE20
-rw-r--r--src/pip/_vendor/html5lib/__init__.py35
-rw-r--r--src/pip/_vendor/html5lib/_ihatexml.py289
-rw-r--r--src/pip/_vendor/html5lib/_inputstream.py918
-rw-r--r--src/pip/_vendor/html5lib/_tokenizer.py1735
-rw-r--r--src/pip/_vendor/html5lib/_trie/__init__.py5
-rw-r--r--src/pip/_vendor/html5lib/_trie/_base.py40
-rw-r--r--src/pip/_vendor/html5lib/_trie/py.py67
-rw-r--r--src/pip/_vendor/html5lib/_utils.py159
-rw-r--r--src/pip/_vendor/html5lib/constants.py2946
-rw-r--r--src/pip/_vendor/html5lib/filters/alphabeticalattributes.py29
-rw-r--r--src/pip/_vendor/html5lib/filters/base.py12
-rw-r--r--src/pip/_vendor/html5lib/filters/inject_meta_charset.py73
-rw-r--r--src/pip/_vendor/html5lib/filters/lint.py93
-rw-r--r--src/pip/_vendor/html5lib/filters/optionaltags.py207
-rw-r--r--src/pip/_vendor/html5lib/filters/sanitizer.py916
-rw-r--r--src/pip/_vendor/html5lib/filters/whitespace.py38
-rw-r--r--src/pip/_vendor/html5lib/html5parser.py2795
-rw-r--r--src/pip/_vendor/html5lib/serializer.py409
-rw-r--r--src/pip/_vendor/html5lib/treeadapters/__init__.py30
-rw-r--r--src/pip/_vendor/html5lib/treeadapters/genshi.py54
-rw-r--r--src/pip/_vendor/html5lib/treeadapters/sax.py50
-rw-r--r--src/pip/_vendor/html5lib/treebuilders/__init__.py88
-rw-r--r--src/pip/_vendor/html5lib/treebuilders/base.py417
-rw-r--r--src/pip/_vendor/html5lib/treebuilders/dom.py239
-rw-r--r--src/pip/_vendor/html5lib/treebuilders/etree.py343
-rw-r--r--src/pip/_vendor/html5lib/treebuilders/etree_lxml.py392
-rw-r--r--src/pip/_vendor/html5lib/treewalkers/__init__.py154
-rw-r--r--src/pip/_vendor/html5lib/treewalkers/base.py252
-rw-r--r--src/pip/_vendor/html5lib/treewalkers/dom.py43
-rw-r--r--src/pip/_vendor/html5lib/treewalkers/etree.py131
-rw-r--r--src/pip/_vendor/html5lib/treewalkers/etree_lxml.py215
-rw-r--r--src/pip/_vendor/html5lib/treewalkers/genshi.py69
-rw-r--r--src/pip/_vendor/idna.pyi1
-rw-r--r--src/pip/_vendor/idna/codec.py15
-rw-r--r--src/pip/_vendor/idna/compat.py9
-rw-r--r--src/pip/_vendor/idna/core.py66
-rw-r--r--src/pip/_vendor/idna/idnadata.py137
-rw-r--r--src/pip/_vendor/idna/intranges.py12
-rw-r--r--src/pip/_vendor/idna/package_data.py2
-rw-r--r--src/pip/_vendor/idna/uts46data.py882
-rw-r--r--src/pip/_vendor/msgpack/__init__.py5
-rw-r--r--src/pip/_vendor/msgpack/_version.py1
-rw-r--r--src/pip/_vendor/msgpack/ext.py8
-rw-r--r--src/pip/_vendor/msgpack/fallback.py289
-rw-r--r--src/pip/_vendor/packaging.pyi1
-rw-r--r--src/pip/_vendor/packaging/__about__.py2
-rw-r--r--src/pip/_vendor/packaging/_musllinux.py2
-rw-r--r--src/pip/_vendor/packaging/_structures.py6
-rw-r--r--src/pip/_vendor/packaging/specifiers.py30
-rw-r--r--src/pip/_vendor/packaging/tags.py23
-rw-r--r--src/pip/_vendor/pep517/__init__.py2
-rw-r--r--src/pip/_vendor/pep517/build.py2
-rw-r--r--src/pip/_vendor/pep517/check.py2
-rw-r--r--src/pip/_vendor/pep517/compat.py11
-rw-r--r--src/pip/_vendor/pep517/envbuild.py2
-rw-r--r--src/pip/_vendor/pep517/in_process/_in_process.py14
-rw-r--r--src/pip/_vendor/pep517/wrappers.py4
-rw-r--r--src/pip/_vendor/pkg_resources/__init__.py4
-rw-r--r--src/pip/_vendor/platformdirs/LICENSE.txt (renamed from src/pip/_vendor/appdirs.LICENSE.txt)1
-rw-r--r--src/pip/_vendor/platformdirs/__init__.py340
-rw-r--r--src/pip/_vendor/platformdirs/__main__.py46
-rw-r--r--src/pip/_vendor/platformdirs/android.py120
-rw-r--r--src/pip/_vendor/platformdirs/api.py156
-rw-r--r--src/pip/_vendor/platformdirs/macos.py64
-rw-r--r--src/pip/_vendor/platformdirs/py.typed0
-rw-r--r--src/pip/_vendor/platformdirs/unix.py181
-rw-r--r--src/pip/_vendor/platformdirs/version.py4
-rw-r--r--src/pip/_vendor/platformdirs/windows.py182
-rw-r--r--src/pip/_vendor/progress.pyi1
-rw-r--r--src/pip/_vendor/progress/LICENSE13
-rw-r--r--src/pip/_vendor/progress/__init__.py177
-rw-r--r--src/pip/_vendor/progress/bar.py91
-rw-r--r--src/pip/_vendor/progress/counter.py41
-rw-r--r--src/pip/_vendor/progress/spinner.py43
-rw-r--r--src/pip/_vendor/pygments.pyi1
-rw-r--r--src/pip/_vendor/pygments/LICENSE25
-rw-r--r--src/pip/_vendor/pygments/__init__.py83
-rw-r--r--src/pip/_vendor/pygments/__main__.py17
-rw-r--r--src/pip/_vendor/pygments/cmdline.py663
-rw-r--r--src/pip/_vendor/pygments/console.py70
-rw-r--r--src/pip/_vendor/pygments/filter.py71
-rw-r--r--src/pip/_vendor/pygments/filters/__init__.py937
-rw-r--r--src/pip/_vendor/pygments/formatter.py94
-rw-r--r--src/pip/_vendor/pygments/formatters/__init__.py153
-rw-r--r--src/pip/_vendor/pygments/formatters/_mapping.py84
-rw-r--r--src/pip/_vendor/pygments/formatters/bbcode.py108
-rw-r--r--src/pip/_vendor/pygments/formatters/groff.py170
-rw-r--r--src/pip/_vendor/pygments/formatters/html.py989
-rw-r--r--src/pip/_vendor/pygments/formatters/img.py641
-rw-r--r--src/pip/_vendor/pygments/formatters/irc.py179
-rw-r--r--src/pip/_vendor/pygments/formatters/latex.py521
-rw-r--r--src/pip/_vendor/pygments/formatters/other.py161
-rw-r--r--src/pip/_vendor/pygments/formatters/pangomarkup.py83
-rw-r--r--src/pip/_vendor/pygments/formatters/rtf.py146
-rw-r--r--src/pip/_vendor/pygments/formatters/svg.py188
-rw-r--r--src/pip/_vendor/pygments/formatters/terminal.py127
-rw-r--r--src/pip/_vendor/pygments/formatters/terminal256.py338
-rw-r--r--src/pip/_vendor/pygments/lexer.py882
-rw-r--r--src/pip/_vendor/pygments/lexers/__init__.py345
-rw-r--r--src/pip/_vendor/pygments/lexers/_mapping.py596
-rw-r--r--src/pip/_vendor/pygments/lexers/python.py1191
-rw-r--r--src/pip/_vendor/pygments/modeline.py43
-rw-r--r--src/pip/_vendor/pygments/plugin.py69
-rw-r--r--src/pip/_vendor/pygments/regexopt.py91
-rw-r--r--src/pip/_vendor/pygments/scanner.py104
-rw-r--r--src/pip/_vendor/pygments/sphinxext.py155
-rw-r--r--src/pip/_vendor/pygments/style.py197
-rw-r--r--src/pip/_vendor/pygments/styles/__init__.py93
-rw-r--r--src/pip/_vendor/pygments/token.py212
-rw-r--r--src/pip/_vendor/pygments/unistring.py153
-rw-r--r--src/pip/_vendor/pygments/util.py308
-rw-r--r--src/pip/_vendor/pyparsing.py7107
-rw-r--r--src/pip/_vendor/pyparsing.pyi1
-rw-r--r--src/pip/_vendor/pyparsing/LICENSE (renamed from src/pip/_vendor/pyparsing.LICENSE)0
-rw-r--r--src/pip/_vendor/pyparsing/__init__.py331
-rw-r--r--src/pip/_vendor/pyparsing/actions.py207
-rw-r--r--src/pip/_vendor/pyparsing/common.py424
-rw-r--r--src/pip/_vendor/pyparsing/core.py5814
-rw-r--r--src/pip/_vendor/pyparsing/diagram/__init__.py642
-rw-r--r--src/pip/_vendor/pyparsing/exceptions.py267
-rw-r--r--src/pip/_vendor/pyparsing/helpers.py1088
-rw-r--r--src/pip/_vendor/pyparsing/py.typed0
-rw-r--r--src/pip/_vendor/pyparsing/results.py760
-rw-r--r--src/pip/_vendor/pyparsing/testing.py331
-rw-r--r--src/pip/_vendor/pyparsing/unicode.py352
-rw-r--r--src/pip/_vendor/pyparsing/util.py235
-rw-r--r--src/pip/_vendor/requests/__init__.py94
-rw-r--r--src/pip/_vendor/requests/__version__.py20
-rw-r--r--src/pip/_vendor/requests/_internal_utils.py24
-rw-r--r--src/pip/_vendor/requests/adapters.py229
-rw-r--r--src/pip/_vendor/requests/api.py18
-rw-r--r--src/pip/_vendor/requests/auth.py156
-rw-r--r--src/pip/_vendor/requests/certs.py3
-rw-r--r--src/pip/_vendor/requests/compat.py89
-rw-r--r--src/pip/_vendor/requests/cookies.py152
-rw-r--r--src/pip/_vendor/requests/exceptions.py32
-rw-r--r--src/pip/_vendor/requests/help.py101
-rw-r--r--src/pip/_vendor/requests/hooks.py7
-rw-r--r--src/pip/_vendor/requests/models.py360
-rw-r--r--src/pip/_vendor/requests/sessions.py288
-rw-r--r--src/pip/_vendor/requests/status_codes.py169
-rw-r--r--src/pip/_vendor/requests/structures.py12
-rw-r--r--src/pip/_vendor/requests/utils.py439
-rw-r--r--src/pip/_vendor/resolvelib.pyi1
-rw-r--r--src/pip/_vendor/resolvelib/__init__.py4
-rw-r--r--src/pip/_vendor/resolvelib/__init__.pyi20
-rw-r--r--src/pip/_vendor/resolvelib/providers.py11
-rw-r--r--src/pip/_vendor/resolvelib/providers.pyi2
-rw-r--r--src/pip/_vendor/resolvelib/reporters.py6
-rw-r--r--src/pip/_vendor/resolvelib/reporters.pyi1
-rw-r--r--src/pip/_vendor/resolvelib/resolvers.py19
-rw-r--r--src/pip/_vendor/resolvelib/resolvers.pyi8
-rw-r--r--src/pip/_vendor/resolvelib/structs.pyi11
-rw-r--r--src/pip/_vendor/rich/LICENSE19
-rw-r--r--src/pip/_vendor/rich/__init__.py176
-rw-r--r--src/pip/_vendor/rich/__main__.py282
-rw-r--r--src/pip/_vendor/rich/_cell_widths.py451
-rw-r--r--src/pip/_vendor/rich/_emoji_codes.py3610
-rw-r--r--src/pip/_vendor/rich/_emoji_replace.py32
-rw-r--r--src/pip/_vendor/rich/_export_format.py78
-rw-r--r--src/pip/_vendor/rich/_extension.py10
-rw-r--r--src/pip/_vendor/rich/_inspect.py270
-rw-r--r--src/pip/_vendor/rich/_log_render.py94
-rw-r--r--src/pip/_vendor/rich/_loop.py43
-rw-r--r--src/pip/_vendor/rich/_palettes.py309
-rw-r--r--src/pip/_vendor/rich/_pick.py17
-rw-r--r--src/pip/_vendor/rich/_ratio.py160
-rw-r--r--src/pip/_vendor/rich/_spinners.py482
-rw-r--r--src/pip/_vendor/rich/_stack.py16
-rw-r--r--src/pip/_vendor/rich/_timer.py19
-rw-r--r--src/pip/_vendor/rich/_win32_console.py662
-rw-r--r--src/pip/_vendor/rich/_windows.py72
-rw-r--r--src/pip/_vendor/rich/_windows_renderer.py56
-rw-r--r--src/pip/_vendor/rich/_wrap.py56
-rw-r--r--src/pip/_vendor/rich/abc.py33
-rw-r--r--src/pip/_vendor/rich/align.py311
-rw-r--r--src/pip/_vendor/rich/ansi.py237
-rw-r--r--src/pip/_vendor/rich/bar.py94
-rw-r--r--src/pip/_vendor/rich/box.py517
-rw-r--r--src/pip/_vendor/rich/cells.py154
-rw-r--r--src/pip/_vendor/rich/color.py615
-rw-r--r--src/pip/_vendor/rich/color_triplet.py38
-rw-r--r--src/pip/_vendor/rich/columns.py187
-rw-r--r--src/pip/_vendor/rich/console.py2572
-rw-r--r--src/pip/_vendor/rich/constrain.py37
-rw-r--r--src/pip/_vendor/rich/containers.py167
-rw-r--r--src/pip/_vendor/rich/control.py225
-rw-r--r--src/pip/_vendor/rich/default_styles.py188
-rw-r--r--src/pip/_vendor/rich/diagnose.py37
-rw-r--r--src/pip/_vendor/rich/emoji.py96
-rw-r--r--src/pip/_vendor/rich/errors.py34
-rw-r--r--src/pip/_vendor/rich/file_proxy.py54
-rw-r--r--src/pip/_vendor/rich/filesize.py89
-rw-r--r--src/pip/_vendor/rich/highlighter.py232
-rw-r--r--src/pip/_vendor/rich/json.py140
-rw-r--r--src/pip/_vendor/rich/jupyter.py101
-rw-r--r--src/pip/_vendor/rich/layout.py445
-rw-r--r--src/pip/_vendor/rich/live.py373
-rw-r--r--src/pip/_vendor/rich/live_render.py113
-rw-r--r--src/pip/_vendor/rich/logging.py280
-rw-r--r--src/pip/_vendor/rich/markup.py246
-rw-r--r--src/pip/_vendor/rich/measure.py151
-rw-r--r--src/pip/_vendor/rich/padding.py141
-rw-r--r--src/pip/_vendor/rich/pager.py34
-rw-r--r--src/pip/_vendor/rich/palette.py100
-rw-r--r--src/pip/_vendor/rich/panel.py251
-rw-r--r--src/pip/_vendor/rich/pretty.py1010
-rw-r--r--src/pip/_vendor/rich/progress.py1703
-rw-r--r--src/pip/_vendor/rich/progress_bar.py224
-rw-r--r--src/pip/_vendor/rich/prompt.py376
-rw-r--r--src/pip/_vendor/rich/protocol.py42
-rw-r--r--src/pip/_vendor/rich/py.typed0
-rw-r--r--src/pip/_vendor/rich/region.py10
-rw-r--r--src/pip/_vendor/rich/repr.py152
-rw-r--r--src/pip/_vendor/rich/rule.py134
-rw-r--r--src/pip/_vendor/rich/scope.py86
-rw-r--r--src/pip/_vendor/rich/screen.py54
-rw-r--r--src/pip/_vendor/rich/segment.py739
-rw-r--r--src/pip/_vendor/rich/spinner.py136
-rw-r--r--src/pip/_vendor/rich/status.py132
-rw-r--r--src/pip/_vendor/rich/style.py771
-rw-r--r--src/pip/_vendor/rich/styled.py42
-rw-r--r--src/pip/_vendor/rich/syntax.py934
-rw-r--r--src/pip/_vendor/rich/table.py996
-rw-r--r--src/pip/_vendor/rich/terminal_theme.py153
-rw-r--r--src/pip/_vendor/rich/text.py1286
-rw-r--r--src/pip/_vendor/rich/theme.py112
-rw-r--r--src/pip/_vendor/rich/themes.py5
-rw-r--r--src/pip/_vendor/rich/traceback.py679
-rw-r--r--src/pip/_vendor/rich/tree.py251
-rw-r--r--src/pip/_vendor/tenacity.pyi1
-rw-r--r--src/pip/_vendor/tomli.pyi1
-rw-r--r--src/pip/_vendor/tomli/__init__.py11
-rw-r--r--src/pip/_vendor/tomli/_parser.py360
-rw-r--r--src/pip/_vendor/tomli/_re.py94
-rw-r--r--src/pip/_vendor/tomli/_types.py10
-rw-r--r--src/pip/_vendor/typing_extensions.LICENSE254
-rw-r--r--src/pip/_vendor/typing_extensions.py2069
-rw-r--r--src/pip/_vendor/typing_extensions.pyi1
-rw-r--r--src/pip/_vendor/urllib3/_version.py2
-rw-r--r--src/pip/_vendor/urllib3/connection.py50
-rw-r--r--src/pip/_vendor/urllib3/connectionpool.py51
-rw-r--r--src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py2
-rw-r--r--src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py1
-rw-r--r--src/pip/_vendor/urllib3/contrib/pyopenssl.py5
-rw-r--r--src/pip/_vendor/urllib3/contrib/securetransport.py5
-rw-r--r--src/pip/_vendor/urllib3/packages/__init__.py5
-rw-r--r--src/pip/_vendor/urllib3/packages/six.py1
-rw-r--r--src/pip/_vendor/urllib3/packages/ssl_match_hostname/__init__.py24
-rw-r--r--src/pip/_vendor/urllib3/poolmanager.py1
-rw-r--r--src/pip/_vendor/urllib3/response.py5
-rw-r--r--src/pip/_vendor/urllib3/util/connection.py3
-rw-r--r--src/pip/_vendor/urllib3/util/proxy.py1
-rw-r--r--src/pip/_vendor/urllib3/util/request.py6
-rw-r--r--src/pip/_vendor/urllib3/util/retry.py24
-rw-r--r--src/pip/_vendor/urllib3/util/ssl_match_hostname.py (renamed from src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py)15
-rw-r--r--src/pip/_vendor/urllib3/util/ssltransport.py4
-rw-r--r--src/pip/_vendor/urllib3/util/url.py5
-rw-r--r--src/pip/_vendor/urllib3/util/wait.py1
-rw-r--r--src/pip/_vendor/vendor.txt37
-rw-r--r--tests/conftest.py364
-rw-r--r--tests/data/indexes/datarequire/fakepackage/index.html1
-rw-r--r--tests/data/indexes/dev/bar/index.html1
-rw-r--r--tests/data/indexes/in dex/simple/index.html1
-rw-r--r--tests/data/indexes/pre/bar/index.html1
-rw-r--r--tests/data/indexes/simple/simple/index.html1
-rw-r--r--tests/data/indexes/yanked/simple/index.html1
-rw-r--r--tests/data/indexes/yanked_all/simple/index.html8
-rw-r--r--tests/data/packages/BrokenEmitsUTF8/setup.py38
-rw-r--r--tests/data/packages/FSPkg/setup.py39
-rw-r--r--tests/data/packages/HackedEggInfo/setup.py6
-rw-r--r--tests/data/packages/LocalEnvironMarker/setup.py14
-rw-r--r--tests/data/packages/LocalExtras-0.0.2/setup.py14
-rw-r--r--tests/data/packages/LocalExtras/setup.py12
-rw-r--r--tests/data/packages/SetupPyLatin1/setup.py7
-rw-r--r--tests/data/packages/SetupPyUTF8/setup.py7
-rw-r--r--tests/data/packages/corruptwheel-1.0-py2.py3-none-any.whl1
-rw-r--r--tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py10
-rw-r--r--tests/data/packages/requiresPaste/requiresPaste.py2
-rw-r--r--tests/data/packages/requires_wheelbroken_upper/setup.py3
-rw-r--r--tests/data/packages/symlinks/setup.py11
-rw-r--r--tests/data/packages3/dinner/index.html1
-rw-r--r--tests/data/packages3/index.html1
-rw-r--r--tests/data/packages3/requiredinner/index.html1
-rw-r--r--tests/data/src/TopoRequires/setup.py6
-rw-r--r--tests/data/src/TopoRequires2/setup.py8
-rw-r--r--tests/data/src/TopoRequires3/setup.py8
-rw-r--r--tests/data/src/TopoRequires4/setup.py8
-rw-r--r--tests/data/src/chattymodule/setup.py8
-rw-r--r--tests/data/src/compilewheel/setup.py5
-rw-r--r--tests/data/src/extension/setup.py4
-rw-r--r--tests/data/src/pep517_setup_cfg_only/setup.cfg3
-rw-r--r--tests/data/src/pep518-3.0/pep518.py2
-rw-r--r--tests/data/src/pep518-3.0/setup.py9
-rw-r--r--tests/data/src/pep518_conflicting_requires/pep518.py2
-rw-r--r--tests/data/src/pep518_conflicting_requires/setup.py6
-rw-r--r--tests/data/src/pep518_forkbomb-235/setup.py4
-rw-r--r--tests/data/src/pep518_invalid_build_system/pep518.py2
-rw-r--r--tests/data/src/pep518_invalid_build_system/setup.py6
-rw-r--r--tests/data/src/pep518_invalid_requires/pep518.py2
-rw-r--r--tests/data/src/pep518_invalid_requires/setup.py6
-rw-r--r--tests/data/src/pep518_missing_requires/pep518.py2
-rw-r--r--tests/data/src/pep518_missing_requires/setup.py6
-rw-r--r--tests/data/src/pep518_twin_forkbombs_first-234/setup.py8
-rw-r--r--tests/data/src/pep518_twin_forkbombs_second-238/setup.py8
-rw-r--r--tests/data/src/pep518_with_extra_and_markers-1.0/pep518_with_extra_and_markers.py2
-rw-r--r--tests/data/src/pep518_with_extra_and_markers-1.0/setup.py13
-rw-r--r--tests/data/src/pep518_with_namespace_package-1.0/setup.py6
-rwxr-xr-xtests/data/src/prjwithdatafile/setup.py10
-rw-r--r--tests/data/src/requires_capitalized/setup.py5
-rw-r--r--tests/data/src/requires_requires_capitalized/setup.py9
-rw-r--r--tests/data/src/requires_simple/setup.py5
-rw-r--r--tests/data/src/requires_simple_extra/setup.py11
-rw-r--r--tests/data/src/setup_error/setup.py11
-rw-r--r--tests/data/src/simple_namespace/setup.py8
-rw-r--r--tests/data/src/simplewheel-1.0/setup.py9
-rw-r--r--tests/data/src/simplewheel-1.0/simplewheel/__init__.py2
-rw-r--r--tests/data/src/simplewheel-2.0/setup.py9
-rw-r--r--tests/data/src/simplewheel-2.0/simplewheel/__init__.py2
-rw-r--r--tests/data/src/singlemodule/setup.py4
-rw-r--r--tests/data/src/withpyproject/setup.py2
-rw-r--r--tests/functional/test_bad_url.py16
-rw-r--r--tests/functional/test_broken_stdout.py52
-rw-r--r--tests/functional/test_build_env.py179
-rw-r--r--tests/functional/test_cache.py330
-rw-r--r--tests/functional/test_check.py223
-rw-r--r--tests/functional/test_cli.py33
-rw-r--r--tests/functional/test_completion.py373
-rw-r--r--tests/functional/test_config_settings.py139
-rw-r--r--tests/functional/test_configuration.py69
-rw-r--r--tests/functional/test_debug.py75
-rw-r--r--tests/functional/test_download.py1587
-rw-r--r--tests/functional/test_fast_deps.py104
-rw-r--r--tests/functional/test_freeze.py737
-rw-r--r--tests/functional/test_hash.py38
-rw-r--r--tests/functional/test_help.py72
-rw-r--r--tests/functional/test_index.py66
-rw-r--r--tests/functional/test_inspect.py45
-rw-r--r--tests/functional/test_install.py1953
-rw-r--r--tests/functional/test_install_check.py82
-rw-r--r--tests/functional/test_install_cleanup.py31
-rw-r--r--tests/functional/test_install_compat.py28
-rw-r--r--tests/functional/test_install_config.py292
-rw-r--r--tests/functional/test_install_direct_url.py36
-rw-r--r--tests/functional/test_install_extras.py172
-rw-r--r--tests/functional/test_install_force_reinstall.py40
-rw-r--r--tests/functional/test_install_index.py93
-rw-r--r--tests/functional/test_install_report.py214
-rw-r--r--tests/functional/test_install_reqs.py726
-rw-r--r--tests/functional/test_install_requested.py85
-rw-r--r--tests/functional/test_install_upgrade.py414
-rw-r--r--tests/functional/test_install_user.py179
-rw-r--r--tests/functional/test_install_vcs_git.py458
-rw-r--r--tests/functional/test_install_wheel.py634
-rw-r--r--tests/functional/test_list.py765
-rw-r--r--tests/functional/test_new_resolver.py1360
-rw-r--r--tests/functional/test_new_resolver_errors.py59
-rw-r--r--tests/functional/test_new_resolver_hashes.py154
-rw-r--r--tests/functional/test_new_resolver_target.py36
-rw-r--r--tests/functional/test_new_resolver_user.py127
-rw-r--r--tests/functional/test_no_color.py33
-rw-r--r--tests/functional/test_pep517.py397
-rw-r--r--tests/functional/test_pep660.py232
-rw-r--r--tests/functional/test_pip_runner_script.py22
-rw-r--r--tests/functional/test_python_option.py41
-rw-r--r--tests/functional/test_requests.py25
-rw-r--r--tests/functional/test_search.py157
-rw-r--r--tests/functional/test_show.py265
-rw-r--r--tests/functional/test_truststore.py61
-rw-r--r--tests/functional/test_uninstall.py544
-rw-r--r--tests/functional/test_uninstall_user.py49
-rw-r--r--tests/functional/test_vcs_bazaar.py22
-rw-r--r--tests/functional/test_vcs_git.py367
-rw-r--r--tests/functional/test_vcs_mercurial.py12
-rw-r--r--tests/functional/test_vcs_subversion.py28
-rw-r--r--tests/functional/test_warning.py52
-rw-r--r--tests/functional/test_wheel.py322
-rw-r--r--tests/lib/__init__.py638
-rw-r--r--tests/lib/certs.py9
-rw-r--r--tests/lib/compat.py39
-rw-r--r--tests/lib/configuration_helpers.py32
-rw-r--r--tests/lib/direct_url.py20
-rw-r--r--tests/lib/filesystem.py34
-rw-r--r--tests/lib/git_submodule_helpers.py21
-rw-r--r--tests/lib/index.py6
-rw-r--r--tests/lib/local_repos.py19
-rw-r--r--tests/lib/options_helpers.py11
-rw-r--r--tests/lib/path.py192
-rw-r--r--tests/lib/requests_mocks.py41
-rw-r--r--tests/lib/server.py135
-rw-r--r--tests/lib/test_lib.py76
-rw-r--r--tests/lib/test_wheel.py56
-rw-r--r--tests/lib/venv.py55
-rw-r--r--tests/lib/wheel.py160
-rw-r--r--tests/requirements-common_wheels.txt2
-rw-r--r--tests/requirements.txt4
-rw-r--r--tests/unit/metadata/test_metadata.py131
-rw-r--r--tests/unit/metadata/test_metadata_pkg_resources.py123
-rw-r--r--tests/unit/resolution_resolvelib/__init__.py0
-rw-r--r--tests/unit/resolution_resolvelib/conftest.py23
-rw-r--r--tests/unit/resolution_resolvelib/test_provider.py78
-rw-r--r--tests/unit/resolution_resolvelib/test_requirement.py46
-rw-r--r--tests/unit/resolution_resolvelib/test_resolver.py118
-rw-r--r--tests/unit/test_appdirs.py227
-rw-r--r--tests/unit/test_base_command.py117
-rw-r--r--tests/unit/test_cache.py33
-rw-r--r--tests/unit/test_cmdoptions.py64
-rw-r--r--tests/unit/test_collector.py866
-rw-r--r--tests/unit/test_command_install.py153
-rw-r--r--tests/unit/test_commands.py61
-rw-r--r--tests/unit/test_compat.py15
-rw-r--r--tests/unit/test_configuration.py125
-rw-r--r--tests/unit/test_direct_url.py82
-rw-r--r--tests/unit/test_direct_url_helpers.py98
-rw-r--r--tests/unit/test_exceptions.py474
-rw-r--r--tests/unit/test_finder.py340
-rw-r--r--tests/unit/test_format_control.py52
-rw-r--r--tests/unit/test_index.py604
-rw-r--r--tests/unit/test_link.py219
-rw-r--r--tests/unit/test_locations.py117
-rw-r--r--tests/unit/test_logging.py114
-rw-r--r--tests/unit/test_metadata.py40
-rw-r--r--tests/unit/test_models.py21
-rw-r--r--tests/unit/test_models_wheel.py163
-rw-r--r--tests/unit/test_network_auth.py270
-rw-r--r--tests/unit/test_network_cache.py30
-rw-r--r--tests/unit/test_network_download.py126
-rw-r--r--tests/unit/test_network_lazy_wheel.py57
-rw-r--r--tests/unit/test_network_session.py195
-rw-r--r--tests/unit/test_network_utils.py33
-rw-r--r--tests/unit/test_operations_prepare.py193
-rw-r--r--tests/unit/test_options.py618
-rw-r--r--tests/unit/test_packaging.py40
-rw-r--r--tests/unit/test_pep517.py85
-rw-r--r--tests/unit/test_pyproject_config.py44
-rw-r--r--tests/unit/test_req.py955
-rw-r--r--tests/unit/test_req_file.py810
-rw-r--r--tests/unit/test_req_install.py60
-rw-r--r--tests/unit/test_req_uninstall.py328
-rw-r--r--tests/unit/test_resolution_legacy_resolver.py297
-rw-r--r--tests/unit/test_search_scope.py38
-rw-r--r--tests/unit/test_self_check_outdated.py377
-rw-r--r--tests/unit/test_target_python.py122
-rw-r--r--tests/unit/test_urls.py71
-rw-r--r--tests/unit/test_utils.py1079
-rw-r--r--tests/unit/test_utils_compatibility_tags.py93
-rw-r--r--tests/unit/test_utils_distutils_args.py45
-rw-r--r--tests/unit/test_utils_filesystem.py51
-rw-r--r--tests/unit/test_utils_parallel.py72
-rw-r--r--tests/unit/test_utils_pkg_resources.py51
-rw-r--r--tests/unit/test_utils_subprocess.py377
-rw-r--r--tests/unit/test_utils_temp_dir.py148
-rw-r--r--tests/unit/test_utils_unpacking.py143
-rw-r--r--tests/unit/test_utils_virtualenv.py106
-rw-r--r--tests/unit/test_utils_wheel.py56
-rw-r--r--tests/unit/test_vcs.py843
-rw-r--r--tests/unit/test_vcs_mercurial.py19
-rw-r--r--tests/unit/test_wheel.py489
-rw-r--r--tests/unit/test_wheel_builder.py148
-rw-r--r--tools/news/template.rst76
-rw-r--r--tools/protected_pip.py (renamed from tools/tox_pip.py)7
-rw-r--r--tools/release/__init__.py29
-rw-r--r--tools/vendoring/patches/appdirs.patch115
-rw-r--r--tools/vendoring/patches/certifi.patch10
-rw-r--r--tools/vendoring/patches/pkg_resources.patch22
-rw-r--r--tools/vendoring/patches/pygments.patch37
-rw-r--r--tools/vendoring/patches/requests.patch98
-rw-r--r--tools/vendoring/patches/urllib3-disable-brotli.patch39
-rw-r--r--tools/vendoring/patches/urllib3.patch29
-rw-r--r--tox.ini81
782 files changed, 91494 insertions, 53535 deletions
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
deleted file mode 100644
index 508153d8d..000000000
--- a/.github/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,3 +0,0 @@
-* pip version:
-* Python version:
-* Operating system:
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index a8f9ec02e..e28e54082 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -3,6 +3,17 @@ description: Something is not working correctly.
labels: "S: needs triage, type: bug"
body:
+ - type: markdown
+ attributes:
+ value: >-
+ Hi there!
+
+ We'd appreciate it if you could search on pip's existing issues prior to filing
+ a bug report.
+
+ We get a lot of duplicate tickets and have limited maintainer capacity to triage
+ them. Thanks!
+
- type: textarea
attributes:
label: Description
@@ -37,7 +48,7 @@ body:
attributes:
label: How to Reproduce
description: Please provide steps to reproduce this bug.
- value: |
+ placeholder: |
1. Get package from '...'
2. Then run '...'
3. An error occurs.
@@ -50,7 +61,11 @@ body:
description: >-
Provide the output of the steps above, including the commands
themselves and pip's output/traceback etc.
- render: sh-session
+
+ If you want to present output from multiple commands, please prefix
+ the line containing the command with `$ `. Please also ensure that
+ the "How to reproduce" section contains matching instructions for
+ reproducing this.
- type: checkboxes
attributes:
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 98a92d60e..416ae0700 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,11 +1,11 @@
-# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
-blank_issues_enabled: true # default
+# Documentation for this file can be found at:
+# https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository
+
+blank_issues_enabled: false
contact_links:
-- name: 💬 Discourse
- url: https://discuss.python.org/c/packaging
- about: |
- Please ask typical Q&A here: general ideas for Python packaging,
- questions about structuring projects and so on
-- name: '💬 IRC: #pypa'
- url: https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa
- about: Chat with devs
+ - name: "💬 IRC: #pypa"
+ url: https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa
+ about: Chat with devs
+ - name: "(maintainers only) Blank issue"
+ url: https://github.com/pypa/pip/issues/new
+ about: For maintainers only.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
deleted file mode 100644
index a0addbdb7..000000000
--- a/.github/ISSUE_TEMPLATE/feature-request.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-
----
-
-**What's the problem this feature will solve?**
-<!-- What are you trying to do, that you are unable to achieve with pip as it currently stands? -->
-
-**Describe the solution you'd like**
-<!-- Clear and concise description of what you want to happen. -->
-
-<!-- Provide examples of real world use cases that this would enable and how it solves the problem described above. -->
-
-**Alternative Solutions**
-<!-- Have you tried to workaround the problem using pip or other tools? Or a different approach to solving this issue? Please elaborate here. -->
-
-**Additional context**
-<!-- Add any other context, links, etc. about the feature here. -->
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
new file mode 100644
index 000000000..bc87da0a0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -0,0 +1,59 @@
+name: Feature request
+description: Suggest an idea for this project
+labels: "S: needs triage, type: feature request"
+
+body:
+ - type: markdown
+ attributes:
+ value: >-
+ Hi there!
+
+ We'd appreciate it if you could search on pip's existing issues prior to filing
+ a feature request.
+
+ We get a lot of duplicate tickets and have limited maintainer capacity to triage
+ them. Thanks!
+
+ - type: textarea
+ attributes:
+ label: What's the problem this feature will solve?
+ description: >-
+ What are you trying to do, that you are unable to achieve with pip as it
+ currently stands?
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Describe the solution you'd like
+ description: >-
+ Clear and concise description of what you want to happen. Please use examples
+ of real world use cases that this would help with, and how it solves the
+ problem described above.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Alternative Solutions
+ description: >-
+ Have you tried to workaround the problem using pip or other tools? Or a
+ different approach to solving this issue? Please elaborate here.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Additional context
+ description: >-
+ Add any other context, links, etc. relevant to the feature request.
+ validations:
+ required: true
+
+ - type: checkboxes
+ attributes:
+ label: Code of Conduct
+ options:
+ - label: >-
+ I agree to follow the [PSF Code of Conduct](https://www.python.org/psf/conduct/).
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/~good-first-issue.md b/.github/ISSUE_TEMPLATE/~good-first-issue.md
deleted file mode 100644
index 912201676..000000000
--- a/.github/ISSUE_TEMPLATE/~good-first-issue.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-name: (Maintainers Only) Good First Issue
-about: For maintainers, to create an issue that is good for new contributors
-labels: ["good first issue"]
-
----
-
-<!-- Write the issue below, provide clear instructions for resolution -->
-
-<!-- End of issue content. -->
-<!-- Leave the following intact -->
-
----
-
-**Good First Issue**: This issue is a good starting point for first time contributors -- the process of fixing this should be a good introduction to pip's development workflow. If you've already contributed to pip, work on [another issue without this label](https://github.com/pypa/pip/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+-label%3A%22good+first+issue%22) instead. If there is not a corresponding pull request for this issue, it is up for grabs. For directions for getting set up, see our [Getting Started Guide](https://pip.pypa.io/en/latest/development/getting-started/). If you are working on this issue and have questions, feel free to ask them here, [`#pypa-dev` on Libera.chat](https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa-dev), or the [distutils-sig mailing list](https://mail.python.org/mailman3/lists/distutils-sig.python.org/).
diff --git a/.github/lock.yml b/.github/lock.yml
deleted file mode 100644
index dd12e3da8..000000000
--- a/.github/lock.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-# Number of days of inactivity before a closed issue or pull request is locked
-daysUntilLock: 30
-# Issues and pull requests with these labels will not be locked.
-exemptLabels: []
-# Label to add before locking, such as `outdated`. Set to `false` to disable
-lockLabel: "S: auto-locked"
-# Comment to post before locking. Set to `false` to disable
-lockComment: false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 019eba65b..c43b7dabb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,26 @@ on:
schedule:
- cron: 0 0 * * MON # Run every Monday at 00:00 UTC
+env:
+ # The "FORCE_COLOR" variable, when set to 1,
+ # tells Nox to colorize itself.
+ FORCE_COLOR: "1"
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
+ cancel-in-progress: true
+
jobs:
+ docs:
+ name: docs
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ - run: pip install nox
+ - run: nox -s docs
+
determine-changes:
runs-on: ubuntu-latest
outputs:
@@ -27,11 +46,13 @@ jobs:
# Anything that's touching "vendored code"
- "src/pip/_vendor/**"
- "pyproject.toml"
+ - "noxfile.py"
tests:
- # Anything that's touching testable stuff
+ # Anything that's touching code-related stuff
- ".github/workflows/ci.yml"
- "src/**"
- "tests/**"
+ - "noxfile.py"
if: github.event_name == 'pull_request'
pre-commit:
@@ -74,8 +95,8 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- - run: pip install vendoring
- - run: vendoring sync . --verbose
+ - run: pip install nox
+ - run: nox -s vendoring
- run: git diff --exit-code
tests-unix:
@@ -92,10 +113,10 @@ jobs:
matrix:
os: [Ubuntu, MacOS]
python:
- - 3.6
- 3.7
- 3.8
- 3.9
+ - "3.10"
steps:
- uses: actions/checkout@v2
@@ -103,17 +124,25 @@ jobs:
with:
python-version: ${{ matrix.python }}
- - run: pip install tox 'virtualenv<20'
+ - name: Install Ubuntu dependencies
+ if: matrix.os == 'Ubuntu'
+ run: sudo apt-get install bzr
+
+ - name: Install MacOS dependencies
+ if: matrix.os == 'MacOS'
+ run: brew install bzr
+
+ - run: pip install nox 'virtualenv<20' 'setuptools != 60.6.0'
# Main check
- name: Run unit tests
run: >-
- tox -e py --
+ nox -s test-${{ matrix.python }} --
-m unit
--verbose --numprocesses auto --showlocals
- name: Run integration tests
run: >-
- tox -e py --
+ nox -s test-${{ matrix.python }} --
-m integration
--verbose --numprocesses auto --showlocals
--durations=5
@@ -132,11 +161,11 @@ jobs:
matrix:
os: [Windows]
python:
- - 3.6
+ - 3.7
# Commented out, since Windows tests are expensively slow.
- # - 3.7
# - 3.8
- - 3.9
+ # - 3.9
+ - "3.10"
group: [1, 2]
steps:
@@ -160,7 +189,7 @@ jobs:
$acl.AddAccessRule($rule)
Set-Acl "R:\Temp" $acl
- - run: pip install tox 'virtualenv<20'
+ - run: pip install nox 'virtualenv<20'
env:
TEMP: "R:\\Temp"
@@ -168,7 +197,7 @@ jobs:
- name: Run unit tests
if: matrix.group == 1
run: >-
- tox -e py --
+ nox -s test-${{ matrix.python }} --
-m unit
--verbose --numprocesses auto --showlocals
env:
@@ -177,7 +206,7 @@ jobs:
- name: Run integration tests (group 1)
if: matrix.group == 1
run: >-
- tox -e py --
+ nox -s test-${{ matrix.python }} --
-m integration -k "not test_install"
--verbose --numprocesses auto --showlocals
env:
@@ -186,8 +215,72 @@ jobs:
- name: Run integration tests (group 2)
if: matrix.group == 2
run: >-
- tox -e py --
+ nox -s test-${{ matrix.python }} --
-m integration -k "test_install"
--verbose --numprocesses auto --showlocals
env:
TEMP: "R:\\Temp"
+
+ tests-zipapp:
+ name: tests / zipapp
+ runs-on: ubuntu-latest
+
+ needs: [pre-commit, packaging, determine-changes]
+ if: >-
+ needs.determine-changes.outputs.tests == 'true' ||
+ github.event_name != 'pull_request'
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: "3.10"
+
+ - name: Install Ubuntu dependencies
+ run: sudo apt-get install bzr
+
+ - run: pip install nox 'virtualenv<20' 'setuptools != 60.6.0'
+
+ # Main check
+ - name: Run integration tests
+ run: >-
+ nox -s test-3.10 --
+ -m integration
+ --verbose --numprocesses auto --showlocals
+ --durations=5
+ --use-zipapp
+
+ # TODO: Remove this when we add Python 3.11 to CI.
+ tests-importlib-metadata:
+ name: tests for importlib.metadata backend
+ runs-on: ubuntu-latest
+ env:
+ _PIP_USE_IMPORTLIB_METADATA: 'true'
+
+ needs: [pre-commit, packaging, determine-changes]
+ if: >-
+ needs.determine-changes.outputs.tests == 'true' ||
+ github.event_name != 'pull_request'
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: '3.10'
+
+ - name: Install Ubuntu dependencies
+ run: sudo apt-get install bzr
+
+ - run: pip install nox 'virtualenv<20'
+
+ - name: Run unit tests
+ run: >-
+ nox -s test-3.10 --
+ -m unit
+ --verbose --numprocesses auto --showlocals
+ - name: Run integration tests
+ run: >-
+ nox -s test-3.10 --
+ -m integration
+ --verbose --numprocesses auto --showlocals
+ --durations=5
diff --git a/.github/workflows/label-merge-conflicts.yml b/.github/workflows/label-merge-conflicts.yml
new file mode 100644
index 000000000..1de897ca1
--- /dev/null
+++ b/.github/workflows/label-merge-conflicts.yml
@@ -0,0 +1,19 @@
+name: Autolabel merge conflicts
+
+permissions:
+ issues: write
+ pull-requests: write
+
+on:
+ push:
+ branches: [main]
+
+jobs:
+ label-merge-conflicts:
+ if: github.repository_owner == 'pypa'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: pradyunsg/auto-label-merge-conflicts@v3
+ with:
+ CONFLICT_LABEL_NAME: "needs rebase or merge"
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/lock-threads.yml b/.github/workflows/lock-threads.yml
new file mode 100644
index 000000000..990440dd6
--- /dev/null
+++ b/.github/workflows/lock-threads.yml
@@ -0,0 +1,23 @@
+name: 'Lock Closed Threads'
+
+on:
+ schedule:
+ - cron: '0 7 * * *' # 7am UTC, daily
+ workflow_dispatch:
+
+permissions:
+ issues: write
+ pull-requests: write
+
+concurrency:
+ group: lock
+
+jobs:
+ action:
+ if: github.repository_owner == 'pypa'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: dessant/lock-threads@v3
+ with:
+ issue-inactive-days: '30'
+ pr-inactive-days: '15'
diff --git a/.github/workflows/news-file.yml b/.github/workflows/news-file.yml
new file mode 100644
index 000000000..da7119a55
--- /dev/null
+++ b/.github/workflows/news-file.yml
@@ -0,0 +1,25 @@
+name: Check
+
+on:
+ pull_request:
+ types: [labeled, unlabeled, opened, reopened, synchronize]
+
+jobs:
+ check-news-entry:
+ name: news entry
+ runs-on: ubuntu-20.04
+
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ # `towncrier check` runs `git diff --name-only origin/main...`, which
+ # needs a non-shallow clone.
+ fetch-depth: 0
+
+ - name: Check news entry
+ if: "!contains(github.event.pull_request.labels.*.name, 'skip news')"
+ run: |
+ if ! pipx run towncrier check --compare-with origin/${{ github.base_ref }}; then
+ echo "Please see https://pip.pypa.io/dev/news-entry-failure for guidance."
+ false
+ fi
diff --git a/.mailmap b/.mailmap
index c8f94a9d8..d0c64300f 100644
--- a/.mailmap
+++ b/.mailmap
@@ -19,6 +19,7 @@ Dongweiming <dongweiming@admaster.com.cn> <ciici1234@hotmail.c
Dustin Ingram <di@di.codes> <di@users.noreply.github.com>
Endoh Takanao <djmchl@gmail.com>
Erik M. Bray <embray@stsci.edu>
+Ee Durbin <ewdurbin@gmail.com>
Gabriel de Perthuis <g2p.code@gmail.com>
Hsiaoming Yang <lepture@me.com>
Hugo van Kemenade <hugovk@users.noreply.github.com> Hugo <hugovk@users.noreply.github.com>
@@ -33,6 +34,7 @@ Ludovic Gasc <gmludo@gmail.com> <git@gmludo.eu>
Markus Hametner <fin+github@xbhd.org>
Masklinn <bitbucket.org@masklinn.net>
Matthew Iversen <teh.ivo@gmail.com> <teh.ivo@gmail.com>
+Ofek Lev <ofekmeister@gmail.com>
Pi Delport <pjdelport@gmail.com>
<pnasrat@gmail.com> <pnasrat@googlemail.com>
Pradyun Gedam <pradyunsg@gmail.com> <pradyunsg@users.noreply.github.com>
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a0580decc..6dde3d6ff 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: 'src/pip/_vendor/'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v3.4.0
+ rev: v4.3.0
hooks:
- id: check-builtin-literals
- id: check-added-large-files
@@ -17,51 +17,45 @@ repos:
exclude: .patch
- repo: https://github.com/psf/black
- rev: 20.8b1
+ rev: 22.6.0
hooks:
- id: black
- exclude: |
- (?x)
- ^src/pip/_internal/commands|
- ^src/pip/_internal/index|
- ^src/pip/_internal/models|
- ^src/pip/_internal/operations|
- ^src/pip/_internal/vcs|
- ^src/pip/_internal/\w+\.py$|
- # Tests
- ^tests/data|
- ^tests/unit|
- ^tests/functional/(?!test_install)|
- ^tests/functional/test_install|
- # A blank ignore, to avoid merge conflicts later.
- ^$
-- repo: https://gitlab.com/pycqa/flake8
- rev: 3.8.4
+- repo: https://github.com/PyCQA/flake8
+ rev: 4.0.1
hooks:
- id: flake8
additional_dependencies: [
- 'flake8-bugbear==20.1.4',
+ 'flake8-bugbear==22.3.23',
'flake8-logging-format==0.6.0',
+ 'flake8-implicit-str-concat==0.3.0',
]
exclude: tests/data
- repo: https://github.com/PyCQA/isort
- rev: 5.7.0
+ rev: 5.10.1
hooks:
- id: isort
files: \.py$
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v0.800
+ rev: v0.961
hooks:
- id: mypy
- exclude: tests
- args: ["--pretty"]
- additional_dependencies: ['nox==2020.12.31']
+ exclude: tests/data
+ args: ["--pretty", "--show-error-codes"]
+ additional_dependencies: [
+ 'keyring==23.0.1',
+ 'nox==2021.6.12',
+ 'pytest==7.1.1',
+ 'types-docutils==0.18.3',
+ 'types-setuptools==57.4.14',
+ 'types-freezegun==1.1.9',
+ 'types-six==1.16.15',
+ ]
- repo: https://github.com/pre-commit/pygrep-hooks
- rev: v1.7.0
+ rev: v1.9.0
hooks:
- id: python-no-log-warn
- id: python-no-eval
@@ -80,7 +74,7 @@ repos:
files: ^news/
- repo: https://github.com/mgedmin/check-manifest
- rev: '0.46'
+ rev: '0.48'
hooks:
- id: check-manifest
stages: [manual]
diff --git a/AUTHORS.txt b/AUTHORS.txt
index dff675925..3db1a3c73 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -18,10 +18,12 @@ Alan Yee
Albert Tugushev
Albert-Guan
albertg
+Alberto Sottile
Aleks Bunin
Alethea Flowers
Alex Gaynor
Alex Grönholm
+Alex Hedges
Alex Loosley
Alex Morega
Alex Stachowiak
@@ -37,6 +39,7 @@ Andre Aguiar
Andreas Lutro
Andrei Geacar
Andrew Gaul
+Andrew Shymanel
Andrey Bienkowski
Andrey Bulgakov
Andrés Delfino
@@ -76,6 +79,7 @@ Bastian Venthur
Ben Bodenmiller
Ben Darnell
Ben Hoyt
+Ben Mares
Ben Rosser
Bence Nagy
Benjamin Peterson
@@ -96,6 +100,7 @@ Bradley Ayers
Brandon L. Reiss
Brandt Bucher
Brett Randall
+Brett Rosen
Brian Cristante
Brian Rosner
briantracy
@@ -122,6 +127,7 @@ Chris Brinker
Chris Hunt
Chris Jerdonek
Chris McDonough
+Chris Pawley
Chris Wolfe
Christian Clauss
Christian Heimes
@@ -147,10 +153,13 @@ Cristina Muñoz
Curtis Doty
cytolentino
Daan De Meyer
+Damian
Damian Quiroga
+Damian Shaw
Dan Black
Dan Savilonis
Dan Sully
+Dane Hillard
daniel
Daniel Collins
Daniel Hahler
@@ -176,6 +185,7 @@ David Hewitt
David Linke
David Poggi
David Pursehouse
+David Runge
David Tucker
David Wales
Davidovich
@@ -191,6 +201,7 @@ DiegoCaraballo
Dimitri Merejkowsky
Dirk Stolle
Dmitry Gladkov
+Dmitry Volodin
Domen Kožar
Dominic Davis-Foster
Donald Stufft
@@ -200,6 +211,8 @@ DrFeathers
Dustin Ingram
Dwayne Bailey
Ed Morley
+Edgar Ramírez
+Ee Durbin
Eitan Adler
ekristina
elainechan
@@ -218,8 +231,6 @@ Eric Hanchrow
Eric Hopper
Erik M. Bray
Erik Rose
-Ernest W Durbin III
-Ernest W. Durbin III
Erwin Janssen
Eugene Vereshchagin
everdimension
@@ -227,6 +238,8 @@ Felix Yan
fiber-space
Filip Kokosiński
Filipe Laíns
+Finn Womack
+finnagin
Florian Briand
Florian Rathgeber
Francesco
@@ -235,16 +248,20 @@ Frost Ming
Gabriel Curio
Gabriel de Perthuis
Garry Polley
+gavin
gdanielson
Geoffrey Sneddon
George Song
Georgi Valkov
+Georgy Pchelkin
ghost
Giftlin Rajaiah
gizmoguy1
gkdoc
+Godefroid Chapelle
Gopinath M
GOTO Hayato
+gousaiyang
gpiks
Greg Roodt
Greg Ward
@@ -258,11 +275,13 @@ Hari Charan
Harsh Vardhan
harupy
Harutaka Kawamura
+Henrich Hartzer
Henry Schreiner
Herbert Pfennig
Hsiaoming Yang
Hugo Lopes Tavares
Hugo van Kemenade
+Hugues Bruant
Hynek Schlawack
Ian Bicking
Ian Cordasco
@@ -287,9 +306,11 @@ Jakub Wilk
James Cleveland
James Curtin
James Firth
+James Gerity
James Polley
Jan Pokorný
Jannis Leidel
+Jarek Potiuk
jarondl
Jason R. Coombs
Jay Graves
@@ -300,9 +321,11 @@ Jelmer Vernooij
jenix21
Jeremy Stanley
Jeremy Zafran
+Jesse Rittner
Jiashuo Li
Jim Fisher
Jim Garrison
+Jiun Bae
Jivan Amara
Joe Michelini
John Paton
@@ -329,7 +352,9 @@ Jussi Kukkonen
jwg4
Jyrki Pulliainen
Kai Chen
+Kai Mueller
Kamal Bin Mustafa
+kasium
kaustav haldar
keanemind
Keith Maxwell
@@ -354,6 +379,7 @@ Laurent Bristiel
Laurent LAPORTE
Laurie O
Laurie Opperman
+layday
Leon Sasson
Lev Givon
Lincoln de Sousa
@@ -361,6 +387,7 @@ Lipis
Loren Carvalho
Lucas Cimon
Ludovic Gasc
+Lukas Juhrich
Luke Macken
Luo Jiebin
luojiebin
@@ -373,6 +400,8 @@ Mariatta
Mark Kohler
Mark Williams
Markus Hametner
+Martey Dodoo
+Martin Fischer
Martin Häcker
Martin Pavlasek
Masaki
@@ -380,6 +409,8 @@ Masklinn
Matej Stuchlik
Mathew Jennings
Mathieu Bridon
+Mathieu Kniewallner
+Matt Bacchi
Matt Good
Matt Maker
Matt Robenolt
@@ -391,6 +422,7 @@ Matthew Trumbell
Matthew Willson
Matthias Bussonnier
mattip
+Maurits van Rees
Max W Chase
Maxim Kurnikov
Maxime Rouyrre
@@ -405,6 +437,7 @@ Michael E. Karpeles
Michael Klich
Michael Williamson
michaelpacer
+Michał Górny
Mickaël Schoentgen
Miguel Araujo Perez
Mihir Singh
@@ -416,7 +449,11 @@ Miro HronÄok
Monica Baluna
montefra
Monty Taylor
+Nadav Wexler
+Nahuel Ambrosini
Nate Coraor
+Nate Prewitt
+Nathan Houghton
Nathaniel J. Smith
Nehal J Wani
Neil Botelho
@@ -430,14 +467,16 @@ Nicole Harris
Nikhil Benesch
Nikita Chepanov
Nikolay Korolev
+Nipunn Koorapati
Nitesh Sharma
+Niyas Sait
Noah
Noah Gorny
Nowell Strite
NtaleGrey
nvdv
OBITORASU
-Ofekmeister
+Ofek Lev
ofrinevo
Oliver Jeeves
Oliver Mannion
@@ -463,9 +502,12 @@ Paul Nasrat
Paul Oswald
Paul van der Linden
Paulus Schoutsen
+Pavel Safronov
Pavithra Eswaramoorthy
Pawel Jasinski
+Paweł Szramowski
Pekka Klärck
+Peter Gessler
Peter Lisák
Peter Waller
petr-tik
@@ -490,6 +532,7 @@ Preet Thakkar
Preston Holmes
Przemek Wrzos
Pulkit Goyal
+q0w
Qiangning Hong
Quentin Lee
Quentin Pradet
@@ -514,15 +557,18 @@ Roey Berman
Rohan Jain
Roman Bogorodskiy
Romuald Brunet
+ronaudinho
Ronny Pfannschmidt
Rory McCann
Ross Brattain
Roy Wellington â…£
Ruairidh MacLeod
+Russell Keith-Magee
Ryan Wooden
ryneeverett
Sachi King
Salvatore Rinchiera
+sandeepkiran-js
Savio Jomton
schlamar
Scott Kitterman
@@ -533,8 +579,10 @@ Sebastian Schaetz
Segev Finer
SeongSoo Cho
Sergey Vasilyev
+Seth Michael Larson
Seth Woodworth
shireenrao
+Shivansh-007
Shlomi Fish
Shovan Maity
Simeon Visser
@@ -564,9 +612,11 @@ Sumana Harihareswara
Surbhi Sharma
Sviatoslav Sydorenko
Swat009
+Sylvain
Takayuki SHIMIZUKAWA
Taneli Hukkinen
tbeswick
+Thiago
Thijs Triemstra
Thomas Fenzl
Thomas Grainger
@@ -574,6 +624,7 @@ Thomas Guettler
Thomas Johansson
Thomas Kluyver
Thomas Smith
+Thomas VINCENT
Tim D. Smith
Tim Gates
Tim Harder
@@ -586,13 +637,16 @@ Tom V
Tomas Hrnciar
Tomas Orsava
Tomer Chachamu
+Tomáš HrnÄiar
Tony Beswick
+Tony Narlock
Tony Zhaocheng Tan
TonyBeswick
toonarmycaptain
Toshio Kuratomi
toxinu
Travis Swicegood
+Tushar Sadhwani
Tzu-ping Chung
Valentin Haenel
Victor Stinner
@@ -625,9 +679,12 @@ Yeray Diaz Diaz
Yoval P
Yu Jian
Yuan Jing Vincent Yan
+Yusuke Hayashi
Zearin
Zhiping Deng
+ziebam
Zvezdan Petkovic
Åukasz Langa
+Роман Донченко
Семён МарьÑÑин
‮rekcäH nitraM‮
diff --git a/LICENSE.txt b/LICENSE.txt
index 00addc272..8e7b65eaf 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2008-2021 The pip developers (see AUTHORS.txt file)
+Copyright (c) 2008-present The pip developers (see AUTHORS.txt file)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/MANIFEST.in b/MANIFEST.in
index 266064db6..ff3825f65 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -6,6 +6,7 @@ include pyproject.toml
include src/pip/_vendor/README.rst
include src/pip/_vendor/vendor.txt
+include src/pip/_vendor/pyparsing/diagram/template.jinja2
recursive-include src/pip/_vendor *LICENSE*
recursive-include src/pip/_vendor *COPYING*
@@ -15,7 +16,6 @@ include docs/requirements.txt
exclude .coveragerc
exclude .mailmap
exclude .appveyor.yml
-exclude .travis.yml
exclude .readthedocs.yml
exclude .pre-commit-config.yaml
exclude tox.ini
@@ -31,7 +31,6 @@ exclude src/pip/_vendor/six/moves
recursive-exclude src/pip/_vendor *.pyi
prune .github
-prune .azure-pipelines
prune docs/build
prune news
prune tasks
diff --git a/NEWS.rst b/NEWS.rst
index e6f86e71a..6485bc159 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,7 +1,461 @@
-21.2.2 (2021-07-31)
+.. note
+
+ You should *NOT* be adding new change log entries to this file, this
+ file is managed by towncrier. You *may* edit previous change logs to
+ fix problems like typo corrections or such.
+
+ To add a new change log entry, please see
+ https://pip.pypa.io/en/latest/development/contributing/#news-entries
+
+.. towncrier release notes start
+
+22.2.2 (2022-08-03)
+===================
+
+Bug Fixes
+---------
+
+- Avoid ``AttributeError`` when removing the setuptools-provided ``_distutils_hack`` and it is missing its implementation. (`#11314 <https://github.com/pypa/pip/issues/11314>`_)
+- Fix import error when reinstalling pip in user site. (`#11319 <https://github.com/pypa/pip/issues/11319>`_)
+- Show pip deprecation warnings by default. (`#11330 <https://github.com/pypa/pip/issues/11330>`_)
+
+
+22.2.1 (2022-07-27)
+===================
+
+Bug Fixes
+---------
+
+- Send the pip upgrade prompt to stderr. (`#11282 <https://github.com/pypa/pip/issues/11282>`_)
+- Ensure that things work correctly in environments where setuptools-injected
+ ``distutils`` is available by default. This is done by cooperating with
+ setuptools' injection logic to ensure that pip uses the ``distutils`` from the
+ Python standard library instead. (`#11298 <https://github.com/pypa/pip/issues/11298>`_)
+- Clarify that ``pip cache``'s wheels-related output is about locally built wheels only. (`#11300 <https://github.com/pypa/pip/issues/11300>`_)
+
+
+22.2 (2022-07-21)
+=================
+
+Deprecations and Removals
+-------------------------
+
+- Remove the ``html5lib`` deprecated feature flag. (`#10825 <https://github.com/pypa/pip/issues/10825>`_)
+- Remove ``--use-deprecated=backtrack-on-build-failures``. (`#11241 <https://github.com/pypa/pip/issues/11241>`_)
+
+Features
+--------
+
+- Add support to use `truststore <https://pypi.org/project/truststore/>`_ as an
+ alternative SSL certificate verification backend. The backend can be enabled on Python
+ 3.10 and later by installing ``truststore`` into the environment, and adding the
+ ``--use-feature=truststore`` flag to various pip commands.
+
+ ``truststore`` differs from the current default verification backend (provided by
+ ``certifi``) in it uses the operating system’s trust store, which can be better
+ controlled and augmented to better support non-standard certificates. Depending on
+ feedback, pip may switch to this as the default certificate verification backend in
+ the future. (`#11082 <https://github.com/pypa/pip/issues/11082>`_)
+- Add ``--dry-run`` option to ``pip install``, to let it print what it would install but
+ not actually change anything in the target environment. (`#11096 <https://github.com/pypa/pip/issues/11096>`_)
+- Record in wheel cache entries the URL of the original artifact that was downloaded
+ to build the cached wheels. The record is named ``origin.json`` and uses the PEP 610
+ Direct URL format. (`#11137 <https://github.com/pypa/pip/issues/11137>`_)
+- Support `PEP 691 <https://peps.python.org/pep-0691/>`_. (`#11158 <https://github.com/pypa/pip/issues/11158>`_)
+- pip's deprecation warnings now subclass the built-in ``DeprecationWarning``, and
+ can be suppressed by running the Python interpreter with
+ ``-W ignore::DeprecationWarning``. (`#11225 <https://github.com/pypa/pip/issues/11225>`_)
+- Add ``pip inspect`` command to obtain the list of installed distributions and other
+ information about the Python environment, in JSON format. (`#11245 <https://github.com/pypa/pip/issues/11245>`_)
+- Significantly speed up isolated environment creation, by using the same
+ sources for pip instead of creating a standalone installation for each
+ environment. (`#11257 <https://github.com/pypa/pip/issues/11257>`_)
+- Add an experimental ``--report`` option to the install command to generate a JSON report
+ of what was installed. In combination with ``--dry-run`` and ``--ignore-installed`` it
+ can be used to resolve the requirements. (`#53 <https://github.com/pypa/pip/issues/53>`_)
+
+Bug Fixes
+---------
+
+- Fix ``pip install --pre`` for packages with pre-release build dependencies defined
+ both in ``pyproject.toml``'s ``build-system.requires`` and ``setup.py``'s
+ ``setup_requires``. (`#10222 <https://github.com/pypa/pip/issues/10222>`_)
+- When pip rewrites the shebang line in a script during wheel installation,
+ update the hash and size in the corresponding ``RECORD`` file entry. (`#10744 <https://github.com/pypa/pip/issues/10744>`_)
+- Do not consider a ``.dist-info`` directory found inside a wheel-like zip file
+ as metadata for an installed distribution. A package in a wheel is (by
+ definition) not installed, and is not guaranteed to work due to how a wheel is
+ structured. (`#11217 <https://github.com/pypa/pip/issues/11217>`_)
+- Use ``importlib.resources`` to read the ``vendor.txt`` file in ``pip debug``.
+ This makes the command safe for use from a zipapp. (`#11248 <https://github.com/pypa/pip/issues/11248>`_)
+- Make the ``--use-pep517`` option of the ``download`` command apply not just
+ to the requirements specified on the command line, but to their dependencies,
+ as well. (`#9523 <https://github.com/pypa/pip/issues/9523>`_)
+
+Process
+-------
+
+- Remove reliance on the stdlib cgi module, which is deprecated in Python 3.11.
+
+Vendored Libraries
+------------------
+
+- Remove html5lib.
+- Upgrade certifi to 2022.6.15
+- Upgrade chardet to 5.0.0
+- Upgrade colorama to 0.4.5
+- Upgrade distlib to 0.3.5
+- Upgrade msgpack to 1.0.4
+- Upgrade pygments to 2.12.0
+- Upgrade pyparsing to 3.0.9
+- Upgrade requests to 2.28.1
+- Upgrade rich to 12.5.1
+- Upgrade typing_extensions to 4.3.0
+- Upgrade urllib3 to 1.26.10
+
+
+22.1.2 (2022-05-31)
+===================
+
+Bug Fixes
+---------
+
+- Revert `#10979 <https://github.com/pypa/pip/issues/10979>`_ since it introduced a regression in certain edge cases. (`#10979 <https://github.com/pypa/pip/issues/10979>`_)
+- Fix an incorrect assertion in the logging logic, that prevented the upgrade prompt from being presented. (`#11136 <https://github.com/pypa/pip/issues/11136>`_)
+
+
+22.1.1 (2022-05-20)
+===================
+
+Bug Fixes
+---------
+
+- Properly filter out optional dependencies (i.e. extras) when checking build environment distributions. (`#11112 <https://github.com/pypa/pip/issues/11112>`_)
+- Change the build environment dependency checking to be opt-in. (`#11116 <https://github.com/pypa/pip/issues/11116>`_)
+- Allow using a pre-release version to satisfy a build requirement. This helps
+ manually populated build environments to more accurately detect build-time
+ requirement conflicts. (`#11123 <https://github.com/pypa/pip/issues/11123>`_)
+
+
+22.1 (2022-05-11)
+=================
+
+Process
+-------
+
+- Enable the ``importlib.metadata`` metadata implementation by default on
+ Python 3.11 (or later). The environment variable ``_PIP_USE_IMPORTLIB_METADATA``
+ can still be used to enable the implementation on 3.10 and earlier, or disable
+ it on 3.11 (by setting it to ``0`` or ``false``).
+
+Bug Fixes
+---------
+
+- Revert `#9243 <https://github.com/pypa/pip/issues/9243>`_ since it introduced a regression in certain edge cases. (`#10962 <https://github.com/pypa/pip/issues/10962>`_)
+- Fix missing ``REQUESTED`` metadata when using URL constraints. (`#11079 <https://github.com/pypa/pip/issues/11079>`_)
+- ``pip config`` now normalizes names by converting underscores into dashes. (`#9330 <https://github.com/pypa/pip/issues/9330>`_)
+
+
+22.1b1 (2022-04-30)
+===================
+
+Process
+-------
+
+- Start migration of distribution metadata implementation from ``pkg_resources``
+ to ``importlib.metadata``. The new implementation is currently not exposed in
+ any user-facing way, but included in the code base for easier development.
+
+Deprecations and Removals
+-------------------------
+
+- Drop ``--use-deprecated=out-of-tree-build``, according to deprecation message. (`#11001 <https://github.com/pypa/pip/issues/11001>`_)
+
+Features
+--------
+
+- Add option to install and uninstall commands to opt-out from running-as-root warning. (`#10556 <https://github.com/pypa/pip/issues/10556>`_)
+- Include Project-URLs in ``pip show`` output. (`#10799 <https://github.com/pypa/pip/issues/10799>`_)
+- Improve error message when ``pip config edit`` is provided an editor that
+ doesn't exist. (`#10812 <https://github.com/pypa/pip/issues/10812>`_)
+- Add a user interface for supplying config settings to build backends. (`#11059 <https://github.com/pypa/pip/issues/11059>`_)
+- Add support for Powershell autocompletion. (`#9024 <https://github.com/pypa/pip/issues/9024>`_)
+- Explains why specified version cannot be retrieved when *Requires-Python* is not satisfied. (`#9615 <https://github.com/pypa/pip/issues/9615>`_)
+- Validate build dependencies when using ``--no-build-isolation``. (`#9794 <https://github.com/pypa/pip/issues/9794>`_)
+
+Bug Fixes
+---------
+
+- Fix conditional checks to prevent ``pip.exe`` from trying to modify itself, on Windows. (`#10560 <https://github.com/pypa/pip/issues/10560>`_)
+- Fix uninstall editable from Windows junction link. (`#10696 <https://github.com/pypa/pip/issues/10696>`_)
+- Fallback to pyproject.toml-based builds if ``setup.py`` is present in a project, but ``setuptools`` cannot be imported. (`#10717 <https://github.com/pypa/pip/issues/10717>`_)
+- When checking for conflicts in the build environment, correctly skip requirements
+ containing markers that do not match the current environment. (`#10883 <https://github.com/pypa/pip/issues/10883>`_)
+- Disable brotli import in vendored urllib3 so brotli could be uninstalled/upgraded by pip. (`#10950 <https://github.com/pypa/pip/issues/10950>`_)
+- Prioritize URL credentials over netrc. (`#10979 <https://github.com/pypa/pip/issues/10979>`_)
+- Filter available distributions using hash declarations from constraints files. (`#9243 <https://github.com/pypa/pip/issues/9243>`_)
+- Fix an error when trying to uninstall packages installed as editable from a network drive. (`#9452 <https://github.com/pypa/pip/issues/9452>`_)
+- Fix pip install issues using a proxy due to an inconsistency in how Requests is currently handling variable precedence in session. (`#9691 <https://github.com/pypa/pip/issues/9691>`_)
+
+Vendored Libraries
+------------------
+
+- Upgrade CacheControl to 0.12.11
+- Upgrade distro to 1.7.0
+- Upgrade platformdirs to 2.5.2
+- Remove ``progress`` from vendored dependencies.
+- Upgrade ``pyparsing`` to 3.0.8 for startup performance improvements.
+- Upgrade rich to 12.2.0
+- Upgrade tomli to 2.0.1
+- Upgrade typing_extensions to 4.2.0
+
+Improved Documentation
+----------------------
+
+- Add more dedicated topic and reference pages to the documentation. (`#10899 <https://github.com/pypa/pip/issues/10899>`_)
+- Capitalise Y as the default for "Proceed (y/n)?" when uninstalling. (`#10936 <https://github.com/pypa/pip/issues/10936>`_)
+- Add ``scheme://`` requirement to ``--proxy`` option's description (`#10951 <https://github.com/pypa/pip/issues/10951>`_)
+- The wheel command now references the build interface section instead of stating the legacy
+ setuptools behavior as the default. (`#10972 <https://github.com/pypa/pip/issues/10972>`_)
+- Improved usefulness of ``pip config --help`` output. (`#11074 <https://github.com/pypa/pip/issues/11074>`_)
+
+
+22.0.4 (2022-03-06)
+===================
+
+Deprecations and Removals
+-------------------------
+
+- Drop the doctype check, that presented a warning for index pages that use non-compliant HTML 5. (`#10903 <https://github.com/pypa/pip/issues/10903>`_)
+
+Vendored Libraries
+------------------
+
+- Downgrade distlib to 0.3.3.
+
+
+22.0.3 (2022-02-03)
+===================
+
+Features
+--------
+
+- Print the exception via ``rich.traceback``, when running with ``--debug``. (`#10791 <https://github.com/pypa/pip/issues/10791>`_)
+
+Bug Fixes
+---------
+
+- Only calculate topological installation order, for packages that are going to be installed/upgraded.
+
+ This fixes an `AssertionError` that occurred when determining installation order, for a very specific combination of upgrading-already-installed-package + change of dependencies + fetching some packages from a package index. This combination was especially common in Read the Docs' builds. (`#10851 <https://github.com/pypa/pip/issues/10851>`_)
+- Use ``html.parser`` by default, instead of falling back to ``html5lib`` when ``--use-deprecated=html5lib`` is not passed. (`#10869 <https://github.com/pypa/pip/issues/10869>`_)
+
+Improved Documentation
+----------------------
+
+- Clarify that using per-requirement overrides disables the usage of wheels. (`#9674 <https://github.com/pypa/pip/issues/9674>`_)
+
+
+22.0.2 (2022-01-30)
+===================
+
+Deprecations and Removals
+-------------------------
+
+- Instead of failing on index pages that use non-compliant HTML 5, print a deprecation warning and fall back to ``html5lib``-based parsing for now. This simplifies the migration for non-compliant index pages, by letting such indexes function with a warning. (`#10847 <https://github.com/pypa/pip/issues/10847>`_)
+
+
+22.0.1 (2022-01-30)
+===================
+
+Bug Fixes
+---------
+
+- Accept lowercase ``<!doctype html>`` on index pages. (`#10844 <https://github.com/pypa/pip/issues/10844>`_)
+- Properly handle links parsed by html5lib, when using ``--use-deprecated=html5lib``. (`#10846 <https://github.com/pypa/pip/issues/10846>`_)
+
+
+22.0 (2022-01-29)
+=================
+
+Process
+-------
+
+- Completely replace :pypi:`tox` in our development workflow, with :pypi:`nox`.
+
+Deprecations and Removals
+-------------------------
+
+- Deprecate alternative progress bar styles, leaving only ``on`` and ``off`` as available choices. (`#10462 <https://github.com/pypa/pip/issues/10462>`_)
+- Drop support for Python 3.6. (`#10641 <https://github.com/pypa/pip/issues/10641>`_)
+- Disable location mismatch warnings on Python versions prior to 3.10.
+
+ These warnings were helping identify potential issues as part of the sysconfig -> distutils transition, and we no longer need to rely on reports from older Python versions for information on the transition. (`#10840 <https://github.com/pypa/pip/issues/10840>`_)
+
+Features
+--------
+
+- Changed ``PackageFinder`` to parse HTML documents using the stdlib :class:`html.parser.HTMLParser` class instead of the ``html5lib`` package.
+
+ For now, the deprecated ``html5lib`` code remains and can be used with the ``--use-deprecated=html5lib`` command line option. However, it will be removed in a future pip release. (`#10291 <https://github.com/pypa/pip/issues/10291>`_)
+- Utilise ``rich`` for presenting pip's default download progress bar. (`#10462 <https://github.com/pypa/pip/issues/10462>`_)
+- Present a better error message when an invalid wheel file is encountered, providing more context where the invalid wheel file is. (`#10535 <https://github.com/pypa/pip/issues/10535>`_)
+- Documents the ``--require-virtualenv`` flag for ``pip install``. (`#10588 <https://github.com/pypa/pip/issues/10588>`_)
+- ``pip install <tab>`` autocompletes paths. (`#10646 <https://github.com/pypa/pip/issues/10646>`_)
+- Allow Python distributors to opt-out from or opt-in to the ``sysconfig`` installation scheme backend by setting ``sysconfig._PIP_USE_SYSCONFIG`` to ``True`` or ``False``. (`#10647 <https://github.com/pypa/pip/issues/10647>`_)
+- Make it possible to deselect tests requiring cryptography package on systems where it cannot be installed. (`#10686 <https://github.com/pypa/pip/issues/10686>`_)
+- Start using Rich for presenting error messages in a consistent format. (`#10703 <https://github.com/pypa/pip/issues/10703>`_)
+- Improve presentation of errors from subprocesses. (`#10705 <https://github.com/pypa/pip/issues/10705>`_)
+- Forward pip's verbosity configuration to VCS tools to control their output accordingly. (`#8819 <https://github.com/pypa/pip/issues/8819>`_)
+
+Bug Fixes
+---------
+
+- Optimize installation order calculation to improve performance when installing requirements that form a complex dependency graph with a large amount of edges. (`#10557 <https://github.com/pypa/pip/issues/10557>`_)
+- When a package is requested by the user for upgrade, correctly identify that the extra-ed variant of that same package depended by another user-requested package is requesting the same package, and upgrade it accordingly. (`#10613 <https://github.com/pypa/pip/issues/10613>`_)
+- Prevent pip from installing yanked releases unless explicitly pinned via the ``==`` or ``===`` operators. (`#10617 <https://github.com/pypa/pip/issues/10617>`_)
+- Stop backtracking on build failures, by instead surfacing them to the user and aborting immediately. This behaviour provides more immediate feedback when a package cannot be built due to missing build dependencies or platform incompatibility. (`#10655 <https://github.com/pypa/pip/issues/10655>`_)
+- Silence ``Value for <location> does not match`` warning caused by an erroneous patch in Slackware-distributed Python 3.9. (`#10668 <https://github.com/pypa/pip/issues/10668>`_)
+- Fix an issue where pip did not consider dependencies with and without extras to be equal (`#9644 <https://github.com/pypa/pip/issues/9644>`_)
+
+Vendored Libraries
+------------------
+
+- Upgrade CacheControl to 0.12.10
+- Upgrade certifi to 2021.10.8
+- Upgrade distlib to 0.3.4
+- Upgrade idna to 3.3
+- Upgrade msgpack to 1.0.3
+- Upgrade packaging to 21.3
+- Upgrade platformdirs to 2.4.1
+- Add pygments 2.11.2 as a vendored dependency.
+- Tree-trim unused portions of vendored pygments, to reduce the distribution size.
+- Upgrade pyparsing to 3.0.7
+- Upgrade Requests to 2.27.1
+- Upgrade resolvelib to 0.8.1
+- Add rich 11.0.0 as a vendored dependency.
+- Tree-trim unused portions of vendored rich, to reduce the distribution size.
+- Add typing_extensions 4.0.1 as a vendored dependency.
+- Upgrade urllib3 to 1.26.8
+
+
+21.3.1 (2021-10-22)
+===================
+
+
+Bug Fixes
+---------
+
+
+- Always refuse installing or building projects that have no ``pyproject.toml`` nor
+ ``setup.py``. (`#10531 <https://github.com/pypa/pip/issues/10531>`_)
+- Tweak running-as-root detection, to check ``os.getuid`` if it exists, on Unix-y and non-Linux/non-MacOS machines. (`#10565 <https://github.com/pypa/pip/issues/10565>`_)
+- When installing projects with a ``pyproject.toml`` in editable mode, and the build
+ backend does not support :pep:`660`, prepare metadata using
+ ``prepare_metadata_for_build_wheel`` instead of ``setup.py egg_info``. Also, refuse
+ installing projects that only have a ``setup.cfg`` and no ``setup.py`` nor
+ ``pyproject.toml``. These restore the pre-21.3 behaviour. (`#10573 <https://github.com/pypa/pip/issues/10573>`_)
+- Restore compatibility of where configuration files are loaded from on MacOS (back to ``Library/Application Support/pip``, instead of ``Preferences/pip``). (`#10585 <https://github.com/pypa/pip/issues/10585>`_)
+
+Vendored Libraries
+------------------
+
+
+- Upgrade pep517 to 0.12.0
+
+
+21.3 (2021-10-11)
+=================
+
+Deprecations and Removals
+-------------------------
+
+- Improve deprecation warning regarding the copying of source trees when installing from a local directory. (`#10128 <https://github.com/pypa/pip/issues/10128>`_)
+- Suppress location mismatch warnings when pip is invoked from a Python source
+ tree, so ``ensurepip`` does not emit warnings on CPython ``make install``. (`#10270 <https://github.com/pypa/pip/issues/10270>`_)
+- On Python 3.10 or later, the installation scheme backend has been changed to use
+ ``sysconfig``. This is to anticipate the deprecation of ``distutils`` in Python
+ 3.10, and its scheduled removal in 3.12. For compatibility considerations, pip
+ installations running on Python 3.9 or lower will continue to use ``distutils``. (`#10358 <https://github.com/pypa/pip/issues/10358>`_)
+- Remove the ``--build-dir`` option and aliases, one last time. (`#10485 <https://github.com/pypa/pip/issues/10485>`_)
+- In-tree builds are now the default. ``--use-feature=in-tree-build`` is now
+ ignored. ``--use-deprecated=out-of-tree-build`` may be used temporarily to ease
+ the transition. (`#10495 <https://github.com/pypa/pip/issues/10495>`_)
+- Un-deprecate source distribution re-installation behaviour. (`#8711 <https://github.com/pypa/pip/issues/8711>`_)
+
+Features
+--------
+
+- Replace vendored appdirs with platformdirs. (`#10202 <https://github.com/pypa/pip/issues/10202>`_)
+- Support `PEP 610 <https://www.python.org/dev/peps/pep-0610/>`_ to detect
+ editable installs in ``pip freeze`` and ``pip list``. The ``pip list`` column output
+ has a new ``Editable project location`` column, and the JSON output has a new
+ ``editable_project_location`` field. (`#10249 <https://github.com/pypa/pip/issues/10249>`_)
+- ``pip freeze`` will now always fallback to reporting the editable project
+ location when it encounters a VCS error while analyzing an editable
+ requirement. Before, it sometimes reported the requirement as non-editable. (`#10410 <https://github.com/pypa/pip/issues/10410>`_)
+- ``pip show`` now sorts ``Requires`` and ``Required-By`` alphabetically. (`#10422 <https://github.com/pypa/pip/issues/10422>`_)
+- Do not raise error when there are no files to remove with ``pip cache purge/remove``.
+ Instead log a warning and continue (to log that we removed 0 files). (`#10459 <https://github.com/pypa/pip/issues/10459>`_)
+- When backtracking during dependency resolution, prefer the dependencies which are involved in the most recent conflict. This can significantly reduce the amount of backtracking required. (`#10479 <https://github.com/pypa/pip/issues/10479>`_)
+- Cache requirement objects, to improve performance reducing reparses of requirement strings. (`#10550 <https://github.com/pypa/pip/issues/10550>`_)
+- Support editable installs for projects that have a ``pyproject.toml`` and use a
+ build backend that supports :pep:`660`. (`#8212 <https://github.com/pypa/pip/issues/8212>`_)
+- When a revision is specified in a Git URL, use git's partial clone feature to speed up source retrieval. (`#9086 <https://github.com/pypa/pip/issues/9086>`_)
+- Add a ``--debug`` flag, to enable a mode that doesn't log errors and propagates them to the top level instead. This is primarily to aid with debugging pip's crashes. (`#9349 <https://github.com/pypa/pip/issues/9349>`_)
+- If a host is explicitly specified as trusted by the user (via the --trusted-host option), cache HTTP responses from it in addition to HTTPS ones. (`#9498 <https://github.com/pypa/pip/issues/9498>`_)
+
+Bug Fixes
+---------
+
+- Present a better error message, when a ``file:`` URL is not found. (`#10263 <https://github.com/pypa/pip/issues/10263>`_)
+- Fix the auth credential cache to allow for the case in which
+ the index url contains the username, but the password comes
+ from an external source, such as keyring. (`#10269 <https://github.com/pypa/pip/issues/10269>`_)
+- Fix double unescape of HTML ``data-requires-python`` and ``data-yanked`` attributes. (`#10378 <https://github.com/pypa/pip/issues/10378>`_)
+- New resolver: Fixes depth ordering of packages during resolution, e.g. a dependency 2 levels deep will be ordered before a dependency 3 levels deep. (`#10482 <https://github.com/pypa/pip/issues/10482>`_)
+- Correctly indent metadata preparation messages in pip output. (`#10524 <https://github.com/pypa/pip/issues/10524>`_)
+
+Vendored Libraries
+------------------
+
+- Remove appdirs as a vendored dependency.
+- Upgrade distlib to 0.3.3
+- Upgrade distro to 1.6.0
+- Patch pkg_resources to use platformdirs rather than appdirs.
+- Add platformdirs as a vendored dependency.
+- Upgrade progress to 1.6
+- Upgrade resolvelib to 0.8.0
+- Upgrade urllib3 to 1.26.7
+
+Improved Documentation
+----------------------
+
+- Update links of setuptools as setuptools moved these documents. The Simple Repository link now points to PyPUG as that is the canonical place of packaging specification, and setuptools's ``easy_install`` is deprecated. (`#10430 <https://github.com/pypa/pip/issues/10430>`_)
+- Create a "Build System Interface" reference section, for documenting how pip interacts with build systems. (`#10497 <https://github.com/pypa/pip/issues/10497>`_)
+
+
+21.2.4 (2021-08-12)
===================
+Bug Fixes
+---------
+
+- Fix 3.6.0 compatibility in link comparison logic. (`#10280 <https://github.com/pypa/pip/issues/10280>`_)
+
+
+21.2.3 (2021-08-06)
+===================
+
+Bug Fixes
+---------
+- Modify the ``sysconfig.get_preferred_scheme`` function check to be
+ compatible with CPython 3.10’s alphareleases. (`#10252 <https://github.com/pypa/pip/issues/10252>`_)
+
+
+21.2.2 (2021-07-31)
+===================
Bug Fixes
---------
@@ -14,8 +468,6 @@ Bug Fixes
21.2.1 (2021-07-25)
===================
-
-
Process
-------
@@ -25,8 +477,6 @@ Process
21.2 (2021-07-24)
=================
-
-
Process
-------
@@ -122,17 +572,6 @@ Vendored Libraries
- Upgrade urllib3 to 1.26.6.
-.. note
-
- You should *NOT* be adding new change log entries to this file, this
- file is managed by towncrier. You *may* edit previous change logs to
- fix problems like typo corrections or such.
-
- To add a new change log entry, please see
- https://pip.pypa.io/en/latest/development/contributing/#news-entries
-
-.. towncrier release notes start
-
21.1.3 (2021-06-26)
===================
diff --git a/docs/html/cli/index.md b/docs/html/cli/index.md
index a3497c308..4b56dbeb4 100644
--- a/docs/html/cli/index.md
+++ b/docs/html/cli/index.md
@@ -16,6 +16,7 @@ pip
pip_install
pip_uninstall
+pip_inspect
pip_list
pip_show
pip_freeze
diff --git a/docs/html/cli/pip.rst b/docs/html/cli/pip.rst
index 1f52630f6..88ef33e11 100644
--- a/docs/html/cli/pip.rst
+++ b/docs/html/cli/pip.rst
@@ -47,7 +47,7 @@ verbosity log will be kept. This option is empty by default. This log appends
to previous logging.
Like all pip options, ``--log`` can also be set as an environment variable, or
-placed into the pip config file. See the :ref:`Configuration` section.
+placed into the pip config file. See the :doc:`../topics/configuration` section.
.. _`exists-action`:
@@ -71,181 +71,13 @@ when decision is needed.
Rename the file or checkout to ``{name}{'.bak' * n}``, where n is some number
of ``.bak`` extensions, such that the file didn't exist at some point.
So the most recent backup will be the one with the largest number after ``.bak``.
-*(a)abort*
+*(a)bort*
Abort pip and return non-zero exit status.
-.. _`build-interface`:
-
-
-Build System Interface
-======================
-
-pip builds packages by invoking the build system. By default, builds will use
-``setuptools``, but if a project specifies a different build system using a
-``pyproject.toml`` file, as per :pep:`517`, pip will use that instead. As well
-as package building, the build system is also invoked to install packages
-direct from source. This is handled by invoking the build system to build a
-wheel, and then installing from that wheel. The built wheel is cached locally
-by pip to avoid repeated identical builds.
-
-The current interface to the build system is via the ``setup.py`` command line
-script - all build actions are defined in terms of the specific ``setup.py``
-command line that will be run to invoke the required action.
-
-Setuptools Injection
-~~~~~~~~~~~~~~~~~~~~
-
-When :pep:`517` is not used, the supported build system is ``setuptools``.
-However, not all packages use ``setuptools`` in their build scripts. To support
-projects that use "pure ``distutils``", pip injects ``setuptools`` into
-``sys.modules`` before invoking ``setup.py``. The injection should be
-transparent to ``distutils``-based projects, but 3rd party build tools wishing
-to provide a ``setup.py`` emulating the commands pip requires may need to be
-aware that it takes place.
-
-Projects using :pep:`517` *must* explicitly use setuptools - pip does not do
-the above injection process in this case.
-
-Build System Output
-~~~~~~~~~~~~~~~~~~~
-
-Any output produced by the build system will be read by pip (for display to the
-user if requested). In order to correctly read the build system output, pip
-requires that the output is written in a well-defined encoding, specifically
-the encoding the user has configured for text output (which can be obtained in
-Python using ``locale.getpreferredencoding``). If the configured encoding is
-ASCII, pip assumes UTF-8 (to account for the behaviour of some Unix systems).
-
-Build systems should ensure that any tools they invoke (compilers, etc) produce
-output in the correct encoding. In practice - and in particular on Windows,
-where tools are inconsistent in their use of the "OEM" and "ANSI" codepages -
-this may not always be possible. pip will therefore attempt to recover cleanly
-if presented with incorrectly encoded build tool output, by translating
-unexpected byte sequences to Python-style hexadecimal escape sequences
-(``"\x80\xff"``, etc). However, it is still possible for output to be displayed
-using an incorrect encoding (mojibake).
-
-Under :pep:`517`, handling of build tool output is the backend's responsibility,
-and pip simply displays the output produced by the backend. (Backends, however,
-will likely still have to address the issues described above).
-
-PEP 517 and 518 Support
-~~~~~~~~~~~~~~~~~~~~~~~
-
-As of version 10.0, pip supports projects declaring dependencies that are
-required at install time using a ``pyproject.toml`` file, in the form described
-in :pep:`518`. When building a project, pip will install the required
-dependencies locally, and make them available to the build process.
-Furthermore, from version 19.0 onwards, pip supports projects specifying the
-build backend they use in ``pyproject.toml``, in the form described in
-:pep:`517`.
-
-When making build requirements available, pip does so in an *isolated
-environment*. That is, pip does not install those requirements into the user's
-``site-packages``, but rather installs them in a temporary directory which it
-adds to the user's ``sys.path`` for the duration of the build. This ensures
-that build requirements are handled independently of the user's runtime
-environment. For example, a project that needs a recent version of setuptools
-to build can still be installed, even if the user has an older version
-installed (and without silently replacing that version).
-
-In certain cases, projects (or redistributors) may have workflows that
-explicitly manage the build environment. For such workflows, build isolation
-can be problematic. If this is the case, pip provides a
-``--no-build-isolation`` flag to disable build isolation. Users supplying this
-flag are responsible for ensuring the build environment is managed
-appropriately (including ensuring that all required build dependencies are
-installed).
-
-By default, pip will continue to use the legacy (direct ``setup.py`` execution
-based) build processing for projects that do not have a ``pyproject.toml`` file.
-Projects with a ``pyproject.toml`` file will use a :pep:`517` backend. Projects
-with a ``pyproject.toml`` file, but which don't have a ``build-system`` section,
-will be assumed to have the following backend settings::
-
- [build-system]
- requires = ["setuptools>=40.8.0", "wheel"]
- build-backend = "setuptools.build_meta:__legacy__"
-
-.. note::
-
- ``setuptools`` 40.8.0 is the first version of setuptools that offers a
- :pep:`517` backend that closely mimics directly executing ``setup.py``.
-
-If a project has ``[build-system]``, but no ``build-backend``, pip will also use
-``setuptools.build_meta:__legacy__``, but will expect the project requirements
-to include ``setuptools`` and ``wheel`` (and will report an error if the
-installed version of ``setuptools`` is not recent enough).
-
-If a user wants to explicitly request :pep:`517` handling even though a project
-doesn't have a ``pyproject.toml`` file, this can be done using the
-``--use-pep517`` command line option. Similarly, to request legacy processing
-even though ``pyproject.toml`` is present, the ``--no-use-pep517`` option is
-available (although obviously it is an error to choose ``--no-use-pep517`` if
-the project has no ``setup.py``, or explicitly requests a build backend). As
-with other command line flags, pip recognises the ``PIP_USE_PEP517``
-environment veriable and a ``use-pep517`` config file option (set to true or
-false) to set this option globally. Note that overriding pip's choice of
-whether to use :pep:`517` processing in this way does *not* affect whether pip
-will use an isolated build environment (which is controlled via
-``--no-build-isolation`` as noted above).
-
-Except in the case noted above (projects with no :pep:`518` ``[build-system]``
-section in ``pyproject.toml``), pip will never implicitly install a build
-system. Projects **must** ensure that the correct build system is listed in
-their ``requires`` list (this applies even if pip assumes that the
-``setuptools`` backend is being used, as noted above).
-
-.. _pep-518-limitations:
-
-**Historical Limitations**:
-
-* ``pip<18.0``: only supports installing build requirements from wheels, and
- does not support the use of environment markers and extras (only version
- specifiers are respected).
-
-* ``pip<18.1``: build dependencies using .pth files are not properly supported;
- as a result namespace packages do not work under Python 3.2 and earlier.
-
-Future Developments
-~~~~~~~~~~~~~~~~~~~
-
-:pep:`426` notes that the intention is to add hooks to project metadata in
-version 2.1 of the metadata spec, to explicitly define how to build a project
-from its source. Once this version of the metadata spec is final, pip will
-migrate to using that interface. At that point, the ``setup.py`` interface
-documented here will be retained solely for legacy purposes, until projects
-have migrated.
-
-Specifically, applications should *not* expect to rely on there being any form
-of backward compatibility guarantees around the ``setup.py`` interface.
-
-
-Build Options
-~~~~~~~~~~~~~
-
-The ``--global-option`` and ``--build-option`` arguments to the ``pip install``
-and ``pip wheel`` inject additional arguments into the ``setup.py`` command
-(``--build-option`` is only available in ``pip wheel``). These arguments are
-included in the command as follows:
-
-.. tab:: Unix/macOS
-
- .. code-block:: console
-
- python setup.py <global_options> BUILD COMMAND <build_options>
-
-.. tab:: Windows
-
- .. code-block:: shell
-
- py setup.py <global_options> BUILD COMMAND <build_options>
-
-The options are passed unmodified, and presently offer direct access to the
-distutils command line. Use of ``--global-option`` and ``--build-option``
-should be considered as build system dependent, and may not be supported in the
-current form if support for alternative build systems is added to pip.
+.. _`2-build-system-interface`:
+.. rubric:: Build System Interface
+This is now covered in :doc:`../reference/build-system/index`.
.. _`General Options`:
diff --git a/docs/html/cli/pip_download.rst b/docs/html/cli/pip_download.rst
index 4f15314d7..f1fe1769e 100644
--- a/docs/html/cli/pip_download.rst
+++ b/docs/html/cli/pip_download.rst
@@ -43,7 +43,9 @@ match the constraint of the current interpreter (but not your target one), it
is recommended to specify all of these options if you are specifying one of
them. Generic dependencies (e.g. universal wheels, or dependencies with no
platform, abi, or implementation constraints) will still match an over-
-constrained download requirement.
+constrained download requirement. If some of your dependencies are not
+available as binaries, you can build them manually for your target platform
+and let pip download know where to find them using ``--find-links``.
diff --git a/docs/html/cli/pip_freeze.rst b/docs/html/cli/pip_freeze.rst
index 3533db793..b90bcad70 100644
--- a/docs/html/cli/pip_freeze.rst
+++ b/docs/html/cli/pip_freeze.rst
@@ -84,7 +84,7 @@ This error occurs, for instance, when the command is installed only for another
user, and the current user doesn't have the permission to execute the other
user's command.
-To solve that issue, you can try one of the followings:
+To solve that issue, you can try one of the following:
- Install the command for yourself (e.g. in your home directory).
- Ask the system admin to allow this command for all users.
diff --git a/docs/html/cli/pip_inspect.rst b/docs/html/cli/pip_inspect.rst
new file mode 100644
index 000000000..2aa8bd3b8
--- /dev/null
+++ b/docs/html/cli/pip_inspect.rst
@@ -0,0 +1,33 @@
+.. _`pip inspect`:
+
+===========
+pip inspect
+===========
+
+.. versionadded:: 22.2
+
+
+Usage
+=====
+
+.. tab:: Unix/macOS
+
+ .. pip-command-usage:: inspect "python -m pip"
+
+.. tab:: Windows
+
+ .. pip-command-usage:: inspect "py -m pip"
+
+
+Description
+===========
+
+.. pip-command-description:: inspect
+
+The format of the JSON output is described in :doc:`../reference/inspect-report`.
+
+
+Options
+=======
+
+.. pip-command-options:: inspect
diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst
index 14a8b46aa..7c17c264a 100644
--- a/docs/html/cli/pip_install.rst
+++ b/docs/html/cli/pip_install.rst
@@ -79,6 +79,18 @@ for an exception regarding pre-release versions). Where more than one source of
the chosen version is available, it is assumed that any source is acceptable
(as otherwise the versions would differ).
+Obtaining information about what was installed
+----------------------------------------------
+
+The install command has a ``--report`` option that will generate a JSON report of what
+pip has installed. In combination with the ``--dry-run`` and ``--ignore-installed`` it
+can be used to *resolve* a set of requirements without actually installing them.
+
+The report can be written to a file, or to standard output (using ``--report -`` in
+combination with ``--quiet``).
+
+The format of the JSON report is described in :doc:`../reference/installation-report`.
+
Installation Order
------------------
@@ -148,204 +160,20 @@ profile:
3. For whatever reason, they don't or won't declare their build dependencies using
``setup_requires``.
+.. _`0-requirements-file-format`:
+.. rubric:: Requirements File Format
-.. _`Requirements File Format`:
-
-Requirements File Format
-------------------------
-
-Each line of the requirements file indicates something to be installed,
-and like arguments to :ref:`pip install`, the following forms are supported::
-
- [[--option]...]
- <requirement specifier> [; markers] [[--option]...]
- <archive url/path>
- [-e] <local project path>
- [-e] <vcs project url>
-
-For details on requirement specifiers, see :ref:`Requirement Specifiers`.
-
-See the :ref:`pip install Examples<pip install Examples>` for examples of all these forms.
-
-A line that begins with ``#`` is treated as a comment and ignored. Whitespace
-followed by a ``#`` causes the ``#`` and the remainder of the line to be
-treated as a comment.
-
-A line ending in an unescaped ``\`` is treated as a line continuation
-and the newline following it is effectively ignored.
-
-Comments are stripped *after* line continuations are processed.
-
-To interpret the requirements file in UTF-8 format add a comment
-``# -*- coding: utf-8 -*-`` to the first or second line of the file.
-
-The following options are supported:
-
-.. pip-requirements-file-options-ref-list::
-
-Please note that the above options are global options, and should be specified on their individual lines.
-The options which can be applied to individual requirements are
-:ref:`--install-option <install_--install-option>`, :ref:`--global-option <install_--global-option>` and ``--hash``.
-
-For example, to specify :ref:`--pre <install_--pre>`, :ref:`--no-index <install_--no-index>` and two
-:ref:`--find-links <install_--find-links>` locations:
-
-::
-
---pre
---no-index
---find-links /my/local/archives
---find-links http://some.archives.com/archives
-
-
-If you wish, you can refer to other requirements files, like this::
-
- -r more_requirements.txt
-
-You can also refer to :ref:`constraints files <Constraints Files>`, like this::
-
- -c some_constraints.txt
-
-.. _`Using Environment Variables`:
-
-Using Environment Variables
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Since version 10, pip supports the use of environment variables inside the
-requirements file. You can now store sensitive data (tokens, keys, etc.) in
-environment variables and only specify the variable name for your requirements,
-letting pip lookup the value at runtime. This approach aligns with the commonly
-used `12-factor configuration pattern <https://12factor.net/config>`_.
-
-You have to use the POSIX format for variable names including brackets around
-the uppercase name as shown in this example: ``${API_TOKEN}``. pip will attempt
-to find the corresponding environment variable defined on the host system at
-runtime.
-
-.. note::
-
- There is no support for other variable expansion syntaxes such as
- ``$VARIABLE`` and ``%VARIABLE%``.
-
-
-.. _`Example Requirements File`:
-
-Example Requirements File
-^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Use ``pip install -r example-requirements.txt`` to install::
-
- #
- ####### example-requirements.txt #######
- #
- ###### Requirements without Version Specifiers ######
- nose
- nose-cov
- beautifulsoup4
- #
- ###### Requirements with Version Specifiers ######
- # See https://www.python.org/dev/peps/pep-0440/#version-specifiers
- docopt == 0.6.1 # Version Matching. Must be version 0.6.1
- keyring >= 4.1.1 # Minimum version 4.1.1
- coverage != 3.5 # Version Exclusion. Anything except version 3.5
- Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.*
- #
- ###### Refer to other requirements files ######
- -r other-requirements.txt
- #
- #
- ###### A particular file ######
- ./downloads/numpy-1.9.2-cp34-none-win32.whl
- http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl
- #
- ###### Additional Requirements without Version Specifiers ######
- # Same as 1st section, just here to show that you can put things in any order.
- rejected
- green
- #
-
-.. _`Requirement Specifiers`:
-
-Requirement Specifiers
-----------------------
-
-pip supports installing from a package index using a :term:`requirement
-specifier <pypug:Requirement Specifier>`. Generally speaking, a requirement
-specifier is composed of a project name followed by optional :term:`version
-specifiers <pypug:Version Specifier>`. :pep:`508` contains a full specification
-of the format of a requirement. Since version 18.1 pip supports the
-``url_req``-form specification.
-
-Some examples:
-
- ::
-
- SomeProject
- SomeProject == 1.3
- SomeProject >=1.2,<2.0
- SomeProject[foo, bar]
- SomeProject~=1.4.2
+This section has been moved to :doc:`../reference/requirements-file-format`.
-Since version 6.0, pip also supports specifiers containing `environment markers
-<https://www.python.org/dev/peps/pep-0508/#environment-markers>`__ like so:
+.. _`0-requirement-specifiers`:
+.. rubric:: Requirement Specifiers
- ::
+This section has been moved to :doc:`../reference/requirement-specifiers`.
- SomeProject ==5.4 ; python_version < '3.8'
- SomeProject; sys_platform == 'win32'
-
-Since version 19.1, pip also supports `direct references
-<https://www.python.org/dev/peps/pep-0440/#direct-references>`__ like so:
-
- ::
-
- SomeProject @ file:///somewhere/...
-
-Environment markers are supported in the command line and in requirements files.
-
-.. note::
-
- Use quotes around specifiers in the shell when using ``>``, ``<``, or when
- using environment markers. Don't use quotes in requirement files. [1]_
-
-
-.. _`Per-requirement Overrides`:
-
-Per-requirement Overrides
--------------------------
-
-Since version 7.0 pip supports controlling the command line options given to
-``setup.py`` via requirements files. This disables the use of wheels (cached or
-otherwise) for that package, as ``setup.py`` does not exist for wheels.
-
-The ``--global-option`` and ``--install-option`` options are used to pass
-options to ``setup.py``. For example:
-
- ::
-
- FooProject >= 1.2 --global-option="--no-user-cfg" \
- --install-option="--prefix='/usr/local'" \
- --install-option="--no-compile"
-
-The above translates roughly into running FooProject's ``setup.py``
-script as:
-
- ::
-
- python setup.py --no-user-cfg install --prefix='/usr/local' --no-compile
-
-Note that the only way of giving more than one option to ``setup.py``
-is through multiple ``--global-option`` and ``--install-option``
-options, as shown in the example above. The value of each option is
-passed as a single argument to the ``setup.py`` script. Therefore, a
-line such as the following is invalid and would result in an
-installation error.
-
-::
-
- # Invalid. Please use '--install-option' twice as shown above.
- FooProject >= 1.2 --install-option="--prefix=/usr/local --no-compile"
+.. _`0-per-requirement-overrides`:
+.. rubric:: Per-requirement Overrides
+This is now covered in :doc:`../reference/requirements-file-format`.
.. _`Pre Release Versions`:
@@ -366,11 +194,8 @@ that enables installation of pre-releases and development releases.
.. _pre-releases: https://www.python.org/dev/peps/pep-0440/#handling-of-pre-releases
-
-.. _`VCS Support`:
-
-VCS Support
------------
+.. _`0-vcs-support`:
+.. rubric:: VCS Support
This is now covered in :doc:`../topics/vcs-support`.
@@ -379,7 +204,7 @@ Finding Packages
pip searches for packages on `PyPI`_ using the
`HTTP simple interface <https://pypi.org/simple/>`_,
-which is documented `here <https://setuptools.readthedocs.io/en/latest/easy_install.html#package-index-api>`_
+which is documented `here <https://packaging.python.org/specifications/simple-repository-api/>`_
and `there <https://www.python.org/dev/peps/pep-0503/>`_.
pip offers a number of package index options for modifying how packages are
@@ -394,341 +219,43 @@ details) is selected.
See the :ref:`pip install Examples<pip install Examples>`.
+.. _`0-ssl certificate verification`:
+.. rubric:: SSL Certificate Verification
-.. _`SSL Certificate Verification`:
-
-SSL Certificate Verification
-----------------------------
-
-Starting with v1.3, pip provides SSL certificate verification over HTTP, to
-prevent man-in-the-middle attacks against PyPI downloads. This does not use
-the system certificate store but instead uses a bundled CA certificate
-store. The default bundled CA certificate store certificate store may be
-overridden by using ``--cert`` option or by using ``PIP_CERT``,
-``REQUESTS_CA_BUNDLE``, or ``CURL_CA_BUNDLE`` environment variables.
-
-
-.. _`Caching`:
-
-Caching
--------
-
-This is now covered in :doc:`../topics/caching`
-
-.. _`Wheel cache`:
-
-Wheel Cache
-^^^^^^^^^^^
-
-This is now covered in :doc:`../topics/caching`
+This is now covered in :doc:`../topics/https-certificates`.
-.. _`hash-checking mode`:
+.. _`0-caching`:
+.. rubric:: Caching
-Hash-Checking Mode
-------------------
+This is now covered in :doc:`../topics/caching`.
-Since version 8.0, pip can check downloaded package archives against local
-hashes to protect against remote tampering. To verify a package against one or
-more hashes, add them to the end of the line::
-
- FooProject == 1.2 --hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 \
- --hash=sha256:486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7
-
-(The ability to use multiple hashes is important when a package has both
-binary and source distributions or when it offers binary distributions for a
-variety of platforms.)
-
-The recommended hash algorithm at the moment is sha256, but stronger ones are
-allowed, including all those supported by ``hashlib``. However, weaker ones
-such as md5, sha1, and sha224 are excluded to avoid giving a false sense of
-security.
-
-Hash verification is an all-or-nothing proposition. Specifying a ``--hash``
-against any requirement not only checks that hash but also activates a global
-*hash-checking mode*, which imposes several other security restrictions:
-
-* Hashes are required for all requirements. This is because a partially-hashed
- requirements file is of little use and thus likely an error: a malicious
- actor could slip bad code into the installation via one of the unhashed
- requirements. Note that hashes embedded in URL-style requirements via the
- ``#md5=...`` syntax suffice to satisfy this rule (regardless of hash
- strength, for legacy reasons), though you should use a stronger
- hash like sha256 whenever possible.
-* Hashes are required for all dependencies. An error results if there is a
- dependency that is not spelled out and hashed in the requirements file.
-* Requirements that take the form of project names (rather than URLs or local
- filesystem paths) must be pinned to a specific version using ``==``. This
- prevents a surprising hash mismatch upon the release of a new version
- that matches the requirement specifier.
-* ``--egg`` is disallowed, because it delegates installation of dependencies
- to setuptools, giving up pip's ability to enforce any of the above.
-
-.. _`--require-hashes`:
-
-Hash-checking mode can be forced on with the ``--require-hashes`` command-line
-option:
+.. _`0-wheel-cache`:
+.. rubric:: Wheel Cache
-.. tab:: Unix/macOS
+This is now covered in :doc:`../topics/caching`.
- .. code-block:: console
+.. _`0-hash-checking-mode`:
+.. rubric:: Hash checking mode
- $ python -m pip install --require-hashes -r requirements.txt
- ...
- Hashes are required in --require-hashes mode (implicitly on when a hash is
- specified for any package). These requirements were missing hashes,
- leaving them open to tampering. These are the hashes the downloaded
- archives actually had. You can add lines like these to your requirements
- files to prevent tampering.
- pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa
- more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0
+This is now covered in :doc:`../topics/secure-installs`.
-.. tab:: Windows
+.. _`0-local-project-installs`:
+.. rubric:: Local Project Installs
- .. code-block:: console
+This is now covered in :doc:`../topics/local-project-installs`.
- C:\> py -m pip install --require-hashes -r requirements.txt
- ...
- Hashes are required in --require-hashes mode (implicitly on when a hash is
- specified for any package). These requirements were missing hashes,
- leaving them open to tampering. These are the hashes the downloaded
- archives actually had. You can add lines like these to your requirements
- files to prevent tampering.
- pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa
- more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0
-
-
-This can be useful in deploy scripts, to ensure that the author of the
-requirements file provided hashes. It is also a convenient way to bootstrap
-your list of hashes, since it shows the hashes of the packages it fetched. It
-fetches only the preferred archive for each package, so you may still need to
-add hashes for alternatives archives using :ref:`pip hash`: for instance if
-there is both a binary and a source distribution.
-
-The :ref:`wheel cache <Wheel cache>` is disabled in hash-checking mode to
-prevent spurious hash mismatch errors. These would otherwise occur while
-installing sdists that had already been automatically built into cached wheels:
-those wheels would be selected for installation, but their hashes would not
-match the sdist ones from the requirements file. A further complication is that
-locally built wheels are nondeterministic: contemporary modification times make
-their way into the archive, making hashes unpredictable across machines and
-cache flushes. Compilation of C code adds further nondeterminism, as many
-compilers include random-seeded values in their output. However, wheels fetched
-from index servers are the same every time. They land in pip's HTTP cache, not
-its wheel cache, and are used normally in hash-checking mode. The only downside
-of having the wheel cache disabled is thus extra build time for sdists, and
-this can be solved by making sure pre-built wheels are available from the index
-server.
-
-Hash-checking mode also works with :ref:`pip download` and :ref:`pip wheel`.
-See :doc:`../topics/repeatable-installs` for a comparison of hash-checking mode
-with other repeatability strategies.
-
-.. warning::
-
- Beware of the ``setup_requires`` keyword arg in :file:`setup.py`. The
- (rare) packages that use it will cause those dependencies to be downloaded
- by setuptools directly, skipping pip's hash-checking. If you need to use
- such a package, see :ref:`Controlling
- setup_requires<controlling-setup-requires>`.
-
-.. warning::
-
- Be careful not to nullify all your security work when you install your
- actual project by using setuptools directly: for example, by calling
- ``python setup.py install``, ``python setup.py develop``, or
- ``easy_install``. Setuptools will happily go out and download, unchecked,
- anything you missed in your requirements file—and it’s easy to miss things
- as your project evolves. To be safe, install your project using pip and
- :ref:`--no-deps <install_--no-deps>`.
-
- Instead of ``python setup.py develop``, use...
+.. _`0-editable-installs`:
+.. rubric:: Editable installs
- .. tab:: Unix/macOS
-
- .. code-block:: shell
-
- python -m pip install --no-deps -e .
-
- .. tab:: Windows
-
- .. code-block:: shell
-
- py -m pip install --no-deps -e .
-
-
- Instead of ``python setup.py install``, use...
-
- .. tab:: Unix/macOS
-
- .. code-block:: shell
-
- python -m pip install --no-deps .
-
- .. tab:: Windows
-
- .. code-block:: shell
-
- py -m pip install --no-deps .
-
-Hashes from PyPI
-^^^^^^^^^^^^^^^^
-
-PyPI provides an MD5 hash in the fragment portion of each package download URL,
-like ``#md5=123...``, which pip checks as a protection against download
-corruption. Other hash algorithms that have guaranteed support from ``hashlib``
-are also supported here: sha1, sha224, sha384, sha256, and sha512. Since this
-hash originates remotely, it is not a useful guard against tampering and thus
-does not satisfy the ``--require-hashes`` demand that every package have a
-local hash.
-
-
-Local project installs
-----------------------
-
-pip supports installing local project in both regular mode and editable mode.
-You can install local projects by specifying the project path to pip:
-
-.. tab:: Unix/macOS
-
- .. code-block:: shell
-
- python -m pip install path/to/SomeProject
-
-.. tab:: Windows
-
- .. code-block:: shell
-
- py -m pip install path/to/SomeProject
-
-During regular installation, pip will copy the entire project directory to a
-temporary location and install from there. The exception is that pip will
-exclude .tox and .nox directories present in the top level of the project from
-being copied. This approach is the cause of several performance and correctness
-issues, so it is planned that pip 21.3 will change to install directly from the
-local project directory. Depending on the build backend used by the project,
-this may generate secondary build artifacts in the project directory, such as
-the ``.egg-info`` and ``build`` directories in the case of the setuptools
-backend.
-
-To opt in to the future behavior, specify the ``--use-feature=in-tree-build``
-option in pip's command line.
-
-
-.. _`editable-installs`:
-
-"Editable" Installs
-^^^^^^^^^^^^^^^^^^^
-
-"Editable" installs are fundamentally `"setuptools develop mode"
-<https://setuptools.readthedocs.io/en/latest/setuptools.html#development-mode>`_
-installs.
-
-You can install local projects or VCS projects in "editable" mode:
-
-.. tab:: Unix/macOS
-
- .. code-block:: shell
-
- python -m pip install -e path/to/SomeProject
- python -m pip install -e git+http://repo/my_project.git#egg=SomeProject
-
-.. tab:: Windows
-
- .. code-block:: shell
-
- py -m pip install -e path/to/SomeProject
- py -m pip install -e git+http://repo/my_project.git#egg=SomeProject
-
-
-(See the :doc:`../topics/vcs-support` section above for more information on VCS-related syntax.)
-
-For local projects, the "SomeProject.egg-info" directory is created relative to
-the project path. This is one advantage over just using ``setup.py develop``,
-which creates the "egg-info" directly relative the current working directory.
-
-
-.. _`controlling-setup-requires`:
-
-Controlling setup_requires
---------------------------
-
-Setuptools offers the ``setup_requires`` `setup() keyword
-<https://setuptools.readthedocs.io/en/latest/setuptools.html#new-and-changed-setup-keywords>`_
-for specifying dependencies that need to be present in order for the
-``setup.py`` script to run. Internally, Setuptools uses ``easy_install``
-to fulfill these dependencies.
-
-pip has no way to control how these dependencies are located. None of the
-package index options have an effect.
-
-The solution is to configure a "system" or "personal" `Distutils configuration
-file
-<https://docs.python.org/3/install/index.html#distutils-configuration-files>`_ to
-manage the fulfillment.
-
-For example, to have the dependency located at an alternate index, add this:
-
-::
-
- [easy_install]
- index_url = https://my.index-mirror.com
-
-To have the dependency located from a local directory and not crawl PyPI, add this:
-
-::
-
- [easy_install]
- allow_hosts = ''
- find_links = file:///path/to/local/archives/
-
-
-Build System Interface
-----------------------
-
-In order for pip to install a package from source, ``setup.py`` must implement
-the following commands::
-
- setup.py egg_info [--egg-base XXX]
- setup.py install --record XXX [--single-version-externally-managed] [--root XXX] [--compile|--no-compile] [--install-headers XXX]
-
-The ``egg_info`` command should create egg metadata for the package, as
-described in the setuptools documentation at
-https://setuptools.readthedocs.io/en/latest/setuptools.html#egg-info-create-egg-metadata-and-set-build-tags
-
-The ``install`` command should implement the complete process of installing the
-package to the target directory XXX.
-
-To install a package in "editable" mode (``pip install -e``), ``setup.py`` must
-implement the following command::
-
- setup.py develop --no-deps
-
-This should implement the complete process of installing the package in
-"editable" mode.
-
-All packages will be attempted to built into wheels::
-
- setup.py bdist_wheel -d XXX
-
-One further ``setup.py`` command is invoked by ``pip install``::
-
- setup.py clean
-
-This command is invoked to clean up temporary commands from the build. (TODO:
-Investigate in more detail when this command is required).
-
-No other build system commands are invoked by the ``pip install`` command.
-
-Installing a package from a wheel does not invoke the build system at all.
-
-.. _PyPI: https://pypi.org/
-.. _setuptools extras: https://setuptools.readthedocs.io/en/latest/userguide/dependency_management.html#optional-dependencies
+This is now covered in :doc:`../topics/local-project-installs`.
+.. _`0-build-system-interface`:
+.. rubric:: Build System Interface
+This is now covered in :doc:`../reference/build-system/index`.
.. _`pip install Options`:
-
Options
=======
@@ -853,7 +380,7 @@ Examples
py -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch
py -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory
-#. Install a package with `setuptools extras`_.
+#. Install a package with `extras`_.
.. tab:: Unix/macOS
@@ -1012,7 +539,5 @@ Examples
py -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1
-----
-
-.. [1] This is true with the exception that pip v7.0 and v7.0.1 required quotes
- around specifiers containing environment markers in requirement files.
+.. _extras: https://www.python.org/dev/peps/pep-0508/#extras
+.. _PyPI: https://pypi.org/
diff --git a/docs/html/cli/pip_list.rst b/docs/html/cli/pip_list.rst
index 5119a804c..739435981 100644
--- a/docs/html/cli/pip_list.rst
+++ b/docs/html/cli/pip_list.rst
@@ -35,55 +35,13 @@ Options
Examples
========
-#. List installed packages.
+#. List installed packages (with the default column formatting).
.. tab:: Unix/macOS
.. code-block:: console
$ python -m pip list
- docutils (0.10)
- Jinja2 (2.7.2)
- MarkupSafe (0.18)
- Pygments (1.6)
- Sphinx (1.2.1)
-
- .. tab:: Windows
-
- .. code-block:: console
-
- C:\> py -m pip list
- docutils (0.10)
- Jinja2 (2.7.2)
- MarkupSafe (0.18)
- Pygments (1.6)
- Sphinx (1.2.1)
-
-#. List outdated packages (excluding editables), and the latest version available.
-
- .. tab:: Unix/macOS
-
- .. code-block:: console
-
- $ python -m pip list --outdated
- docutils (Current: 0.10 Latest: 0.11)
- Sphinx (Current: 1.2.1 Latest: 1.2.2)
-
- .. tab:: Windows
-
- .. code-block:: console
-
- C:\> py -m pip list --outdated
- docutils (Current: 0.10 Latest: 0.11)
- Sphinx (Current: 1.2.1 Latest: 1.2.2)
-
-#. List installed packages with column formatting.
-
- .. tab:: Unix/macOS
-
- .. code-block:: console
-
- $ python -m pip list --format columns
Package Version
------- -------
docopt 0.6.2
@@ -94,7 +52,7 @@ Examples
.. code-block:: console
- C:\> py -m pip list --format columns
+ C:\> py -m pip list
Package Version
------- -------
docopt 0.6.2
@@ -107,7 +65,7 @@ Examples
.. code-block:: console
- $ python -m pip list -o --format columns
+ $ python -m pip list --outdated --format columns
Package Version Latest Type
---------- ------- ------ -----
retry 0.8.1 0.9.1 wheel
@@ -117,7 +75,7 @@ Examples
.. code-block:: console
- C:\> py -m pip list -o --format columns
+ C:\> py -m pip list --outdated --format columns
Package Version Latest Type
---------- ------- ------ -----
retry 0.8.1 0.9.1 wheel
@@ -131,36 +89,18 @@ Examples
.. code-block:: console
$ python -m pip list --outdated --not-required
- docutils (Current: 0.10 Latest: 0.11)
+ Package Version Latest Type
+ -------- ------- ------ -----
+ docutils 0.14 0.17.1 wheel
.. tab:: Windows
.. code-block:: console
C:\> py -m pip list --outdated --not-required
- docutils (Current: 0.10 Latest: 0.11)
-
-#. Use legacy formatting
-
- .. tab:: Unix/macOS
-
- .. code-block:: console
-
- $ python -m pip list --format=legacy
- colorama (0.3.7)
- docopt (0.6.2)
- idlex (1.13)
- jedi (0.9.0)
-
- .. tab:: Windows
-
- .. code-block:: console
-
- C:\> py -m pip list --format=legacy
- colorama (0.3.7)
- docopt (0.6.2)
- idlex (1.13)
- jedi (0.9.0)
+ Package Version Latest Type
+ -------- ------- ------ -----
+ docutils 0.14 0.17.1 wheel
#. Use json formatting
@@ -199,3 +139,93 @@ Examples
docopt==0.6.2
idlex==1.13
jedi==0.9.0
+
+#. List packages installed in editable mode
+
+When some packages are installed in editable mode, ``pip list`` outputs an
+additional column that shows the directory where the editable project is
+located (i.e. the directory that contains the ``pyproject.toml`` or
+``setup.py`` file).
+
+ .. tab:: Unix/macOS
+
+ .. code-block:: console
+
+ $ python -m pip list
+ Package Version Editable project location
+ ---------------- -------- -------------------------------------
+ pip 21.2.4
+ pip-test-package 0.1.1 /home/you/.venv/src/pip-test-package
+ setuptools 57.4.0
+ wheel 0.36.2
+
+
+ .. tab:: Windows
+
+ .. code-block:: console
+
+ C:\> py -m pip list
+ Package Version Editable project location
+ ---------------- -------- ----------------------------------------
+ pip 21.2.4
+ pip-test-package 0.1.1 C:\Users\You\.venv\src\pip-test-package
+ setuptools 57.4.0
+ wheel 0.36.2
+
+The json format outputs an additional ``editable_project_location`` field.
+
+ .. tab:: Unix/macOS
+
+ .. code-block:: console
+
+ $ python -m pip list --format=json | python -m json.tool
+ [
+ {
+ "name": "pip",
+ "version": "21.2.4",
+ },
+ {
+ "name": "pip-test-package",
+ "version": "0.1.1",
+ "editable_project_location": "/home/you/.venv/src/pip-test-package"
+ },
+ {
+ "name": "setuptools",
+ "version": "57.4.0"
+ },
+ {
+ "name": "wheel",
+ "version": "0.36.2"
+ }
+ ]
+
+ .. tab:: Windows
+
+ .. code-block:: console
+
+ C:\> py -m pip list --format=json | py -m json.tool
+ [
+ {
+ "name": "pip",
+ "version": "21.2.4",
+ },
+ {
+ "name": "pip-test-package",
+ "version": "0.1.1",
+ "editable_project_location": "C:\Users\You\.venv\src\pip-test-package"
+ },
+ {
+ "name": "setuptools",
+ "version": "57.4.0"
+ },
+ {
+ "name": "wheel",
+ "version": "0.36.2"
+ }
+ ]
+
+.. note::
+
+ Contrary to the ``freeze`` command, ``pip list --format=freeze`` will not
+ report editable install information, but the version of the package at the
+ time it was installed.
diff --git a/docs/html/cli/pip_uninstall.rst b/docs/html/cli/pip_uninstall.rst
index e6eeb5ebf..0dd52619d 100644
--- a/docs/html/cli/pip_uninstall.rst
+++ b/docs/html/cli/pip_uninstall.rst
@@ -43,7 +43,7 @@ Examples
Uninstalling simplejson:
/home/me/env/lib/python3.9/site-packages/simplejson
/home/me/env/lib/python3.9/site-packages/simplejson-2.2.1-py3.9.egg-info
- Proceed (y/n)? y
+ Proceed (Y/n)? y
Successfully uninstalled simplejson
.. tab:: Windows
@@ -54,5 +54,5 @@ Examples
Uninstalling simplejson:
/home/me/env/lib/python3.9/site-packages/simplejson
/home/me/env/lib/python3.9/site-packages/simplejson-2.2.1-py3.9.egg-info
- Proceed (y/n)? y
+ Proceed (Y/n)? y
Successfully uninstalled simplejson
diff --git a/docs/html/cli/pip_wheel.rst b/docs/html/cli/pip_wheel.rst
index c2a9543fc..bfd19a0cc 100644
--- a/docs/html/cli/pip_wheel.rst
+++ b/docs/html/cli/pip_wheel.rst
@@ -25,63 +25,18 @@ Description
.. pip-command-description:: wheel
-Build System Interface
-----------------------
+.. _`1-build-system-interface`:
+.. rubric:: Build System Interface
-In order for pip to build a wheel, ``setup.py`` must implement the
-``bdist_wheel`` command with the following syntax:
-
-.. tab:: Unix/macOS
-
- .. code-block:: shell
-
- python setup.py bdist_wheel -d TARGET
-
-.. tab:: Windows
-
- .. code-block:: shell
-
- py setup.py bdist_wheel -d TARGET
-
-
-This command must create a wheel compatible with the invoking Python
-interpreter, and save that wheel in the directory TARGET.
-
-No other build system commands are invoked by the ``pip wheel`` command.
-
-Customising the build
-^^^^^^^^^^^^^^^^^^^^^
-
-It is possible using ``--global-option`` to include additional build commands
-with their arguments in the ``setup.py`` command. This is currently the only
-way to influence the building of C extensions from the command line. For
-example:
-
-.. tab:: Unix/macOS
-
- .. code-block:: shell
-
- python -m pip wheel --global-option bdist_ext --global-option -DFOO wheel
-
-.. tab:: Windows
-
- .. code-block:: shell
-
- py -m pip wheel --global-option bdist_ext --global-option -DFOO wheel
-
-
-will result in a build command of
-
-::
-
- setup.py bdist_ext -DFOO bdist_wheel -d TARGET
-
-which passes a preprocessor symbol to the extension build.
-
-Such usage is considered highly build-system specific and more an accident of
-the current implementation than a supported interface.
+This is now covered in :doc:`../reference/build-system/index`.
+Differences to ``build``
+------------------------
+`build <https://pypi.org/project/build/>`_ is a simple tool which can among other things build
+wheels for projects using PEP 517. It is comparable to the execution of ``pip wheel --no-deps .``.
+It can also build source distributions which is not possible with ``pip``.
+``pip wheel`` covers the wheel scope of ``build`` but offers many additional features.
Options
=======
diff --git a/docs/html/conf.py b/docs/html/conf.py
index 64fddeffc..cc967e0ba 100644
--- a/docs/html/conf.py
+++ b/docs/html/conf.py
@@ -55,6 +55,7 @@ print("pip release:", release)
# -- Options for myst-parser ----------------------------------------------------------
myst_enable_extensions = ["deflist"]
+myst_heading_anchors = 3
# -- Options for smartquotes ----------------------------------------------------------
diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst
index 4d58b4cff..98708f2af 100644
--- a/docs/html/development/architecture/anatomy.rst
+++ b/docs/html/development/architecture/anatomy.rst
@@ -24,39 +24,33 @@ The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the
* ``README.rst``
* ``setup.cfg``
* ``setup.py``
-* ``tox.ini`` -- ``pip`` uses Tox, an automation tool, configured by this `tox.ini`_ file. ``tox.ini`` describes a few environments ``pip`` uses during development for simplifying how tests are run (complicated situation there). Example: ``tox -e -py36``. We can run tests for different versions of Python by changing “36†to “27†or similar.
-* ``.coveragerc``
+* ``noxfile.py`` -- ``pip`` uses Nox, an automation tool, configured by this file. ``noxfile.py`` describes a few environments ``pip`` uses during development for simplifying how tests are run (complicated situation there). Example: ``nox -s lint``, ``nox -s test-3.10``. We can run tests for different versions of Python by changing “3.10†to “3.7†or similar.
* ``.gitattributes``
* ``.gitignore``
* ``.mailmap``
-* ``.readthedocs.yml``
-* ``.travis.yml``
* ``docs/`` *[documentation, built with Sphinx]*
* ``html/`` *[sources to HTML documentation avail. online]*
* ``man/`` has man pages the distros can use by running ``man pip``
* ``pip_sphinxext.py`` *[an extension -- pip-specific plugins to Sphinx that do not apply to other packages]*
+ * ``requirements.txt``
* ``news/`` *[pip stores news fragments… Every time pip makes a user-facing change, a file is added to this directory (usually a short note referring to a GitHub issue) with the right extension & name so it gets included in changelog…. So every release the maintainers will be deleting old files in this directory? Yes - we use the towncrier automation to generate a NEWS file, and auto-delete old stuff. There’s more about this in the contributor documentation!]*
* ``template.rst`` *[template for changelog -- this is a file towncrier uses…. Is this jinja? I don’t know, check towncrier docs]*
* ``src/`` *[source; see below]*
-* ``tasks/`` *[invoke is a PyPI library which uses files in this directory to define automation commands that are used in pip’s development processes -- not discussing further right now. For instance, automating the release.]*
+* ``tools/`` *[misc development workflow tools, like requirements files & CI files & helpers. For instance, automating the release.]*
* ``tests/`` -- contains tests you can run. There are instructions in :doc:`../getting-started`.
* ``__init__.py``
* ``conftest.py``
- * ``data/`` *[test data for running tests -- pesudo package index in it! Lots of small packages that are invalid or are valid. Test fixtures. Used by functional tests]*
+ * ``data/`` *[test data for running tests -- pseudo package index in it! Lots of small packages that are invalid or are valid. Test fixtures. Used by functional tests]*
* ``functional/`` *[functional tests of pip’s CLI -- end-to-end, invoke pip in subprocess & check results of execution against desired result. This also is what makes test suite slow]*
* ``lib/`` *[helpers for tests]*
* ``unit/`` *[unit tests -- fast and small and nice!]*
-* ``tools`` *[misc development workflow tools, like requirements files & Travis CI files & helpers for tox]*
-* ``.azure-pipelines``
* ``.github``
-* ``.tox``
-
src directory
@@ -69,24 +63,23 @@ dependencies (code from other packages).
Within ``src/``:
-* ``pip.egg-info/`` *[ignore the contents for now]*
* ``pip/``
* ``__init__.py``
* ``__main__.py``
- * ``__pycache__/`` *[not discussing contents right now]*
* ``_internal/`` *[where all the pip code lives that’s written by pip maintainers -- underscore means private. pip is not a library -- it’s a command line tool! A very important distinction! People who want to install stuff with pip should not use the internals -- they should use the CLI. There’s a note on this in the docs.]*
* ``__init__.py``
- * ``build_env.py`` [not discussing now]
+ * ``build_env.py``
* ``cache.py`` *[has all the info for how to handle caching within pip -- cache-handling stuff. Uses cachecontrol from PyPI, vendored into pip]*
* ``cli/`` *[subpackage containing helpers & additional code for managing the command line interface. Uses argparse from stdlib]*
* ``commands/`` *[literally - each file is the name of the command on the pip CLI. Each has a class that defines what’s needed to set it up, what happens]*
* ``configuration.py``
* ``download.py``
* ``exceptions.py``
- * ``index.py``
- * ``locations.py``
+ * ``index/``
+ * ``locations/``
+ * ``main.py`` *[legacy entry point]*
* ``models/`` *[in-process refactoring! Goal: improve how pip internally models representations it has for data -- data representation. General overall cleanup. Data reps are spread throughout codebase….link is defined in a class in 1 file, and then another file imports Link from that file. Sometimes cyclic dependency?!?! To prevent future situations like this, etc., Pradyun started moving these into a models directory.]*
* ``operations/`` -- a bit of a weird directory….. ``Freeze.py`` used to be in there. Freeze is an operation -- there was an operations.freeze. Then “prepare†got added (the operation of preparing a pkg). Then “check†got added for checking the state of an env.] [what’s a command vs an operation? Command is on CLI; an operation would be an internal bit of code that actually does some subset of the operation the command says. ``install`` command uses bits of ``check`` and ``prepare``, for instance. In the long run, Pradyun’s goal: ``prepare.py`` goes away (gets refactored into other files) such that ``operations`` is just ``check`` and ``freeze``..... … Pradyun plans to refactor this. [how does this compare to ``utils``?]
@@ -104,5 +97,4 @@ Within ``src/``:
.. _`tracking issue`: https://github.com/pypa/pip/issues/6831
.. _GitHub repository: https://github.com/pypa/pip/
-.. _tox.ini: https://github.com/pypa/pip/blob/main/tox.ini
.. _improving the pip dependency resolver: https://github.com/pypa/pip/issues/988
diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst
index ac51e9ffa..ac65f8165 100644
--- a/docs/html/development/ci.rst
+++ b/docs/html/development/ci.rst
@@ -18,16 +18,17 @@ Supported interpreters
pip support a variety of Python interpreters:
-- CPython 3.6
- CPython 3.7
- CPython 3.8
+- CPython 3.9
+- CPython 3.10
- Latest PyPy3
on different operating systems:
- Linux
- Windows
-- MacOS
+- macOS
and on different architectures:
@@ -61,15 +62,9 @@ interpreters.
Services
========
-pip test suite and checks are distributed on three different platforms that
-provides free executors for open source packages:
+pip test suite and checks are distributed on `GitHub Actions`_ which provides
+free executors for open source packages.
-- `GitHub Actions`_ (Used for code quality and development tasks)
-- `Azure DevOps CI`_ (Used for tests)
-- `Travis CI`_ (Used for PyPy tests)
-
-.. _`Travis CI`: https://travis-ci.org/
-.. _`Azure DevOps CI`: https://azure.microsoft.com/en-us/services/devops/
.. _`GitHub Actions`: https://github.com/features/actions
@@ -82,9 +77,9 @@ Developer tasks
======== =============== ================ ================== =============
OS docs lint vendoring packaging
======== =============== ================ ================== =============
-Linux Travis, Github Travis, Github Travis, Github Azure
-Windows Github Github Github Azure
-MacOS Github Github Github Azure
+Linux GitHub GitHub GitHub GitHub
+Windows GitHub GitHub GitHub GitHub
+macOS GitHub GitHub GitHub GitHub
======== =============== ================ ================== =============
Actual testing
@@ -93,52 +88,61 @@ Actual testing
+------------------------------+---------------+-----------------+
| **interpreter** | **unit** | **integration** |
+-----------+----------+-------+---------------+-----------------+
+| | x86 | CP3.7 | | |
| | +-------+---------------+-----------------+
-| | | CP3.6 | Azure | |
+| | | CP3.8 | | |
| | +-------+---------------+-----------------+
-| | x86 | CP3.7 | Azure | |
+| | | CP3.9 | | |
| | +-------+---------------+-----------------+
-| | | CP3.8 | Azure | |
+| | | CP3.10| | |
| | +-------+---------------+-----------------+
| | | PyPy3 | | |
| Windows +----------+-------+---------------+-----------------+
-| | | CP3.6 | Azure | |
+| | x64 | CP3.7 | GitHub | GitHub |
| | +-------+---------------+-----------------+
-| | x64 | CP3.7 | Azure | |
+| | | CP3.8 | | |
| | +-------+---------------+-----------------+
-| | | CP3.8 | Azure | Azure |
+| | | CP3.9 | | |
+| | +-------+---------------+-----------------+
+| | | CP3.10| GitHub | GitHub |
| | +-------+---------------+-----------------+
| | | PyPy3 | | |
+-----------+----------+-------+---------------+-----------------+
-| | | CP3.6 | | |
-| | +-------+---------------+-----------------+
| | x86 | CP3.7 | | |
| | +-------+---------------+-----------------+
| | | CP3.8 | | |
| | +-------+---------------+-----------------+
+| | | CP3.9 | | |
+| | +-------+---------------+-----------------+
| | | PyPy3 | | |
| Linux +----------+-------+---------------+-----------------+
-| | | CP3.6 | Azure | Azure |
+| | x64 | CP3.7 | GitHub | GitHub |
| | +-------+---------------+-----------------+
-| | x64 | CP3.7 | Azure | Azure |
+| | | CP3.8 | GitHub | GitHub |
| | +-------+---------------+-----------------+
-| | | CP3.8 | Azure | Azure |
+| | | CP3.9 | GitHub | GitHub |
| | +-------+---------------+-----------------+
-| | | PyPy3 | Travis | Travis |
-+-----------+----------+-------+---------------+-----------------+
-| | | CP3.6 | | |
+| | | CP3.10| GitHub | GitHub |
| | +-------+---------------+-----------------+
-| | x86 | CP3.7 | | |
+| | | PyPy3 | | |
++-----------+----------+-------+---------------+-----------------+
+| | arm64 | CP3.7 | | |
| | +-------+---------------+-----------------+
| | | CP3.8 | | |
| | +-------+---------------+-----------------+
+| | | CP3.9 | | |
+| | +-------+---------------+-----------------+
+| | | CP3.10| | |
+| | +-------+---------------+-----------------+
| | | PyPy3 | | |
-| MacOS +----------+-------+---------------+-----------------+
-| | | CP3.6 | Github | Github |
+| macOS +----------+-------+---------------+-----------------+
+| | x64 | CP3.7 | GitHub | GitHub |
+| | +-------+---------------+-----------------+
+| | | CP3.8 | GitHub | GitHub |
| | +-------+---------------+-----------------+
-| | x64 | CP3.7 | Github | Github |
+| | | CP3.9 | GitHub | GitHub |
| | +-------+---------------+-----------------+
-| | | CP3.8 | Github | Github |
+| | | CP3.10| GitHub | GitHub |
| | +-------+---------------+-----------------+
| | | PyPy3 | | |
+-----------+----------+-------+---------------+-----------------+
diff --git a/docs/html/development/configuration.rst b/docs/html/development/configuration.rst
deleted file mode 100644
index 8615065aa..000000000
--- a/docs/html/development/configuration.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-:orphan:
-
-=============
-Configuration
-=============
-
-This content is now covered in the :ref:`Configuration` section of the :doc:`User Guide </user_guide>`.
diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst
index 7d2e64902..87734ee4d 100644
--- a/docs/html/development/contributing.rst
+++ b/docs/html/development/contributing.rst
@@ -39,9 +39,8 @@ separately, as a "formatting cleanup" PR, if needed.
Automated Testing
=================
-All pull requests and merges to 'main' branch are tested using `Travis CI`_,
-`Azure Pipelines`_ and `GitHub Actions`_ based on our `.travis.yml`_,
-`.azure-pipelines`_ and `.github/workflows`_ files. More details about pip's
+All pull requests and merges to 'main' branch are tested using `GitHub
+Actions`_ based on our `.github/workflows`_ files. More details about pip's
Continuous Integration can be found in the `CI Documentation`_
@@ -89,7 +88,7 @@ The contents of this file are reStructuredText formatted text that
will be used as the content of the news file entry. You do not need to
reference the issue or PR numbers in the entry, since ``towncrier``
will automatically add a reference to all of the affected issues when
-rendering the NEWS file.
+rendering the NEWS file. There must be a newline at the end of the file.
In order to maintain a consistent style in the ``NEWS.rst`` file, it is
preferred to keep the news entry to the point, in sentence case, shorter than
@@ -264,11 +263,7 @@ will initiate a vote among the existing maintainers.
.. _`Studies have shown`: https://www.kessler.de/prd/smartbear/BestPracticesForPeerCodeReview.pdf
.. _`resolve merge conflicts`: https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line
-.. _`Travis CI`: https://travis-ci.org/
-.. _`Azure Pipelines`: https://azure.microsoft.com/en-in/services/devops/pipelines/
.. _`GitHub Actions`: https://github.com/features/actions
-.. _`.travis.yml`: https://github.com/pypa/pip/blob/main/.travis.yml
-.. _`.azure-pipelines`: https://github.com/pypa/pip/blob/main/.azure-pipelines
.. _`.github/workflows`: https://github.com/pypa/pip/blob/main/.github/workflows
.. _`CI Documentation`: https://pip.pypa.io/en/latest/development/ci/
.. _`towncrier`: https://pypi.org/project/towncrier/
diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst
index 1d625613b..730f5ece0 100644
--- a/docs/html/development/getting-started.rst
+++ b/docs/html/development/getting-started.rst
@@ -27,8 +27,8 @@ Development Environment
pip is a command line application written in Python. For developing pip,
you should `install Python`_ on your computer.
-For developing pip, you need to install :pypi:`tox`. Often, you can run
-``python -m pip install tox`` to install and use it.
+For developing pip, you need to install :pypi:`nox`. Often, you can run
+``python -m pip install nox`` to install and use it.
Running pip From Source Tree
@@ -42,8 +42,8 @@ You can then invoke your local source tree pip normally.
.. code-block:: shell
- virtualenv venv # You can also use "python -m venv venv" from python3.3+
- source venv/bin/activate
+ python -m venv .venv
+ source .venv/bin/activate
python -m pip install -e .
python -m pip --version
@@ -51,17 +51,17 @@ You can then invoke your local source tree pip normally.
.. code-block:: shell
- virtualenv venv # You can also use "py -m venv venv" from python3.3+
- venv\Scripts\activate
+ py -m venv .venv
+ .venv\Scripts\activate
py -m pip install -e .
py -m pip --version
Running Tests
=============
-pip's tests are written using the :pypi:`pytest` test framework, :pypi:`mock`
-and :pypi:`pretend`. :pypi:`tox` is used to automate the setup and execution of
-pip's tests.
+pip's tests are written using the :pypi:`pytest` test framework and
+:mod:`unittest.mock`. :pypi:`nox` is used to automate the setup and execution
+of pip's tests.
It is preferable to run the tests in parallel for better experience during development,
since the tests can take a long time to finish when run sequentially.
@@ -70,38 +70,39 @@ To run tests:
.. code-block:: console
- $ tox -e py36 -- -n auto
+ $ nox -s test-3.10 -- -n auto
To run tests without parallelization, run:
.. code-block:: console
- $ tox -e py36
+ $ nox -s test-3.10
-The example above runs tests against Python 3.6. You can also use other
-versions like ``py39`` and ``pypy3``.
+The example above runs tests against Python 3.10. You can also use other
+versions like ``3.9`` and ``pypy3``.
-``tox`` has been configured to forward any additional arguments it is given to
+``nox`` has been configured to forward any additional arguments it is given to
``pytest``. This enables the use of pytest's `rich CLI`_. As an example, you
can select tests using the various ways that pytest provides:
.. code-block:: console
$ # Using file name
- $ tox -e py36 -- tests/functional/test_install.py
+ $ nox -s test-3.10 -- tests/functional/test_install.py
$ # Using markers
- $ tox -e py36 -- -m unit
+ $ nox -s test-3.10 -- -m unit
$ # Using keywords
- $ tox -e py36 -- -k "install and not wheel"
+ $ nox -s test-3.10 -- -k "install and not wheel"
-Running pip's test suite requires supported version control tools (subversion,
-bazaar, git, and mercurial) to be installed. If you are missing one of the VCS
-tools, you can tell pip to skip those tests:
+Running pip's entire test suite requires supported version control tools
+(subversion, bazaar, git, and mercurial) to be installed. If you are missing
+any of these VCS, those tests should be skipped automatically. You can also
+explicitly tell pytest to skip those tests:
.. code-block:: console
- $ tox -e py36 -- -k "not svn"
- $ tox -e py36 -- -k "not (svn or git)"
+ $ nox -s test-3.10 -- -k "not svn"
+ $ nox -s test-3.10 -- -k "not (svn or git)"
Running Linters
@@ -115,7 +116,7 @@ To use linters locally, run:
.. code-block:: console
- $ tox -e lint
+ $ nox -s lint
.. note::
@@ -125,6 +126,25 @@ To use linters locally, run:
readability problems.
+Running pip under a debugger
+============================
+
+In order to debug pip's behavior, you can run it under a debugger like so:
+
+.. code-block:: console
+
+ $ python -m pdb -m pip --debug ...
+
+
+Replace the ``...`` with arguments you'd like to run pip with. Give PDB the
+``c`` ("continue") command afterwards, to run the process.
+
+The ``--debug`` flag disables pip's exception handler, which would normally
+catch all unhandled exceptions. With this flag, pip will let these exceptions
+propagate outside of its main subroutine, letting them get caught by the
+debugger. This way you'll be able to debug an exception post-mortem via PDB.
+
+
Building Documentation
======================
@@ -135,7 +155,7 @@ To build it locally, run:
.. code-block:: console
- $ tox -e docs
+ $ nox -s docs
The built documentation can be found in the ``docs/build`` folder.
diff --git a/docs/html/development/issue-triage.md b/docs/html/development/issue-triage.md
new file mode 100644
index 000000000..6861df7d8
--- /dev/null
+++ b/docs/html/development/issue-triage.md
@@ -0,0 +1,346 @@
+```{note}
+This section of the documentation is currently being written. pip
+developers welcome your help to complete this documentation. If
+you're interested in helping out, please let us know in the
+[tracking issue](https://github.com/pypa/pip/issues/6583), or
+just submit a pull request and mention it in that tracking issue.
+```
+
+# Issue Triage
+
+This serves as an introduction to issue tracking in pip as well as
+how to help triage reported issues.
+
+## Issue Tracker
+
+The [pip issue tracker](https://github.com/pypa/pip/issues) is hosted on
+GitHub alongside the project.
+
+Currently, the issue tracker is used for bugs, feature requests, and general
+user support.
+
+In the pip issue tracker, we make use of labels and milestones to organize and
+track work.
+
+### Labels
+
+Issue labels are used to:
+
+1. Categorize issues
+2. Provide status information for contributors and reporters
+3. Help contributors find tasks to work on
+
+The current set of labels are divided into several categories identified by
+prefix:
+
+**C - Category**
+: which area of `pip` functionality a feature request or issue is related to
+
+**K - Kind**
+**O - Operating System**
+: for issues that are OS-specific
+
+**P - Project/Platform**
+: related to something external to `pip`
+
+**R - Resolution**
+: no more discussion is really needed, an action has been identified and the
+issue is waiting or closed
+
+**S - State**
+: for some automatic labels and other indicators that work is needed
+
+**type**
+: the role or flavor of an issue
+
+The specific labels falling into each category have a description that can be
+seen on the [Labels](https://github.com/pypa/pip/labels) page.
+
+In addition, there are several standalone labels:
+
+**good first issue**
+: this label marks an issue as beginner-friendly and shows up in banners that
+GitHub displays for first-time visitors to the repository
+
+**triage**
+: default label given to issues when they are created
+
+**trivial**
+: special label for pull requests that removes the
+{ref}`news file requirement <choosing-news-entry-type>`
+
+**needs rebase or merge**
+
+: this is a special label used by BrownTruck to mark PRs that have merge
+conflicts
+
+### Automation
+
+There are several helpers to manage issues and pull requests.
+
+Issues created on the issue tracker are automatically given the
+`triage` label by the
+[triage-new-issues](https://github.com/apps/triage-new-issues)
+bot. The label is automatically removed when another label is added.
+
+When an issue needs feedback from the author we can label it with
+`S: awaiting response`. When the author responds, the
+[no-response](https://github.com/apps/no-response) bot removes the label.
+
+After an issue has been closed for 30 days, the
+[lock](https://github.com/apps/lock) bot locks the issue and adds the
+`S: auto-locked` label. This allows us to avoid monitoring existing closed
+issues, but unfortunately prevents and references to issues from showing up as
+links on the closed issue.
+
+## Triage Issues
+
+Users can make issues for a number of reasons:
+
+1. Suggestions about pip features that could be added or improved
+2. Problems using pip
+3. Concerns about pip usability
+4. General packaging problems to be solved with pip
+5. Problems installing or using Python packages
+6. Problems managing virtual environments
+7. Problems managing Python installations
+
+To triage issues means to identify what kind of issue is happening and
+
+- confirm bugs
+- provide support
+- discuss and design around the uses of the tool
+
+Specifically, to address an issue:
+
+1. Read issue title
+2. Scan issue description
+3. Ask questions
+4. If time is available, try to reproduce
+5. Search for or remember related issues and link to them
+6. Identify an appropriate area of concern (if applicable)
+
+Keep in mind that all communication is happening with other people and
+should be done with respect per the
+[Code of Conduct](https://www.pypa.io/en/latest/code-of-conduct/).
+
+The lifecycle of an issue (bug or support) generally looks like:
+
+1. waiting for triage (marked with label `triage`)
+
+2. confirming issue - some discussion with the user, gathering
+ details, trying to reproduce the issue (may be marked with a specific
+ category, `S: awaiting-response`, `S: discussion-needed`, or
+ `S: need-repro`)
+
+3. confirmed - the issue is pretty consistently reproducible in a
+ straightforward way, or a mechanism that could be causing the issue has been
+ identified
+
+4. awaiting fix - the fix is identified and no real discussion on the issue
+ is needed, should be marked `R: awaiting PR`
+
+5. closed - can be for several reasons
+
+ - fixed
+ - could not be reproduced, no more details could be obtained, and no
+ progress can be made
+ - actual issue was with another project or related to system
+ configuration and pip cannot (or will not) be adapted for it
+
+### Requesting information
+
+Requesting more information to better understand the context and environment
+that led to the issue. Examples of specific information that may be useful
+depending on the situation:
+
+- pip debug: `pip debug`
+- pip version: `pip -V`
+- Python version: `python -VV`
+- Python path: `python -c 'import sys; print(sys.executable)'`
+- `python` on `PATH`: Unix: `which python`; Windows: `where python`
+- Python as resolved by the shell: `type python`
+- Origin of pip (get-pip.py, OS-level package manager, ensurepip, manual
+ installation)
+- Using a virtual environment (with `--system-site-packages`?)
+- Using a conda environment
+- `PATH` environment variable
+- Network situation (e.g. airgapped environment, firewalls)
+- `--verbose` output of a failing command
+- (Unix) `strace` output from a failing command (be careful not to output
+ into the same directory as a package that's being installed, otherwise pip
+ will loop forever copying the log file...)
+- (Windows)
+ [procmon](https://docs.microsoft.com/en-us/sysinternals/downloads/procmon)
+ output during a failing command
+ ([example request](https://github.com/pypa/pip/issues/6814#issuecomment-516611389))
+- Listing of files relevant to the issue (e.g. `ls -l venv/lib/pythonX.Y/problem-package.dist-info/`)
+- whether the unexpected behavior ever worked as expected - if so then what
+ were the details of the setup (same information as above)
+
+Generally, information is good to request if it can help confirm or rule out
+possible sources of error. We shouldn't request information that does not
+improve our understanding of the situation.
+
+### Reproducing issues
+
+Whenever an issue happens and the cause isn't obvious, it is important
+that we be able to reproduce it independently. This serves several purposes:
+
+1. If it is a pip bug, then any fix will need tests - a good reproducer
+ is most of the way towards that.
+2. If it is not reproducible using the provided instructions, that helps
+ rule out a lot of possible causes.
+3. A clear set of instructions is an easy way to get on the same page as
+ someone reporting an issue.
+
+The best way to reproduce an issue is with a script.
+
+A script can be copied into a file and executed, whereas shell output
+has to be manually copied a line at a time.
+
+Scripts to reproduce issues should be:
+
+- portable (few/no assumptions about the system, other that it being Unix or Windows as applicable)
+- non-destructive
+- convenient
+- require little/no setup on the part of the runner
+
+Examples:
+
+- creating and installing multiple wheels with different versions
+ ([link](https://github.com/pypa/pip/issues/4331#issuecomment-520156471))
+- using a small web server for authentication errors
+ ([link](https://github.com/pypa/pip/issues/2920#issuecomment-508953118))
+- using docker to test system or global configuration-related issues
+ ([link](https://github.com/pypa/pip/issues/5533#issuecomment-520159896))
+- using docker to test special filesystem permission/configurations
+ ([link](https://github.com/pypa/pip/issues/6364#issuecomment-507074729))
+- using docker for global installation with get-pip
+ ([link](https://github.com/pypa/pip/issues/6498#issuecomment-513501112))
+- get-pip on system with no `/usr/lib64`
+ ([link](https://github.com/pypa/pip/issues/5379#issuecomment-515270576))
+- reproducing with `pip` from current development branch
+ ([link](https://github.com/pypa/pip/issues/6707#issue-467770959))
+
+### Reaching resolution
+
+Some user support questions are more related to system configuration than pip.
+It's important to treat these issues with the same care and attention as
+others, specifically:
+
+1. Unless the issue is very old and the user doesn't seem active, wait for
+ confirmation before closing the issue
+
+2. Direct the user to the most appropriate forum for their questions:
+
+ - For Ubuntu, [askubuntu](https://askubuntu.com/)
+ - For Other linuxes/unixes, [serverfault](https://serverfault.com/)
+ - For network connectivity issues,
+ [serverfault](https://serverfault.com/)
+
+3. Just because a user support question is best solved using some other forum
+ doesn't mean that we can't make things easier. Try to extract and
+ understand from the user query how things could have been made easier for
+ them or you, for example with better warning or error messages. If an issue
+ does not exist covering that case then create one. If an issue does exist then
+ make sure to reference that issue before closing this one.
+
+4. A user may be having trouble installing a package, where the package
+ `setup.py` or build-backend configuration is non-trivial. In these cases we
+ can help to troubleshoot but the best advice is going to be to direct them
+ to the support channels for the related projects.
+
+5. Do not be hasty to assume it is one cause or another. What looks like
+ someone else's problem may still be an issue in pip or at least something
+ that could be improved.
+
+6. For general discussion on Python packaging:
+
+ - [pypa/packaging](https://github.com/pypa/packaging-problems)
+ - [discuss.python.org/packaging](https://discuss.python.org/c/packaging)
+
+### Closing issues
+
+An issue may be considered resolved and closed when:
+
+- for each possible improvement or problem represented in the issue
+ discussion:
+
+ - Consensus has been reached on a specific action and the actions
+ appear to be external to the project, with no follow up needed
+ in the project afterwards.
+
+ - PEP updates (with a corresponding issue in
+ [python/peps](https://github.com/python/peps))
+ - already tracked by another issue
+
+ - A project-specific issue has been identified and the issue no
+ longer occurs as of the latest commit on the main branch.
+
+- An enhancement or feature request no longer has a proponent and the maintainers
+ don't think it's worth keeping open.
+
+- An issue has been identified as a duplicate, and it is clearly a duplicate (i.e. the
+ original report was very good and points directly to the issue)
+
+- The issue has been fixed, and can be independently validated as no longer being an
+ issue. If this is with code then the specific change/PR that led to it should be
+ identified and posted for tracking.
+
+## Common issues
+
+1. network-related issues - any issue involving retries, address lookup, or
+ anything like that are typically network issues.
+
+2. issues related to having multiple Python versions, or an OS package
+ manager-managed pip/python installation (specifically with Debian/Ubuntu).
+ These typically present themselves as:
+
+ 1. Not being able to find installed packages
+ 2. basic libraries not able to be found, fundamental OS components missing
+ 3. In these situations you will want to make sure that we know how they got
+ their Python and pip. Knowing the relevant package manager commands can
+ help, e.g. `dpkg -S`.
+
+## For issues caused by changes by redistributors
+
+Certain issues are caused by patches that redistributors of Python/pip
+make to Python/pip.
+
+Certain redistributors have shared preferred wording to redirect users
+to their issue trackers.
+
+Fedora, RHEL, CentOS (and probably other derivatives – Rocky, Scientific, CloudLinux):
+
+```
+This issue looks like it's caused by changes that Fedora or Red Hat
+made in their pip packaging. Please file a Fedora bug at
+https://bugzilla.redhat.com/enter_bug.cgi?product=Fedora&component=python-pip
+
+cc @encukou @hroncok
+```
+
+Debian:
+
+```
+This issue looks like it's caused by changes that Debian made in
+their pip packaging. Please file a bug with Debian, with
+`reportbug python3-pip` [Docs](https://www.debian.org/Bugs/Reporting).
+You can link to this issue in your bug report.
+
+In the meantime, you may be able to work-around your issue by upgrading
+pip inside your virtualenv: `python -m pip install -U pip`
+```
+
+Ubuntu:
+
+```
+This issue looks like it's caused by changes that Ubuntu made in
+their pip packaging. Please file a bug with Ubuntu, with
+`ubuntu-bug python3-pip` [Docs](https://help.ubuntu.com/community/ReportingBugs).
+You can link to this issue in your bug report.
+
+In the meantime, you may be able to work-around your issue by upgrading
+pip inside your virtualenv: `python -m pip install -U pip`
+```
diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst
deleted file mode 100644
index c21da1fc6..000000000
--- a/docs/html/development/issue-triage.rst
+++ /dev/null
@@ -1,312 +0,0 @@
-.. note::
-
- This section of the documentation is currently being written. pip
- developers welcome your help to complete this documentation. If
- you're interested in helping out, please let us know in the
- `tracking issue <https://github.com/pypa/pip/issues/6583>`__, or
- just submit a pull request and mention it in that tracking issue.
-
-============
-Issue Triage
-============
-
-This serves as an introduction to issue tracking in pip as well as
-how to help triage reported issues.
-
-
-Issue Tracker
-=============
-
-The `pip issue tracker <https://github.com/pypa/pip/issues>`__ is hosted on
-GitHub alongside the project.
-
-Currently, the issue tracker is used for bugs, feature requests, and general
-user support.
-
-In the pip issue tracker, we make use of labels and milestones to organize and
-track work.
-
-Labels
-------
-
-Issue labels are used to:
-
-#. Categorize issues
-#. Provide status information for contributors and reporters
-#. Help contributors find tasks to work on
-
-The current set of labels are divided into several categories identified by
-prefix:
-
-**C - Category**
- which area of ``pip`` functionality a feature request or issue is related to
-
-**K - Kind**
-
-**O - Operating System**
- for issues that are OS-specific
-
-**P - Project/Platform**
- related to something external to ``pip``
-
-**R - Resolution**
- no more discussion is really needed, an action has been identified and the
- issue is waiting or closed
-
-**S - State**
- for some automatic labels and other indicators that work is needed
-
-**type**
- the role or flavor of an issue
-
-The specific labels falling into each category have a description that can be
-seen on the `Labels <https://github.com/pypa/pip/labels>`__ page.
-
-In addition, there are several standalone labels:
-
-**good first issue**
- this label marks an issue as beginner-friendly and shows up in banners that
- GitHub displays for first-time visitors to the repository
-
-**triage**
- default label given to issues when they are created
-
-**trivial**
- special label for pull requests that removes the
- :ref:`news file requirement <choosing-news-entry-type>`
-
-**needs rebase or merge**
- this is a special label used by BrownTruck to mark PRs that have merge
- conflicts
-
-Automation
-----------
-
-There are several helpers to manage issues and pull requests.
-
-Issues created on the issue tracker are automatically given the
-``triage`` label by the
-`triage-new-issues <https://github.com/apps/triage-new-issues>`__
-bot. The label is automatically removed when another label is added.
-
-When an issue needs feedback from the author we can label it with
-``S: awaiting response``. When the author responds, the
-`no-response <https://github.com/apps/no-response>`__ bot removes the label.
-
-After an issue has been closed for 30 days, the
-`lock <https://github.com/apps/lock>`__ bot locks the issue and adds the
-``S: auto-locked`` label. This allows us to avoid monitoring existing closed
-issues, but unfortunately prevents and references to issues from showing up as
-links on the closed issue.
-
-
-Triage Issues
-=============
-
-Users can make issues for a number of reasons:
-
-#. Suggestions about pip features that could be added or improved
-#. Problems using pip
-#. Concerns about pip usability
-#. General packaging problems to be solved with pip
-#. Problems installing or using Python packages
-#. Problems managing virtual environments
-#. Problems managing Python installations
-
-To triage issues means to identify what kind of issue is happening and
-
-* confirm bugs
-* provide support
-* discuss and design around the uses of the tool
-
-Specifically, to address an issue:
-
-#. Read issue title
-#. Scan issue description
-#. Ask questions
-#. If time is available, try to reproduce
-#. Search for or remember related issues and link to them
-#. Identify an appropriate area of concern (if applicable)
-
-Keep in mind that all communication is happening with other people and
-should be done with respect per the
-`Code of Conduct <https://www.pypa.io/en/latest/code-of-conduct/>`__.
-
-The lifecycle of an issue (bug or support) generally looks like:
-
-#. waiting for triage (marked with label ``triage``)
-#. confirming issue - some discussion with the user, gathering
- details, trying to reproduce the issue (may be marked with a specific
- category, ``S: awaiting-respose``, ``S: discussion-needed``, or
- ``S: need-repro``)
-#. confirmed - the issue is pretty consistently reproducible in a
- straightforward way, or a mechanism that could be causing the issue has been
- identified
-#. awaiting fix - the fix is identified and no real discussion on the issue
- is needed, should be marked ``R: awaiting PR``
-#. closed - can be for several reasons
-
- * fixed
- * could not be reproduced, no more details could be obtained, and no
- progress can be made
- * actual issue was with another project or related to system
- configuration and pip cannot (or will not) be adapted for it
-
-
-Requesting information
-----------------------
-
-Requesting more information to better understand the context and environment
-that led to the issue. Examples of specific information that may be useful
-depending on the situation:
-
-* pip debug: ``pip debug``
-* pip version: ``pip -V``
-* Python version: ``python -VV``
-* Python path: ``python -c 'import sys; print(sys.executable)'``
-* ``python`` on ``PATH``: Unix: ``which python``; Windows: ``where python``
-* Python as resolved by the shell: ``type python``
-* Origin of pip (get-pip.py, OS-level package manager, ensurepip, manual
- installation)
-* Using a virtual environment (with ``--system-site-packages``?)
-* Using a conda environment
-* ``PATH`` environment variable
-* Network situation (e.g. airgapped environment, firewalls)
-* ``--verbose`` output of a failing command
-* (Unix) ``strace`` output from a failing command (be careful not to output
- into the same directory as a package that's being installed, otherwise pip
- will loop forever copying the log file...)
-* (Windows)
- `procmon <https://docs.microsoft.com/en-us/sysinternals/downloads/procmon>`__
- output during a failing command
- (`example request <https://github.com/pypa/pip/issues/6814#issuecomment-516611389>`__)
-* Listing of files relevant to the issue (e.g. ``ls -l venv/lib/pythonX.Y/problem-package.dist-info/``)
-* whether the unexpected behavior ever worked as expected - if so then what
- were the details of the setup (same information as above)
-
-
-Generally, information is good to request if it can help confirm or rule out
-possible sources of error. We shouldn't request information that does not
-improve our understanding of the situation.
-
-
-Reproducing issues
-------------------
-
-Whenever an issue happens and the cause isn't obvious, it is important
-that we be able to reproduce it independently. This serves several purposes:
-
-#. If it is a pip bug, then any fix will need tests - a good reproducer
- is most of the way towards that.
-#. If it is not reproducible using the provided instructions, that helps
- rule out a lot of possible causes.
-#. A clear set of instructions is an easy way to get on the same page as
- someone reporting an issue.
-
-The best way to reproduce an issue is with a script.
-
-A script can be copied into a file and executed, whereas shell output
-has to be manually copied a line at a time.
-
-Scripts to reproduce issues should be:
-
-- portable (few/no assumptions about the system, other that it being Unix or Windows as applicable)
-- non-destructive
-- convenient
-- require little/no setup on the part of the runner
-
-Examples:
-
-- creating and installing multiple wheels with different versions
- (`link <https://github.com/pypa/pip/issues/4331#issuecomment-520156471>`__)
-- using a small web server for authentication errors
- (`link <https://github.com/pypa/pip/issues/2920#issuecomment-508953118>`__)
-- using docker to test system or global configuration-related issues
- (`link <https://github.com/pypa/pip/issues/5533#issuecomment-520159896>`__)
-- using docker to test special filesystem permission/configurations
- (`link <https://github.com/pypa/pip/issues/6364#issuecomment-507074729>`__)
-- using docker for global installation with get-pip
- (`link <https://github.com/pypa/pip/issues/6498#issuecomment-513501112>`__)
-- get-pip on system with no ``/usr/lib64``
- (`link <https://github.com/pypa/pip/issues/5379#issuecomment-515270576>`__)
-- reproducing with ``pip`` from current development branch
- (`link <https://github.com/pypa/pip/issues/6707#issue-467770959>`__)
-
-
-Reaching resolution
--------------------
-
-Some user support questions are more related to system configuration than pip.
-It's important to treat these issues with the same care and attention as
-others, specifically:
-
-#. Unless the issue is very old and the user doesn't seem active, wait for
- confirmation before closing the issue
-#. Direct the user to the most appropriate forum for their questions:
-
- * For Ubuntu, `askubuntu <https://askubuntu.com/>`__
- * For Other linuxes/unixes, `serverfault <https://serverfault.com/>`__
- * For network connectivity issues,
- `serverfault <https://serverfault.com/>`__
-
-#. Just because a user support question is best solved using some other forum
- doesn't mean that we can't make things easier. Try to extract and
- understand from the user query how things could have been made easier for
- them or you, for example with better warning or error messages. If an issue
- does not exist covering that case then create one. If an issue does exist then
- make sure to reference that issue before closing this one.
-#. A user may be having trouble installing a package, where the package
- ``setup.py`` or build-backend configuration is non-trivial. In these cases we
- can help to troubleshoot but the best advice is going to be to direct them
- to the support channels for the related projects.
-#. Do not be hasty to assume it is one cause or another. What looks like
- someone else's problem may still be an issue in pip or at least something
- that could be improved.
-#. For general discussion on Python packaging:
-
- * `pypa/packaging <https://github.com/pypa/packaging-problems>`__
- * `discuss.python.org/packaging <https://discuss.python.org/c/packaging>`__
-
-
-Closing issues
---------------
-
-An issue may be considered resolved and closed when:
-
-- for each possible improvement or problem represented in the issue
- discussion:
-
- - Consensus has been reached on a specific action and the actions
- appear to be external to the project, with no follow up needed
- in the project afterwards.
-
- - PEP updates (with a corresponding issue in
- `python/peps <https://github.com/python/peps>`__)
- - already tracked by another issue
-
- - A project-specific issue has been identified and the issue no
- longer occurs as of the latest commit on the main branch.
-
-- An enhancement or feature request no longer has a proponent and the maintainers
- don't think it's worth keeping open.
-- An issue has been identified as a duplicate, and it is clearly a duplicate (i.e. the
- original report was very good and points directly to the issue)
-- The issue has been fixed, and can be independently validated as no longer being an
- issue. If this is with code then the specific change/PR that led to it should be
- identified and posted for tracking.
-
-
-Common issues
-=============
-
-#. network-related issues - any issue involving retries, address lookup, or
- anything like that are typically network issues.
-#. issues related to having multiple Python versions, or an OS package
- manager-managed pip/python installation (specifically with Debian/Ubuntu).
- These typically present themselves as:
-
- #. Not being able to find installed packages
- #. basic libraries not able to be found, fundamental OS components missing
- #. In these situations you will want to make sure that we know how they got
- their Python and pip. Knowing the relevant package manager commands can
- help, e.g. ``dpkg -S``.
diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst
index ee1595cec..188d3e87b 100644
--- a/docs/html/development/release-process.rst
+++ b/docs/html/development/release-process.rst
@@ -116,15 +116,17 @@ Release Process
Creating a new release
----------------------
-#. Checkout the current pip ``main`` branch.
#. Ensure you have the latest ``nox`` installed.
+#. Create a new ``release/YY.N`` branch off ``main`` and switch to it.
#. Prepare for release using ``nox -s prepare-release -- YY.N``.
This will update the relevant files and tag the correct commit.
+#. Submit the ``release/YY.N`` branch as a pull request and ensure CI passes.
+ Merge the changes back into ``main`` and pull them back locally.
#. Build the release artifacts using ``nox -s build-release -- YY.N``.
This will checkout the tag, generate the distribution files to be
uploaded and checkout the main branch again.
#. Upload the release to PyPI using ``nox -s upload-release -- YY.N``.
-#. Push all of the changes including the tag.
+#. Push the tag created by ``prepare-release``.
#. Regenerate the ``get-pip.py`` script in the `get-pip repository`_ (as
documented there) and commit the results.
#. Submit a Pull Request to `CPython`_ adding the new version of pip (and upgrading
@@ -168,7 +170,7 @@ order to create one of these the changes should already be merged into the
#. Push the ``release/YY.N.Z+1`` branch to github and submit a PR for it against
the ``main`` branch and wait for the tests to run.
#. Once tests run, merge the ``release/YY.N.Z+1`` branch into ``main``, and
- follow the above release process starting with step 4.
+ follow the above release process starting with step 5.
.. _`get-pip repository`: https://github.com/pypa/get-pip
.. _`psf-salt repository`: https://github.com/python/psf-salt
diff --git a/docs/html/getting-started.md b/docs/html/getting-started.md
index 5c22d1abe..0967b0eb9 100644
--- a/docs/html/getting-started.md
+++ b/docs/html/getting-started.md
@@ -81,10 +81,8 @@ Successfully installed sampleproject
```{pip-cli}
$ pip install --upgrade sampleproject
-Uninstalling sampleproject:
[...]
-Proceed (y/n)? y
-Successfully uninstalled sampleproject
+Successfully installed sampleproject
```
### Uninstall a package
@@ -93,7 +91,7 @@ Successfully uninstalled sampleproject
$ pip uninstall sampleproject
Uninstalling sampleproject:
[...]
-Proceed (y/n)? y
+Proceed (Y/n)? y
Successfully uninstalled sampleproject
```
diff --git a/docs/html/index.md b/docs/html/index.md
index 4b565b9a3..ab0b40dc1 100644
--- a/docs/html/index.md
+++ b/docs/html/index.md
@@ -14,6 +14,7 @@ getting-started
installation
user_guide
topics/index
+reference/index
cli/index
```
@@ -47,3 +48,5 @@ lists or chat rooms:
[packaging-discourse]: https://discuss.python.org/c/packaging/14
[irc-pypa]: https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa
[irc-pypa-dev]: https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa-dev
+
+If you find any security issues, please report to [security@python.org](mailto:security@python.org)
diff --git a/docs/html/installation.md b/docs/html/installation.md
index ecb71a4aa..7db0d47ab 100644
--- a/docs/html/installation.md
+++ b/docs/html/installation.md
@@ -14,7 +14,7 @@ If your Python environment does not have pip installed, there are 2 mechanisms
to install pip supported directly by pip's maintainers:
- [`ensurepip`](#ensurepip)
-- [`get-pip.py`](#get-pip-py)
+- [`get-pip.py`](#get-pippy)
### `ensurepip`
@@ -45,6 +45,34 @@ More details about this script can be found in [pypa/get-pip]'s README.
[pypa/get-pip]: https://github.com/pypa/get-pip
+### Standalone zip application
+
+```{note}
+The zip application is currently experimental. We test that pip runs correctly
+in this form, but it is possible that there could be issues in some situations.
+We will accept bug reports in such cases, but for now the zip application should
+not be used in production environments.
+```
+
+In addition to installing pip in your environment, pip is available as a
+standalone [zip application](https://docs.python.org/3.11/library/zipapp.html).
+This can be downloaded from <https://bootstrap.pypa.io/pip/pip.pyz>. There are
+also zip applications for specific pip versions, named `pip-X.Y.Z.pyz`.
+
+The zip application can be run using any supported version of Python:
+
+```{pip-cli}
+$ python pip.pyz --help
+```
+
+If run directly:
+
+```{pip-cli}
+$ pip.pyz --help
+```
+
+then the currently active Python interpreter will be used.
+
## Alternative Methods
Depending on how you installed Python, there might be other mechanisms
@@ -60,6 +88,14 @@ If you face issues when using Python and pip installed using these mechanisms,
it is recommended to request for support from the relevant provider (eg: Linux
distro community, cloud provider support channels, etc).
+## Upgrading `pip`
+
+Upgrading your `pip` by running:
+
+```{pip-cli}
+$ pip install --upgrade pip
+```
+
(compatibility-requirements)=
## Compatibility
@@ -67,14 +103,15 @@ distro community, cloud provider support channels, etc).
The current version of pip works on:
- Windows, Linux and MacOS.
-- CPython 3.6, 3.7, 3.8, 3.9 and latest PyPy3.
+- CPython 3.7, 3.8, 3.9, 3.10 and latest PyPy3.
pip is tested to work on the latest patch version of the Python interpreter,
for each of the minor versions listed above. Previous patch versions are
supported on a best effort approach.
-pip's maintainers do not provide support for users on older versions of Python,
-and these users should request for support from the relevant provider
-(eg: Linux distro community, cloud provider support channels, etc).
+Other operating systems and Python versions are not supported by pip's
+maintainers.
+
+Users who are on unsupported platforms should be aware that if they hit issues, they may have to resolve them for themselves. If they received pip from a source which provides support for their platform, they should request pip support from that source.
[^python]: The `ensurepip` module was added to the Python standard library in Python 3.4.
diff --git a/docs/html/news.rst b/docs/html/news.rst
index 829e6b74f..af1d10479 100644
--- a/docs/html/news.rst
+++ b/docs/html/news.rst
@@ -7,6 +7,6 @@ Changelog
Major and minor releases of pip also include changes listed within
prior beta releases.
-.. towncrier-draft-entries:: |release|, unreleased as on
+.. towncrier-draft-entries:: Not yet released
.. pip-news-include:: ../../NEWS.rst
diff --git a/docs/html/reference/build-system/index.md b/docs/html/reference/build-system/index.md
new file mode 100644
index 000000000..ed43fec37
--- /dev/null
+++ b/docs/html/reference/build-system/index.md
@@ -0,0 +1,127 @@
+(build-interface)=
+
+# Build System Interface
+
+When dealing with installable source distributions of a package, pip does not
+directly handle the build process for the package. This responsibility is
+delegated to "build backends" -- also known as "build systems". This means
+that pip needs an interface, to interact with these build backends.
+
+There are two main interfaces that pip uses for these interactions:
+
+```{toctree}
+:hidden:
+
+pyproject-toml
+setup-py
+```
+
+<!-- prettier-ignore-start -->
+[`pyproject.toml` based](pyproject-toml)
+: Standards-backed interface, that has explicit declaration and management of
+ build dependencies.
+
+[`setup.py` based](setup-py)
+: Legacy interface, that we're working to migrate users away from. Has no good
+ mechanisms to declare build dependencies.
+<!-- prettier-ignore-end -->
+
+Details on the individual interfaces can be found on their dedicated pages,
+linked above. This document covers the nuances around which build system
+interface pip will use for a project, as well as details that apply to all
+the build system interfaces that pip may use.
+
+## Determining which build system interface is used
+
+Currently, pip uses the `pyproject.toml` based build system interface, if a
+`pyproject.toml` file exists. If not, the legacy build system interface is used.
+The intention is to switch to using the `pyproject.toml` build system interface
+unconditionally and to drop support for the legacy build system interface at
+some point in the future.
+
+When performing a build, pip will mention which build system interface it is
+using. Typically, this will take the form of a message like:
+
+```none
+Building wheel for pip (pyproject.toml)... done
+```
+
+```none
+Building wheel for pip (setup.py)... done
+```
+
+The content in the brackets, refers to which build system interface is being
+used.
+
+```{versionchanged} 21.3
+The output uses "pyproject.toml" instead of "PEP 517" to refer to be
+`pyproject.toml` based build system interface.
+```
+
+## Controlling which build system interface is used
+
+The [`--use-pep517`](install_--use-pep517) flag (and corresponding environment
+variable: `PIP_USE_PEP517`) can be used to force all packages to build using
+the `pyproject.toml` based build system interface. There is no way to force
+the use of the legacy build system interface.
+
+(controlling-setup_requires)=
+
+## Controlling `setup_requires`
+
+```{hint}
+This is only relevant for projects that use setuptools as the build backend,
+and use the `setup_requires` keyword argument in their setup.py file.
+```
+
+The `setup_requires` argument in `setup.py` is used to specify build-time
+dependencies for a package. This has been superseded by the
+`build-system.requires` key in `pyproject.toml` files (per {pep}`518`).
+However, there are situations where you might encounter a package that uses
+`setup_requires` (eg: the package has not been updated to use the newer
+approach yet!).
+
+If you control the package, consider adding a `pyproject.toml` file to utilise
+the modern build system interface. That avoids invoking the problematic
+behaviour by deferring to pip for the installations.
+
+For the end users, the best solution for dealing with packages with
+`setup_requires` is to install the packages listed in `setup_requires`
+beforehand, using a prior `pip install` command. This is because there is no
+way to control how these dependencies are located by `easy_install`, or how
+setuptools will invoke `pip` using pip's command line options -- which makes it
+tricky to get things working appropriately.
+
+If you wish to ensure that `easy_install` invocations do not reach out to PyPI,
+you will need to configure its behaviour using a
+[`distutils` configuration file][distutils-config]. Here are some examples:
+
+- To have the dependency located at an alternate index with `easy_install`
+
+ ```ini
+ [easy_install]
+ index_url = https://my.index-mirror.com
+ ```
+
+- To have the dependency located from a local directory and not crawl PyPI, add this:
+
+ ```ini
+ [easy_install]
+ allow_hosts = ''
+ find_links = file:///path/to/local/archives/
+ ```
+
+```{admonition} Historical context
+`setuptools < 52.0` will use `easy_install` to try to fulfill `setup_requires`
+dependencies, which can result in weird failures -- `easy_install` does not
+understand many of the modern Python packaging standards, and will usually
+attempt to install incompatible package versions or to build packages
+incorrectly. It also generates improper script wrappers, which don't do the
+right thing in many situations.
+
+Newer versions of `setuptools` will use `pip` for these installations, but have
+limited ability to pass through any command line arguments. This can also result
+in weird failures and subtly-incorrect behaviour.
+```
+
+[distutils-config]: https://docs.python.org/3/install/index.html#distutils-configuration-files
diff --git a/docs/html/reference/build-system/pyproject-toml.md b/docs/html/reference/build-system/pyproject-toml.md
new file mode 100644
index 000000000..d2ec0323e
--- /dev/null
+++ b/docs/html/reference/build-system/pyproject-toml.md
@@ -0,0 +1,164 @@
+# `pyproject.toml`
+
+```{versionadded} 10.0
+
+```
+
+Modern Python packages can contain a `pyproject.toml` file, first introduced in
+{pep}`518` and later expanded in {pep}`517`, {pep}`621` and {pep}`660`.
+This file contains build system requirements and information, which are used by
+pip to build the package.
+
+## Build process
+
+The overall process for building a package is:
+
+- Create an isolated build environment.
+- Populate the build environment with build dependencies.
+- Generate the package's metadata, if necessary and possible.
+- Generate a wheel for the package.
+
+The wheel can then be used to perform an installation, if necessary.
+
+### Build Isolation
+
+For building packages using this interface, pip uses an _isolated environment_.
+That is, pip will install build-time Python dependencies in a temporary
+directory which will be added to `sys.path` for the build commands. This ensures
+that build requirements are handled independently of the user's runtime
+environment.
+
+For example, a project that needs an older version of setuptools to build can
+still be installed, even if the user has an newer version installed (and
+without silently replacing that version).
+
+### Build-time dependencies
+
+Introduced in {pep}`518`, the `build-system.requires` key in the
+`pyproject.toml` file is a list of requirement specifiers for build-time
+dependencies of a package.
+
+```toml
+[build-system]
+requires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]
+```
+
+It is also possible for a build backend to provide dynamically calculated
+build dependencies, using {pep}`517`'s `get_requires_for_build_wheel` hook. This
+hook will be called by pip, and dependencies it describes will also be installed
+in the build environment. For example, newer versions of setuptools expose the
+contents of `setup_requires` to pip via this hook.
+
+Build-time requirement specifiers follow {pep}`508`, so it's possible to
+reference packages with URLs. For example:
+
+```toml
+[build-system]
+requires = ["setuptools @ git+https://github.com/pypa/setuptools.git@main"]
+```
+
+### Metadata Generation
+
+```{versionadded} 19.0
+
+```
+
+Once the build environment has been created and populated with build-time
+dependencies, `pip` will usually need metadata about a package (name, version,
+dependencies, and more).
+
+If {pep}`517`'s `prepare_metadata_for_build_wheel` hook is provided by the
+build backend, that will be used to generate the packages' metadata. Otherwise,
+a wheel will be generated (as described below) and the metadata contained
+within such a wheel will be used.
+
+### Wheel Generation
+
+```{versionadded} 19.0
+
+```
+
+For generating a wheel, pip uses the {pep}`517` `build_wheel` hook that has
+to be provided by the build backend. The build backend will generate a wheel,
+which may involve compiling extension code written in C/C++ (or other
+languages).
+
+Wheels generated using this mechanism can be [cached](wheel-caching) for reuse,
+to speed up future installations.
+
+### Editable Installation
+
+```{versionadded} 21.3
+
+```
+
+For performing editable installs, pip will use {pep}`660`
+`build_wheel_for_editable` hook that has to be provided by the build backend.
+The wheels generated using this mechanism are not cached.
+
+```{admonition} Compatibility fallback
+If this hook is missing on the build backend _and_ there's a `setup.py` file
+in the project, pip will fallback to the legacy setup.py-based editable
+installation.
+
+This is considered a stopgap solution until setuptools adds support for
+{pep}`660`, at which point this functionality will be removed; following pip's
+regular {ref}`deprecation policy <Deprecation Policy>`.
+```
+
+### Backend Configuration
+
+Build backends have the ability to accept configuration settings, which can
+change the way the build is handled. These settings take the form of a
+series of `key=value` pairs. The user can supply configuration settings
+using the `--config-settings` command line option (which can be supplied
+multiple times, in order to specify multiple settings).
+
+The supplied configuration settings are passed to every backend hook call.
+
+## Build output
+
+It is the responsibility of the build backend to ensure that the output is
+in the correct encoding, as described in {pep}`517`. This likely involves
+dealing with [the same challenges as pip has for legacy builds](build-output).
+
+## Fallback Behaviour
+
+If a project does not have a `pyproject.toml` file containing a `build-system`
+section, it will be assumed to have the following backend settings:
+
+```toml
+[build-system]
+requires = ["setuptools>=40.8.0", "wheel"]
+build-backend = "setuptools.build_meta:__legacy__"
+```
+
+If a project has a `build-system` section but no `build-backend`, then:
+
+- It is expected to include `setuptools` and `wheel` as build requirements. An
+ error is reported if the available version of `setuptools` is not recent
+ enough.
+
+- The `setuptools.build_meta:__legacy__` build backend will be used.
+
+## Disabling build isolation
+
+This can be disabled using the `--no-build-isolation` flag -- users supplying
+this flag are responsible for ensuring the build environment is managed
+appropriately, including ensuring that all required build-time dependencies are
+installed, since pip does not manage build-time dependencies when this flag is
+passed.
+
+## Historical notes
+
+As this feature was incrementally rolled out, there have been various notable
+changes and improvements in it.
+
+- setuptools 40.8.0 is the first version of setuptools that offers a
+ {pep}`517` backend that closely mimics directly executing `setup.py`.
+- Prior to pip 18.0, pip only supports installing build requirements from
+ wheels, and does not support the use of environment markers and extras (only
+ version specifiers are respected).
+- Prior to pip 18.1, build dependencies using `.pth` files are not properly
+ supported; as a result namespace packages do not work under Python 3.2 and
+ earlier.
diff --git a/docs/html/reference/build-system/setup-py.md b/docs/html/reference/build-system/setup-py.md
new file mode 100644
index 000000000..53917b8a4
--- /dev/null
+++ b/docs/html/reference/build-system/setup-py.md
@@ -0,0 +1,133 @@
+# `setup.py` (legacy)
+
+Prior to the introduction of pyproject.toml-based builds (in {pep}`517` and
+{pep}`518`), pip had only supported installing packages using `setup.py` files
+that were built using {pypi}`setuptools`.
+
+The interface documented here is retained currently solely for legacy purposes,
+until the migration to `pyproject.toml`-based builds can be completed.
+
+```{caution}
+The arguments and syntax of the various invocations of `setup.py` made by
+pip, are considered an implementation detail that is strongly coupled with
+{pypi}`setuptools`. This build system interface is not meant to be used by any
+other build backend, which should be based on the {doc}`pyproject-toml` build
+system interface instead.
+
+Further, projects should _not_ expect to rely on there being any form of
+backward compatibility guarantees around the `setup.py` interface.
+```
+
+## Build process
+
+The overall process for building a package is:
+
+- Generate the package's metadata.
+- Generate a wheel for the package.
+ - If this fails and we're trying to install the package, attempt a direct
+ installation.
+
+The wheel can then be used to perform an installation, if necessary.
+
+### Metadata Generation
+
+As a first step, `pip` needs to get metadata about a package (name, version,
+dependencies, and more). It collects this by calling `setup.py egg_info`.
+
+The `egg_info` command generates the metadata for the package, which pip can
+then consume and proceed to gather all the dependencies of the package. Once
+the dependency resolution process is complete, pip will proceed to the next
+stage of the build process for these packages.
+
+### Wheel Generation
+
+When provided with a {term}`pypug:source distribution (or "sdist")` for a
+package, pip will attempt to build a {term}`pypug:wheel`. Since wheel
+distributions can be [cached](wheel-caching), this can greatly speed up future
+installations for the package.
+
+This is done by calling `setup.py bdist_wheel` which requires the {pypi}`wheel`
+package to be installed.
+
+If this wheel generation is successful (this can include compiling C/C++ code,
+depending on the package), the generated wheel is added to pip's wheel cache
+and will be used for this installation. The built wheel is cached locally
+by pip to avoid repeated identical builds.
+
+If this wheel generation fails, pip runs `setup.py clean` to clean up any build
+artifacts that may have been generated. After that, pip will attempt a direct
+installation.
+
+### Direct Installation
+
+When all else fails, pip will invoke `setup.py install` to install a package
+using setuptools' mechanisms to perform the installation. This is currently the
+last-resort fallback for projects that cannot be built into wheels, and may not
+be supported in the future.
+
+### Editable Installation
+
+For installing packages in "editable" mode
+({ref}`pip install --editable <install_--editable>`), pip will invoke
+`setup.py develop`, which will use setuptools' mechanisms to perform an
+editable/development installation.
+
+## Setuptools Injection
+
+To support projects that directly use `distutils`, pip injects `setuptools` into
+`sys.modules` before invoking `setup.py`. This injection should be transparent
+to `distutils`-based projects.
+
+## Customising the build
+
+The `--global-option` and `--build-option` arguments to the `pip install`
+and `pip wheel` inject additional arguments into the `setup.py` command
+(`--build-option` is only available in `pip wheel`).
+
+```{attention}
+The use of `--global-option` and `--build-option` is highly setuptools
+specific, and is considered more an accident of the current implementation than
+a supported interface. It is documented here for completeness. These flags will
+not be supported, once this build system interface is dropped.
+```
+
+These arguments are included in the command as follows:
+
+```
+python setup.py <global_options> BUILD COMMAND <build_options>
+```
+
+The options are passed unmodified, and presently offer direct access to the
+distutils command line. For example:
+
+```{pip-cli}
+$ pip wheel --global-option bdist_ext --global-option -DFOO wheel
+```
+
+will result in pip invoking:
+
+```
+setup.py bdist_ext -DFOO bdist_wheel -d TARGET
+```
+
+This passes a preprocessor symbol to the extension build.
+
+(build-output)=
+
+## Build Output
+
+Any output produced by the build system will be read by pip (for display to the
+user if requested). In order to correctly read the build system output, pip
+requires that the output is written in a well-defined encoding, specifically
+the encoding the user has configured for text output (which can be obtained in
+Python using `locale.getpreferredencoding`). If the configured encoding is
+ASCII, pip assumes UTF-8 (to account for the behaviour of some Unix systems).
+
+Build systems should ensure that any tools they invoke (compilers, etc) produce
+output in the correct encoding. In practice - and in particular on Windows,
+where tools are inconsistent in their use of the "OEM" and "ANSI" codepages -
+this may not always be possible. pip will therefore attempt to recover cleanly
+if presented with incorrectly encoded build tool output, by translating
+unexpected byte sequences to Python-style hexadecimal escape sequences
+(`"\x80\xff"`, etc). However, it is still possible for output to be displayed
+using an incorrect encoding (mojibake).
diff --git a/docs/html/reference/index.md b/docs/html/reference/index.md
new file mode 100644
index 000000000..63ce1ca4d
--- /dev/null
+++ b/docs/html/reference/index.md
@@ -0,0 +1,14 @@
+# Reference
+
+Reference provides information about various file formats, interfaces and
+interoperability standards that pip utilises/implements.
+
+```{toctree}
+:titlesonly:
+
+build-system/index
+requirement-specifiers
+requirements-file-format
+installation-report
+inspect-report
+```
diff --git a/docs/html/reference/index.rst b/docs/html/reference/index.rst
deleted file mode 100644
index 5e81105c9..000000000
--- a/docs/html/reference/index.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-:orphan:
-
-.. meta::
-
- :http-equiv=refresh: 3; url=../cli/
-
-This page has moved
-===================
-
-You should be redirected automatically in 3 seconds. If that didn't
-work, here's a link: :doc:`../cli/index`
diff --git a/docs/html/reference/inspect-report.md b/docs/html/reference/inspect-report.md
new file mode 100644
index 000000000..4d367da9d
--- /dev/null
+++ b/docs/html/reference/inspect-report.md
@@ -0,0 +1,217 @@
+# `pip inspect` JSON output specification
+
+```{versionadded} 22.2
+```
+
+The `pip inspect` command produces a detailed JSON report of the Python
+environment, including installed distributions.
+
+## Specification
+
+The report is a JSON object with the following properties:
+
+- `version`: the string `0`, denoting that the inspect command is an experimental
+ feature. This value will change to `1`, when the feature is deemed stable after
+ gathering user feedback (likely in pip 22.3 or 23.0). Backward incompatible changes
+ may be introduced in version `1` without notice. After that, it will change only if
+ and when backward incompatible changes are introduced, such as removing mandatory
+ fields or changing the semantics or data type of existing fields. The introduction of
+ backward incompatible changes will follow the usual pip processes such as the
+ deprecation cycle or feature flags. Tools must check this field to ensure they support
+ the corresponding version.
+
+- `pip_version`: a string with the version of pip used to produce the report.
+
+- `installed`: an array of [InspectReportItem](InspectReportItem) representing the
+ distribution packages that are installed.
+
+- `environment`: an object describing the environment where the installation report was
+ generated. See [PEP 508 environment
+ markers](https://peps.python.org/pep-0508/#environment-markers) for more information.
+ Values have a string type.
+
+(InspectReportItem)=
+
+An `InspectReportItem` is an object describing an installed distribution package with
+the following properties:
+
+- `metadata`: the metadata of the distribution, converted to a JSON object according to
+ the [PEP 566
+ transformation](https://www.python.org/dev/peps/pep-0566/#json-compatible-metadata).
+
+- `metadata_location`: the location of the metadata of the installed distribution. Most
+ of the time this is the `.dist-info` directory. For legacy installs it is the
+ `.egg-info` directory.
+
+ ```{warning}
+ This field may not necessary point to a directory, for instance, in the case of older
+ `.egg` installs.
+ ```
+
+- `direct_url`: Information about the direct URL that was used for installation, if any,
+ using the [direct
+ URL](https://packaging.python.org/en/latest/specifications/direct-url/) data
+ structure. In most case, this field corresponds to the `direct_url.json` metadata,
+ except for legacy editable installs, where it is emulated.
+
+- `requested`: `true` if the `REQUESTED` metadata is present, `false` otherwise. This
+ field is only present for modern `.dist-info` installations.
+
+ ```{note}
+ The `REQUESTED` metadata may not be generated by all installers.
+ It is generated by pip since version 20.2.
+ ```
+
+- `installer`: the content of the `INSTALLER` metadata, if present and not empty.
+
+## Example
+
+Running the ``pip inspect`` command, in an environment where `pip` is installed in
+editable mode and `packaging` is installed as well, will produce an output similar to
+this (metadata abriged for brevity):
+
+```json
+{
+ "version": "0",
+ "pip_version": "22.2.dev0",
+ "installed": [
+ {
+ "metadata": {
+ "metadata_version": "2.1",
+ "name": "pyparsing",
+ "version": "3.0.9",
+ "summary": "pyparsing module - Classes and methods to define and execute parsing grammars",
+ "description_content_type": "text/x-rst",
+ "author_email": "Paul McGuire <ptmcg.gm+pyparsing@gmail.com>",
+ "classifier": [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Information Technology",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Typing :: Typed"
+ ],
+ "requires_dist": [
+ "railroad-diagrams ; extra == \"diagrams\"",
+ "jinja2 ; extra == \"diagrams\""
+ ],
+ "requires_python": ">=3.6.8",
+ "project_url": [
+ "Homepage, https://github.com/pyparsing/pyparsing/"
+ ],
+ "provides_extra": [
+ "diagrams"
+ ],
+ "description": "..."
+ },
+ "metadata_location": "/home/me/.virtualenvs/demoenv/lib/python3.8/site-packages/pyparsing-3.0.9.dist-info",
+ "installer": "pip",
+ "requested": false
+ },
+ {
+ "metadata": {
+ "metadata_version": "2.1",
+ "name": "packaging",
+ "version": "21.3",
+ "platform": [
+ "UNKNOWN"
+ ],
+ "summary": "Core utilities for Python packages",
+ "description_content_type": "text/x-rst",
+ "home_page": "https://github.com/pypa/packaging",
+ "author": "Donald Stufft and individual contributors",
+ "author_email": "donald@stufft.io",
+ "license": "BSD-2-Clause or Apache-2.0",
+ "classifier": [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "License :: OSI Approved :: BSD License",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy"
+ ],
+ "requires_dist": [
+ "pyparsing (!=3.0.5,>=2.0.2)"
+ ],
+ "requires_python": ">=3.6",
+ "description": "..."
+ },
+ "metadata_location": "/home/me/.virtualenvs/demoenv/lib/python3.8/site-packages/packaging-21.3.dist-info",
+ "installer": "pip",
+ "requested": true
+ },
+ {
+ "metadata": {
+ "metadata_version": "2.1",
+ "name": "pip",
+ "version": "22.2.dev0",
+ "summary": "The PyPA recommended tool for installing Python packages.",
+ "home_page": "https://pip.pypa.io/",
+ "author": "The pip developers",
+ "author_email": "distutils-sig@python.org",
+ "license": "MIT",
+ "classifier": [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Topic :: Software Development :: Build Tools",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy"
+ ],
+ "requires_python": ">=3.7",
+ "project_url": [
+ "Documentation, https://pip.pypa.io",
+ "Source, https://github.com/pypa/pip",
+ "Changelog, https://pip.pypa.io/en/stable/news/"
+ ],
+ "description": "..."
+ },
+ "metadata_location": "/home/me/pip/src/pip.egg-info",
+ "direct_url": {
+ "url": "file:///home/me/pip/src",
+ "dir_info": {
+ "editable": true
+ }
+ }
+ }
+ ],
+ "environment": {
+ "implementation_name": "cpython",
+ "implementation_version": "3.8.10",
+ "os_name": "posix",
+ "platform_machine": "x86_64",
+ "platform_release": "5.13-generic",
+ "platform_system": "Linux",
+ "platform_version": "...",
+ "python_full_version": "3.8.10",
+ "platform_python_implementation": "CPython",
+ "python_version": "3.8",
+ "sys_platform": "linux"
+ }
+}
+```
diff --git a/docs/html/reference/installation-report.md b/docs/html/reference/installation-report.md
new file mode 100644
index 000000000..fff37242d
--- /dev/null
+++ b/docs/html/reference/installation-report.md
@@ -0,0 +1,192 @@
+# Installation Report
+
+```{versionadded} 22.2
+```
+
+The `--report` option of the pip install command produces a detailed JSON report of what
+it did install (or what it would have installed, if used with the `--dry-run` option).
+
+```{note}
+When considering use cases, please bear in mind that
+
+- while the `--report` option may be used to implement requirement locking tools (among
+ other use cases), this format is *not* meant to be a lock file format as such;
+- there is no plan for pip to accept an installation report as input for the `install`,
+ `download` or `wheel` commands;
+- the `--report` option and this format is intended to become a supported pip feature
+ (when the format is stabilized to version 1);
+- it is however *not* a PyPA interoperability standard and as such its evolution will be
+ governed by the pip processes and not the PyPA standardization processes.
+```
+
+## Specification
+
+The report is a JSON object with the following properties:
+
+- `version`: the string `0`, denoting that the installation report is an experimental
+ feature. This value will change to `1`, when the feature is deemed stable after
+ gathering user feedback (likely in pip 22.3 or 23.0). Backward incompatible changes
+ may be introduced in version `1` without notice. After that, it will change only if
+ and when backward incompatible changes are introduced, such as removing mandatory
+ fields or changing the semantics or data type of existing fields. The introduction of
+ backward incompatible changes will follow the usual pip processes such as the
+ deprecation cycle or feature flags. Tools must check this field to ensure they support
+ the corresponding version.
+
+- `pip_version`: a string with the version of pip used to produce the report.
+
+- `install`: an array of [InstallationReportItem](InstallationReportItem) representing
+ the distribution packages (to be) installed.
+
+- `environment`: an object describing the environment where the installation report was
+ generated. See [PEP 508 environment
+ markers](https://peps.python.org/pep-0508/#environment-markers) for more information.
+ Values have a string type.
+
+(InstallationReportItem)=
+
+An `InstallationReportItem` is an object describing a (to be) installed distribution
+package with the following properties:
+
+- `metadata`: the metadata of the distribution, converted to a JSON object according to
+ the [PEP 566
+ transformation](https://www.python.org/dev/peps/pep-0566/#json-compatible-metadata).
+
+- `is_direct`: `true` if the requirement was provided as, or constrained to, a direct
+ URL reference. `false` if the requirements was provided as a name and version
+ specifier.
+
+- `download_info`: Information about the artifact (to be) downloaded for installation,
+ using the [direct
+ URL](https://packaging.python.org/en/latest/specifications/direct-url/) data
+ structure. When `is_direct` is `true`, this field is the same as the `direct_url.json`
+ metadata, otherwise it represents the URL of the artifact obtained from the index or
+ `--find-links`.
+
+ ```{note}
+ For source archives, `download_info.archive_info.hash` may
+ be absent when the requirement was installed from the wheel cache
+ and the cache entry was populated by an older pip version that did not
+ record the origin URL of the downloaded artifact.
+ ```
+
+- `requested`: `true` if the requirement was explicitly provided by the user, either
+ directely via a command line argument or indirectly via a requirements file. `false`
+ if the requirement was installed as a dependency of another requirement.
+
+- `requested_extras`: extras requested by the user. This field is only present when the
+ `requested` field is true.
+
+## Example
+
+The following command:
+
+```console
+pip install \
+ --ignore-installed --dry-run --quiet \
+ --report - \
+ "pydantic>=1.9" git+https://github.com/pypa/packaging@main
+```
+
+will produce an output similar to this (metadata abriged for brevity):
+
+```json
+{
+ "version": "0",
+ "pip_version": "22.2",
+ "install": [
+ {
+ "download_info": {
+ "url": "https://files.pythonhosted.org/packages/a4/0c/fbaa7319dcb5eecd3484686eb5a5c5702a6445adb566f01aee6de3369bc4/pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ "archive_info": {
+ "hash": "sha256=18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"
+ }
+ },
+ "is_direct": false,
+ "requested": true,
+ "metadata": {
+ "name": "pydantic",
+ "version": "1.9.1",
+ "requires_dist": [
+ "typing-extensions (>=3.7.4.3)",
+ "dataclasses (>=0.6) ; python_version < \"3.7\"",
+ "python-dotenv (>=0.10.4) ; extra == 'dotenv'",
+ "email-validator (>=1.0.3) ; extra == 'email'"
+ ],
+ "requires_python": ">=3.6.1",
+ "provides_extra": [
+ "dotenv",
+ "email"
+ ]
+ }
+ },
+ {
+ "download_info": {
+ "url": "https://github.com/pypa/packaging",
+ "vcs_info": {
+ "vcs": "git",
+ "requested_revision": "main",
+ "commit_id": "4f42225e91a0be634625c09e84dd29ea82b85e27"
+ }
+ },
+ "is_direct": true,
+ "requested": true,
+ "metadata": {
+ "name": "packaging",
+ "version": "21.4.dev0",
+ "requires_dist": [
+ "pyparsing (!=3.0.5,>=2.0.2)"
+ ],
+ "requires_python": ">=3.7"
+ }
+ },
+ {
+ "download_info": {
+ "url": "https://files.pythonhosted.org/packages/6c/10/a7d0fa5baea8fe7b50f448ab742f26f52b80bfca85ac2be9d35cdd9a3246/pyparsing-3.0.9-py3-none-any.whl",
+ "archive_info": {
+ "hash": "sha256=5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
+ }
+ },
+ "is_direct": false,
+ "requested": false,
+ "metadata": {
+ "name": "pyparsing",
+ "version": "3.0.9",
+ "requires_dist": [
+ "railroad-diagrams ; extra == \"diagrams\"",
+ "jinja2 ; extra == \"diagrams\""
+ ],
+ "requires_python": ">=3.6.8"
+ }
+ },
+ {
+ "download_info": {
+ "url": "https://files.pythonhosted.org/packages/75/e1/932e06004039dd670c9d5e1df0cd606bf46e29a28e65d5bb28e894ea29c9/typing_extensions-4.2.0-py3-none-any.whl",
+ "archive_info": {
+ "hash": "sha256=6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"
+ }
+ },
+ "is_direct": false,
+ "requested": false,
+ "metadata": {
+ "name": "typing_extensions",
+ "version": "4.2.0",
+ "requires_python": ">=3.7"
+ }
+ }
+ ],
+ "environment": {
+ "implementation_name": "cpython",
+ "implementation_version": "3.10.5",
+ "os_name": "posix",
+ "platform_machine": "x86_64",
+ "platform_release": "5.13-generic",
+ "platform_system": "Linux",
+ "platform_version": "...",
+ "python_full_version": "3.10.5",
+ "platform_python_implementation": "CPython",
+ "python_version": "3.10",
+ "sys_platform": "linux"
+ }
+}
+```
diff --git a/docs/html/reference/requirement-specifiers.md b/docs/html/reference/requirement-specifiers.md
new file mode 100644
index 000000000..d1449e5ef
--- /dev/null
+++ b/docs/html/reference/requirement-specifiers.md
@@ -0,0 +1,61 @@
+(Requirement Specifiers)=
+
+# Requirement Specifiers
+
+pip supports installing from a package index using a {term}`requirement specifier <pypug:Requirement Specifier>`. Generally speaking, a requirement specifier is composed of a project name followed by optional {term}`version specifiers <pypug:Version Specifier>`.
+
+{pep}`508` contains a full specification of the format of a requirement.
+
+```{versionadded} 6.0
+Support for environment markers.
+```
+
+```{versionadded} 19.1
+Support for the direct URL reference form.
+```
+
+## Overview
+
+A requirement specifier comes in two forms:
+
+- name-based, which is composed of:
+
+ - a package name (eg: `requests`)
+ - optionally, a set of "extras" that serve to install optional dependencies (eg: `security`)
+ - optionally, constraints to apply on the version of the package
+ - optionally, environment markers
+
+- URL-based, which is composed of:
+
+ - a package name (eg: `requests`)
+ - optionally, a set of "extras" that serve to install optional dependencies (eg: `security`)
+ - a URL for the package
+ - optionally, environment markers
+
+## Examples
+
+A few example name-based requirement specifiers:
+
+```
+SomeProject
+SomeProject == 1.3
+SomeProject >= 1.2, < 2.0
+SomeProject[foo, bar]
+SomeProject ~= 1.4.2
+SomeProject == 5.4 ; python_version < '3.8'
+SomeProject ; sys_platform == 'win32'
+requests [security] >= 2.8.1, == 2.8.* ; python_version < "2.7"
+```
+
+```{note}
+Use quotes around specifiers in the shell when using `>`, `<`, or when using environment markers.
+
+Do _not_ use quotes in requirement files. There is only one exception: pip v7.0 and v7.0.1 (from May 2015) required quotes around specifiers containing environment markers in requirement files.
+```
+
+A few example URL-based requirement specifiers:
+
+```none
+pip @ https://github.com/pypa/pip/archive/22.0.2.zip
+requests [security] @ https://github.com/psf/requests/archive/refs/heads/main.zip ; python_version >= "3.11"
+```
diff --git a/docs/html/reference/requirements-file-format.md b/docs/html/reference/requirements-file-format.md
new file mode 100644
index 000000000..75e6d0b1e
--- /dev/null
+++ b/docs/html/reference/requirements-file-format.md
@@ -0,0 +1,185 @@
+(requirements-file-format)=
+
+# Requirements File Format
+
+Requirements files serve as a list of items to be installed by pip, when
+using {ref}`pip install`. Files that use this format are often called
+"pip requirements.txt files", since `requirements.txt` is usually what
+these files are named (although, that is not a requirement).
+
+```{note}
+The requirements file format is closely tied to a number of internal details of
+pip (e.g., pip's command line options). The basic format is relatively stable
+and portable but the full syntax, as described here, is only intended for
+consumption by pip, and other tools should take that into account before using
+it for their own purposes.
+```
+
+## Example
+
+```
+# This is a comment, to show how #-prefixed lines are ignored.
+# It is possible to specify requirements as plain names.
+pytest
+pytest-cov
+beautifulsoup4
+
+# The syntax supported here is the same as that of requirement specifiers.
+docopt == 0.6.1
+requests [security] >= 2.8.1, == 2.8.* ; python_version < "2.7"
+urllib3 @ https://github.com/urllib3/urllib3/archive/refs/tags/1.26.8.zip
+
+# It is possible to refer to other requirement files or constraints files.
+-r other-requirements.txt
+-c constraints.txt
+
+# It is possible to refer to specific local distribution paths.
+./downloads/numpy-1.9.2-cp34-none-win32.whl
+
+# It is possible to refer to URLs.
+http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl
+```
+
+## Structure
+
+Each line of the requirements file indicates something to be installed,
+or arguments to {ref}`pip install`. The following forms are supported:
+
+- `[[--option]...]`
+- `<requirement specifier>`
+- `<archive url/path>`
+- `[-e] <local project path>`
+- `[-e] <vcs project url>`
+
+For details on requirement specifiers, see {ref}`Requirement Specifiers`. For
+examples of all these forms, see {ref}`pip install Examples`.
+
+### Encoding
+
+Requirements files are `utf-8` encoding by default and also support
+{pep}`263` style comments to change the encoding (i.e.
+`# -*- coding: <encoding name> -*-`).
+
+### Line continuations
+
+A line ending in an unescaped `\` is treated as a line continuation
+and the newline following it is effectively ignored.
+
+### Comments
+
+A line that begins with `#` is treated as a comment and ignored. Whitespace
+followed by a `#` causes the `#` and the remainder of the line to be
+treated as a comment.
+
+Comments are stripped _after_ line continuations are processed.
+
+## Supported options
+
+Requirements files only supports certain pip install options, which are listed
+below.
+
+### Global options
+
+The following options have an effect on the _entire_ `pip install` run, and
+must be specified on their individual lines.
+
+```{eval-rst}
+.. pip-requirements-file-options-ref-list::
+```
+
+````{admonition} Example
+To specify {ref}`--pre <install_--pre>`, {ref}`--no-index <install_--no-index>`
+and two {ref}`--find-links <install_--find-links>` locations:
+
+```
+--pre
+--no-index
+--find-links /my/local/archives
+--find-links http://some.archives.com/archives
+```
+````
+
+(per-requirement-options)=
+
+### Per-requirement options
+
+```{versionadded} 7.0
+
+```
+
+The options which can be applied to individual requirements are:
+
+- {ref}`--install-option <install_--install-option>`
+- {ref}`--global-option <install_--global-option>`
+- {ref}`--config-settings <install_--config-settings>`
+- `--hash` (for {ref}`Hash-checking mode`)
+
+## Referring to other requirements files
+
+If you wish, you can refer to other requirements files, like this:
+
+```
+-r more_requirements.txt
+```
+
+You can also refer to {ref}`constraints files <Constraints Files>`, like this:
+
+```
+-c some_constraints.txt
+```
+
+## Using environment variables
+
+```{versionadded} 10.0
+
+```
+
+pip supports the use of environment variables inside the
+requirements file.
+
+You have to use the POSIX format for variable names including brackets around
+the uppercase name as shown in this example: `${API_TOKEN}`. pip will attempt
+to find the corresponding environment variable defined on the host system at
+runtime.
+
+```{note}
+There is no support for other variable expansion syntaxes such as `$VARIABLE`
+and `%VARIABLE%`.
+```
+
+You can now store sensitive data (tokens, keys, etc.) in environment variables
+and only specify the variable name for your requirements, letting pip lookup
+the value at runtime. This approach aligns with the commonly used
+[12-factor configuration pattern](https://12factor.net/config).
+
+
+## Influencing the build system
+
+```{danger}
+This disables the use of wheels (cached or otherwise). This could mean that builds will be slower, less deterministic, less reliable and may not behave correctly upon installation.
+
+This mechanism is only preserved for backwards compatibility and should be considered deprecated. A future release of pip may drop these options.
+```
+
+The `--global-option` and `--install-option` options are used to pass options to `setup.py`.
+
+```{attention}
+These options are highly coupled with how pip invokes setuptools using the {doc}`../reference/build-system/setup-py` build system interface. It is not compatible with newer {doc}`../reference/build-system/pyproject-toml` build system interface.
+
+This is will not work with other build-backends or newer setup.cfg-only projects.
+```
+
+If you have a declaration like:
+
+ FooProject >= 1.2 --global-option="--no-user-cfg" \
+ --install-option="--prefix='/usr/local'" \
+ --install-option="--no-compile"
+
+The above translates roughly into running FooProject's `setup.py` script as:
+
+ python setup.py --no-user-cfg install --prefix='/usr/local' --no-compile
+
+Note that the only way of giving more than one option to `setup.py` is through multiple `--global-option` and `--install-option` options, as shown in the example above. The value of each option is passed as a single argument to the `setup.py` script. Therefore, a line such as the following is invalid and would result in an installation error.
+
+ # Invalid. Please use '--install-option' twice as shown above.
+ FooProject >= 1.2 --install-option="--prefix=/usr/local --no-compile"
diff --git a/docs/html/topics/caching.md b/docs/html/topics/caching.md
index 0f4dfe9b9..929ac3541 100644
--- a/docs/html/topics/caching.md
+++ b/docs/html/topics/caching.md
@@ -1,6 +1,7 @@
# Caching
```{versionadded} 6.0
+
```
pip provides an on-by-default caching, designed to reduce the amount of time
@@ -26,6 +27,8 @@ While this cache attempts to minimize network activity, it does not prevent
network access altogether. If you want a local install solution that
circumvents accessing PyPI, see {ref}`Installing from local packages`.
+(wheel-caching)=
+
### Locally built wheels
pip attempts to use wheels from its local wheel cache whenever possible.
@@ -38,11 +41,52 @@ wheel using the package's build system. If the build is successful, this wheel
is added to the cache and used in subsequent installs for the same package
version.
+Wheels built from source distributions provided to pip as a direct path (such
+as `pip install .`) are not cached across runs, though they may be reused within
+the same `pip` execution.
+
```{versionchanged} 20.0
pip now caches wheels when building from an immutable Git reference
(i.e. a commit hash).
```
+## Where is the cache stored
+
+```{caution}
+The exact filesystem structure of pip's cache's contents is considered to be
+an implementation detail and may change between any two versions of pip.
+```
+
+### `pip cache dir`
+
+```{versionadded} 20.1
+
+```
+
+You can use `pip cache dir` to get the cache directory that pip is currently configured to use.
+
+### Default paths
+
+````{tab} Unix
+```
+~/.cache/pip
+```
+
+pip will also respect `XDG_CACHE_HOME`.
+````
+
+````{tab} MacOS
+```
+~/Library/Caches/pip
+```
+````
+
+````{tab} Windows
+```
+%LocalAppData%\pip\Cache
+```
+````
+
## Avoiding caching
pip tries to use its cache whenever possible, and it is designed do the right
@@ -74,12 +118,27 @@ It is also a good idea to remove the offending cached wheel using the
The {ref}`pip cache` command can be used to manage pip's cache.
-The exact filesystem structure of pip's cache is considered to be an
-implementation detail and may change between any two versions of pip.
+### General overview
+
+`pip cache info` provides an overview of the contents of pip's cache, such as the total size and location of various parts of it.
+
+### Removing a single package
+
+`pip cache remove setuptools` removes all wheel files related to setuptools from pip's cache.
+
+### Removing the cache
+
+`pip cache purge` will clear all wheel files from pip's cache.
+
+### Listing cached files
+
+`pip cache list` will list all wheel files from pip's cache.
+
+`pip cache list setuptools` will list all setuptools-related wheel files from pip's cache.
## Disabling caching
-pip's caching behaviour is disabled by passing the ``--no-cache-dir`` option.
+pip's caching behaviour is disabled by passing the `--no-cache-dir` option.
It is, however, recommended to **NOT** disable pip's caching. Doing so can
significantly slow down pip (due to repeated operations and package builds)
diff --git a/docs/html/topics/configuration.md b/docs/html/topics/configuration.md
index eb392ecd4..e4aafcd2b 100644
--- a/docs/html/topics/configuration.md
+++ b/docs/html/topics/configuration.md
@@ -1,3 +1,5 @@
+(configuration)=
+
# Configuration
pip allows a user to change its behaviour via 3 mechanisms:
@@ -9,6 +11,12 @@ pip allows a user to change its behaviour via 3 mechanisms:
This page explains how the configuration files and environment variables work,
and how they are related to pip's various command line options.
+```{seealso}
+{doc}`../cli/pip_config` command, which helps manage pip's configuration.
+```
+
+(config-file)=
+
## Configuration Files
Configuration files can change the default values for command line option.
@@ -29,11 +37,10 @@ complexity for backwards compatibility reasons.
```{tab} Unix
Global
-: {file}`/etc/pip.conf`
+: In a "pip" subdirectory of any of the paths set in the environment variable
+ `XDG_CONFIG_DIRS` (if it exists), for example {file}`/etc/xdg/pip/pip.conf`.
- Alternatively, it may be in a "pip" subdirectory of any of the paths set
- in the environment variable `XDG_CONFIG_DIRS` (if it exists), for
- example {file}`/etc/xdg/pip/pip.conf`.
+ This will be followed by loading {file}`/etc/pip.conf`.
User
: {file}`$HOME/.config/pip/pip.conf`, which respects the `XDG_CONFIG_HOME` environment variable.
@@ -85,6 +92,8 @@ a configuration file that's loaded first, and whose values are overridden by
the values set in the aforementioned files. Setting this to {any}`os.devnull`
disables the loading of _all_ configuration files.
+(config-precedence)=
+
### Loading order
When multiple configuration files are found, pip combines them in the following
@@ -214,9 +223,9 @@ Use `no`, `false` or `0` instead.
## Precedence / Override order
-Command line options have override environment variables, which override the
+Command line options override environment variables, which override the
values in a configuration file. Within the configuration file, values in
-command-specific sections over values in the global section.
+command-specific sections override values in the global section.
Examples:
diff --git a/docs/html/topics/dependency-resolution.md b/docs/html/topics/dependency-resolution.md
index 0394e1a1d..6d02866a7 100644
--- a/docs/html/topics/dependency-resolution.md
+++ b/docs/html/topics/dependency-resolution.md
@@ -106,3 +106,214 @@ Backtracking reduces the risk that installing a new package will accidentally
break an existing installed package, and so reduces the risk that your
environment gets messed up. To do this, pip has to do more work, to find out
which version of a package is a good candidate to install.
+
+## Possible ways to reduce backtracking
+
+There is no one-size-fits-all answer to situations where pip is backtracking
+excessively during dependency resolution. There are ways to reduce the
+degree to which pip might backtrack though. Nearly all of these approaches
+require some amount of trial and error.
+
+### Allow pip to complete its backtracking
+
+In most cases, pip will complete the backtracking process successfully.
+This could take a very long time to complete, so this may not be your
+preferred option.
+
+However, it is a possible that pip will not be able to find a set of
+compatible versions. For this, pip will try every possible combination that
+it needs to and determine that there is no compatible set.
+
+If you'd prefer not to wait, you can interrupt pip (Ctrl+c) and try the
+strategies listed below.
+
+### Reduce the number of versions pip is trying to use
+
+It is usually a good idea to add constraints the package(s) that pip is backtracking on (e.g. in the above example - `cup`).
+
+You could try something like:
+
+```{pip-cli}
+$ pip install tea "cup >= 3.13"
+```
+
+This will reduce the number of versions of `cup` it tries, and
+possibly reduce the time pip takes to install.
+
+There is a possibility that the addition constraint is incorrect. When this
+happens, the reduced search space makes it easier for pip to more quickly
+determine what caused the conflict and present that to the user. It could also
+result in pip backtracking on a different package due to some other conflict.
+
+### Use constraint files or lockfiles
+
+This option is a progression of the previous section. It requires users to know
+how to inspect:
+
+- the packages they're trying to install
+- the package release frequency and compatibility policies
+- their release notes and changelogs from past versions
+
+During deployment, you can create a lockfile stating the exact package and
+version number for for each dependency of that package. You can create this
+with [pip-tools](https://github.com/jazzband/pip-tools/).
+
+This means the "work" is done once during development process, and thus
+will avoid performing dependency resolution during deployment.
+
+(Fixing conflicting dependencies)=
+
+## Dealing with dependency conflicts
+
+This section provides practical suggestions to pip users who encounter
+a `ResolutionImpossible` error, where pip cannot install their specified
+packages due to conflicting dependencies.
+
+### Understanding your error message
+
+When you get a `ResolutionImpossible` error, you might see something
+like this:
+
+```{pip-cli}
+$ pip install "pytest < 4.6" pytest-cov==2.12.1
+[regular pip output]
+ERROR: Cannot install pytest-cov==2.12.1 and pytest<4.6 because these package versions have conflicting dependencies.
+
+The conflict is caused by:
+ The user requested pytest<4.6
+ pytest-cov 2.12.1 depends on pytest>=4.6
+```
+
+In this example, pip cannot install the packages requested because they are
+asking for conflicting versions of pytest.
+
+- `pytest-cov` version `2.12.1`, requires `pytest` with a version or equal to
+ `4.6`.
+- `package_tea` version `4.3.0` depends on version `2.3.1` of
+ `package_water`
+
+Sometimes these messages are straightforward to read, because they use
+commonly understood comparison operators to specify the required version
+(e.g. `<` or `>`).
+
+However, Python packaging also supports some more complex ways for
+specifying package versions (e.g. `~=` or `*`):
+
+| Operator | Description | Example |
+| -------- | -------------------------------------------------------------- | --------------------------------------------------- |
+| `>` | Any version greater than the specified version. | `>3.1`: any version greater than `3.1`. |
+| `<` | Any version less than the specified version. | `<3.1`: any version less than `3.1`. |
+| `<=` | Any version less than or equal to the specified version. | `<=3.1`: any version less than or equal to `3.1`. |
+| `>=` | Any version greater than or equal to the specified version. | `>=3.1`: version `3.1` and greater. |
+| `==` | Exactly the specified version. | `==3.1`: only `3.1`. |
+| `!=` | Any version not equal to the specified version. | `!=3.1`: any version other than `3.1`. |
+| `~=` | Any compatible{sup}`1` version. | `~=3.1`: any version compatible{sup}`1` with `3.1`. |
+| `*` | Can be used at the end of a version number to represent _all_. | `==3.1.*`: any version that starts with `3.1`. |
+
+{sup}`1` Compatible versions are higher versions that only differ in the final segment.
+`~=3.1.2` is equivalent to `>=3.1.2, ==3.1.*`. `~=3.1` is equivalent to `>=3.1, ==3.*`.
+
+The detailed specification of supported comparison operators can be
+found in {pep}`440`.
+
+### Possible solutions
+
+The solution to your error will depend on your individual use case. Here
+are some things to try:
+
+#### Audit your top level requirements
+
+As a first step, it is useful to audit your project and remove any
+unnecessary or out of date requirements (e.g. from your `setup.py` or
+`requirements.txt` files). Removing these can significantly reduce the
+complexity of your dependency tree, thereby reducing opportunities for
+conflicts to occur.
+
+#### Loosen your top level requirements
+
+Sometimes the packages that you have asked pip to install are
+incompatible because you have been too strict when you specified the
+package version.
+
+In our first example both `package_coffee` and `package_tea` have been
+_pinned_ to use specific versions
+(`package_coffee==0.44.1b0 package_tea==4.3.0`).
+
+To find a version of both `package_coffee` and `package_tea` that depend on
+the same version of `package_water`, you might consider:
+
+- Loosening the range of packages that you are prepared to install
+ (e.g. `pip install "package_coffee>0.44.*" "package_tea>4.0.0"`)
+- Asking pip to install _any_ version of `package_coffee` and `package_tea`
+ by removing the version specifiers altogether (e.g.
+ `pip install package_coffee package_tea`)
+
+In the second case, pip will automatically find a version of both
+`package_coffee` and `package_tea` that depend on the same version of
+`package_water`, installing:
+
+- `package_coffee 0.46.0b0`, which depends on `package_water 2.6.1`
+- `package_tea 4.3.0` which _also_ depends on `package_water 2.6.1`
+
+If you want to prioritize one package over another, you can add version
+specifiers to _only_ the more important package:
+
+```{pip-cli}
+$ pip install package_coffee==0.44.1b0 package_tea
+```
+
+This will result in:
+
+- `package_coffee 0.44.1b0`, which depends on `package_water 2.6.1`
+- `package_tea 4.1.3` which also depends on `package_water 2.6.1`
+
+Now that you have resolved the issue, you can repin the compatible
+package versions as required.
+
+#### Loosen the requirements of your dependencies
+
+Assuming that you cannot resolve the conflict by loosening the version
+of the package you require (as above), you can try to fix the issue on
+your _dependency_ by:
+
+- Requesting that the package maintainers loosen _their_ dependencies
+- Forking the package and loosening the dependencies yourself
+
+```{warning}
+If you choose to fork the package yourself, you are _opting out_ of
+any support provided by the package maintainers. Proceed at your own risk!
+```
+
+#### All requirements are appropriate, but a solution does not exist
+
+Sometimes it's simply impossible to find a combination of package
+versions that do not conflict. Welcome to [dependency hell].
+
+In this situation, you could consider:
+
+- Using an alternative package, if that is acceptable for your project.
+ See [Awesome Python] for similar packages.
+- Refactoring your project to reduce the number of dependencies (for
+ example, by breaking up a monolithic code base into smaller pieces).
+
+### Getting help
+
+If none of the suggestions above work for you, we recommend that you ask
+for help on:
+
+- [Python user Discourse](https://discuss.python.org/c/users/7)
+- [Python user forums](https://www.python.org/community/forums/)
+- [Python developers Slack channel](https://pythondev.slack.com/)
+- [Python IRC](https://www.python.org/community/irc/)
+- [Stack Overflow](https://stackoverflow.com/questions/tagged/python)
+
+See ["How do I ask a good question?"] for tips on asking for help.
+
+Unfortunately, **the pip team cannot provide support for individual
+dependency conflict errors**. Please _only_ open a ticket on
+[pip's issue tracker](https://github.com/pypa/pip/issues) if you believe
+that your problem has exposed a bug in pip.
+
+["how do i ask a good question?"]: https://stackoverflow.com/help/how-to-ask
+[awesome python]: https://python.libhunt.com/
+[dependency hell]: https://en.wikipedia.org/wiki/Dependency_hell
diff --git a/docs/html/topics/https-certificates.md b/docs/html/topics/https-certificates.md
new file mode 100644
index 000000000..b42c463e6
--- /dev/null
+++ b/docs/html/topics/https-certificates.md
@@ -0,0 +1,71 @@
+(SSL Certificate Verification)=
+
+# HTTPS Certificates
+
+```{versionadded} 1.3
+
+```
+
+By default, pip will perform SSL certificate verification for network
+connections it makes over HTTPS. These serve to prevent man-in-the-middle
+attacks against package downloads. This does not use the system certificate
+store but, instead, uses a bundled CA certificate store from {pypi}`certifi`.
+
+## Using a specific certificate store
+
+The `--cert` option (and the corresponding `PIP_CERT` environment variable)
+allow users to specify a different certificate store/bundle for pip to use. It
+is also possible to use `REQUESTS_CA_BUNDLE` or `CURL_CA_BUNDLE` environment
+variables.
+
+## Using system certificate stores
+
+```{versionadded} 22.2
+Experimental support, behind `--use-feature=truststore`.
+```
+
+It is possible to use the system trust store, instead of the bundled certifi
+certificates for verifying HTTPS certificates. This approach will typically
+support corporate proxy certificates without additional configuration.
+
+In order to use system trust stores, you need to:
+
+- Use Python 3.10 or newer.
+- Install the {pypi}`truststore` package, in the Python environment you're
+ running pip in.
+
+ This is typically done by installing this package using a system package
+ manager or by using pip in {ref}`Hash-checking mode` for this package and
+ trusting the network using the `--trusted-host` flag.
+
+ ```{pip-cli}
+ $ python -m pip install truststore
+ [...]
+ $ python -m pip install SomePackage --use-feature=truststore
+ [...]
+ Successfully installed SomePackage
+ ```
+
+### When to use
+
+You should try using system trust stores when there is a custom certificate
+chain configured for your system that pip isn't aware of. Typically, this
+situation will manifest with an `SSLCertVerificationError` with the message
+"certificate verify failed: unable to get local issuer certificate":
+
+```{pip-cli}
+$ pip install -U SomePackage
+[...]
+ SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (\_ssl.c:997)'))) - skipping
+```
+
+This error means that OpenSSL wasn't able to find a trust anchor to verify the
+chain against. Using system trust stores instead of certifi will likely solve
+this issue.
+
+If you encounter a TLS/SSL error when using the `truststore` feature you should
+open an issue on the [truststore GitHub issue tracker] instead of pip's issue
+tracker. The maintainers of truststore will help diagnose and fix the issue.
+
+[truststore github issue tracker]:
+ https://github.com/sethmlarson/truststore/issues
diff --git a/docs/html/topics/index.md b/docs/html/topics/index.md
index 5f4c49f04..ad4676150 100644
--- a/docs/html/topics/index.md
+++ b/docs/html/topics/index.md
@@ -15,6 +15,10 @@ caching
configuration
dependency-resolution
more-dependency-resolution
+https-certificates
+local-project-installs
repeatable-installs
+secure-installs
vcs-support
+python-option
```
diff --git a/docs/html/topics/local-project-installs.md b/docs/html/topics/local-project-installs.md
new file mode 100644
index 000000000..151035b00
--- /dev/null
+++ b/docs/html/topics/local-project-installs.md
@@ -0,0 +1,67 @@
+# Local project installs
+
+It is extremely common to have a project, available in a folder/directory on your computer [^1] that you wish to install.
+
+With pip, depending on your usecase, there are two ways to do this:
+
+- A regular install
+- An editable install
+
+## Regular installs
+
+You can install local projects by specifying the project path to pip:
+
+```{pip-cli}
+$ pip install path/to/SomeProject
+```
+
+This will install the project into the Python that pip is associated with, in a manner similar to how it would actually be installed.
+
+This is what should be used in CI system and for deployments, since it most closely mirrors how a package would get installed if you build a distribution and installed from it (because that's _exactly_ what it does).
+
+(editable-installs)=
+
+## Editable installs
+
+You can install local projects in "editable" mode:
+
+```{pip-cli}
+$ pip install -e path/to/SomeProject
+```
+
+Editable installs allow you to install your project without copying any files. Instead, the files in the development directory are added to Python's import path. This approach is well suited for development and is also known as a "development installation".
+
+With an editable install, you only need to perform a re-installation if you change the project metadata (eg: version, what scripts need to be generated etc). You will still need to run build commands when you need to perform a compilation for non-Python code in the project (eg: C extensions).
+
+```{caution}
+It is possible to see behaviour differences between regular installs vs editable installs. In case you distribute the project as a "distribution package", users will see the behaviour of regular installs -- thus, it is important to ensure that regular installs work correctly.
+```
+
+```{note}
+This is functionally the same as [setuptools' develop mode], and that's precisely the mechanism used for setuptools-based projects.
+
+There are two advantages over using `setup.py develop` directly:
+
+- This works with non-setuptools build-backends as well.
+- The ".egg-info" directory is created relative to the project path, when using pip. This is generally a better location than setuptools, which dumps it in the current working directory.
+```
+
+[setuptools' develop mode]: https://setuptools.readthedocs.io/en/latest/userguide/development_mode.html
+
+## Build artifacts
+
+```{versionchanged} 21.3
+The project being installed is no longer copied to a temporary directory before invoking the build system, by default. A `--use-deprecated=out-of-tree-build` option is provided as a temporary fallback to aid user migrations.
+```
+
+```{versionchanged} 22.1
+The `--use-deprecated=out-of-tree-build` option has been removed.
+```
+
+When provided with a project that's in a local directory, pip will invoke the build system "in place". This behaviour has several consequences:
+
+- Local project builds will now be significantly faster, for certain kinds of projects and on systems with slow I/O (eg: via network attached storage or overly aggressive antivirus software).
+- Certain build backends (eg: `setuptools`) will litter the project directory with secondary build artifacts (eg: `.egg-info` directories).
+- Certain build backends (eg: `setuptools`) may not be able to perform with parallel builds anymore, since they previously relied on the fact that pip invoked them in a separate directory for each build.
+
+[^1]: Specifically, the current machine's filesystem.
diff --git a/docs/html/topics/python-option.md b/docs/html/topics/python-option.md
new file mode 100644
index 000000000..5ad46e7af
--- /dev/null
+++ b/docs/html/topics/python-option.md
@@ -0,0 +1,29 @@
+# Managing a different Python interpreter
+
+```{versionadded} 22.3
+```
+
+Occasionally, you may want to use pip to manage a Python installation other than
+the one pip is installed into. In this case, you can use the `--python` option
+to specify the interpreter you want to manage. This option can take one of two
+values:
+
+1. The path to a Python executable.
+2. The path to a virtual environment.
+
+In both cases, pip will run exactly as if it had been invoked from that Python
+environment.
+
+One example of where this might be useful is to manage a virtual environment
+that does not have pip installed.
+
+```{pip-cli}
+$ python -m venv .venv --without-pip
+$ pip --python .venv install SomePackage
+[...]
+Successfully installed SomePackage
+```
+
+You could also use `--python .venv/bin/python` (or on Windows,
+`--python .venv\Scripts\python.exe`) if you wanted to be explicit, but the
+virtual environment name is shorter and works exactly the same.
diff --git a/docs/html/topics/repeatable-installs.md b/docs/html/topics/repeatable-installs.md
index eca633d4a..d4609f968 100644
--- a/docs/html/topics/repeatable-installs.md
+++ b/docs/html/topics/repeatable-installs.md
@@ -1,3 +1,4 @@
+(repeatability)=
# Repeatable Installs
pip can be used to achieve various levels of repeatable environments. This page
@@ -94,5 +95,5 @@ identical packages.
Beware of the `setup_requires` keyword arg in {file}`setup.py`. The (rare)
packages that use it will cause those dependencies to be downloaded by
setuptools directly, skipping pip's protections. If you need to use such a
-package, see {ref}`Controlling setup_requires <controlling-setup-requires>`.
+package, see {ref}`Controlling setup_requires <controlling-setup_requires>`.
```
diff --git a/docs/html/topics/secure-installs.md b/docs/html/topics/secure-installs.md
new file mode 100644
index 000000000..f012842b2
--- /dev/null
+++ b/docs/html/topics/secure-installs.md
@@ -0,0 +1,100 @@
+# Secure installs
+
+By default, pip does not perform any checks to protect against remote tampering and involves running arbitrary code from distributions. It is, however, possible to use pip in a manner that changes these behaviours, to provide a more secure installation mechanism.
+
+This can be achieved by doing the following:
+
+- Enable {ref}`Hash-checking mode`, by passing {any}`--require-hashes`
+- Disallow source distributions, by passing {any}`--only-binary :all: <--only-binary>`
+
+(Hash-checking mode)=
+
+## Hash-checking Mode
+
+```{versionadded} 8.0
+
+```
+
+This mode uses local hashes, embedded in a requirements.txt file, to protect against remote tampering and network issues. These hashes are specified using a `--hash` [per requirement option](per-requirement-options).
+
+Note that hash-checking is an all-or-nothing proposition. Specifying `--hash` against _any_ requirement will activate this mode globally.
+
+To add hashes for a package, add them to line as follows:
+
+```
+FooProject == 1.2 \
+ --hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 \
+ --hash=sha256:486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7
+```
+
+### Additional restrictions
+
+- Hashes are required for _all_ requirements.
+
+ This is because a partially-hashed requirements file is of little use and thus likely an error: a malicious actor could slip bad code into the installation via one of the unhashed requirements.
+
+ Note that hashes embedded in URL-style requirements via the `#md5=...` syntax suffice to satisfy this rule (regardless of hash strength, for legacy reasons), though you should use a stronger hash like sha256 whenever possible.
+
+- Hashes are required for _all_ dependencies.
+
+ If there is a dependency that is not spelled out and hashed in the requirements file, it will result in an error.
+
+- Requirements must be pinned (either to a URL, filesystem path or using `==`).
+
+ This prevents a surprising hash mismatch upon the release of a new version that matches the requirement specifier.
+
+### Forcing Hash-checking mode
+
+It is possible to force the hash checking mode to be enabled, by passing `--require-hashes` command-line option.
+
+This can be useful in deploy scripts, to ensure that the author of the requirements file provided hashes. It is also a convenient way to bootstrap your list of hashes, since it shows the hashes of the packages it fetched. It fetches only the preferred archive for each package, so you may still need to add hashes for alternatives archives using {ref}`pip hash`: for instance if there is both a binary and a source distribution.
+
+### Hash algorithms
+
+The recommended hash algorithm at the moment is sha256, but stronger ones are allowed, including all those supported by `hashlib`. However, weaker ones such as md5, sha1, and sha224 are excluded to avoid giving a false sense of security.
+
+### Multiple hashes per package
+
+It is possible to use multiple hashes for each package. This is important when a package offers binary distributions for a variety of platforms or when it is important to allow both binary and source distributions.
+
+### Interaction with caching
+
+The {ref}`locally-built wheel cache <wheel-caching>` is disabled in hash-checking mode to prevent spurious hash mismatch errors.
+
+These would otherwise occur while installing sdists that had already been automatically built into cached wheels: those wheels would be selected for installation, but their hashes would not match the sdist ones from the requirements file.
+
+A further complication is that locally built wheels are nondeterministic: contemporary modification times make their way into the archive, making hashes unpredictable across machines and cache flushes. Compilation of C code adds further nondeterminism, as many compilers include random-seeded values in their output.
+
+However, wheels fetched from index servers are required to be the same every time. They land in pip's HTTP cache, not its wheel cache, and are used normally in hash-checking mode. The only downside of having the wheel cache disabled is thus extra build time for sdists, and this can be solved by making sure pre-built wheels are available from the index server.
+
+### Using hashes from PyPI (or other index servers)
+
+PyPI (and certain other index servers) provides a hash for the distribution, in the fragment portion of each download URL, like `#sha256=123...`, which pip checks as a protection against download corruption.
+
+Other hash algorithms that have guaranteed support from `hashlib` are also supported here: sha1, sha224, sha384, sha256, and sha512. Since this hash originates remotely, it is not a useful guard against tampering and thus does not satisfy the `--require-hashes` demand that every package have a local hash.
+
+## Repeatable installs
+
+Hash-checking mode also works with {ref}`pip download` and {ref}`pip wheel`. See {doc}`../topics/repeatable-installs` for a comparison of hash-checking mode with other repeatability strategies.
+
+```{warning}
+Beware of the `setup_requires` keyword arg in {file}`setup.py`. The (rare) packages that use it will cause those dependencies to be downloaded by setuptools directly, skipping pip's hash-checking. If you need to use such a package, see {ref}`controlling setup_requires <controlling-setup_requires>`.
+```
+
+## Do not use setuptools directly
+
+Be careful not to nullify all your security work by installing your actual project by using setuptools' deprecated interfaces directly: for example, by calling `python setup.py install`, `python setup.py develop`, or `easy_install`.
+
+These will happily go out and download, unchecked, anything you missed in your requirements file and it’s easy to miss things as your project evolves. To be safe, install your project using pip and {any}`--no-deps`.
+
+Instead of `python setup.py install`, use:
+
+```{pip-cli}
+$ pip install --no-deps .
+```
+
+Instead of `python setup.py develop`, use:
+
+```{pip-cli}
+$ pip install --no-deps -e .
+```
diff --git a/docs/html/topics/vcs-support.md b/docs/html/topics/vcs-support.md
index 30bb57930..70bb5beb9 100644
--- a/docs/html/topics/vcs-support.md
+++ b/docs/html/topics/vcs-support.md
@@ -1,3 +1,4 @@
+(vcs support)=
# VCS Support
pip supports installing from various version control systems (VCS).
@@ -17,9 +18,9 @@ The supported schemes are `git+file`, `git+https`, `git+ssh`, `git+http`,
`git+git` and `git`. Here are some of the supported forms:
```none
-git+ssh://git.example.com/MyProject#egg=MyProject
-git+file:///home/user/projects/MyProject#egg=MyProject
-git+https://git.example.com/MyProject#egg=MyProject
+MyProject @ git+ssh://git.example.com/MyProject
+MyProject @ git+file:///home/user/projects/MyProject
+MyProject @ git+https://git.example.com/MyProject
```
```{warning}
@@ -34,10 +35,10 @@ It is also possible to specify a "git ref" such as branch name, a commit hash or
a tag name:
```none
-git+https://git.example.com/MyProject.git@master#egg=MyProject
-git+https://git.example.com/MyProject.git@v1.0#egg=MyProject
-git+https://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject
-git+https://git.example.com/MyProject.git@refs/pull/123/head#egg=MyProject
+MyProject @ git+https://git.example.com/MyProject.git@master
+MyProject @ git+https://git.example.com/MyProject.git@v1.0
+MyProject @ git+https://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709
+MyProject @ git+https://git.example.com/MyProject.git@refs/pull/123/head
```
When passing a commit hash, specifying a full hash is preferable to a partial
@@ -50,20 +51,20 @@ The supported schemes are `hg+file`, `hg+http`, `hg+https`, `hg+ssh`
and `hg+static-http`. Here are some of the supported forms:
```
-hg+http://hg.myproject.org/MyProject#egg=MyProject
-hg+https://hg.myproject.org/MyProject#egg=MyProject
-hg+ssh://hg.myproject.org/MyProject#egg=MyProject
-hg+file:///home/user/projects/MyProject#egg=MyProject
+MyProject @ hg+http://hg.myproject.org/MyProject
+MyProject @ hg+https://hg.myproject.org/MyProject
+MyProject @ hg+ssh://hg.myproject.org/MyProject
+MyProject @ hg+file:///home/user/projects/MyProject
```
It is also possible to specify a revision number, a revision hash, a tag name
or a local branch name:
```none
-hg+http://hg.example.com/MyProject@da39a3ee5e6b#egg=MyProject
-hg+http://hg.example.com/MyProject@2019#egg=MyProject
-hg+http://hg.example.com/MyProject@v1.0#egg=MyProject
-hg+http://hg.example.com/MyProject@special_feature#egg=MyProject
+MyProject @ hg+http://hg.example.com/MyProject@da39a3ee5e6b
+MyProject @ hg+http://hg.example.com/MyProject@2019
+MyProject @ hg+http://hg.example.com/MyProject@v1.0
+MyProject @ hg+http://hg.example.com/MyProject@special_feature
```
### Subversion
@@ -72,9 +73,9 @@ The supported schemes are `svn`, `svn+svn`, `svn+http`, `svn+https` and
`svn+ssh`. Here are some of the supported forms:
```none
-svn+https://svn.example.com/MyProject#egg=MyProject
-svn+ssh://svn.example.com/MyProject#egg=MyProject
-svn+ssh://user@svn.example.com/MyProject#egg=MyProject
+MyProject @ svn+https://svn.example.com/MyProject
+MyProject @ svn+ssh://svn.example.com/MyProject
+MyProject @ svn+ssh://user@svn.example.com/MyProject
```
You can also give specific revisions to an SVN URL, like so:
@@ -93,18 +94,18 @@ The supported schemes are `bzr+http`, `bzr+https`, `bzr+ssh`, `bzr+sftp`,
`bzr+ftp` and `bzr+lp`. Here are the supported forms:
```none
-bzr+http://bzr.example.com/MyProject/trunk#egg=MyProject
-bzr+sftp://user@example.com/MyProject/trunk#egg=MyProject
-bzr+ssh://user@example.com/MyProject/trunk#egg=MyProject
-bzr+ftp://user@example.com/MyProject/trunk#egg=MyProject
-bzr+lp:MyProject#egg=MyProject
+MyProject @ bzr+http://bzr.example.com/MyProject/trunk
+MyProject @ bzr+sftp://user@example.com/MyProject/trunk
+MyProject @ bzr+ssh://user@example.com/MyProject/trunk
+MyProject @ bzr+ftp://user@example.com/MyProject/trunk
+MyProject @ bzr+lp:MyProject
```
Tags or revisions can be installed like so:
```none
-bzr+https://bzr.example.com/MyProject/trunk@2019#egg=MyProject
-bzr+http://bzr.example.com/MyProject/trunk@v1.0#egg=MyProject
+MyProject @ bzr+https://bzr.example.com/MyProject/trunk@2019
+MyProject @ bzr+http://bzr.example.com/MyProject/trunk@v1.0
```
(editable-vcs-installs)=
diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst
index 919209ffc..b90b778b8 100644
--- a/docs/html/user_guide.rst
+++ b/docs/html/user_guide.rst
@@ -2,6 +2,15 @@
User Guide
==========
+.. Hello there!
+
+ If you're thinking of adding content to this page... please take a moment
+ to consider if this content can live on its own, within a topic guide or a
+ reference page.
+
+ There is active effort being put toward *reducing* the amount of content on
+ this specific page (https://github.com/pypa/pip/issues/9475) and moving it
+ into more focused single-page documents that cover that specific topic.
Running pip
===========
@@ -17,7 +26,7 @@ to your system, which can be run from the command prompt as follows:
``python -m pip`` executes pip using the Python interpreter you
specified as python. So ``/usr/bin/python3.7 -m pip`` means
- you are executing pip for your interpreter located at /usr/bin/python3.7.
+ you are executing pip for your interpreter located at ``/usr/bin/python3.7``.
.. tab:: Windows
@@ -59,19 +68,18 @@ For more information and examples, see the :ref:`pip install` reference.
.. _PyPI: https://pypi.org/
-
-Basic Authentication Credentials
-================================
+.. _`0-basic-authentication-credentials`:
+.. rubric:: Basic Authentication Credentials
This is now covered in :doc:`topics/authentication`.
-netrc Support
--------------
+.. _`0-netrc-support`:
+.. rubric:: netrc Support
This is now covered in :doc:`topics/authentication`.
-Keyring Support
----------------
+.. _`0-keyring-support`:
+.. rubric:: Keyring Support
This is now covered in :doc:`topics/authentication`.
@@ -84,7 +92,7 @@ in many corporate environments requires an outbound HTTP proxy server.
pip can be configured to connect through a proxy server in various ways:
* using the ``--proxy`` command-line option to specify a proxy in the form
- ``[user:passwd@]proxy.server:port``
+ ``scheme://[user:passwd@]proxy.server:port``
* using ``proxy`` in a :ref:`config-file`
* by setting the standard environment-variables ``http_proxy``, ``https_proxy``
and ``no_proxy``.
@@ -113,7 +121,7 @@ installed using :ref:`pip install` like so:
py -m pip install -r requirements.txt
-Details on the format of the files are here: :ref:`Requirements File Format`.
+Details on the format of the files are here: :ref:`requirements-file-format`.
Logically, a Requirements file is just a list of :ref:`pip install` arguments
placed in a file. Note that you should not rely on the items in the file being
@@ -180,12 +188,12 @@ In practice, there are 4 common uses of Requirements files:
It's important to be clear that pip determines package dependencies using
`install_requires metadata
-<https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-dependencies>`_,
+<https://setuptools.readthedocs.io/en/latest/userguide/dependency_management.html>`_,
not by discovering ``requirements.txt`` files embedded in projects.
See also:
-* :ref:`Requirements File Format`
+* :ref:`requirements-file-format`
* :ref:`pip freeze`
* `"setup.py vs requirements.txt" (an article by Donald Stufft)
<https://caremad.io/2013/07/setup-vs-requirement/>`_
@@ -451,34 +459,26 @@ packages.
For more information and examples, see the :ref:`pip search` reference.
-.. _`Configuration`:
-
-
-Configuration
-=============
+.. _`0-configuration`:
+.. rubric:: Configuration
This is now covered in :doc:`topics/configuration`.
-.. _config-file:
-
-Config file
------------
+.. _`0-config-file`:
+.. rubric:: Config file
This is now covered in :doc:`topics/configuration`.
-Environment Variables
----------------------
+.. _`0-environment-variables`:
+.. rubric:: Environment Variables
This is now covered in :doc:`topics/configuration`.
-.. _config-precedence:
-
-Config Precedence
------------------
+.. _`0-config-precedence`:
+.. rubric:: Config Precedence
This is now covered in :doc:`topics/configuration`.
-
Command Completion
==================
@@ -496,6 +496,10 @@ To setup for fish::
python -m pip completion --fish > ~/.config/fish/completions/pip.fish
+To setup for powershell::
+
+ python -m pip completion --powershell | Out-File -Encoding default -Append $PROFILE
+
Alternatively, you can use the result of the ``completion`` command directly
with the eval function of your shell, e.g. by adding the following to your
startup file::
@@ -629,7 +633,7 @@ Moreover, the "user scheme" can be customized by setting the
``PYTHONUSERBASE`` environment variable, which updates the value of
``site.USER_BASE``.
-To install "SomePackage" into an environment with site.USER_BASE customized to
+To install "SomePackage" into an environment with ``site.USER_BASE`` customized to
'/myappenv', do the following:
.. tab:: Unix/macOS
@@ -776,449 +780,16 @@ is the latest version:
[...]
Successfully installed SomePackage
-.. _`Repeatability`:
-
-
-Ensuring Repeatability
-======================
+.. _`0-repeatability`:
+.. _`0-ensuring-repeatability`:
+.. rubric:: Ensuring Repeatability
This is now covered in :doc:`../topics/repeatable-installs`.
-.. _`Fixing conflicting dependencies`:
-
-Fixing conflicting dependencies
-===============================
-
-The purpose of this section of documentation is to provide practical suggestions to
-pip users who encounter an error where pip cannot install their
-specified packages due to conflicting dependencies (a
-``ResolutionImpossible`` error).
-
-This documentation is specific to the new resolver, which is the
-default behavior in pip 20.3 and later. If you are using pip 20.2, you
-can invoke the new resolver by using the flag
-``--use-feature=2020-resolver``.
-
-Understanding your error message
---------------------------------
-
-When you get a ``ResolutionImpossible`` error, you might see something
-like this:
-
-.. tab:: Unix/macOS
-
- .. code-block:: shell
-
- python -m pip install package_coffee==0.44.1 package_tea==4.3.0
-
-.. tab:: Windows
-
- .. code-block:: shell
-
- py -m pip install package_coffee==0.44.1 package_tea==4.3.0
-
-::
-
- Due to conflicting dependencies pip cannot install
- package_coffee and package_tea:
- - package_coffee depends on package_water<3.0.0,>=2.4.2
- - package_tea depends on package_water==2.3.1
-
-In this example, pip cannot install the packages you have requested,
-because they each depend on different versions of the same package
-(``package_water``):
-
-- ``package_coffee`` version ``0.44.1`` depends on a version of
- ``package_water`` that is less than ``3.0.0`` but greater than or equal to
- ``2.4.2``
-- ``package_tea`` version ``4.3.0`` depends on version ``2.3.1`` of
- ``package_water``
-
-Sometimes these messages are straightforward to read, because they use
-commonly understood comparison operators to specify the required version
-(e.g. ``<`` or ``>``).
-
-However, Python packaging also supports some more complex ways for
-specifying package versions (e.g. ``~=`` or ``*``):
-
-+----------+---------------------------------+--------------------------------+
-| Operator | Description | Example |
-+==========+=================================+================================+
-| ``>`` | Any version greater than | ``>3.1``: any version |
-| | the specified version. | greater than ``3.1``. |
-+----------+---------------------------------+--------------------------------+
-| ``<`` | Any version less than | ``<3.1``: any version |
-| | the specified version. | less than ``3.1``. |
-+----------+---------------------------------+--------------------------------+
-| ``<=`` | Any version less than or | ``<=3.1``: any version |
-| | equal to the specified version. | less than or equal to ``3.1``. |
-+----------+---------------------------------+--------------------------------+
-| ``>=`` | Any version greater than or | ``>=3.1``: |
-| | equal to the specified version. | version ``3.1`` and greater. |
-+----------+---------------------------------+--------------------------------+
-| ``==`` | Exactly the specified version. | ``==3.1``: only ``3.1``. |
-+----------+---------------------------------+--------------------------------+
-| ``!=`` | Any version not equal | ``!=3.1``: any version |
-| | to the specified version. | other than ``3.1``. |
-+----------+---------------------------------+--------------------------------+
-| ``~=`` | Any compatible release. | ``~=3.1``: version ``3.1`` |
-| | Compatible releases are | or later, but not |
-| | releases that are within the | version ``4.0`` or later. |
-| | same major or minor version, | ``~=3.1.2``: version ``3.1.2`` |
-| | assuming the package author | or later, but not |
-| | is using semantic versioning. | version ``3.2.0`` or later. |
-+----------+---------------------------------+--------------------------------+
-| ``*`` | Can be used at the end of | ``==3.1.*``: any version |
-| | a version number to represent | that starts with ``3.1``. |
-| | *all*, | Equivalent to ``~=3.1.0``. |
-+----------+---------------------------------+--------------------------------+
-
-The detailed specification of supported comparison operators can be
-found in :pep:`440`.
-
-Possible solutions
-------------------
-
-The solution to your error will depend on your individual use case. Here
-are some things to try:
-
-1. Audit your top level requirements
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-As a first step it is useful to audit your project and remove any
-unnecessary or out of date requirements (e.g. from your ``setup.py`` or
-``requirements.txt`` files). Removing these can significantly reduce the
-complexity of your dependency tree, thereby reducing opportunities for
-conflicts to occur.
-
-2. Loosen your top level requirements
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Sometimes the packages that you have asked pip to install are
-incompatible because you have been too strict when you specified the
-package version.
-
-In our first example both ``package_coffee`` and ``package_tea`` have been
-*pinned* to use specific versions
-(``package_coffee==0.44.1b0 package_tea==4.3.0``).
-
-To find a version of both ``package_coffee`` and ``package_tea`` that depend on
-the same version of ``package_water``, you might consider:
-
-- Loosening the range of packages that you are prepared to install
- (e.g. ``pip install "package_coffee>0.44.*" "package_tea>4.0.0"``)
-- Asking pip to install *any* version of ``package_coffee`` and ``package_tea``
- by removing the version specifiers altogether (e.g.
- ``python -m pip install package_coffee package_tea``)
-
-In the second case, pip will automatically find a version of both
-``package_coffee`` and ``package_tea`` that depend on the same version of
-``package_water``, installing:
-
-- ``package_coffee 0.46.0b0``, which depends on ``package_water 2.6.1``
-- ``package_tea 4.3.0`` which *also* depends on ``package_water 2.6.1``
-
-If you want to prioritize one package over another, you can add version
-specifiers to *only* the more important package:
-
-.. tab:: Unix/macOS
-
- .. code-block:: shell
-
- python -m pip install package_coffee==0.44.1b0 package_tea
-
-.. tab:: Windows
-
- .. code-block:: shell
-
- py -m pip install package_coffee==0.44.1b0 package_tea
-
-This will result in:
-
-- ``package_coffee 0.44.1b0``, which depends on ``package_water 2.6.1``
-- ``package_tea 4.1.3`` which also depends on ``package_water 2.6.1``
-
-Now that you have resolved the issue, you can repin the compatible
-package versions as required.
-
-3. Loosen the requirements of your dependencies
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Assuming that you cannot resolve the conflict by loosening the version
-of the package you require (as above), you can try to fix the issue on
-your *dependency* by:
-
-- Requesting that the package maintainers loosen *their* dependencies
-- Forking the package and loosening the dependencies yourself
-
-.. warning::
-
- If you choose to fork the package yourself, you are *opting out* of
- any support provided by the package maintainers. Proceed at your own risk!
-
-4. All requirements are loose, but a solution does not exist
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Sometimes it's simply impossible to find a combination of package
-versions that do not conflict. Welcome to `dependency hell`_.
-
-In this situation, you could consider:
-
-- Using an alternative package, if that is acceptable for your project.
- See `Awesome Python`_ for similar packages.
-- Refactoring your project to reduce the number of dependencies (for
- example, by breaking up a monolithic code base into smaller pieces)
-
-.. _`Getting help`:
-
-Getting help
-------------
+.. _`0-fixing-conflicting-dependencies`:
+.. rubric:: Fixing conflicting dependencies
-If none of the suggestions above work for you, we recommend that you ask
-for help on:
-
-- `Python user Discourse`_
-- `Python user forums`_
-- `Python developers Slack channel`_
-- `Python IRC`_
-- `Stack Overflow`_
-
-See `"How do I ask a good question?"`_ for tips on asking for help.
-
-Unfortunately, **the pip team cannot provide support for individual
-dependency conflict errors**. Please *only* open a ticket on the `pip
-issue tracker`_ if you believe that your problem has exposed a bug in pip.
-
-.. _dependency hell: https://en.wikipedia.org/wiki/Dependency_hell
-.. _Awesome Python: https://python.libhunt.com/
-.. _Python user Discourse: https://discuss.python.org/c/users/7
-.. _Python user forums: https://www.python.org/community/forums/
-.. _Python developers Slack channel: https://pythondev.slack.com/
-.. _Python IRC: https://www.python.org/community/irc/
-.. _Stack Overflow: https://stackoverflow.com/questions/tagged/python
-.. _"How do I ask a good question?": https://stackoverflow.com/help/how-to-ask
-.. _pip issue tracker: https://github.com/pypa/pip/issues
-
-.. _`Dependency resolution backtracking`:
-
-Dependency resolution backtracking
-==================================
-
-Or more commonly known as *"Why does pip download multiple versions of
-the same package over and over again during an install?"*.
-
-The purpose of this section is to provide explanation of why
-backtracking happens, and practical suggestions to pip users who
-encounter it during a ``pip install``.
-
-What is backtracking?
----------------------
-
-Backtracking is not a bug, or an unexpected behaviour. It is part of the
-way pip's dependency resolution process works.
-
-During a pip install (e.g. ``pip install tea``), pip needs to work out
-the package's dependencies (e.g. ``spoon``, ``hot-water``, ``cup`` etc.), the
-versions of each of these packages it needs to install. For each package
-pip needs to decide which version is a good candidate to install.
-
-A "good candidate" means a version of each package that is compatible with all
-the other package versions being installed at the same time.
-
-In the case where a package has a lot of versions, arriving at a good
-candidate can take a lot of time. (The amount of time depends on the
-package size, the number of versions pip must try, and other concerns.)
-
-How does backtracking work?
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-When doing a pip install, pip starts by making assumptions about the
-packages it needs to install. During the install process it needs to check these
-assumptions as it goes along.
-
-When pip finds that an assumption is incorrect, it has to try another approach
-(backtrack), which means discarding some of the work that has already been done,
-and going back to choose another path.
-
-For example; The user requests ``pip install tea``. ``tea`` has dependencies of
-``cup``, ``hot-water``, ``spoon`` amongst others.
-
-pip starts by installing a version of ``cup``. If it finds out it isn’t
-compatible (with the other package versions) it needs to “go backâ€
-(backtrack) and download an older version.
-
-It then tries to install that version. If it is successful, it will continue
-onto the next package. If not it will continue to backtrack until it finds a
-compatible version.
-
-This backtrack behaviour can end in 2 ways - either 1) it will
-successfully find a set of packages it can install (good news!), or 2) it will
-eventually display a `resolution impossible <https://pip.pypa.io/en/latest/user_guide/#id35>`__ error
-message (not so good).
-
-If pip starts backtracking during dependency resolution, it does not
-know how long it will backtrack, and how much computation would be
-needed. For the user this means it can take a long time to complete.
-
-Why does backtracking occur?
-----------------------------
-
-With the release of the new resolver (:ref:`Resolver changes 2020`), pip is now
-more strict in the package versions it installs when a user runs a
-``pip install`` command.
-
-Pip needs to backtrack because initially, it doesn't have all the information it
-needs to work out the correct set of packages. This is because package indexes
-don't provide full package dependency information before you have downloaded
-the package.
-
-This new resolver behaviour means that pip works harder to find out which
-version of a package is a good candidate to install. It reduces the risk that
-installing a new package will accidentally break an existing installed package,
-and so reduces the risk that your environment gets messed up.
-
-What does this behaviour look like?
------------------------------------
-
-Right now backtracking behaviour looks like this:
-
-::
-
- $ pip install tea==1.9.8
- Collecting tea==1.9.8
- Downloading tea-1.9.8-py2.py3-none-any.whl (346 kB)
- |████████████████████████████████| 346 kB 10.4 MB/s
- Collecting spoon==2.27.0
- Downloading spoon-2.27.0-py2.py3-none-any.whl (312 kB)
- |████████████████████████████████| 312 kB 19.2 MB/s
- Collecting hot-water>=0.1.9
- Downloading hot-water-0.1.13-py3-none-any.whl (9.3 kB)
- Collecting cup>=1.6.0
- Downloading cup-3.22.0-py2.py3-none-any.whl (397 kB)
- |████████████████████████████████| 397 kB 28.2 MB/s
- INFO: pip is looking at multiple versions of this package to determine
- which version is compatible with other requirements.
- This could take a while.
- Downloading cup-3.21.0-py2.py3-none-any.whl (395 kB)
- |████████████████████████████████| 395 kB 27.0 MB/s
- Downloading cup-3.20.0-py2.py3-none-any.whl (394 kB)
- |████████████████████████████████| 394 kB 24.4 MB/s
- Downloading cup-3.19.1-py2.py3-none-any.whl (394 kB)
- |████████████████████████████████| 394 kB 21.3 MB/s
- Downloading cup-3.19.0-py2.py3-none-any.whl (394 kB)
- |████████████████████████████████| 394 kB 26.2 MB/s
- Downloading cup-3.18.0-py2.py3-none-any.whl (393 kB)
- |████████████████████████████████| 393 kB 22.1 MB/s
- Downloading cup-3.17.0-py2.py3-none-any.whl (382 kB)
- |████████████████████████████████| 382 kB 23.8 MB/s
- Downloading cup-3.16.0-py2.py3-none-any.whl (376 kB)
- |████████████████████████████████| 376 kB 27.5 MB/s
- Downloading cup-3.15.1-py2.py3-none-any.whl (385 kB)
- |████████████████████████████████| 385 kB 30.4 MB/s
- INFO: pip is looking at multiple versions of this package to determine
- which version is compatible with other requirements.
- This could take a while.
- Downloading cup-3.15.0-py2.py3-none-any.whl (378 kB)
- |████████████████████████████████| 378 kB 21.4 MB/s
- Downloading cup-3.14.0-py2.py3-none-any.whl (372 kB)
- |████████████████████████████████| 372 kB 21.1 MB/s
- Downloading cup-3.13.1-py2.py3-none-any.whl (381 kB)
- |████████████████████████████████| 381 kB 21.8 MB/s
- This is taking longer than usual. You might need to provide the
- dependency resolver with stricter constraints to reduce runtime.
- If you want to abort this run, you can press Ctrl + C to do so.
- Downloading cup-3.13.0-py2.py3-none-any.whl (374 kB)
-
-In the above sample output, pip had to download multiple versions of
-package ``cup`` - cup-3.22.0 to cup-3.13.0 - to find a version that will be
-compatible with the other packages - ``spoon``, ``hot-water``, etc.
-
-These multiple ``Downloading cup-version`` lines show pip backtracking.
-
-Possible ways to reduce backtracking occurring
-----------------------------------------------
-
-It's important to mention backtracking behaviour is expected during a
-``pip install`` process. What pip is trying to do is complicated - it is
-working through potentially millions of package versions to identify the
-compatible versions.
-
-There is no guaranteed solution to backtracking but you can reduce it -
-here are a number of ways.
-
-.. _1-allow-pip-to-complete-its-backtracking:
-
-1. Allow pip to complete its backtracking
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-In most cases, pip will complete the backtracking process successfully.
-It is possible this could take a very long time to complete - this may
-not be your preferred option.
-
-However, there is a possibility pip will not be able to find a set of
-compatible versions.
-
-If you'd prefer not to wait, you can interrupt pip (ctrl and c) and use
-:ref:`Constraints Files`: to reduce the number of package versions it tries.
-
-.. _2-reduce-the-versions-of-the-backtracking-package:
-
-2. Reduce the number of versions pip will try to backtrack through
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-If pip is backtracking more than you'd like, the next option is to
-constrain the number of package versions it tries.
-
-A first good candidate for this constraining is the package(s) it is
-backtracking on (e.g. in the above example - ``cup``).
-
-You could try:
-
-``pip install tea "cup > 3.13"``
-
-This will reduce the number of versions of ``cup`` it tries, and
-possibly reduce the time pip takes to install.
-
-There is a possibility that if you're wrong (in this case an older
-version would have worked) then you missed the chance to use it. This
-can be trial and error.
-
-.. _3-use-constraint-files-or-lockfiles:
-
-3. Use constraint files or lockfiles
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-This option is a progression of 2 above. It requires users to know how
-to inspect:
-
-- the packages they're trying to install
-- the package release frequency and compatibility policies
-- their release notes and changelogs from past versions
-
-During deployment, you can create a lockfile stating the exact package and
-version number for for each dependency of that package. You can create this
-with `pip-tools <https://github.com/jazzband/pip-tools/>`__.
-
-This means the "work" is done once during development process, and so
-will save users this work during deployment.
-
-The pip team is not available to provide support in helping you create a
-suitable constraints file.
-
-.. _4-be-more-strict-on-package-dependencies-during-development:
-
-4. Be more strict on package dependencies during development
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-For package maintainers during software development, give pip some help by
-creating constraint files for the dependency tree. This will reduce the
-number of versions it will try.
-
-Getting help
-------------
-
-If none of the suggestions above work for you, we recommend that you ask
-for help. :ref:`Getting help`.
+This is now covered in :doc:`../topics/dependency-resolution`.
.. _`Using pip from your program`:
@@ -1437,9 +1008,9 @@ How to upgrade and migrate
4. **Troubleshoot and try these workarounds if necessary.**
- - If pip is taking longer to install packages, read
- :ref:`Dependency resolution backtracking` for ways to reduce the
- time pip spends backtracking due to dependency conflicts.
+ - If pip is taking longer to install packages, read :doc:`Dependency
+ resolution backtracking <topics/dependency-resolution>` for ways to
+ reduce the time pip spends backtracking due to dependency conflicts.
- If you don't want pip to actually resolve dependencies, use the
``--no-deps`` option. This is useful when you have a set of package
versions that work together in reality, even though their metadata says
@@ -1575,3 +1146,8 @@ announcements on the `low-traffic packaging announcements list`_ and
.. _our survey on upgrades that create conflicts: https://docs.google.com/forms/d/e/1FAIpQLSeBkbhuIlSofXqCyhi3kGkLmtrpPOEBwr6iJA6SzHdxWKfqdA/viewform
.. _the official Python blog: https://blog.python.org/
.. _Python Windows launcher: https://docs.python.org/3/using/windows.html#launcher
+
+.. _`0-using-system-trust-stores-for-verifying-https`:
+.. rubric:: Using system trust stores for verifying HTTPS
+
+This is now covered in :doc:`topics/https-certificates`.
diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py
index 12e098fc0..f398b7d09 100644
--- a/docs/pip_sphinxext.py
+++ b/docs/pip_sphinxext.py
@@ -127,8 +127,7 @@ class PipOptions(rst.Directive):
line += f" <{metavar.lower()}>"
# fix defaults
assert option.help is not None
- # https://github.com/python/typeshed/pull/5080
- opt_help = option.help.replace("%default", str(option.default)) # type: ignore
+ opt_help = option.help.replace("%default", str(option.default))
# fix paths with sys.prefix
opt_help = opt_help.replace(sys.prefix, "<sys.prefix>")
return [bookmark_line, "", line, "", " " + opt_help, ""]
diff --git a/docs/requirements.txt b/docs/requirements.txt
index e311562e6..fa3a7390c 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,4 +1,4 @@
-sphinx ~= 4.1.0
+sphinx ~= 4.2, != 4.4.0
towncrier
furo
myst_parser
diff --git a/news/10128.removal.rst b/news/10128.removal.rst
deleted file mode 100644
index 850b64564..000000000
--- a/news/10128.removal.rst
+++ /dev/null
@@ -1 +0,0 @@
-Improve deprecation warning regarding the copying of source trees when installing from a local directory.
diff --git a/news/10165.trivial.rst b/news/10165.trivial.rst
deleted file mode 100644
index 45d40200c..000000000
--- a/news/10165.trivial.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Add a ``feature_flag`` optional kwarg to the ``deprecated()`` function
-``pip._internal.utils.deprecation:deprecated``. Also formulate a corresponding canned
-message which suggests using the ``--use-feature={feature_flag}`` to test upcoming
-behavior.
diff --git a/news/10233.bugfix.rst b/news/10233.bugfix.rst
deleted file mode 100644
index 8578a7dd0..000000000
--- a/news/10233.bugfix.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-New resolver: When a package is specified with extras in constraints, and with
-extras in non-constraint requirements, the resolver now correctly identifies the
-constraint's existence and avoids backtracking.
diff --git a/news/10716.feature.rst b/news/10716.feature.rst
new file mode 100644
index 000000000..ef09e1b8f
--- /dev/null
+++ b/news/10716.feature.rst
@@ -0,0 +1 @@
+Use ``shell=True`` for opening the editor with ``pip config edit``.
diff --git a/news/11111.feature.rst b/news/11111.feature.rst
new file mode 100644
index 000000000..39cb4b35c
--- /dev/null
+++ b/news/11111.feature.rst
@@ -0,0 +1 @@
+Use the ``data-dist-info-metadata`` attribute from :pep:`658` to resolve distribution metadata without downloading the dist yet.
diff --git a/news/11250.feature.rst b/news/11250.feature.rst
new file mode 100644
index 000000000..a80c54699
--- /dev/null
+++ b/news/11250.feature.rst
@@ -0,0 +1 @@
+Add an option to run the test suite with pip built as a zipapp.
diff --git a/src/pip/_vendor/html5lib/filters/__init__.py b/news/11254.trivial.rst
index e69de29bb..e69de29bb 100644
--- a/src/pip/_vendor/html5lib/filters/__init__.py
+++ b/news/11254.trivial.rst
diff --git a/news/11276.bugfix.rst b/news/11276.bugfix.rst
new file mode 100644
index 000000000..af8f518be
--- /dev/null
+++ b/news/11276.bugfix.rst
@@ -0,0 +1,2 @@
+Fix ``--no-index`` when ``--index-url`` or ``--extra-index-url`` is specified
+inside a requirements file.
diff --git a/news/11309.bugfix.rst b/news/11309.bugfix.rst
new file mode 100644
index 000000000..9ee54057d
--- /dev/null
+++ b/news/11309.bugfix.rst
@@ -0,0 +1 @@
+Ensure that the candidate ``pip`` executable exists, when checking for a new version of pip.
diff --git a/news/11320.feature.rst b/news/11320.feature.rst
new file mode 100644
index 000000000..843eac7c9
--- /dev/null
+++ b/news/11320.feature.rst
@@ -0,0 +1,2 @@
+Add a ``--python`` option to allow pip to manage Python environments other
+than the one pip is installed in.
diff --git a/news/11352.bugfix.rst b/news/11352.bugfix.rst
new file mode 100644
index 000000000..78016c912
--- /dev/null
+++ b/news/11352.bugfix.rst
@@ -0,0 +1,2 @@
+Ignore distributions with invalid ``Name`` in metadata instead of crashing, when
+using the ``importlib.metadata`` backend.
diff --git a/news/11357.doc.rst b/news/11357.doc.rst
new file mode 100644
index 000000000..887928a08
--- /dev/null
+++ b/news/11357.doc.rst
@@ -0,0 +1 @@
+Mention that --quiet must be used when writing the installation report to stdout.
diff --git a/news/11459.feature.rst b/news/11459.feature.rst
new file mode 100644
index 000000000..a4a11c093
--- /dev/null
+++ b/news/11459.feature.rst
@@ -0,0 +1 @@
+Document the new (experimental) zipapp distribution of pip.
diff --git a/news/5580954E-E089-4CDB-857A-868BA1F7435D.trivial.rst b/news/5580954E-E089-4CDB-857A-868BA1F7435D.trivial.rst
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/news/5580954E-E089-4CDB-857A-868BA1F7435D.trivial.rst
diff --git a/news/8559.removal.rst b/news/8559.removal.rst
new file mode 100644
index 000000000..aa9f81412
--- /dev/null
+++ b/news/8559.removal.rst
@@ -0,0 +1,2 @@
+Deprecate installation with 'setup.py install' when the 'wheel' package is absent for
+source distributions without 'pyproject.toml'.
diff --git a/noxfile.py b/noxfile.py
index dae585738..8199b64a6 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -21,7 +21,7 @@ nox.options.sessions = ["lint"]
LOCATIONS = {
"common-wheels": "tests/data/common_wheels",
- "protected-pip": "tools/tox_pip.py",
+ "protected-pip": "tools/protected_pip.py",
}
REQUIREMENTS = {
"docs": "docs/requirements.txt",
@@ -40,8 +40,7 @@ def run_with_protected_pip(session: nox.Session, *arguments: str) -> None:
(stable) version, and not the code being tested. This ensures pip being
used is not the code being tested.
"""
- # https://github.com/theacodes/nox/pull/377
- env = {"VIRTUAL_ENV": session.virtualenv.location} # type: ignore
+ env = {"VIRTUAL_ENV": session.virtualenv.location}
command = ("python", LOCATIONS["protected-pip"]) + arguments
session.run(*command, env=env, silent=True)
@@ -66,11 +65,8 @@ def should_update_common_wheels() -> bool:
# -----------------------------------------------------------------------------
# Development Commands
-# These are currently prototypes to evaluate whether we want to switch over
-# completely to nox for all our automation. Contributors should prefer using
-# `tox -e ...` until this note is removed.
# -----------------------------------------------------------------------------
-@nox.session(python=["3.6", "3.7", "3.8", "3.9", "pypy3"])
+@nox.session(python=["3.7", "3.8", "3.9", "3.10", "pypy3"])
def test(session: nox.Session) -> None:
# Get the common wheels.
if should_update_common_wheels():
@@ -87,8 +83,7 @@ def test(session: nox.Session) -> None:
session.log(msg)
# Build source distribution
- # https://github.com/theacodes/nox/pull/377
- sdist_dir = os.path.join(session.virtualenv.location, "sdist") # type: ignore
+ sdist_dir = os.path.join(session.virtualenv.location, "sdist")
if os.path.exists(sdist_dir):
shutil.rmtree(sdist_dir, ignore_errors=True)
@@ -115,7 +110,13 @@ def test(session: nox.Session) -> None:
# Run the tests
# LC_CTYPE is set to get UTF-8 output inside of the subprocesses that our
# tests use.
- session.run("pytest", *arguments, env={"LC_CTYPE": "en_US.UTF-8"})
+ session.run(
+ "pytest",
+ *arguments,
+ env={
+ "LC_CTYPE": "en_US.UTF-8",
+ },
+ )
@nox.session
@@ -123,8 +124,7 @@ def docs(session: nox.Session) -> None:
session.install("-e", ".")
session.install("-r", REQUIREMENTS["docs"])
- def get_sphinx_build_command(kind):
- # type: (str) -> List[str]
+ def get_sphinx_build_command(kind: str) -> List[str]:
# Having the conf.py in the docs/html is weird but needed because we
# can not use a different configuration directory vs source directory
# on RTD currently. So, we'll pass "-c docs/html" here.
@@ -174,14 +174,13 @@ def lint(session: nox.Session) -> None:
@nox.session
def vendoring(session: nox.Session) -> None:
- session.install("vendoring>=0.3.0")
+ session.install("vendoring~=1.2.0")
if "--upgrade" not in session.posargs:
- session.run("vendoring", "sync", ".", "-v")
+ session.run("vendoring", "sync", "-v")
return
- def pinned_requirements(path):
- # type: (Path) -> Iterator[Tuple[str, str]]
+ def pinned_requirements(path: Path) -> Iterator[Tuple[str, str]]:
for line in path.read_text().splitlines(keepends=False):
one, sep, two = line.partition("==")
if not sep:
@@ -227,6 +226,28 @@ def vendoring(session: nox.Session) -> None:
release.commit_file(session, ".", message=message)
+@nox.session
+def coverage(session: nox.Session) -> None:
+ # Install source distribution
+ run_with_protected_pip(session, "install", ".")
+
+ # Install test dependencies
+ run_with_protected_pip(session, "install", "-r", REQUIREMENTS["tests"])
+
+ if not os.path.exists(".coverage-output"):
+ os.mkdir(".coverage-output")
+ session.run(
+ "pytest",
+ "--cov=pip",
+ "--cov-config=./setup.cfg",
+ *session.posargs,
+ env={
+ "COVERAGE_OUTPUT_DIR": "./.coverage-output",
+ "COVERAGE_PROCESS_START": os.fsdecode(Path("setup.cfg").resolve()),
+ },
+ )
+
+
# -----------------------------------------------------------------------------
# Release Commands
# -----------------------------------------------------------------------------
diff --git a/pyproject.toml b/pyproject.toml
index 9f6dbe12a..a02457eef 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,13 +3,19 @@ requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[tool.towncrier]
+# For finding the __version__
package = "pip"
package_dir = "src"
+# For writing into the correct file
filename = "NEWS.rst"
+# For finding the news fragments
directory = "news/"
-title_format = "{version} ({project_date})"
+
+# For rendering properly for this project
issue_format = "`#{issue} <https://github.com/pypa/pip/issues/{issue}>`_"
template = "tools/news/template.rst"
+
+# Grouping of entries, within our changelog
type = [
{ name = "Process", directory = "process", showcontent = true },
{ name = "Deprecations and Removals", directory = "removal", showcontent = true },
@@ -33,6 +39,7 @@ substitute = [
# pkg_resource's vendored packages are directly vendored in pip.
{ match='pkg_resources\.extern', replace="pip._vendor" },
{ match='from \.extern', replace="from pip._vendor" },
+ { match='''\('pygments\.lexers\.''', replace="('pip._vendor.pygments.lexers." },
]
drop = [
# contains unnecessary scripts
@@ -44,19 +51,21 @@ drop = [
"setuptools",
"pkg_resources/_vendor/",
"pkg_resources/extern/",
- # unneeded part for tenacity
- "tenacity/tests",
+ # trim vendored pygments styles and lexers
+ "pygments/styles/[!_]*.py",
+ '^pygments/lexers/(?!python|__init__|_mapping).*\.py$',
+ # trim rich's markdown support
+ "rich/markdown.py",
]
[tool.vendoring.typing-stubs]
six = ["six.__init__", "six.moves.__init__", "six.moves.configparser"]
-appdirs = []
+distro = []
[tool.vendoring.license.directories]
setuptools = "pkg_resources"
-msgpack-python = "msgpack"
[tool.vendoring.license.fallback-urls]
-pytoml = "https://github.com/avakar/pytoml/raw/master/LICENSE"
-resolvelib = "https://github.com/sarugaku/resolvelib/raw/master/LICENSE"
+CacheControl = "https://raw.githubusercontent.com/ionrock/cachecontrol/v0.12.6/LICENSE.txt"
+distlib = "https://bitbucket.org/pypa/distlib/raw/master/LICENSE.txt"
webencodings = "https://github.com/SimonSapin/python-webencodings/raw/master/LICENSE"
diff --git a/setup.cfg b/setup.cfg
index d5dfb587e..dae2f21b1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -24,6 +24,10 @@ extend-ignore =
G200, G202,
# black adds spaces around ':'
E203,
+ # using a cache
+ B019,
+ # reassigning variables in a loop
+ B020,
per-file-ignores =
# G: The plugin logging-format treats every .log and .error as logging.
noxfile.py: G
@@ -31,10 +35,12 @@ per-file-ignores =
tests/*: B011
[mypy]
+mypy_path = $MYPY_CONFIG_FILE_DIR/src
ignore_missing_imports = True
disallow_untyped_defs = True
disallow_any_generics = True
warn_unused_ignores = True
+no_implicit_optional = True
[mypy-pip._vendor.*]
ignore_errors = True
@@ -50,13 +56,13 @@ follow_imports = skip
follow_imports = skip
[mypy-pip._vendor.requests.*]
follow_imports = skip
-[mypy-pip._vendor.retrying]
-follow_imports = skip
[tool:pytest]
addopts = --ignore src/pip/_vendor --ignore tests/tests_cache -r aR --color=yes
+xfail_strict = True
markers =
network: tests that need network
+ incompatible_with_sysconfig
incompatible_with_test_venv
incompatible_with_venv
no_auto_tempdir_manager
@@ -103,8 +109,6 @@ exclude_lines =
pragma: no cover
# This excludes typing-specific code, which will be validated by mypy anyway.
if TYPE_CHECKING
- # Can be set to exclude e.g. `if PY2:` on Python 3
- ${PIP_CI_COVERAGE_EXCLUDES}
[metadata]
license_file = LICENSE.txt
diff --git a/setup.py b/setup.py
index a233bd253..2179d34d2 100644
--- a/setup.py
+++ b/setup.py
@@ -37,10 +37,11 @@ setup(
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
],
@@ -63,7 +64,14 @@ setup(
"pip._vendor.certifi": ["*.pem"],
"pip._vendor.requests": ["*.pem"],
"pip._vendor.distlib._backport": ["sysconfig.cfg"],
- "pip._vendor.distlib": ["t32.exe", "t64.exe", "w32.exe", "w64.exe"],
+ "pip._vendor.distlib": [
+ "t32.exe",
+ "t64.exe",
+ "t64-arm.exe",
+ "w32.exe",
+ "w64.exe",
+ "w64-arm.exe",
+ ],
},
entry_points={
"console_scripts": [
@@ -73,5 +81,7 @@ setup(
],
},
zip_safe=False,
- python_requires=">=3.6",
+ # NOTE: python_requires is duplicated in __pip-runner__.py.
+ # When changing this value, please change the other copy as well.
+ python_requires=">=3.7",
)
diff --git a/src/pip/__init__.py b/src/pip/__init__.py
index e8bb5c837..a40148f00 100644
--- a/src/pip/__init__.py
+++ b/src/pip/__init__.py
@@ -1,6 +1,6 @@
from typing import List, Optional
-__version__ = "21.3.dev0"
+__version__ = "22.3.dev0"
def main(args: Optional[List[str]] = None) -> int:
diff --git a/src/pip/__pip-runner__.py b/src/pip/__pip-runner__.py
new file mode 100644
index 000000000..49a148a09
--- /dev/null
+++ b/src/pip/__pip-runner__.py
@@ -0,0 +1,50 @@
+"""Execute exactly this copy of pip, within a different environment.
+
+This file is named as it is, to ensure that this module can't be imported via
+an import statement.
+"""
+
+# /!\ This version compatibility check section must be Python 2 compatible. /!\
+
+import sys
+
+# Copied from setup.py
+PYTHON_REQUIRES = (3, 7)
+
+
+def version_str(version): # type: ignore
+ return ".".join(str(v) for v in version)
+
+
+if sys.version_info[:2] < PYTHON_REQUIRES:
+ raise SystemExit(
+ "This version of pip does not support python {} (requires >={}).".format(
+ version_str(sys.version_info[:2]), version_str(PYTHON_REQUIRES)
+ )
+ )
+
+# From here on, we can use Python 3 features, but the syntax must remain
+# Python 2 compatible.
+
+import runpy # noqa: E402
+from importlib.machinery import PathFinder # noqa: E402
+from os.path import dirname # noqa: E402
+
+PIP_SOURCES_ROOT = dirname(dirname(__file__))
+
+
+class PipImportRedirectingFinder:
+ @classmethod
+ def find_spec(self, fullname, path=None, target=None): # type: ignore
+ if fullname != "pip":
+ return None
+
+ spec = PathFinder.find_spec(fullname, [PIP_SOURCES_ROOT], target)
+ assert spec, (PIP_SOURCES_ROOT, fullname)
+ return spec
+
+
+sys.meta_path.insert(0, PipImportRedirectingFinder())
+
+assert __name__ == "__main__", "Cannot run __pip-runner__.py as a non-main module"
+runpy.run_module("pip", run_name="__main__", alter_sys=True)
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
index de98163d7..6213eedd1 100644
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -1,17 +1,15 @@
"""Build Environment used for isolation during sdist building
"""
-import contextlib
import logging
import os
import pathlib
import sys
import textwrap
-import zipfile
from collections import OrderedDict
from sysconfig import get_paths
from types import TracebackType
-from typing import TYPE_CHECKING, Iterable, Iterator, List, Optional, Set, Tuple, Type
+from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type
from pip._vendor.certifi import where
from pip._vendor.packaging.requirements import Requirement
@@ -20,7 +18,7 @@ from pip._vendor.packaging.version import Version
from pip import __file__ as pip_location
from pip._internal.cli.spinners import open_spinner
from pip._internal.locations import get_platlib, get_prefixed_libs, get_purelib
-from pip._internal.metadata import get_environment
+from pip._internal.metadata import get_default_environment, get_environment
from pip._internal.utils.subprocess import call_subprocess
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
@@ -31,61 +29,45 @@ logger = logging.getLogger(__name__)
class _Prefix:
-
- def __init__(self, path):
- # type: (str) -> None
+ def __init__(self, path: str) -> None:
self.path = path
self.setup = False
self.bin_dir = get_paths(
- 'nt' if os.name == 'nt' else 'posix_prefix',
- vars={'base': path, 'platbase': path}
- )['scripts']
+ "nt" if os.name == "nt" else "posix_prefix",
+ vars={"base": path, "platbase": path},
+ )["scripts"]
self.lib_dirs = get_prefixed_libs(path)
-@contextlib.contextmanager
-def _create_standalone_pip() -> Iterator[str]:
- """Create a "standalone pip" zip file.
+def get_runnable_pip() -> str:
+ """Get a file to pass to a Python executable, to run the currently-running pip.
- The zip file's content is identical to the currently-running pip.
- It will be used to install requirements into the build environment.
+ This is used to run a pip subprocess, for installing requirements into the build
+ environment.
"""
source = pathlib.Path(pip_location).resolve().parent
- # Return the current instance if `source` is not a directory. We can't build
- # a zip from this, and it likely means the instance is already standalone.
if not source.is_dir():
- yield str(source)
- return
+ # This would happen if someone is using pip from inside a zip file. In that
+ # case, we can use that directly.
+ return str(source)
- with TempDirectory(kind="standalone-pip") as tmp_dir:
- pip_zip = os.path.join(tmp_dir.path, "__env_pip__.zip")
- kwargs = {}
- if sys.version_info >= (3, 8):
- kwargs["strict_timestamps"] = False
- with zipfile.ZipFile(pip_zip, "w", **kwargs) as zf:
- for child in source.rglob("*"):
- zf.write(child, child.relative_to(source.parent).as_posix())
- yield os.path.join(pip_zip, "pip")
+ return os.fsdecode(source / "__pip-runner__.py")
class BuildEnvironment:
- """Creates and manages an isolated environment to install build deps
- """
+ """Creates and manages an isolated environment to install build deps"""
- def __init__(self):
- # type: () -> None
- temp_dir = TempDirectory(
- kind=tempdir_kinds.BUILD_ENV, globally_managed=True
- )
+ def __init__(self) -> None:
+ temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV, globally_managed=True)
self._prefixes = OrderedDict(
(name, _Prefix(os.path.join(temp_dir.path, name)))
- for name in ('normal', 'overlay')
+ for name in ("normal", "overlay")
)
- self._bin_dirs = [] # type: List[str]
- self._lib_dirs = [] # type: List[str]
+ self._bin_dirs: List[str] = []
+ self._lib_dirs: List[str] = []
for prefix in reversed(list(self._prefixes.values())):
self._bin_dirs.append(prefix.bin_dir)
self._lib_dirs.extend(prefix.lib_dirs)
@@ -96,12 +78,15 @@ class BuildEnvironment:
system_sites = {
os.path.normcase(site) for site in (get_purelib(), get_platlib())
}
- self._site_dir = os.path.join(temp_dir.path, 'site')
+ self._site_dir = os.path.join(temp_dir.path, "site")
if not os.path.exists(self._site_dir):
os.mkdir(self._site_dir)
- with open(os.path.join(self._site_dir, 'sitecustomize.py'), 'w') as fp:
- fp.write(textwrap.dedent(
- '''
+ with open(
+ os.path.join(self._site_dir, "sitecustomize.py"), "w", encoding="utf-8"
+ ) as fp:
+ fp.write(
+ textwrap.dedent(
+ """
import os, site, sys
# First, drop system-sites related paths.
@@ -124,54 +109,64 @@ class BuildEnvironment:
for path in {lib_dirs!r}:
assert not path in sys.path
site.addsitedir(path)
- '''
- ).format(system_sites=system_sites, lib_dirs=self._lib_dirs))
+ """
+ ).format(system_sites=system_sites, lib_dirs=self._lib_dirs)
+ )
- def __enter__(self):
- # type: () -> None
+ def __enter__(self) -> None:
self._save_env = {
name: os.environ.get(name, None)
- for name in ('PATH', 'PYTHONNOUSERSITE', 'PYTHONPATH')
+ for name in ("PATH", "PYTHONNOUSERSITE", "PYTHONPATH")
}
path = self._bin_dirs[:]
- old_path = self._save_env['PATH']
+ old_path = self._save_env["PATH"]
if old_path:
path.extend(old_path.split(os.pathsep))
pythonpath = [self._site_dir]
- os.environ.update({
- 'PATH': os.pathsep.join(path),
- 'PYTHONNOUSERSITE': '1',
- 'PYTHONPATH': os.pathsep.join(pythonpath),
- })
+ os.environ.update(
+ {
+ "PATH": os.pathsep.join(path),
+ "PYTHONNOUSERSITE": "1",
+ "PYTHONPATH": os.pathsep.join(pythonpath),
+ }
+ )
def __exit__(
self,
- exc_type, # type: Optional[Type[BaseException]]
- exc_val, # type: Optional[BaseException]
- exc_tb # type: Optional[TracebackType]
- ):
- # type: (...) -> None
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
for varname, old_value in self._save_env.items():
if old_value is None:
os.environ.pop(varname, None)
else:
os.environ[varname] = old_value
- def check_requirements(self, reqs):
- # type: (Iterable[str]) -> Tuple[Set[Tuple[str, str]], Set[str]]
+ def check_requirements(
+ self, reqs: Iterable[str]
+ ) -> Tuple[Set[Tuple[str, str]], Set[str]]:
"""Return 2 sets:
- - conflicting requirements: set of (installed, wanted) reqs tuples
- - missing requirements: set of reqs
+ - conflicting requirements: set of (installed, wanted) reqs tuples
+ - missing requirements: set of reqs
"""
missing = set()
conflicting = set()
if reqs:
- env = get_environment(self._lib_dirs)
+ env = (
+ get_environment(self._lib_dirs)
+ if hasattr(self, "_lib_dirs")
+ else get_default_environment()
+ )
for req_str in reqs:
req = Requirement(req_str)
+ # We're explicitly evaluating with an empty extra value, since build
+ # environments are not provided any mechanism to select specific extras.
+ if req.marker is not None and not req.marker.evaluate({"extra": ""}):
+ continue
dist = env.get_distribution(req.name)
if not dist:
missing.add(req_str)
@@ -180,40 +175,31 @@ class BuildEnvironment:
installed_req_str = f"{req.name}=={dist.version}"
else:
installed_req_str = f"{req.name}==={dist.version}"
- if dist.version not in req.specifier:
+ if not req.specifier.contains(dist.version, prereleases=True):
conflicting.add((installed_req_str, req_str))
# FIXME: Consider direct URL?
return conflicting, missing
def install_requirements(
self,
- finder, # type: PackageFinder
- requirements, # type: Iterable[str]
- prefix_as_string, # type: str
- message # type: str
- ):
- # type: (...) -> None
+ finder: "PackageFinder",
+ requirements: Iterable[str],
+ prefix_as_string: str,
+ *,
+ kind: str,
+ ) -> None:
prefix = self._prefixes[prefix_as_string]
assert not prefix.setup
prefix.setup = True
if not requirements:
return
- with contextlib.ExitStack() as ctx:
- # TODO: Remove this block when dropping 3.6 support. Python 3.6
- # lacks importlib.resources and pep517 has issues loading files in
- # a zip, so we fallback to the "old" method by adding the current
- # pip directory to the child process's sys.path.
- if sys.version_info < (3, 7):
- pip_runnable = os.path.dirname(pip_location)
- else:
- pip_runnable = ctx.enter_context(_create_standalone_pip())
- self._install_requirements(
- pip_runnable,
- finder,
- requirements,
- prefix,
- message,
- )
+ self._install_requirements(
+ get_runnable_pip(),
+ finder,
+ requirements,
+ prefix,
+ kind=kind,
+ )
@staticmethod
def _install_requirements(
@@ -221,74 +207,84 @@ class BuildEnvironment:
finder: "PackageFinder",
requirements: Iterable[str],
prefix: _Prefix,
- message: str,
+ *,
+ kind: str,
) -> None:
- args = [
- sys.executable, pip_runnable, 'install',
- '--ignore-installed', '--no-user', '--prefix', prefix.path,
- '--no-warn-script-location',
- ] # type: List[str]
+ args: List[str] = [
+ sys.executable,
+ pip_runnable,
+ "install",
+ "--ignore-installed",
+ "--no-user",
+ "--prefix",
+ prefix.path,
+ "--no-warn-script-location",
+ ]
if logger.getEffectiveLevel() <= logging.DEBUG:
- args.append('-v')
- for format_control in ('no_binary', 'only_binary'):
+ args.append("-v")
+ for format_control in ("no_binary", "only_binary"):
formats = getattr(finder.format_control, format_control)
- args.extend(('--' + format_control.replace('_', '-'),
- ','.join(sorted(formats or {':none:'}))))
+ args.extend(
+ (
+ "--" + format_control.replace("_", "-"),
+ ",".join(sorted(formats or {":none:"})),
+ )
+ )
index_urls = finder.index_urls
if index_urls:
- args.extend(['-i', index_urls[0]])
+ args.extend(["-i", index_urls[0]])
for extra_index in index_urls[1:]:
- args.extend(['--extra-index-url', extra_index])
+ args.extend(["--extra-index-url", extra_index])
else:
- args.append('--no-index')
+ args.append("--no-index")
for link in finder.find_links:
- args.extend(['--find-links', link])
+ args.extend(["--find-links", link])
for host in finder.trusted_hosts:
- args.extend(['--trusted-host', host])
+ args.extend(["--trusted-host", host])
if finder.allow_all_prereleases:
- args.append('--pre')
+ args.append("--pre")
if finder.prefer_binary:
- args.append('--prefer-binary')
- args.append('--')
+ args.append("--prefer-binary")
+ args.append("--")
args.extend(requirements)
extra_environ = {"_PIP_STANDALONE_CERT": where()}
- with open_spinner(message) as spinner:
- call_subprocess(args, spinner=spinner, extra_environ=extra_environ)
+ with open_spinner(f"Installing {kind}") as spinner:
+ call_subprocess(
+ args,
+ command_desc=f"pip subprocess to install {kind}",
+ spinner=spinner,
+ extra_environ=extra_environ,
+ )
class NoOpBuildEnvironment(BuildEnvironment):
- """A no-op drop-in replacement for BuildEnvironment
- """
+ """A no-op drop-in replacement for BuildEnvironment"""
- def __init__(self):
- # type: () -> None
+ def __init__(self) -> None:
pass
- def __enter__(self):
- # type: () -> None
+ def __enter__(self) -> None:
pass
def __exit__(
self,
- exc_type, # type: Optional[Type[BaseException]]
- exc_val, # type: Optional[BaseException]
- exc_tb # type: Optional[TracebackType]
- ):
- # type: (...) -> None
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
pass
- def cleanup(self):
- # type: () -> None
+ def cleanup(self) -> None:
pass
def install_requirements(
self,
- finder, # type: PackageFinder
- requirements, # type: Iterable[str]
- prefix_as_string, # type: str
- message # type: str
- ):
- # type: (...) -> None
+ finder: "PackageFinder",
+ requirements: Iterable[str],
+ prefix_as_string: str,
+ *,
+ kind: str,
+ ) -> None:
raise NotImplementedError()
diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py
index 7ef51b92e..e51edd515 100644
--- a/src/pip/_internal/cache.py
+++ b/src/pip/_internal/cache.py
@@ -5,12 +5,14 @@ import hashlib
import json
import logging
import os
+from pathlib import Path
from typing import Any, Dict, List, Optional, Set
from pip._vendor.packaging.tags import Tag, interpreter_name, interpreter_version
from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.exceptions import InvalidWheelFilename
+from pip._internal.models.direct_url import DirectUrl
from pip._internal.models.format_control import FormatControl
from pip._internal.models.link import Link
from pip._internal.models.wheel import Wheel
@@ -19,9 +21,10 @@ from pip._internal.utils.urls import path_to_url
logger = logging.getLogger(__name__)
+ORIGIN_JSON_NAME = "origin.json"
-def _hash_dict(d):
- # type: (Dict[str, str]) -> str
+
+def _hash_dict(d: Dict[str, str]) -> str:
"""Return a stable sha224 of a dictionary."""
s = json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
return hashlib.sha224(s.encode("ascii")).hexdigest()
@@ -31,15 +34,16 @@ class Cache:
"""An abstract class - provides cache directories for data from links
- :param cache_dir: The root of the cache.
- :param format_control: An object of FormatControl class to limit
- binaries being read from the cache.
- :param allowed_formats: which formats of files the cache should store.
- ('binary' and 'source' are the only allowed values)
+ :param cache_dir: The root of the cache.
+ :param format_control: An object of FormatControl class to limit
+ binaries being read from the cache.
+ :param allowed_formats: which formats of files the cache should store.
+ ('binary' and 'source' are the only allowed values)
"""
- def __init__(self, cache_dir, format_control, allowed_formats):
- # type: (str, FormatControl, Set[str]) -> None
+ def __init__(
+ self, cache_dir: str, format_control: FormatControl, allowed_formats: Set[str]
+ ) -> None:
super().__init__()
assert not cache_dir or os.path.isabs(cache_dir)
self.cache_dir = cache_dir or None
@@ -49,10 +53,8 @@ class Cache:
_valid_formats = {"source", "binary"}
assert self.allowed_formats.union(_valid_formats) == _valid_formats
- def _get_cache_path_parts(self, link):
- # type: (Link) -> List[str]
- """Get parts of part that must be os.path.joined with cache_dir
- """
+ def _get_cache_path_parts(self, link: Link) -> List[str]:
+ """Get parts of part that must be os.path.joined with cache_dir"""
# We want to generate an url to use as our cache key, we don't want to
# just re-use the URL because it might have other items in the fragment
@@ -84,19 +86,12 @@ class Cache:
return parts
- def _get_candidates(self, link, canonical_package_name):
- # type: (Link, str) -> List[Any]
- can_not_cache = (
- not self.cache_dir or
- not canonical_package_name or
- not link
- )
+ def _get_candidates(self, link: Link, canonical_package_name: str) -> List[Any]:
+ can_not_cache = not self.cache_dir or not canonical_package_name or not link
if can_not_cache:
return []
- formats = self.format_control.get_allowed_formats(
- canonical_package_name
- )
+ formats = self.format_control.get_allowed_formats(canonical_package_name)
if not self.allowed_formats.intersection(formats):
return []
@@ -107,19 +102,16 @@ class Cache:
candidates.append((candidate, path))
return candidates
- def get_path_for_link(self, link):
- # type: (Link) -> str
- """Return a directory to store cached items in for link.
- """
+ def get_path_for_link(self, link: Link) -> str:
+ """Return a directory to store cached items in for link."""
raise NotImplementedError()
def get(
self,
- link, # type: Link
- package_name, # type: Optional[str]
- supported_tags, # type: List[Tag]
- ):
- # type: (...) -> Link
+ link: Link,
+ package_name: Optional[str],
+ supported_tags: List[Tag],
+ ) -> Link:
"""Returns a link to a cached item if it exists, otherwise returns the
passed link.
"""
@@ -127,15 +119,12 @@ class Cache:
class SimpleWheelCache(Cache):
- """A cache of wheels for future installs.
- """
+ """A cache of wheels for future installs."""
- def __init__(self, cache_dir, format_control):
- # type: (str, FormatControl) -> None
+ def __init__(self, cache_dir: str, format_control: FormatControl) -> None:
super().__init__(cache_dir, format_control, {"binary"})
- def get_path_for_link(self, link):
- # type: (Link) -> str
+ def get_path_for_link(self, link: Link) -> str:
"""Return a directory to store cached wheels for link
Because there are M wheels for any one sdist, we provide a directory
@@ -157,20 +146,17 @@ class SimpleWheelCache(Cache):
def get(
self,
- link, # type: Link
- package_name, # type: Optional[str]
- supported_tags, # type: List[Tag]
- ):
- # type: (...) -> Link
+ link: Link,
+ package_name: Optional[str],
+ supported_tags: List[Tag],
+ ) -> Link:
candidates = []
if not package_name:
return link
canonical_package_name = canonicalize_name(package_name)
- for wheel_name, wheel_dir in self._get_candidates(
- link, canonical_package_name
- ):
+ for wheel_name, wheel_dir in self._get_candidates(link, canonical_package_name):
try:
wheel = Wheel(wheel_name)
except InvalidWheelFilename:
@@ -179,7 +165,9 @@ class SimpleWheelCache(Cache):
logger.debug(
"Ignoring cached wheel %s for %s as it "
"does not match the expected distribution name %s.",
- wheel_name, link, package_name,
+ wheel_name,
+ link,
+ package_name,
)
continue
if not wheel.supported(supported_tags):
@@ -201,11 +189,9 @@ class SimpleWheelCache(Cache):
class EphemWheelCache(SimpleWheelCache):
- """A SimpleWheelCache that creates it's own temporary cache directory
- """
+ """A SimpleWheelCache that creates it's own temporary cache directory"""
- def __init__(self, format_control):
- # type: (FormatControl) -> None
+ def __init__(self, format_control: FormatControl) -> None:
self._temp_dir = TempDirectory(
kind=tempdir_kinds.EPHEM_WHEEL_CACHE,
globally_managed=True,
@@ -217,11 +203,15 @@ class EphemWheelCache(SimpleWheelCache):
class CacheEntry:
def __init__(
self,
- link, # type: Link
- persistent, # type: bool
+ link: Link,
+ persistent: bool,
):
self.link = link
self.persistent = persistent
+ self.origin: Optional[DirectUrl] = None
+ origin_direct_url_path = Path(self.link.file_path).parent / ORIGIN_JSON_NAME
+ if origin_direct_url_path.exists():
+ self.origin = DirectUrl.from_json(origin_direct_url_path.read_text())
class WheelCache(Cache):
@@ -231,27 +221,23 @@ class WheelCache(Cache):
when a certain link is not found in the simple wheel cache first.
"""
- def __init__(self, cache_dir, format_control):
- # type: (str, FormatControl) -> None
- super().__init__(cache_dir, format_control, {'binary'})
+ def __init__(self, cache_dir: str, format_control: FormatControl) -> None:
+ super().__init__(cache_dir, format_control, {"binary"})
self._wheel_cache = SimpleWheelCache(cache_dir, format_control)
self._ephem_cache = EphemWheelCache(format_control)
- def get_path_for_link(self, link):
- # type: (Link) -> str
+ def get_path_for_link(self, link: Link) -> str:
return self._wheel_cache.get_path_for_link(link)
- def get_ephem_path_for_link(self, link):
- # type: (Link) -> str
+ def get_ephem_path_for_link(self, link: Link) -> str:
return self._ephem_cache.get_path_for_link(link)
def get(
self,
- link, # type: Link
- package_name, # type: Optional[str]
- supported_tags, # type: List[Tag]
- ):
- # type: (...) -> Link
+ link: Link,
+ package_name: Optional[str],
+ supported_tags: List[Tag],
+ ) -> Link:
cache_entry = self.get_cache_entry(link, package_name, supported_tags)
if cache_entry is None:
return link
@@ -259,11 +245,10 @@ class WheelCache(Cache):
def get_cache_entry(
self,
- link, # type: Link
- package_name, # type: Optional[str]
- supported_tags, # type: List[Tag]
- ):
- # type: (...) -> Optional[CacheEntry]
+ link: Link,
+ package_name: Optional[str],
+ supported_tags: List[Tag],
+ ) -> Optional[CacheEntry]:
"""Returns a CacheEntry with a link to a cached item if it exists or
None. The cache entry indicates if the item was found in the persistent
or ephemeral cache.
@@ -285,3 +270,20 @@ class WheelCache(Cache):
return CacheEntry(retval, persistent=False)
return None
+
+ @staticmethod
+ def record_download_origin(cache_dir: str, download_info: DirectUrl) -> None:
+ origin_path = Path(cache_dir) / ORIGIN_JSON_NAME
+ if origin_path.is_file():
+ origin = DirectUrl.from_json(origin_path.read_text())
+ # TODO: use DirectUrl.equivalent when https://github.com/pypa/pip/pull/10564
+ # is merged.
+ if origin.url != download_info.url:
+ logger.warning(
+ "Origin URL %s in cache entry %s does not match download URL %s. "
+ "This is likely a pip bug or a cache corruption issue.",
+ origin.url,
+ cache_dir,
+ download_info.url,
+ )
+ origin_path.write_text(download_info.to_json(), encoding="utf-8")
diff --git a/src/pip/_internal/cli/autocompletion.py b/src/pip/_internal/cli/autocompletion.py
index 3cad14860..226fe84dc 100644
--- a/src/pip/_internal/cli/autocompletion.py
+++ b/src/pip/_internal/cli/autocompletion.py
@@ -59,6 +59,14 @@ def autocomplete() -> None:
print(dist)
sys.exit(1)
+ should_list_installables = (
+ not current.startswith("-") and subcommand_name == "install"
+ )
+ if should_list_installables:
+ for path in auto_complete_paths(current, "path"):
+ print(path)
+ sys.exit(1)
+
subcommand = create_command(subcommand_name)
for opt in subcommand.parser.option_list_all:
@@ -138,7 +146,7 @@ def auto_complete_paths(current: str, completion_type: str) -> Iterable[str]:
starting with ``current``.
:param current: The word to be completed
- :param completion_type: path completion type(`file`, `path` or `dir`)i
+ :param completion_type: path completion type(``file``, ``path`` or ``dir``)
:return: A generator of regular files and/or directories
"""
directory, filename = os.path.split(current)
diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py
index eea38306a..0774f2608 100644
--- a/src/pip/_internal/cli/base_command.py
+++ b/src/pip/_internal/cli/base_command.py
@@ -1,5 +1,6 @@
"""Base Command class, and related routines"""
+import functools
import logging
import logging.config
import optparse
@@ -7,7 +8,9 @@ import os
import sys
import traceback
from optparse import Values
-from typing import Any, List, Optional, Tuple
+from typing import Any, Callable, List, Optional, Tuple
+
+from pip._vendor.rich import traceback as rich_traceback
from pip._internal.cli import cmdoptions
from pip._internal.cli.command_context import CommandContextMixIn
@@ -21,12 +24,12 @@ from pip._internal.cli.status_codes import (
from pip._internal.exceptions import (
BadCommand,
CommandError,
+ DiagnosticPipError,
InstallationError,
NetworkConnectionError,
PreviousBuildDirError,
UninstallationError,
)
-from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.filesystem import check_path_owner
from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
from pip._internal.utils.misc import get_prog, normalize_path
@@ -85,10 +88,10 @@ class Command(CommandContextMixIn):
# are present.
assert not hasattr(options, "no_index")
- def run(self, options: Values, args: List[Any]) -> int:
+ def run(self, options: Values, args: List[str]) -> int:
raise NotImplementedError
- def parse_args(self, args: List[str]) -> Tuple[Any, Any]:
+ def parse_args(self, args: List[str]) -> Tuple[Values, List[str]]:
# factored out for testability
return self.parser.parse_args(args)
@@ -148,20 +151,6 @@ class Command(CommandContextMixIn):
)
options.cache_dir = None
- if getattr(options, "build_dir", None):
- deprecated(
- reason=(
- "The -b/--build/--build-dir/--build-directory "
- "option is deprecated and has no effect anymore."
- ),
- replacement=(
- "use the TMPDIR/TEMP/TMP environment variable, "
- "possibly combined with --no-clean"
- ),
- gone_in="21.3",
- issue=8333,
- )
-
if "2020-resolver" in options.features_enabled:
logger.warning(
"--use-feature=2020-resolver no longer has any effect, "
@@ -169,46 +158,66 @@ class Command(CommandContextMixIn):
"This will become an error in pip 21.0."
)
+ def intercepts_unhandled_exc(
+ run_func: Callable[..., int]
+ ) -> Callable[..., int]:
+ @functools.wraps(run_func)
+ def exc_logging_wrapper(*args: Any) -> int:
+ try:
+ status = run_func(*args)
+ assert isinstance(status, int)
+ return status
+ except DiagnosticPipError as exc:
+ logger.error("[present-rich] %s", exc)
+ logger.debug("Exception information:", exc_info=True)
+
+ return ERROR
+ except PreviousBuildDirError as exc:
+ logger.critical(str(exc))
+ logger.debug("Exception information:", exc_info=True)
+
+ return PREVIOUS_BUILD_DIR_ERROR
+ except (
+ InstallationError,
+ UninstallationError,
+ BadCommand,
+ NetworkConnectionError,
+ ) as exc:
+ logger.critical(str(exc))
+ logger.debug("Exception information:", exc_info=True)
+
+ return ERROR
+ except CommandError as exc:
+ logger.critical("%s", exc)
+ logger.debug("Exception information:", exc_info=True)
+
+ return ERROR
+ except BrokenStdoutLoggingError:
+ # Bypass our logger and write any remaining messages to
+ # stderr because stdout no longer works.
+ print("ERROR: Pipe to stdout was broken", file=sys.stderr)
+ if level_number <= logging.DEBUG:
+ traceback.print_exc(file=sys.stderr)
+
+ return ERROR
+ except KeyboardInterrupt:
+ logger.critical("Operation cancelled by user")
+ logger.debug("Exception information:", exc_info=True)
+
+ return ERROR
+ except BaseException:
+ logger.critical("Exception:", exc_info=True)
+
+ return UNKNOWN_ERROR
+
+ return exc_logging_wrapper
+
try:
- status = self.run(options, args)
- assert isinstance(status, int)
- return status
- except PreviousBuildDirError as exc:
- logger.critical(str(exc))
- logger.debug("Exception information:", exc_info=True)
-
- return PREVIOUS_BUILD_DIR_ERROR
- except (
- InstallationError,
- UninstallationError,
- BadCommand,
- NetworkConnectionError,
- ) as exc:
- logger.critical(str(exc))
- logger.debug("Exception information:", exc_info=True)
-
- return ERROR
- except CommandError as exc:
- logger.critical("%s", exc)
- logger.debug("Exception information:", exc_info=True)
-
- return ERROR
- except BrokenStdoutLoggingError:
- # Bypass our logger and write any remaining messages to stderr
- # because stdout no longer works.
- print("ERROR: Pipe to stdout was broken", file=sys.stderr)
- if level_number <= logging.DEBUG:
- traceback.print_exc(file=sys.stderr)
-
- return ERROR
- except KeyboardInterrupt:
- logger.critical("Operation cancelled by user")
- logger.debug("Exception information:", exc_info=True)
-
- return ERROR
- except BaseException:
- logger.critical("Exception:", exc_info=True)
-
- return UNKNOWN_ERROR
+ if not options.debug_mode:
+ run = intercepts_unhandled_exc(self.run)
+ else:
+ run = self.run
+ rich_traceback.install(show_locals=True)
+ return run(options, args)
finally:
self.handle_pip_version_check(options)
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index b4f0f83c6..84e0e7838 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -10,9 +10,10 @@ pass on state. To be consistent, all options will follow this design.
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
+import importlib.util
+import logging
import os
import textwrap
-import warnings
from functools import partial
from optparse import SUPPRESS_HELP, Option, OptionGroup, OptionParser, Values
from textwrap import dedent
@@ -21,7 +22,6 @@ from typing import Any, Callable, Dict, Optional, Tuple
from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.cli.parser import ConfigOptionParser
-from pip._internal.cli.progress_bars import BAR_TYPES
from pip._internal.exceptions import CommandError
from pip._internal.locations import USER_CACHE_DIR, get_src_prefix
from pip._internal.models.format_control import FormatControl
@@ -30,6 +30,8 @@ from pip._internal.models.target_python import TargetPython
from pip._internal.utils.hashes import STRONG_HASHES
from pip._internal.utils.misc import strtobool
+logger = logging.getLogger(__name__)
+
def raise_option_error(parser: OptionParser, option: Option, msg: str) -> None:
"""
@@ -76,10 +78,9 @@ def check_install_build_global(
if any(map(getname, names)):
control = options.format_control
control.disallow_binaries()
- warnings.warn(
+ logger.warning(
"Disabling all use of wheels due to the use of --build-option "
"/ --global-option / --install-option.",
- stacklevel=2,
)
@@ -151,6 +152,18 @@ help_: Callable[..., Option] = partial(
help="Show help.",
)
+debug_mode: Callable[..., Option] = partial(
+ Option,
+ "--debug",
+ dest="debug_mode",
+ action="store_true",
+ default=False,
+ help=(
+ "Let unhandled exceptions propagate outside the main subroutine, "
+ "instead of logging them to stderr."
+ ),
+)
+
isolated_mode: Callable[..., Option] = partial(
Option,
"--isolated",
@@ -165,13 +178,22 @@ isolated_mode: Callable[..., Option] = partial(
require_virtualenv: Callable[..., Option] = partial(
Option,
- # Run only if inside a virtualenv, bail if not.
"--require-virtualenv",
"--require-venv",
dest="require_venv",
action="store_true",
default=False,
- help=SUPPRESS_HELP,
+ help=(
+ "Allow pip to only run in a virtual environment; "
+ "exit with an error otherwise."
+ ),
+)
+
+python: Callable[..., Option] = partial(
+ Option,
+ "--python",
+ dest="python",
+ help="Run pip with the specified Python interpreter.",
)
verbose: Callable[..., Option] = partial(
@@ -221,13 +243,9 @@ progress_bar: Callable[..., Option] = partial(
"--progress-bar",
dest="progress_bar",
type="choice",
- choices=list(BAR_TYPES.keys()),
+ choices=["on", "off"],
default="on",
- help=(
- "Specify type of progress to be displayed ["
- + "|".join(BAR_TYPES.keys())
- + "] (default: %default)"
- ),
+ help="Specify whether the progress bar should be used [on, off] (default: on)",
)
log: Callable[..., Option] = partial(
@@ -257,7 +275,7 @@ proxy: Callable[..., Option] = partial(
dest="proxy",
type="str",
default="",
- help="Specify a proxy in the form [user:passwd@]proxy.server:port.",
+ help="Specify a proxy in the form scheme://[user:passwd@]proxy.server:port.",
)
retries: Callable[..., Option] = partial(
@@ -719,18 +737,6 @@ no_deps: Callable[..., Option] = partial(
help="Don't install package dependencies.",
)
-build_dir: Callable[..., Option] = partial(
- PipOption,
- "-b",
- "--build",
- "--build-dir",
- "--build-directory",
- dest="build_dir",
- type="path",
- metavar="dir",
- help=SUPPRESS_HELP,
-)
-
ignore_requires_python: Callable[..., Option] = partial(
Option,
"--ignore-requires-python",
@@ -750,6 +756,15 @@ no_build_isolation: Callable[..., Option] = partial(
"if this option is used.",
)
+check_build_deps: Callable[..., Option] = partial(
+ Option,
+ "--check-build-dependencies",
+ dest="check_build_deps",
+ action="store_true",
+ default=False,
+ help="Check the build dependencies when PEP517 is used.",
+)
+
def _handle_no_use_pep517(
option: Option, opt: str, value: str, parser: OptionParser
@@ -772,6 +787,12 @@ def _handle_no_use_pep517(
"""
raise_option_error(parser, option=option, msg=msg)
+ # If user doesn't wish to use pep517, we check if setuptools is installed
+ # and raise error if it is not.
+ if not importlib.util.find_spec("setuptools"):
+ msg = "It is not possible to use --no-use-pep517 without setuptools installed."
+ raise_option_error(parser, option=option, msg=msg)
+
# Otherwise, --no-use-pep517 was passed via the command-line.
parser.values.use_pep517 = False
@@ -796,6 +817,33 @@ no_use_pep517: Any = partial(
help=SUPPRESS_HELP,
)
+
+def _handle_config_settings(
+ option: Option, opt_str: str, value: str, parser: OptionParser
+) -> None:
+ key, sep, val = value.partition("=")
+ if sep != "=":
+ parser.error(f"Arguments to {opt_str} must be of the form KEY=VAL") # noqa
+ dest = getattr(parser.values, option.dest)
+ if dest is None:
+ dest = {}
+ setattr(parser.values, option.dest, dest)
+ dest[key] = val
+
+
+config_settings: Callable[..., Option] = partial(
+ Option,
+ "--config-settings",
+ dest="config_settings",
+ type=str,
+ action="callback",
+ callback=_handle_config_settings,
+ metavar="settings",
+ help="Configuration settings to be passed to the PEP 517 build backend. "
+ "Settings take the form KEY=VALUE. Use multiple --config-settings options "
+ "to pass multiple keys to the backend.",
+)
+
install_options: Callable[..., Option] = partial(
Option,
"--install-option",
@@ -855,6 +903,15 @@ disable_pip_version_check: Callable[..., Option] = partial(
"of pip is available for download. Implied with --no-index.",
)
+root_user_action: Callable[..., Option] = partial(
+ Option,
+ "--root-user-action",
+ dest="root_user_action",
+ default="warn",
+ choices=["warn", "ignore"],
+ help="Action if pip is run as a root user. By default, a warning message is shown.",
+)
+
def _handle_merge_hash(
option: Option, opt_str: str, value: str, parser: OptionParser
@@ -950,7 +1007,7 @@ use_new_feature: Callable[..., Option] = partial(
metavar="feature",
action="append",
default=[],
- choices=["2020-resolver", "fast-deps", "in-tree-build"],
+ choices=["2020-resolver", "fast-deps", "truststore"],
help="Enable new functionality, that may be backward incompatible.",
)
@@ -961,7 +1018,9 @@ use_deprecated_feature: Callable[..., Option] = partial(
metavar="feature",
action="append",
default=[],
- choices=["legacy-resolver"],
+ choices=[
+ "legacy-resolver",
+ ],
help=("Enable deprecated functionality, that will be removed in the future."),
)
@@ -974,8 +1033,10 @@ general_group: Dict[str, Any] = {
"name": "General Options",
"options": [
help_,
+ debug_mode,
isolated_mode,
require_virtualenv,
+ python,
verbose,
version,
quiet,
diff --git a/src/pip/_internal/cli/command_context.py b/src/pip/_internal/cli/command_context.py
index ed6832237..139995ac3 100644
--- a/src/pip/_internal/cli/command_context.py
+++ b/src/pip/_internal/cli/command_context.py
@@ -1,5 +1,5 @@
from contextlib import ExitStack, contextmanager
-from typing import ContextManager, Iterator, TypeVar
+from typing import ContextManager, Generator, TypeVar
_T = TypeVar("_T", covariant=True)
@@ -11,7 +11,7 @@ class CommandContextMixIn:
self._main_context = ExitStack()
@contextmanager
- def main_context(self) -> Iterator[None]:
+ def main_context(self) -> Generator[None, None, None]:
assert not self._in_main_context
self._in_main_context = True
diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py
index 3666ab04c..5ade356b9 100644
--- a/src/pip/_internal/cli/main_parser.py
+++ b/src/pip/_internal/cli/main_parser.py
@@ -2,9 +2,11 @@
"""
import os
+import subprocess
import sys
-from typing import List, Tuple
+from typing import List, Optional, Tuple
+from pip._internal.build_env import get_runnable_pip
from pip._internal.cli import cmdoptions
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip._internal.commands import commands_dict, get_similar_commands
@@ -45,6 +47,25 @@ def create_main_parser() -> ConfigOptionParser:
return parser
+def identify_python_interpreter(python: str) -> Optional[str]:
+ # If the named file exists, use it.
+ # If it's a directory, assume it's a virtual environment and
+ # look for the environment's Python executable.
+ if os.path.exists(python):
+ if os.path.isdir(python):
+ # bin/python for Unix, Scripts/python.exe for Windows
+ # Try both in case of odd cases like cygwin.
+ for exe in ("bin/python", "Scripts/python.exe"):
+ py = os.path.join(python, exe)
+ if os.path.exists(py):
+ return py
+ else:
+ return python
+
+ # Could not find the interpreter specified
+ return None
+
+
def parse_command(args: List[str]) -> Tuple[str, List[str]]:
parser = create_main_parser()
@@ -57,6 +78,32 @@ def parse_command(args: List[str]) -> Tuple[str, List[str]]:
# args_else: ['install', '--user', 'INITools']
general_options, args_else = parser.parse_args(args)
+ # --python
+ if general_options.python and "_PIP_RUNNING_IN_SUBPROCESS" not in os.environ:
+ # Re-invoke pip using the specified Python interpreter
+ interpreter = identify_python_interpreter(general_options.python)
+ if interpreter is None:
+ raise CommandError(
+ f"Could not locate Python interpreter {general_options.python}"
+ )
+
+ pip_cmd = [
+ interpreter,
+ get_runnable_pip(),
+ ]
+ pip_cmd.extend(args)
+
+ # Set a flag so the child doesn't re-invoke itself, causing
+ # an infinite loop.
+ os.environ["_PIP_RUNNING_IN_SUBPROCESS"] = "1"
+ returncode = 0
+ try:
+ proc = subprocess.run(pip_cmd)
+ returncode = proc.returncode
+ except (subprocess.SubprocessError, OSError) as exc:
+ raise CommandError(f"Failed to run pip under {interpreter}: {exc}")
+ sys.exit(returncode)
+
# --version
if general_options.version:
sys.stdout.write(parser.version)
diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py
index a1c99a8cb..c762cf278 100644
--- a/src/pip/_internal/cli/parser.py
+++ b/src/pip/_internal/cli/parser.py
@@ -6,7 +6,7 @@ import shutil
import sys
import textwrap
from contextlib import suppress
-from typing import Any, Dict, Iterator, List, Tuple
+from typing import Any, Dict, Generator, List, Tuple
from pip._internal.cli.status_codes import UNKNOWN_ERROR
from pip._internal.configuration import Configuration, ConfigurationError
@@ -175,7 +175,9 @@ class ConfigOptionParser(CustomOptionParser):
print(f"An error occurred during configuration: {exc}")
sys.exit(3)
- def _get_ordered_configuration_items(self) -> Iterator[Tuple[str, Any]]:
+ def _get_ordered_configuration_items(
+ self,
+ ) -> Generator[Tuple[str, Any], None, None]:
# Configuration gives keys in an unordered manner. Order them.
override_order = ["global", self.name, ":env:"]
diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py
index f3db29519..0ad14031c 100644
--- a/src/pip/_internal/cli/progress_bars.py
+++ b/src/pip/_internal/cli/progress_bars.py
@@ -1,250 +1,68 @@
-import itertools
-import sys
-from signal import SIGINT, default_int_handler, signal
-from typing import Any
+import functools
+from typing import Callable, Generator, Iterable, Iterator, Optional, Tuple
+
+from pip._vendor.rich.progress import (
+ BarColumn,
+ DownloadColumn,
+ FileSizeColumn,
+ Progress,
+ ProgressColumn,
+ SpinnerColumn,
+ TextColumn,
+ TimeElapsedColumn,
+ TimeRemainingColumn,
+ TransferSpeedColumn,
+)
-from pip._vendor.progress.bar import Bar, FillingCirclesBar, IncrementalBar
-from pip._vendor.progress.spinner import Spinner
-
-from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.logging import get_indentation
-from pip._internal.utils.misc import format_size
-
-try:
- from pip._vendor import colorama
-# Lots of different errors can come from this, including SystemError and
-# ImportError.
-except Exception:
- colorama = None
-
-
-def _select_progress_class(preferred: Bar, fallback: Bar) -> Bar:
- encoding = getattr(preferred.file, "encoding", None)
- # If we don't know what encoding this file is in, then we'll just assume
- # that it doesn't support unicode and use the ASCII bar.
- if not encoding:
- return fallback
-
- # Collect all of the possible characters we want to use with the preferred
- # bar.
- characters = [
- getattr(preferred, "empty_fill", ""),
- getattr(preferred, "fill", ""),
- ]
- characters += list(getattr(preferred, "phases", []))
-
- # Try to decode the characters we're using for the bar using the encoding
- # of the given file, if this works then we'll assume that we can use the
- # fancier bar and if not we'll fall back to the plaintext bar.
- try:
- "".join(characters).encode(encoding)
- except UnicodeEncodeError:
- return fallback
+DownloadProgressRenderer = Callable[[Iterable[bytes]], Iterator[bytes]]
+
+
+def _rich_progress_bar(
+ iterable: Iterable[bytes],
+ *,
+ bar_type: str,
+ size: int,
+) -> Generator[bytes, None, None]:
+ assert bar_type == "on", "This should only be used in the default mode."
+
+ if not size:
+ total = float("inf")
+ columns: Tuple[ProgressColumn, ...] = (
+ TextColumn("[progress.description]{task.description}"),
+ SpinnerColumn("line", speed=1.5),
+ FileSizeColumn(),
+ TransferSpeedColumn(),
+ TimeElapsedColumn(),
+ )
else:
- return preferred
-
-
-_BaseBar: Any = _select_progress_class(IncrementalBar, Bar)
-
-
-class InterruptibleMixin:
- """
- Helper to ensure that self.finish() gets called on keyboard interrupt.
-
- This allows downloads to be interrupted without leaving temporary state
- (like hidden cursors) behind.
-
- This class is similar to the progress library's existing SigIntMixin
- helper, but as of version 1.2, that helper has the following problems:
-
- 1. It calls sys.exit().
- 2. It discards the existing SIGINT handler completely.
- 3. It leaves its own handler in place even after an uninterrupted finish,
- which will have unexpected delayed effects if the user triggers an
- unrelated keyboard interrupt some time after a progress-displaying
- download has already completed, for example.
- """
-
- def __init__(self, *args: Any, **kwargs: Any) -> None:
- """
- Save the original SIGINT handler for later.
- """
- # https://github.com/python/mypy/issues/5887
- super().__init__(*args, **kwargs) # type: ignore
-
- self.original_handler = signal(SIGINT, self.handle_sigint)
-
- # If signal() returns None, the previous handler was not installed from
- # Python, and we cannot restore it. This probably should not happen,
- # but if it does, we must restore something sensible instead, at least.
- # The least bad option should be Python's default SIGINT handler, which
- # just raises KeyboardInterrupt.
- if self.original_handler is None:
- self.original_handler = default_int_handler
-
- def finish(self) -> None:
- """
- Restore the original SIGINT handler after finishing.
-
- This should happen regardless of whether the progress display finishes
- normally, or gets interrupted.
- """
- super().finish() # type: ignore
- signal(SIGINT, self.original_handler)
-
- def handle_sigint(self, signum, frame): # type: ignore
- """
- Call self.finish() before delegating to the original SIGINT handler.
-
- This handler should only be in place while the progress display is
- active.
- """
- self.finish()
- self.original_handler(signum, frame)
-
-
-class SilentBar(Bar):
- def update(self) -> None:
- pass
-
-
-class BlueEmojiBar(IncrementalBar):
-
- suffix = "%(percent)d%%"
- bar_prefix = " "
- bar_suffix = " "
- phases = ("\U0001F539", "\U0001F537", "\U0001F535")
-
-
-class DownloadProgressMixin:
- def __init__(self, *args: Any, **kwargs: Any) -> None:
- # https://github.com/python/mypy/issues/5887
- super().__init__(*args, **kwargs) # type: ignore
- self.message: str = (" " * (get_indentation() + 2)) + self.message
-
- @property
- def downloaded(self) -> str:
- return format_size(self.index) # type: ignore
-
- @property
- def download_speed(self) -> str:
- # Avoid zero division errors...
- if self.avg == 0.0: # type: ignore
- return "..."
- return format_size(1 / self.avg) + "/s" # type: ignore
-
- @property
- def pretty_eta(self) -> str:
- if self.eta: # type: ignore
- return f"eta {self.eta_td}" # type: ignore
- return ""
-
- def iter(self, it): # type: ignore
- for x in it:
- yield x
- # B305 is incorrectly raised here
- # https://github.com/PyCQA/flake8-bugbear/issues/59
- self.next(len(x)) # noqa: B305
- self.finish()
-
-
-class WindowsMixin:
- def __init__(self, *args: Any, **kwargs: Any) -> None:
- # The Windows terminal does not support the hide/show cursor ANSI codes
- # even with colorama. So we'll ensure that hide_cursor is False on
- # Windows.
- # This call needs to go before the super() call, so that hide_cursor
- # is set in time. The base progress bar class writes the "hide cursor"
- # code to the terminal in its init, so if we don't set this soon
- # enough, we get a "hide" with no corresponding "show"...
- if WINDOWS and self.hide_cursor: # type: ignore
- self.hide_cursor = False
-
- # https://github.com/python/mypy/issues/5887
- super().__init__(*args, **kwargs) # type: ignore
-
- # Check if we are running on Windows and we have the colorama module,
- # if we do then wrap our file with it.
- if WINDOWS and colorama:
- self.file = colorama.AnsiToWin32(self.file) # type: ignore
- # The progress code expects to be able to call self.file.isatty()
- # but the colorama.AnsiToWin32() object doesn't have that, so we'll
- # add it.
- self.file.isatty = lambda: self.file.wrapped.isatty()
- # The progress code expects to be able to call self.file.flush()
- # but the colorama.AnsiToWin32() object doesn't have that, so we'll
- # add it.
- self.file.flush = lambda: self.file.wrapped.flush()
-
-
-class BaseDownloadProgressBar(WindowsMixin, InterruptibleMixin, DownloadProgressMixin):
-
- file = sys.stdout
- message = "%(percent)d%%"
- suffix = "%(downloaded)s %(download_speed)s %(pretty_eta)s"
-
-
-class DefaultDownloadProgressBar(BaseDownloadProgressBar, _BaseBar):
- pass
-
-
-class DownloadSilentBar(BaseDownloadProgressBar, SilentBar):
- pass
-
-
-class DownloadBar(BaseDownloadProgressBar, Bar):
- pass
-
-
-class DownloadFillingCirclesBar(BaseDownloadProgressBar, FillingCirclesBar):
- pass
-
-
-class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, BlueEmojiBar):
- pass
-
-
-class DownloadProgressSpinner(
- WindowsMixin, InterruptibleMixin, DownloadProgressMixin, Spinner
-):
-
- file = sys.stdout
- suffix = "%(downloaded)s %(download_speed)s"
-
- def next_phase(self) -> str:
- if not hasattr(self, "_phaser"):
- self._phaser = itertools.cycle(self.phases)
- return next(self._phaser)
-
- def update(self) -> None:
- message = self.message % self
- phase = self.next_phase()
- suffix = self.suffix % self
- line = "".join(
- [
- message,
- " " if message else "",
- phase,
- " " if suffix else "",
- suffix,
- ]
+ total = size
+ columns = (
+ TextColumn("[progress.description]{task.description}"),
+ BarColumn(),
+ DownloadColumn(),
+ TransferSpeedColumn(),
+ TextColumn("eta"),
+ TimeRemainingColumn(),
)
- self.writeln(line)
-
+ progress = Progress(*columns, refresh_per_second=30)
+ task_id = progress.add_task(" " * (get_indentation() + 2), total=total)
+ with progress:
+ for chunk in iterable:
+ yield chunk
+ progress.update(task_id, advance=len(chunk))
-BAR_TYPES = {
- "off": (DownloadSilentBar, DownloadSilentBar),
- "on": (DefaultDownloadProgressBar, DownloadProgressSpinner),
- "ascii": (DownloadBar, DownloadProgressSpinner),
- "pretty": (DownloadFillingCirclesBar, DownloadProgressSpinner),
- "emoji": (DownloadBlueEmojiProgressBar, DownloadProgressSpinner),
-}
+def get_download_progress_renderer(
+ *, bar_type: str, size: Optional[int] = None
+) -> DownloadProgressRenderer:
+ """Get an object that can be used to render the download progress.
-def DownloadProgressProvider(progress_bar, max=None): # type: ignore
- if max is None or max == 0:
- return BAR_TYPES[progress_bar][1]().iter
+ Returns a callable, that takes an iterable to "wrap".
+ """
+ if bar_type == "on":
+ return functools.partial(_rich_progress_bar, bar_type=bar_type, size=size)
else:
- return BAR_TYPES[progress_bar][0](max=max).iter
+ return iter # no-op, when passed an iterator
diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py
index 4129bf7e1..1044809f0 100644
--- a/src/pip/_internal/cli/req_command.py
+++ b/src/pip/_internal/cli/req_command.py
@@ -10,7 +10,7 @@ import os
import sys
from functools import partial
from optparse import Values
-from typing import Any, List, Optional, Tuple
+from typing import TYPE_CHECKING, Any, List, Optional, Tuple
from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
@@ -22,6 +22,7 @@ from pip._internal.index.package_finder import PackageFinder
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.models.target_python import TargetPython
from pip._internal.network.session import PipSession
+from pip._internal.operations.build.build_tracker import BuildTracker
from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req.constructors import (
install_req_from_editable,
@@ -31,7 +32,6 @@ from pip._internal.req.constructors import (
)
from pip._internal.req.req_file import parse_requirements
from pip._internal.req.req_install import InstallRequirement
-from pip._internal.req.req_tracker import RequirementTracker
from pip._internal.resolution.base import BaseResolver
from pip._internal.self_outdated_check import pip_self_version_check
from pip._internal.utils.temp_dir import (
@@ -41,9 +41,33 @@ from pip._internal.utils.temp_dir import (
)
from pip._internal.utils.virtualenv import running_under_virtualenv
+if TYPE_CHECKING:
+ from ssl import SSLContext
+
logger = logging.getLogger(__name__)
+def _create_truststore_ssl_context() -> Optional["SSLContext"]:
+ if sys.version_info < (3, 10):
+ raise CommandError("The truststore feature is only available for Python 3.10+")
+
+ try:
+ import ssl
+ except ImportError:
+ logger.warning("Disabling truststore since ssl support is missing")
+ return None
+
+ try:
+ import truststore
+ except ImportError:
+ raise CommandError(
+ "To use the truststore feature, 'truststore' must be installed into "
+ "pip's current environment."
+ )
+
+ return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+
+
class SessionCommandMixin(CommandContextMixIn):
"""
@@ -83,15 +107,27 @@ class SessionCommandMixin(CommandContextMixIn):
options: Values,
retries: Optional[int] = None,
timeout: Optional[int] = None,
+ fallback_to_certifi: bool = False,
) -> PipSession:
- assert not options.cache_dir or os.path.isabs(options.cache_dir)
+ cache_dir = options.cache_dir
+ assert not cache_dir or os.path.isabs(cache_dir)
+
+ if "truststore" in options.features_enabled:
+ try:
+ ssl_context = _create_truststore_ssl_context()
+ except Exception:
+ if not fallback_to_certifi:
+ raise
+ ssl_context = None
+ else:
+ ssl_context = None
+
session = PipSession(
- cache=(
- os.path.join(options.cache_dir, "http") if options.cache_dir else None
- ),
+ cache=os.path.join(cache_dir, "http") if cache_dir else None,
retries=retries if retries is not None else options.retries,
trusted_hosts=options.trusted_hosts,
index_urls=self._get_index_urls(options),
+ ssl_context=ssl_context,
)
# Handle custom ca-bundles from the user
@@ -141,7 +177,14 @@ class IndexGroupCommand(Command, SessionCommandMixin):
# Otherwise, check if we're using the latest version of pip available.
session = self._build_session(
- options, retries=0, timeout=min(5, options.timeout)
+ options,
+ retries=0,
+ timeout=min(5, options.timeout),
+ # This is set to ensure the function does not fail when truststore is
+ # specified in use-feature but cannot be loaded. This usually raises a
+ # CommandError and shows a nice user-facing error, but this function is not
+ # called in that try-except block.
+ fallback_to_certifi=True,
)
with session:
pip_self_version_check(session, options)
@@ -172,9 +215,10 @@ def warn_if_run_as_root() -> None:
# checks: https://mypy.readthedocs.io/en/stable/common_issues.html
if sys.platform == "win32" or sys.platform == "cygwin":
return
- if sys.platform == "darwin" or sys.platform == "linux":
- if os.getuid() != 0:
- return
+
+ if os.getuid() != 0:
+ return
+
logger.warning(
"Running pip as the 'root' user can result in broken permissions and "
"conflicting behaviour with the system package manager. "
@@ -230,11 +274,12 @@ class RequirementCommand(IndexGroupCommand):
cls,
temp_build_dir: TempDirectory,
options: Values,
- req_tracker: RequirementTracker,
+ build_tracker: BuildTracker,
session: PipSession,
finder: PackageFinder,
use_user_site: bool,
download_dir: Optional[str] = None,
+ verbosity: int = 0,
) -> RequirementPreparer:
"""
Create a RequirementPreparer instance for the given parameters.
@@ -265,14 +310,15 @@ class RequirementCommand(IndexGroupCommand):
src_dir=options.src_dir,
download_dir=download_dir,
build_isolation=options.build_isolation,
- req_tracker=req_tracker,
+ check_build_deps=options.check_build_deps,
+ build_tracker=build_tracker,
session=session,
progress_bar=options.progress_bar,
finder=finder,
require_hashes=options.require_hashes,
use_user_site=use_user_site,
lazy_wheel=lazy_wheel,
- in_tree_build="in-tree-build" in options.features_enabled,
+ verbosity=verbosity,
)
@classmethod
@@ -297,6 +343,7 @@ class RequirementCommand(IndexGroupCommand):
install_req_from_req_string,
isolated=options.isolated_mode,
use_pep517=use_pep517,
+ config_settings=getattr(options, "config_settings", None),
)
resolver_variant = cls.determine_resolver_variant(options)
# The long import name and duplicated invocation is needed to convince
@@ -367,6 +414,7 @@ class RequirementCommand(IndexGroupCommand):
isolated=options.isolated_mode,
use_pep517=options.use_pep517,
user_supplied=True,
+ config_settings=getattr(options, "config_settings", None),
)
requirements.append(req_to_add)
@@ -376,6 +424,7 @@ class RequirementCommand(IndexGroupCommand):
user_supplied=True,
isolated=options.isolated_mode,
use_pep517=options.use_pep517,
+ config_settings=getattr(options, "config_settings", None),
)
requirements.append(req_to_add)
diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py
index 1e313e109..cf2b976f3 100644
--- a/src/pip/_internal/cli/spinners.py
+++ b/src/pip/_internal/cli/spinners.py
@@ -3,9 +3,7 @@ import itertools
import logging
import sys
import time
-from typing import IO, Iterator
-
-from pip._vendor.progress import HIDE_CURSOR, SHOW_CURSOR
+from typing import IO, Generator, Optional
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.logging import get_indentation
@@ -25,7 +23,7 @@ class InteractiveSpinner(SpinnerInterface):
def __init__(
self,
message: str,
- file: IO[str] = None,
+ file: Optional[IO[str]] = None,
spin_chars: str = "-\\|/",
# Empirically, 8 updates/second looks nice
min_update_interval_seconds: float = 0.125,
@@ -115,7 +113,7 @@ class RateLimiter:
@contextlib.contextmanager
-def open_spinner(message: str) -> Iterator[SpinnerInterface]:
+def open_spinner(message: str) -> Generator[SpinnerInterface, None, None]:
# Interactive spinner goes directly to sys.stdout rather than being routed
# through the logging system, but it acts like it has level INFO,
# i.e. it's only displayed if we're at level INFO or better.
@@ -138,8 +136,12 @@ def open_spinner(message: str) -> Iterator[SpinnerInterface]:
spinner.finish("done")
+HIDE_CURSOR = "\x1b[?25l"
+SHOW_CURSOR = "\x1b[?25h"
+
+
@contextlib.contextmanager
-def hidden_cursor(file: IO[str]) -> Iterator[None]:
+def hidden_cursor(file: IO[str]) -> Generator[None, None, None]:
# The Windows terminal does not support the hide/show cursor ANSI codes,
# even via colorama. So don't even try.
if WINDOWS:
diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py
index 8e94b38f7..858a41014 100644
--- a/src/pip/_internal/commands/__init__.py
+++ b/src/pip/_internal/commands/__init__.py
@@ -3,87 +3,107 @@ Package containing all pip commands
"""
import importlib
-from collections import OrderedDict, namedtuple
+from collections import namedtuple
from typing import Any, Dict, Optional
from pip._internal.cli.base_command import Command
-CommandInfo = namedtuple('CommandInfo', 'module_path, class_name, summary')
+CommandInfo = namedtuple("CommandInfo", "module_path, class_name, summary")
-# The ordering matters for help display.
-# Also, even though the module path starts with the same
-# "pip._internal.commands" prefix in each case, we include the full path
-# because it makes testing easier (specifically when modifying commands_dict
-# in test setup / teardown by adding info for a FakeCommand class defined
-# in a test-related module).
-# Finally, we need to pass an iterable of pairs here rather than a dict
-# so that the ordering won't be lost when using Python 2.7.
-commands_dict: Dict[str, CommandInfo] = OrderedDict([
- ('install', CommandInfo(
- 'pip._internal.commands.install', 'InstallCommand',
- 'Install packages.',
- )),
- ('download', CommandInfo(
- 'pip._internal.commands.download', 'DownloadCommand',
- 'Download packages.',
- )),
- ('uninstall', CommandInfo(
- 'pip._internal.commands.uninstall', 'UninstallCommand',
- 'Uninstall packages.',
- )),
- ('freeze', CommandInfo(
- 'pip._internal.commands.freeze', 'FreezeCommand',
- 'Output installed packages in requirements format.',
- )),
- ('list', CommandInfo(
- 'pip._internal.commands.list', 'ListCommand',
- 'List installed packages.',
- )),
- ('show', CommandInfo(
- 'pip._internal.commands.show', 'ShowCommand',
- 'Show information about installed packages.',
- )),
- ('check', CommandInfo(
- 'pip._internal.commands.check', 'CheckCommand',
- 'Verify installed packages have compatible dependencies.',
- )),
- ('config', CommandInfo(
- 'pip._internal.commands.configuration', 'ConfigurationCommand',
- 'Manage local and global configuration.',
- )),
- ('search', CommandInfo(
- 'pip._internal.commands.search', 'SearchCommand',
- 'Search PyPI for packages.',
- )),
- ('cache', CommandInfo(
- 'pip._internal.commands.cache', 'CacheCommand',
+# This dictionary does a bunch of heavy lifting for help output:
+# - Enables avoiding additional (costly) imports for presenting `--help`.
+# - The ordering matters for help display.
+#
+# Even though the module path starts with the same "pip._internal.commands"
+# prefix, the full path makes testing easier (specifically when modifying
+# `commands_dict` in test setup / teardown).
+commands_dict: Dict[str, CommandInfo] = {
+ "install": CommandInfo(
+ "pip._internal.commands.install",
+ "InstallCommand",
+ "Install packages.",
+ ),
+ "download": CommandInfo(
+ "pip._internal.commands.download",
+ "DownloadCommand",
+ "Download packages.",
+ ),
+ "uninstall": CommandInfo(
+ "pip._internal.commands.uninstall",
+ "UninstallCommand",
+ "Uninstall packages.",
+ ),
+ "freeze": CommandInfo(
+ "pip._internal.commands.freeze",
+ "FreezeCommand",
+ "Output installed packages in requirements format.",
+ ),
+ "inspect": CommandInfo(
+ "pip._internal.commands.inspect",
+ "InspectCommand",
+ "Inspect the python environment.",
+ ),
+ "list": CommandInfo(
+ "pip._internal.commands.list",
+ "ListCommand",
+ "List installed packages.",
+ ),
+ "show": CommandInfo(
+ "pip._internal.commands.show",
+ "ShowCommand",
+ "Show information about installed packages.",
+ ),
+ "check": CommandInfo(
+ "pip._internal.commands.check",
+ "CheckCommand",
+ "Verify installed packages have compatible dependencies.",
+ ),
+ "config": CommandInfo(
+ "pip._internal.commands.configuration",
+ "ConfigurationCommand",
+ "Manage local and global configuration.",
+ ),
+ "search": CommandInfo(
+ "pip._internal.commands.search",
+ "SearchCommand",
+ "Search PyPI for packages.",
+ ),
+ "cache": CommandInfo(
+ "pip._internal.commands.cache",
+ "CacheCommand",
"Inspect and manage pip's wheel cache.",
- )),
- ('index', CommandInfo(
- 'pip._internal.commands.index', 'IndexCommand',
+ ),
+ "index": CommandInfo(
+ "pip._internal.commands.index",
+ "IndexCommand",
"Inspect information available from package indexes.",
- )),
- ('wheel', CommandInfo(
- 'pip._internal.commands.wheel', 'WheelCommand',
- 'Build wheels from your requirements.',
- )),
- ('hash', CommandInfo(
- 'pip._internal.commands.hash', 'HashCommand',
- 'Compute hashes of package archives.',
- )),
- ('completion', CommandInfo(
- 'pip._internal.commands.completion', 'CompletionCommand',
- 'A helper command used for command completion.',
- )),
- ('debug', CommandInfo(
- 'pip._internal.commands.debug', 'DebugCommand',
- 'Show information useful for debugging.',
- )),
- ('help', CommandInfo(
- 'pip._internal.commands.help', 'HelpCommand',
- 'Show help for commands.',
- )),
-])
+ ),
+ "wheel": CommandInfo(
+ "pip._internal.commands.wheel",
+ "WheelCommand",
+ "Build wheels from your requirements.",
+ ),
+ "hash": CommandInfo(
+ "pip._internal.commands.hash",
+ "HashCommand",
+ "Compute hashes of package archives.",
+ ),
+ "completion": CommandInfo(
+ "pip._internal.commands.completion",
+ "CompletionCommand",
+ "A helper command used for command completion.",
+ ),
+ "debug": CommandInfo(
+ "pip._internal.commands.debug",
+ "DebugCommand",
+ "Show information useful for debugging.",
+ ),
+ "help": CommandInfo(
+ "pip._internal.commands.help",
+ "HelpCommand",
+ "Show help for commands.",
+ ),
+}
def create_command(name: str, **kwargs: Any) -> Command:
diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py
index 3a5bb9c88..c5f03302d 100644
--- a/src/pip/_internal/commands/cache.py
+++ b/src/pip/_internal/commands/cache.py
@@ -39,17 +39,17 @@ class CacheCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
- '--format',
- action='store',
- dest='list_format',
+ "--format",
+ action="store",
+ dest="list_format",
default="human",
- choices=('human', 'abspath'),
- help="Select the output format among: human (default) or abspath"
+ choices=("human", "abspath"),
+ help="Select the output format among: human (default) or abspath",
)
self.parser.insert_option_group(0, self.cmd_opts)
- def run(self, options: Values, args: List[Any]) -> int:
+ def run(self, options: Values, args: List[str]) -> int:
handlers = {
"dir": self.get_cache_dir,
"info": self.get_cache_info,
@@ -59,8 +59,7 @@ class CacheCommand(Command):
}
if not options.cache_dir:
- logger.error("pip cache commands can not "
- "function since cache is disabled.")
+ logger.error("pip cache commands can not function since cache is disabled.")
return ERROR
# Determine action
@@ -84,69 +83,73 @@ class CacheCommand(Command):
def get_cache_dir(self, options: Values, args: List[Any]) -> None:
if args:
- raise CommandError('Too many arguments')
+ raise CommandError("Too many arguments")
logger.info(options.cache_dir)
def get_cache_info(self, options: Values, args: List[Any]) -> None:
if args:
- raise CommandError('Too many arguments')
+ raise CommandError("Too many arguments")
num_http_files = len(self._find_http_files(options))
- num_packages = len(self._find_wheels(options, '*'))
+ num_packages = len(self._find_wheels(options, "*"))
- http_cache_location = self._cache_dir(options, 'http')
- wheels_cache_location = self._cache_dir(options, 'wheels')
+ http_cache_location = self._cache_dir(options, "http")
+ wheels_cache_location = self._cache_dir(options, "wheels")
http_cache_size = filesystem.format_directory_size(http_cache_location)
- wheels_cache_size = filesystem.format_directory_size(
- wheels_cache_location
+ wheels_cache_size = filesystem.format_directory_size(wheels_cache_location)
+
+ message = (
+ textwrap.dedent(
+ """
+ Package index page cache location: {http_cache_location}
+ Package index page cache size: {http_cache_size}
+ Number of HTTP files: {num_http_files}
+ Locally built wheels location: {wheels_cache_location}
+ Locally built wheels size: {wheels_cache_size}
+ Number of locally built wheels: {package_count}
+ """
+ )
+ .format(
+ http_cache_location=http_cache_location,
+ http_cache_size=http_cache_size,
+ num_http_files=num_http_files,
+ wheels_cache_location=wheels_cache_location,
+ package_count=num_packages,
+ wheels_cache_size=wheels_cache_size,
+ )
+ .strip()
)
- message = textwrap.dedent("""
- Package index page cache location: {http_cache_location}
- Package index page cache size: {http_cache_size}
- Number of HTTP files: {num_http_files}
- Wheels location: {wheels_cache_location}
- Wheels size: {wheels_cache_size}
- Number of wheels: {package_count}
- """).format(
- http_cache_location=http_cache_location,
- http_cache_size=http_cache_size,
- num_http_files=num_http_files,
- wheels_cache_location=wheels_cache_location,
- package_count=num_packages,
- wheels_cache_size=wheels_cache_size,
- ).strip()
-
logger.info(message)
def list_cache_items(self, options: Values, args: List[Any]) -> None:
if len(args) > 1:
- raise CommandError('Too many arguments')
+ raise CommandError("Too many arguments")
if args:
pattern = args[0]
else:
- pattern = '*'
+ pattern = "*"
files = self._find_wheels(options, pattern)
- if options.list_format == 'human':
+ if options.list_format == "human":
self.format_for_human(files)
else:
self.format_for_abspath(files)
def format_for_human(self, files: List[str]) -> None:
if not files:
- logger.info('Nothing cached.')
+ logger.info("No locally built wheels cached.")
return
results = []
for filename in files:
wheel = os.path.basename(filename)
size = filesystem.format_file_size(filename)
- results.append(f' - {wheel} ({size})')
- logger.info('Cache contents:\n')
- logger.info('\n'.join(sorted(results)))
+ results.append(f" - {wheel} ({size})")
+ logger.info("Cache contents:\n")
+ logger.info("\n".join(sorted(results)))
def format_for_abspath(self, files: List[str]) -> None:
if not files:
@@ -156,23 +159,27 @@ class CacheCommand(Command):
for filename in files:
results.append(filename)
- logger.info('\n'.join(sorted(results)))
+ logger.info("\n".join(sorted(results)))
def remove_cache_items(self, options: Values, args: List[Any]) -> None:
if len(args) > 1:
- raise CommandError('Too many arguments')
+ raise CommandError("Too many arguments")
if not args:
- raise CommandError('Please provide a pattern')
+ raise CommandError("Please provide a pattern")
files = self._find_wheels(options, args[0])
- # Only fetch http files if no specific pattern given
- if args[0] == '*':
+ no_matching_msg = "No matching packages"
+ if args[0] == "*":
+ # Only fetch http files if no specific pattern given
files += self._find_http_files(options)
+ else:
+ # Add the pattern to the log message
+ no_matching_msg += ' for pattern "{}"'.format(args[0])
if not files:
- raise CommandError('No matching packages')
+ logger.warning(no_matching_msg)
for filename in files:
os.unlink(filename)
@@ -181,19 +188,19 @@ class CacheCommand(Command):
def purge_cache(self, options: Values, args: List[Any]) -> None:
if args:
- raise CommandError('Too many arguments')
+ raise CommandError("Too many arguments")
- return self.remove_cache_items(options, ['*'])
+ return self.remove_cache_items(options, ["*"])
def _cache_dir(self, options: Values, subdir: str) -> str:
return os.path.join(options.cache_dir, subdir)
def _find_http_files(self, options: Values) -> List[str]:
- http_dir = self._cache_dir(options, 'http')
- return filesystem.find_files(http_dir, '*')
+ http_dir = self._cache_dir(options, "http")
+ return filesystem.find_files(http_dir, "*")
def _find_wheels(self, options: Values, pattern: str) -> List[str]:
- wheel_dir = self._cache_dir(options, 'wheels')
+ wheel_dir = self._cache_dir(options, "wheels")
# The wheel filename format, as specified in PEP 427, is:
# {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl
diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py
index f9412a7a9..3864220b2 100644
--- a/src/pip/_internal/commands/check.py
+++ b/src/pip/_internal/commands/check.py
@@ -1,6 +1,6 @@
import logging
from optparse import Values
-from typing import Any, List
+from typing import List
from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import ERROR, SUCCESS
@@ -19,7 +19,7 @@ class CheckCommand(Command):
usage = """
%prog [options]"""
- def run(self, options: Values, args: List[Any]) -> int:
+ def run(self, options: Values, args: List[str]) -> int:
package_set, parsing_probs = create_package_set_from_installed()
missing, conflicting = check_package_set(package_set)
@@ -29,7 +29,9 @@ class CheckCommand(Command):
for dependency in missing[project_name]:
write_output(
"%s %s requires %s, which is not installed.",
- project_name, version, dependency[0],
+ project_name,
+ version,
+ dependency[0],
)
for project_name in conflicting:
@@ -37,7 +39,11 @@ class CheckCommand(Command):
for dep_name, dep_version, req in conflicting[project_name]:
write_output(
"%s %s has requirement %s, but you have %s %s.",
- project_name, version, req, dep_name, dep_version,
+ project_name,
+ version,
+ req,
+ dep_name,
+ dep_version,
)
if missing or conflicting or parsing_probs:
diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py
index 9ce7888ef..deaa30899 100644
--- a/src/pip/_internal/commands/completion.py
+++ b/src/pip/_internal/commands/completion.py
@@ -12,7 +12,7 @@ BASE_COMPLETION = """
"""
COMPLETION_SCRIPTS = {
- 'bash': """
+ "bash": """
_pip_completion()
{{
COMPREPLY=( $( COMP_WORDS="${{COMP_WORDS[*]}}" \\
@@ -21,7 +21,7 @@ COMPLETION_SCRIPTS = {
}}
complete -o default -F _pip_completion {prog}
""",
- 'zsh': """
+ "zsh": """
function _pip_completion {{
local words cword
read -Ac words
@@ -32,7 +32,7 @@ COMPLETION_SCRIPTS = {
}}
compctl -K _pip_completion {prog}
""",
- 'fish': """
+ "fish": """
function __fish_complete_pip
set -lx COMP_WORDS (commandline -o) ""
set -lx COMP_CWORD ( \\
@@ -43,6 +43,28 @@ COMPLETION_SCRIPTS = {
end
complete -fa "(__fish_complete_pip)" -c {prog}
""",
+ "powershell": """
+ if ((Test-Path Function:\\TabExpansion) -and -not `
+ (Test-Path Function:\\_pip_completeBackup)) {{
+ Rename-Item Function:\\TabExpansion _pip_completeBackup
+ }}
+ function TabExpansion($line, $lastWord) {{
+ $lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
+ if ($lastBlock.StartsWith("{prog} ")) {{
+ $Env:COMP_WORDS=$lastBlock
+ $Env:COMP_CWORD=$lastBlock.Split().Length - 1
+ $Env:PIP_AUTO_COMPLETE=1
+ (& {prog}).Split()
+ Remove-Item Env:COMP_WORDS
+ Remove-Item Env:COMP_CWORD
+ Remove-Item Env:PIP_AUTO_COMPLETE
+ }}
+ elseif (Test-Path Function:\\_pip_completeBackup) {{
+ # Fall back on existing tab expansion
+ _pip_completeBackup $line $lastWord
+ }}
+ }}
+ """,
}
@@ -53,39 +75,52 @@ class CompletionCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
- '--bash', '-b',
- action='store_const',
- const='bash',
- dest='shell',
- help='Emit completion code for bash')
+ "--bash",
+ "-b",
+ action="store_const",
+ const="bash",
+ dest="shell",
+ help="Emit completion code for bash",
+ )
+ self.cmd_opts.add_option(
+ "--zsh",
+ "-z",
+ action="store_const",
+ const="zsh",
+ dest="shell",
+ help="Emit completion code for zsh",
+ )
self.cmd_opts.add_option(
- '--zsh', '-z',
- action='store_const',
- const='zsh',
- dest='shell',
- help='Emit completion code for zsh')
+ "--fish",
+ "-f",
+ action="store_const",
+ const="fish",
+ dest="shell",
+ help="Emit completion code for fish",
+ )
self.cmd_opts.add_option(
- '--fish', '-f',
- action='store_const',
- const='fish',
- dest='shell',
- help='Emit completion code for fish')
+ "--powershell",
+ "-p",
+ action="store_const",
+ const="powershell",
+ dest="shell",
+ help="Emit completion code for powershell",
+ )
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
"""Prints the completion code of the given shell"""
shells = COMPLETION_SCRIPTS.keys()
- shell_options = ['--' + shell for shell in sorted(shells)]
+ shell_options = ["--" + shell for shell in sorted(shells)]
if options.shell in shells:
script = textwrap.dedent(
- COMPLETION_SCRIPTS.get(options.shell, '').format(
- prog=get_prog())
+ COMPLETION_SCRIPTS.get(options.shell, "").format(prog=get_prog())
)
print(BASE_COMPLETION.format(script=script, shell=options.shell))
return SUCCESS
else:
sys.stderr.write(
- 'ERROR: You must pass {}\n' .format(' or '.join(shell_options))
+ "ERROR: You must pass {}\n".format(" or ".join(shell_options))
)
return SUCCESS
diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py
index 6e47b8732..84b134e49 100644
--- a/src/pip/_internal/commands/configuration.py
+++ b/src/pip/_internal/commands/configuration.py
@@ -27,14 +27,20 @@ class ConfigurationCommand(Command):
- list: List the active configuration (or from the file specified)
- edit: Edit the configuration file in an editor
- - get: Get the value associated with name
- - set: Set the name=value
- - unset: Unset the value associated with name
+ - get: Get the value associated with command.option
+ - set: Set the command.option=value
+ - unset: Unset the value associated with command.option
- debug: List the configuration files and values defined under them
+ Configuration keys should be dot separated command and option name,
+ with the special prefix "global" affecting any command. For example,
+ "pip config set global.index-url https://example.org/" would configure
+ the index url for all commands, but "pip config set download.timeout 10"
+ would configure a 10 second timeout only for "pip download" commands.
+
If none of --user, --global and --site are passed, a virtual
environment configuration file is used if one is active and the file
- exists. Otherwise, all modifications happen on the to the user file by
+ exists. Otherwise, all modifications happen to the user file by
default.
"""
@@ -43,46 +49,46 @@ class ConfigurationCommand(Command):
%prog [<file-option>] list
%prog [<file-option>] [--editor <editor-path>] edit
- %prog [<file-option>] get name
- %prog [<file-option>] set name value
- %prog [<file-option>] unset name
+ %prog [<file-option>] get command.option
+ %prog [<file-option>] set command.option value
+ %prog [<file-option>] unset command.option
%prog [<file-option>] debug
"""
def add_options(self) -> None:
self.cmd_opts.add_option(
- '--editor',
- dest='editor',
- action='store',
+ "--editor",
+ dest="editor",
+ action="store",
default=None,
help=(
- 'Editor to use to edit the file. Uses VISUAL or EDITOR '
- 'environment variables if not provided.'
- )
+ "Editor to use to edit the file. Uses VISUAL or EDITOR "
+ "environment variables if not provided."
+ ),
)
self.cmd_opts.add_option(
- '--global',
- dest='global_file',
- action='store_true',
+ "--global",
+ dest="global_file",
+ action="store_true",
default=False,
- help='Use the system-wide configuration file only'
+ help="Use the system-wide configuration file only",
)
self.cmd_opts.add_option(
- '--user',
- dest='user_file',
- action='store_true',
+ "--user",
+ dest="user_file",
+ action="store_true",
default=False,
- help='Use the user configuration file only'
+ help="Use the user configuration file only",
)
self.cmd_opts.add_option(
- '--site',
- dest='site_file',
- action='store_true',
+ "--site",
+ dest="site_file",
+ action="store_true",
default=False,
- help='Use the current environment configuration file only'
+ help="Use the current environment configuration file only",
)
self.parser.insert_option_group(0, self.cmd_opts)
@@ -133,11 +139,15 @@ class ConfigurationCommand(Command):
return SUCCESS
def _determine_file(self, options: Values, need_value: bool) -> Optional[Kind]:
- file_options = [key for key, value in (
- (kinds.USER, options.user_file),
- (kinds.GLOBAL, options.global_file),
- (kinds.SITE, options.site_file),
- ) if value]
+ file_options = [
+ key
+ for key, value in (
+ (kinds.USER, options.user_file),
+ (kinds.GLOBAL, options.global_file),
+ (kinds.SITE, options.site_file),
+ )
+ if value
+ ]
if not file_options:
if not need_value:
@@ -194,24 +204,22 @@ class ConfigurationCommand(Command):
for fname in files:
with indent_log():
file_exists = os.path.exists(fname)
- write_output("%s, exists: %r",
- fname, file_exists)
+ write_output("%s, exists: %r", fname, file_exists)
if file_exists:
self.print_config_file_values(variant)
def print_config_file_values(self, variant: Kind) -> None:
"""Get key-value pairs from the file of a variant"""
- for name, value in self.configuration.\
- get_values_in_config(variant).items():
+ for name, value in self.configuration.get_values_in_config(variant).items():
with indent_log():
write_output("%s: %s", name, value)
def print_env_var_values(self) -> None:
"""Get key-values pairs present as environment variables"""
- write_output("%s:", 'env_var')
+ write_output("%s:", "env_var")
with indent_log():
for key, value in sorted(self.configuration.get_environ_vars()):
- env_var = f'PIP_{key.upper()}'
+ env_var = f"PIP_{key.upper()}"
write_output("%s=%r", env_var, value)
def open_in_editor(self, options: Values, args: List[str]) -> None:
@@ -220,21 +228,29 @@ class ConfigurationCommand(Command):
fname = self.configuration.get_file_to_edit()
if fname is None:
raise PipError("Could not determine appropriate file.")
+ elif '"' in fname:
+ # This shouldn't happen, unless we see a username like that.
+ # If that happens, we'd appreciate a pull request fixing this.
+ raise PipError(
+ f'Can not open an editor for a file name containing "\n{fname}'
+ )
try:
- subprocess.check_call([editor, fname])
+ subprocess.check_call(f'{editor} "{fname}"', shell=True)
+ except FileNotFoundError as e:
+ if not e.filename:
+ e.filename = editor
+ raise
except subprocess.CalledProcessError as e:
raise PipError(
- "Editor Subprocess exited with exit code {}"
- .format(e.returncode)
+ "Editor Subprocess exited with exit code {}".format(e.returncode)
)
def _get_n_args(self, args: List[str], example: str, n: int) -> Any:
- """Helper to make sure the command got the right number of arguments
- """
+ """Helper to make sure the command got the right number of arguments"""
if len(args) != n:
msg = (
- 'Got unexpected number of arguments, expected {}. '
+ "Got unexpected number of arguments, expected {}. "
'(example: "{} config {}")'
).format(n, get_prog(), example)
raise PipError(msg)
diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py
index b316b67bd..6fad1fe89 100644
--- a/src/pip/_internal/commands/debug.py
+++ b/src/pip/_internal/commands/debug.py
@@ -1,3 +1,4 @@
+import importlib.resources
import locale
import logging
import os
@@ -10,7 +11,6 @@ import pip._vendor
from pip._vendor.certifi import where
from pip._vendor.packaging.version import parse as parse_version
-from pip import __file__ as pip_location
from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import Command
from pip._internal.cli.cmdoptions import make_target_python
@@ -24,55 +24,46 @@ logger = logging.getLogger(__name__)
def show_value(name: str, value: Any) -> None:
- logger.info('%s: %s', name, value)
+ logger.info("%s: %s", name, value)
def show_sys_implementation() -> None:
- logger.info('sys.implementation:')
+ logger.info("sys.implementation:")
implementation_name = sys.implementation.name
with indent_log():
- show_value('name', implementation_name)
+ show_value("name", implementation_name)
def create_vendor_txt_map() -> Dict[str, str]:
- vendor_txt_path = os.path.join(
- os.path.dirname(pip_location),
- '_vendor',
- 'vendor.txt'
- )
-
- with open(vendor_txt_path) as f:
+ with importlib.resources.open_text("pip._vendor", "vendor.txt") as f:
# Purge non version specifying lines.
# Also, remove any space prefix or suffixes (including comments).
- lines = [line.strip().split(' ', 1)[0]
- for line in f.readlines() if '==' in line]
+ lines = [
+ line.strip().split(" ", 1)[0] for line in f.readlines() if "==" in line
+ ]
# Transform into "module" -> version dict.
- return dict(line.split('==', 1) for line in lines) # type: ignore
+ return dict(line.split("==", 1) for line in lines)
def get_module_from_module_name(module_name: str) -> ModuleType:
# Module name can be uppercase in vendor.txt for some reason...
module_name = module_name.lower()
# PATCH: setuptools is actually only pkg_resources.
- if module_name == 'setuptools':
- module_name = 'pkg_resources'
-
- __import__(
- f'pip._vendor.{module_name}',
- globals(),
- locals(),
- level=0
- )
+ if module_name == "setuptools":
+ module_name = "pkg_resources"
+
+ __import__(f"pip._vendor.{module_name}", globals(), locals(), level=0)
return getattr(pip._vendor, module_name)
def get_vendor_version_from_module(module_name: str) -> Optional[str]:
module = get_module_from_module_name(module_name)
- version = getattr(module, '__version__', None)
+ version = getattr(module, "__version__", None)
if not version:
# Try to find version in debundled module info.
+ assert module.__file__ is not None
env = get_environment([os.path.dirname(module.__file__)])
dist = env.get_distribution(module_name)
if dist:
@@ -86,20 +77,24 @@ def show_actual_vendor_versions(vendor_txt_versions: Dict[str, str]) -> None:
a conflict or if the actual version could not be imported.
"""
for module_name, expected_version in vendor_txt_versions.items():
- extra_message = ''
+ extra_message = ""
actual_version = get_vendor_version_from_module(module_name)
if not actual_version:
- extra_message = ' (Unable to locate actual module version, using'\
- ' vendor.txt specified version)'
+ extra_message = (
+ " (Unable to locate actual module version, using"
+ " vendor.txt specified version)"
+ )
actual_version = expected_version
elif parse_version(actual_version) != parse_version(expected_version):
- extra_message = ' (CONFLICT: vendor.txt suggests version should'\
- ' be {})'.format(expected_version)
- logger.info('%s==%s%s', module_name, actual_version, extra_message)
+ extra_message = (
+ " (CONFLICT: vendor.txt suggests version should"
+ " be {})".format(expected_version)
+ )
+ logger.info("%s==%s%s", module_name, actual_version, extra_message)
def show_vendor_versions() -> None:
- logger.info('vendored library versions:')
+ logger.info("vendored library versions:")
vendor_txt_versions = create_vendor_txt_map()
with indent_log():
@@ -114,11 +109,11 @@ def show_tags(options: Values) -> None:
# Display the target options that were explicitly provided.
formatted_target = target_python.format_given()
- suffix = ''
+ suffix = ""
if formatted_target:
- suffix = f' (target: {formatted_target})'
+ suffix = f" (target: {formatted_target})"
- msg = 'Compatible tags: {}{}'.format(len(tags), suffix)
+ msg = "Compatible tags: {}{}".format(len(tags), suffix)
logger.info(msg)
if options.verbose < 1 and len(tags) > tag_limit:
@@ -133,8 +128,7 @@ def show_tags(options: Values) -> None:
if tags_limited:
msg = (
- '...\n'
- '[First {tag_limit} tags shown. Pass --verbose to show all.]'
+ "...\n[First {tag_limit} tags shown. Pass --verbose to show all.]"
).format(tag_limit=tag_limit)
logger.info(msg)
@@ -142,20 +136,20 @@ def show_tags(options: Values) -> None:
def ca_bundle_info(config: Configuration) -> str:
levels = set()
for key, _ in config.items():
- levels.add(key.split('.')[0])
+ levels.add(key.split(".")[0])
if not levels:
return "Not specified"
- levels_that_override_global = ['install', 'wheel', 'download']
+ levels_that_override_global = ["install", "wheel", "download"]
global_overriding_level = [
level for level in levels if level in levels_that_override_global
]
if not global_overriding_level:
- return 'global'
+ return "global"
- if 'global' in levels:
- levels.remove('global')
+ if "global" in levels:
+ levels.remove("global")
return ", ".join(levels)
@@ -180,20 +174,21 @@ class DebugCommand(Command):
"details, since the output and options of this command may "
"change without notice."
)
- show_value('pip version', get_pip_version())
- show_value('sys.version', sys.version)
- show_value('sys.executable', sys.executable)
- show_value('sys.getdefaultencoding', sys.getdefaultencoding())
- show_value('sys.getfilesystemencoding', sys.getfilesystemencoding())
+ show_value("pip version", get_pip_version())
+ show_value("sys.version", sys.version)
+ show_value("sys.executable", sys.executable)
+ show_value("sys.getdefaultencoding", sys.getdefaultencoding())
+ show_value("sys.getfilesystemencoding", sys.getfilesystemencoding())
show_value(
- 'locale.getpreferredencoding', locale.getpreferredencoding(),
+ "locale.getpreferredencoding",
+ locale.getpreferredencoding(),
)
- show_value('sys.platform', sys.platform)
+ show_value("sys.platform", sys.platform)
show_sys_implementation()
show_value("'cert' config value", ca_bundle_info(self.parser.config))
- show_value("REQUESTS_CA_BUNDLE", os.environ.get('REQUESTS_CA_BUNDLE'))
- show_value("CURL_CA_BUNDLE", os.environ.get('CURL_CA_BUNDLE'))
+ show_value("REQUESTS_CA_BUNDLE", os.environ.get("REQUESTS_CA_BUNDLE"))
+ show_value("CURL_CA_BUNDLE", os.environ.get("CURL_CA_BUNDLE"))
show_value("pip._vendor.certifi.where()", where())
show_value("pip._vendor.DEBUNDLED", pip._vendor.DEBUNDLED)
diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py
index 230264591..26a5080c7 100644
--- a/src/pip/_internal/commands/download.py
+++ b/src/pip/_internal/commands/download.py
@@ -7,7 +7,7 @@ from pip._internal.cli import cmdoptions
from pip._internal.cli.cmdoptions import make_target_python
from pip._internal.cli.req_command import RequirementCommand, with_cleanup
from pip._internal.cli.status_codes import SUCCESS
-from pip._internal.req.req_tracker import get_requirement_tracker
+from pip._internal.operations.build.build_tracker import get_build_tracker
from pip._internal.utils.misc import ensure_dir, normalize_path, write_output
from pip._internal.utils.temp_dir import TempDirectory
@@ -37,7 +37,6 @@ class DownloadCommand(RequirementCommand):
def add_options(self) -> None:
self.cmd_opts.add_option(cmdoptions.constraints())
self.cmd_opts.add_option(cmdoptions.requirements())
- self.cmd_opts.add_option(cmdoptions.build_dir())
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.global_options())
self.cmd_opts.add_option(cmdoptions.no_binary())
@@ -50,14 +49,18 @@ class DownloadCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
self.cmd_opts.add_option(cmdoptions.use_pep517())
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
+ self.cmd_opts.add_option(cmdoptions.check_build_deps())
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(
- '-d', '--dest', '--destination-dir', '--destination-directory',
- dest='download_dir',
- metavar='dir',
+ "-d",
+ "--dest",
+ "--destination-dir",
+ "--destination-directory",
+ dest="download_dir",
+ metavar="dir",
default=os.curdir,
- help=("Download packages into <dir>."),
+ help="Download packages into <dir>.",
)
cmdoptions.add_target_python_options(self.cmd_opts)
@@ -93,7 +96,7 @@ class DownloadCommand(RequirementCommand):
ignore_requires_python=options.ignore_requires_python,
)
- req_tracker = self.enter_context(get_requirement_tracker())
+ build_tracker = self.enter_context(get_build_tracker())
directory = TempDirectory(
delete=not options.no_clean,
@@ -106,11 +109,12 @@ class DownloadCommand(RequirementCommand):
preparer = self.make_requirement_preparer(
temp_build_dir=directory,
options=options,
- req_tracker=req_tracker,
+ build_tracker=build_tracker,
session=session,
finder=finder,
download_dir=options.download_dir,
use_user_site=False,
+ verbosity=self.verbosity,
)
resolver = self.make_resolver(
@@ -118,14 +122,13 @@ class DownloadCommand(RequirementCommand):
finder=finder,
options=options,
ignore_requires_python=options.ignore_requires_python,
+ use_pep517=options.use_pep517,
py_version_info=options.python_version,
)
self.trace_basic_info(finder)
- requirement_set = resolver.resolve(
- reqs, check_supported_wheels=True
- )
+ requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
downloaded: List[str] = []
for req in requirement_set.requirements.values():
@@ -134,6 +137,6 @@ class DownloadCommand(RequirementCommand):
preparer.save_linked_requirement(req)
downloaded.append(req.name)
if downloaded:
- write_output('Successfully downloaded %s', ' '.join(downloaded))
+ write_output("Successfully downloaded %s", " ".join(downloaded))
return SUCCESS
diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py
index 1ccc87525..5fa6d39b2 100644
--- a/src/pip/_internal/commands/freeze.py
+++ b/src/pip/_internal/commands/freeze.py
@@ -8,7 +8,7 @@ from pip._internal.cli.status_codes import SUCCESS
from pip._internal.operations.freeze import freeze
from pip._internal.utils.compat import stdlib_pkgs
-DEV_PKGS = {'pip', 'setuptools', 'distribute', 'wheel'}
+DEV_PKGS = {"pip", "setuptools", "distribute", "wheel"}
class FreezeCommand(Command):
@@ -24,39 +24,52 @@ class FreezeCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
- '-r', '--requirement',
- dest='requirements',
- action='append',
+ "-r",
+ "--requirement",
+ dest="requirements",
+ action="append",
default=[],
- metavar='file',
- help="Use the order in the given requirements file and its "
- "comments when generating output. This option can be "
- "used multiple times.")
+ metavar="file",
+ help=(
+ "Use the order in the given requirements file and its "
+ "comments when generating output. This option can be "
+ "used multiple times."
+ ),
+ )
self.cmd_opts.add_option(
- '-l', '--local',
- dest='local',
- action='store_true',
+ "-l",
+ "--local",
+ dest="local",
+ action="store_true",
default=False,
- help='If in a virtualenv that has global access, do not output '
- 'globally-installed packages.')
+ help=(
+ "If in a virtualenv that has global access, do not output "
+ "globally-installed packages."
+ ),
+ )
self.cmd_opts.add_option(
- '--user',
- dest='user',
- action='store_true',
+ "--user",
+ dest="user",
+ action="store_true",
default=False,
- help='Only output packages installed in user-site.')
+ help="Only output packages installed in user-site.",
+ )
self.cmd_opts.add_option(cmdoptions.list_path())
self.cmd_opts.add_option(
- '--all',
- dest='freeze_all',
- action='store_true',
- help='Do not skip these packages in the output:'
- ' {}'.format(', '.join(DEV_PKGS)))
+ "--all",
+ dest="freeze_all",
+ action="store_true",
+ help=(
+ "Do not skip these packages in the output:"
+ " {}".format(", ".join(DEV_PKGS))
+ ),
+ )
self.cmd_opts.add_option(
- '--exclude-editable',
- dest='exclude_editable',
- action='store_true',
- help='Exclude editable package from output.')
+ "--exclude-editable",
+ dest="exclude_editable",
+ action="store_true",
+ help="Exclude editable package from output.",
+ )
self.cmd_opts.add_option(cmdoptions.list_exclude())
self.parser.insert_option_group(0, self.cmd_opts)
@@ -80,5 +93,5 @@ class FreezeCommand(Command):
skip=skip,
exclude_editable=options.exclude_editable,
):
- sys.stdout.write(line + '\n')
+ sys.stdout.write(line + "\n")
return SUCCESS
diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py
index 3e4c32f35..042dac813 100644
--- a/src/pip/_internal/commands/hash.py
+++ b/src/pip/_internal/commands/hash.py
@@ -20,18 +20,21 @@ class HashCommand(Command):
installs.
"""
- usage = '%prog [options] <file> ...'
+ usage = "%prog [options] <file> ..."
ignore_require_venv = True
def add_options(self) -> None:
self.cmd_opts.add_option(
- '-a', '--algorithm',
- dest='algorithm',
+ "-a",
+ "--algorithm",
+ dest="algorithm",
choices=STRONG_HASHES,
- action='store',
+ action="store",
default=FAVORITE_HASH,
- help='The hash algorithm to use: one of {}'.format(
- ', '.join(STRONG_HASHES)))
+ help="The hash algorithm to use: one of {}".format(
+ ", ".join(STRONG_HASHES)
+ ),
+ )
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
@@ -41,14 +44,15 @@ class HashCommand(Command):
algorithm = options.algorithm
for path in args:
- write_output('%s:\n--hash=%s:%s',
- path, algorithm, _hash_of_file(path, algorithm))
+ write_output(
+ "%s:\n--hash=%s:%s", path, algorithm, _hash_of_file(path, algorithm)
+ )
return SUCCESS
def _hash_of_file(path: str, algorithm: str) -> str:
"""Return the hash digest of a file."""
- with open(path, 'rb') as archive:
+ with open(path, "rb") as archive:
hash = hashlib.new(algorithm)
for chunk in read_chunks(archive):
hash.update(chunk)
diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py
index 811ce89d5..62066318b 100644
--- a/src/pip/_internal/commands/help.py
+++ b/src/pip/_internal/commands/help.py
@@ -33,7 +33,7 @@ class HelpCommand(Command):
if guess:
msg.append(f'maybe you meant "{guess}"')
- raise CommandError(' - '.join(msg))
+ raise CommandError(" - ".join(msg))
command = create_command(cmd_name)
command.parser.print_help()
diff --git a/src/pip/_internal/commands/index.py b/src/pip/_internal/commands/index.py
index c505464f6..b4bf0ac06 100644
--- a/src/pip/_internal/commands/index.py
+++ b/src/pip/_internal/commands/index.py
@@ -44,7 +44,7 @@ class IndexCommand(IndexGroupCommand):
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, self.cmd_opts)
- def run(self, options: Values, args: List[Any]) -> int:
+ def run(self, options: Values, args: List[str]) -> int:
handlers = {
"versions": self.get_available_package_versions,
}
@@ -101,7 +101,7 @@ class IndexCommand(IndexGroupCommand):
def get_available_package_versions(self, options: Values, args: List[Any]) -> None:
if len(args) != 1:
- raise CommandError('You need to specify exactly one argument')
+ raise CommandError("You need to specify exactly one argument")
target_python = cmdoptions.make_target_python(options)
query = args[0]
@@ -115,25 +115,24 @@ class IndexCommand(IndexGroupCommand):
)
versions: Iterable[Union[LegacyVersion, Version]] = (
- candidate.version
- for candidate in finder.find_all_candidates(query)
+ candidate.version for candidate in finder.find_all_candidates(query)
)
if not options.pre:
# Remove prereleases
- versions = (version for version in versions
- if not version.is_prerelease)
+ versions = (
+ version for version in versions if not version.is_prerelease
+ )
versions = set(versions)
if not versions:
raise DistributionNotFound(
- 'No matching distribution found for {}'.format(query))
+ "No matching distribution found for {}".format(query)
+ )
- formatted_versions = [str(ver) for ver in sorted(
- versions, reverse=True)]
+ formatted_versions = [str(ver) for ver in sorted(versions, reverse=True)]
latest = formatted_versions[0]
- write_output('{} ({})'.format(query, latest))
- write_output('Available versions: {}'.format(
- ', '.join(formatted_versions)))
+ write_output("{} ({})".format(query, latest))
+ write_output("Available versions: {}".format(", ".join(formatted_versions)))
print_dist_installation_info(query, latest)
diff --git a/src/pip/_internal/commands/inspect.py b/src/pip/_internal/commands/inspect.py
new file mode 100644
index 000000000..a4e359930
--- /dev/null
+++ b/src/pip/_internal/commands/inspect.py
@@ -0,0 +1,97 @@
+import logging
+from optparse import Values
+from typing import Any, Dict, List
+
+from pip._vendor.packaging.markers import default_environment
+from pip._vendor.rich import print_json
+
+from pip import __version__
+from pip._internal.cli import cmdoptions
+from pip._internal.cli.req_command import Command
+from pip._internal.cli.status_codes import SUCCESS
+from pip._internal.metadata import BaseDistribution, get_environment
+from pip._internal.utils.compat import stdlib_pkgs
+from pip._internal.utils.urls import path_to_url
+
+logger = logging.getLogger(__name__)
+
+
+class InspectCommand(Command):
+ """
+ Inspect the content of a Python environment and produce a report in JSON format.
+ """
+
+ ignore_require_venv = True
+ usage = """
+ %prog [options]"""
+
+ def add_options(self) -> None:
+ self.cmd_opts.add_option(
+ "--local",
+ action="store_true",
+ default=False,
+ help=(
+ "If in a virtualenv that has global access, do not list "
+ "globally-installed packages."
+ ),
+ )
+ self.cmd_opts.add_option(
+ "--user",
+ dest="user",
+ action="store_true",
+ default=False,
+ help="Only output packages installed in user-site.",
+ )
+ self.cmd_opts.add_option(cmdoptions.list_path())
+ self.parser.insert_option_group(0, self.cmd_opts)
+
+ def run(self, options: Values, args: List[str]) -> int:
+ logger.warning(
+ "pip inspect is currently an experimental command. "
+ "The output format may change in a future release without prior warning."
+ )
+
+ cmdoptions.check_list_path_option(options)
+ dists = get_environment(options.path).iter_installed_distributions(
+ local_only=options.local,
+ user_only=options.user,
+ skip=set(stdlib_pkgs),
+ )
+ output = {
+ "version": "0",
+ "pip_version": __version__,
+ "installed": [self._dist_to_dict(dist) for dist in dists],
+ "environment": default_environment(),
+ # TODO tags? scheme?
+ }
+ print_json(data=output)
+ return SUCCESS
+
+ def _dist_to_dict(self, dist: BaseDistribution) -> Dict[str, Any]:
+ res: Dict[str, Any] = {
+ "metadata": dist.metadata_dict,
+ "metadata_location": dist.info_location,
+ }
+ # direct_url. Note that we don't have download_info (as in the installation
+ # report) since it is not recorded in installed metadata.
+ direct_url = dist.direct_url
+ if direct_url is not None:
+ res["direct_url"] = direct_url.to_dict()
+ else:
+ # Emulate direct_url for legacy editable installs.
+ editable_project_location = dist.editable_project_location
+ if editable_project_location is not None:
+ res["direct_url"] = {
+ "url": path_to_url(editable_project_location),
+ "dir_info": {
+ "editable": True,
+ },
+ }
+ # installer
+ installer = dist.installer
+ if dist.installer:
+ res["installer"] = installer
+ # requested
+ if dist.installed_with_dist_info:
+ res["requested"] = dist.requested
+ return res
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index 02da0777a..dcf5ce8c6 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -1,4 +1,5 @@
import errno
+import json
import operator
import os
import shutil
@@ -7,6 +8,7 @@ from optparse import SUPPRESS_HELP, Values
from typing import Iterable, List, Optional
from pip._vendor.packaging.utils import canonicalize_name
+from pip._vendor.rich import print_json
from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
@@ -21,11 +23,13 @@ from pip._internal.exceptions import CommandError, InstallationError
from pip._internal.locations import get_scheme
from pip._internal.metadata import get_environment
from pip._internal.models.format_control import FormatControl
+from pip._internal.models.installation_report import InstallationReport
+from pip._internal.operations.build.build_tracker import get_build_tracker
from pip._internal.operations.check import ConflictDetails, check_install_conflicts
from pip._internal.req import install_given_reqs
from pip._internal.req.req_install import InstallRequirement
-from pip._internal.req.req_tracker import get_requirement_tracker
from pip._internal.utils.compat import WINDOWS
+from pip._internal.utils.deprecation import LegacyInstallReasonFailedBdistWheel
from pip._internal.utils.distutils_args import parse_distutils_args
from pip._internal.utils.filesystem import test_writable_dir
from pip._internal.utils.logging import getLogger
@@ -41,7 +45,7 @@ from pip._internal.utils.virtualenv import (
virtualenv_no_global,
)
from pip._internal.wheel_builder import (
- BinaryAllowedPredicate,
+ BdistWheelAllowedPredicate,
build,
should_build_for_install_command,
)
@@ -49,7 +53,9 @@ from pip._internal.wheel_builder import (
logger = getLogger(__name__)
-def get_check_binary_allowed(format_control: FormatControl) -> BinaryAllowedPredicate:
+def get_check_bdist_wheel_allowed(
+ format_control: FormatControl,
+) -> BdistWheelAllowedPredicate:
def check_binary_allowed(req: InstallRequirement) -> bool:
canonical_name = canonicalize_name(req.name or "")
allowed_formats = format_control.get_allowed_formats(canonical_name)
@@ -86,94 +92,123 @@ class InstallCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.editable())
self.cmd_opts.add_option(
- '-t', '--target',
- dest='target_dir',
- metavar='dir',
+ "--dry-run",
+ action="store_true",
+ dest="dry_run",
+ default=False,
+ help=(
+ "Don't actually install anything, just print what would be. "
+ "Can be used in combination with --ignore-installed "
+ "to 'resolve' the requirements."
+ ),
+ )
+ self.cmd_opts.add_option(
+ "-t",
+ "--target",
+ dest="target_dir",
+ metavar="dir",
default=None,
- help='Install packages into <dir>. '
- 'By default this will not replace existing files/folders in '
- '<dir>. Use --upgrade to replace existing packages in <dir> '
- 'with new versions.'
+ help=(
+ "Install packages into <dir>. "
+ "By default this will not replace existing files/folders in "
+ "<dir>. Use --upgrade to replace existing packages in <dir> "
+ "with new versions."
+ ),
)
cmdoptions.add_target_python_options(self.cmd_opts)
self.cmd_opts.add_option(
- '--user',
- dest='use_user_site',
- action='store_true',
- help="Install to the Python user install directory for your "
- "platform. Typically ~/.local/, or %APPDATA%\\Python on "
- "Windows. (See the Python documentation for site.USER_BASE "
- "for full details.)")
+ "--user",
+ dest="use_user_site",
+ action="store_true",
+ help=(
+ "Install to the Python user install directory for your "
+ "platform. Typically ~/.local/, or %APPDATA%\\Python on "
+ "Windows. (See the Python documentation for site.USER_BASE "
+ "for full details.)"
+ ),
+ )
self.cmd_opts.add_option(
- '--no-user',
- dest='use_user_site',
- action='store_false',
- help=SUPPRESS_HELP)
+ "--no-user",
+ dest="use_user_site",
+ action="store_false",
+ help=SUPPRESS_HELP,
+ )
self.cmd_opts.add_option(
- '--root',
- dest='root_path',
- metavar='dir',
+ "--root",
+ dest="root_path",
+ metavar="dir",
default=None,
- help="Install everything relative to this alternate root "
- "directory.")
+ help="Install everything relative to this alternate root directory.",
+ )
self.cmd_opts.add_option(
- '--prefix',
- dest='prefix_path',
- metavar='dir',
+ "--prefix",
+ dest="prefix_path",
+ metavar="dir",
default=None,
- help="Installation prefix where lib, bin and other top-level "
- "folders are placed")
-
- self.cmd_opts.add_option(cmdoptions.build_dir())
+ help=(
+ "Installation prefix where lib, bin and other top-level "
+ "folders are placed"
+ ),
+ )
self.cmd_opts.add_option(cmdoptions.src())
self.cmd_opts.add_option(
- '-U', '--upgrade',
- dest='upgrade',
- action='store_true',
- help='Upgrade all specified packages to the newest available '
- 'version. The handling of dependencies depends on the '
- 'upgrade-strategy used.'
+ "-U",
+ "--upgrade",
+ dest="upgrade",
+ action="store_true",
+ help=(
+ "Upgrade all specified packages to the newest available "
+ "version. The handling of dependencies depends on the "
+ "upgrade-strategy used."
+ ),
)
self.cmd_opts.add_option(
- '--upgrade-strategy',
- dest='upgrade_strategy',
- default='only-if-needed',
- choices=['only-if-needed', 'eager'],
- help='Determines how dependency upgrading should be handled '
- '[default: %default]. '
- '"eager" - dependencies are upgraded regardless of '
- 'whether the currently installed version satisfies the '
- 'requirements of the upgraded package(s). '
- '"only-if-needed" - are upgraded only when they do not '
- 'satisfy the requirements of the upgraded package(s).'
+ "--upgrade-strategy",
+ dest="upgrade_strategy",
+ default="only-if-needed",
+ choices=["only-if-needed", "eager"],
+ help=(
+ "Determines how dependency upgrading should be handled "
+ "[default: %default]. "
+ '"eager" - dependencies are upgraded regardless of '
+ "whether the currently installed version satisfies the "
+ "requirements of the upgraded package(s). "
+ '"only-if-needed" - are upgraded only when they do not '
+ "satisfy the requirements of the upgraded package(s)."
+ ),
)
self.cmd_opts.add_option(
- '--force-reinstall',
- dest='force_reinstall',
- action='store_true',
- help='Reinstall all packages even if they are already '
- 'up-to-date.')
+ "--force-reinstall",
+ dest="force_reinstall",
+ action="store_true",
+ help="Reinstall all packages even if they are already up-to-date.",
+ )
self.cmd_opts.add_option(
- '-I', '--ignore-installed',
- dest='ignore_installed',
- action='store_true',
- help='Ignore the installed packages, overwriting them. '
- 'This can break your system if the existing package '
- 'is of a different version or was installed '
- 'with a different package manager!'
+ "-I",
+ "--ignore-installed",
+ dest="ignore_installed",
+ action="store_true",
+ help=(
+ "Ignore the installed packages, overwriting them. "
+ "This can break your system if the existing package "
+ "is of a different version or was installed "
+ "with a different package manager!"
+ ),
)
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
self.cmd_opts.add_option(cmdoptions.use_pep517())
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
+ self.cmd_opts.add_option(cmdoptions.check_build_deps())
+ self.cmd_opts.add_option(cmdoptions.config_settings())
self.cmd_opts.add_option(cmdoptions.install_options())
self.cmd_opts.add_option(cmdoptions.global_options())
@@ -206,12 +241,12 @@ class InstallCommand(RequirementCommand):
default=True,
help="Do not warn about broken dependencies",
)
-
self.cmd_opts.add_option(cmdoptions.no_binary())
self.cmd_opts.add_option(cmdoptions.only_binary())
self.cmd_opts.add_option(cmdoptions.prefer_binary())
self.cmd_opts.add_option(cmdoptions.require_hashes())
self.cmd_opts.add_option(cmdoptions.progress_bar())
+ self.cmd_opts.add_option(cmdoptions.root_user_action())
index_opts = cmdoptions.make_option_group(
cmdoptions.index_group,
@@ -221,6 +256,22 @@ class InstallCommand(RequirementCommand):
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, self.cmd_opts)
+ self.cmd_opts.add_option(
+ "--report",
+ dest="json_report_file",
+ metavar="file",
+ default=None,
+ help=(
+ "Generate a JSON file describing what pip did to install "
+ "the provided requirements. "
+ "Can be used in combination with --dry-run and --ignore-installed "
+ "to 'resolve' the requirements. "
+ "When - is used as file name it writes to stdout. "
+ "When writing to stdout, please combine with the --quiet option "
+ "to avoid mixing pip logging output with JSON output."
+ ),
+ )
+
@with_cleanup
def run(self, options: Values, args: List[str]) -> int:
if options.use_user_site and options.target_dir is not None:
@@ -249,11 +300,14 @@ class InstallCommand(RequirementCommand):
if options.target_dir:
options.ignore_installed = True
options.target_dir = os.path.abspath(options.target_dir)
- if (os.path.exists(options.target_dir) and not
- os.path.isdir(options.target_dir)):
+ if (
+ # fmt: off
+ os.path.exists(options.target_dir) and
+ not os.path.isdir(options.target_dir)
+ # fmt: on
+ ):
raise CommandError(
- "Target path exists but is not a directory, will not "
- "continue."
+ "Target path exists but is not a directory, will not continue."
)
# Create a target directory for using with the target option
@@ -274,7 +328,7 @@ class InstallCommand(RequirementCommand):
)
wheel_cache = WheelCache(options.cache_dir, options.format_control)
- req_tracker = self.enter_context(get_requirement_tracker())
+ build_tracker = self.enter_context(get_build_tracker())
directory = TempDirectory(
delete=not options.no_clean,
@@ -285,17 +339,22 @@ class InstallCommand(RequirementCommand):
try:
reqs = self.get_requirements(args, options, finder, session)
- reject_location_related_install_options(
- reqs, options.install_options
- )
+ # Only when installing is it permitted to use PEP 660.
+ # In other circumstances (pip wheel, pip download) we generate
+ # regular (i.e. non editable) metadata and wheels.
+ for req in reqs:
+ req.permit_editable_wheels = True
+
+ reject_location_related_install_options(reqs, options.install_options)
preparer = self.make_requirement_preparer(
temp_build_dir=directory,
options=options,
- req_tracker=req_tracker,
+ build_tracker=build_tracker,
session=session,
finder=finder,
use_user_site=options.use_user_site,
+ verbosity=self.verbosity,
)
resolver = self.make_resolver(
preparer=preparer,
@@ -316,6 +375,32 @@ class InstallCommand(RequirementCommand):
reqs, check_supported_wheels=not options.target_dir
)
+ if options.json_report_file:
+ logger.warning(
+ "--report is currently an experimental option. "
+ "The output format may change in a future release "
+ "without prior warning."
+ )
+
+ report = InstallationReport(requirement_set.requirements_to_install)
+ if options.json_report_file == "-":
+ print_json(data=report.to_dict())
+ else:
+ with open(options.json_report_file, "w", encoding="utf-8") as f:
+ json.dump(report.to_dict(), f, indent=2, ensure_ascii=False)
+
+ if options.dry_run:
+ would_install_items = sorted(
+ (r.metadata["name"], r.metadata["version"])
+ for r in requirement_set.requirements_to_install
+ )
+ if would_install_items:
+ write_output(
+ "Would install %s",
+ " ".join("-".join(item) for item in would_install_items),
+ )
+ return SUCCESS
+
try:
pip_req = requirement_set.get_requirement("pip")
except KeyError:
@@ -324,19 +409,16 @@ class InstallCommand(RequirementCommand):
# If we're not replacing an already installed pip,
# we're not modifying it.
modifying_pip = pip_req.satisfied_by is None
- protect_pip_from_modification_on_windows(
- modifying_pip=modifying_pip
- )
+ protect_pip_from_modification_on_windows(modifying_pip=modifying_pip)
- check_binary_allowed = get_check_binary_allowed(
+ check_bdist_wheel_allowed = get_check_bdist_wheel_allowed(
finder.format_control
)
reqs_to_build = [
- r for r in requirement_set.requirements.values()
- if should_build_for_install_command(
- r, check_binary_allowed
- )
+ r
+ for r in requirement_set.requirements.values()
+ if should_build_for_install_command(r, check_bdist_wheel_allowed)
]
_, build_failures = build(
@@ -347,36 +429,32 @@ class InstallCommand(RequirementCommand):
global_options=[],
)
- # If we're using PEP 517, we cannot do a direct install
+ # If we're using PEP 517, we cannot do a legacy setup.py install
# so we fail here.
pep517_build_failure_names: List[str] = [
- r.name # type: ignore
- for r in build_failures if r.use_pep517
+ r.name for r in build_failures if r.use_pep517 # type: ignore
]
if pep517_build_failure_names:
raise InstallationError(
- "Could not build wheels for {} which use"
- " PEP 517 and cannot be installed directly".format(
+ "Could not build wheels for {}, which is required to "
+ "install pyproject.toml-based projects".format(
", ".join(pep517_build_failure_names)
)
)
# For now, we just warn about failures building legacy
- # requirements, as we'll fall through to a direct
- # install for those.
+ # requirements, as we'll fall through to a setup.py install for
+ # those.
for r in build_failures:
if not r.use_pep517:
- r.legacy_install_reason = 8368
+ r.legacy_install_reason = LegacyInstallReasonFailedBdistWheel
- to_install = resolver.get_installation_order(
- requirement_set
- )
+ to_install = resolver.get_installation_order(requirement_set)
# Check for conflicts in the package set we're installing.
conflicts: Optional[ConflictDetails] = None
should_warn_about_conflicts = (
- not options.ignore_dependencies and
- options.warn_about_conflicts
+ not options.ignore_dependencies and options.warn_about_conflicts
)
if should_warn_about_conflicts:
conflicts = self._determine_conflicts(to_install)
@@ -408,7 +486,7 @@ class InstallCommand(RequirementCommand):
)
env = get_environment(lib_locations)
- installed.sort(key=operator.attrgetter('name'))
+ installed.sort(key=operator.attrgetter("name"))
items = []
for result in installed:
item = result.name
@@ -426,16 +504,19 @@ class InstallCommand(RequirementCommand):
resolver_variant=self.determine_resolver_variant(options),
)
- installed_desc = ' '.join(items)
+ installed_desc = " ".join(items)
if installed_desc:
write_output(
- 'Successfully installed %s', installed_desc,
+ "Successfully installed %s",
+ installed_desc,
)
except OSError as error:
- show_traceback = (self.verbosity >= 1)
+ show_traceback = self.verbosity >= 1
message = create_os_error_message(
- error, show_traceback, options.use_user_site,
+ error,
+ show_traceback,
+ options.use_user_site,
)
logger.error(message, exc_info=show_traceback) # noqa
@@ -446,8 +527,8 @@ class InstallCommand(RequirementCommand):
self._handle_target_dir(
options.target_dir, target_temp_dir, options.upgrade
)
-
- warn_if_run_as_root()
+ if options.root_user_action == "warn":
+ warn_if_run_as_root()
return SUCCESS
def _handle_target_dir(
@@ -461,7 +542,7 @@ class InstallCommand(RequirementCommand):
# Checking both purelib and platlib directories for installed
# packages to be moved to target directory
- scheme = get_scheme('', home=target_temp_dir.path)
+ scheme = get_scheme("", home=target_temp_dir.path)
purelib_dir = scheme.purelib
platlib_dir = scheme.platlib
data_dir = scheme.data
@@ -483,18 +564,18 @@ class InstallCommand(RequirementCommand):
if os.path.exists(target_item_dir):
if not upgrade:
logger.warning(
- 'Target directory %s already exists. Specify '
- '--upgrade to force replacement.',
- target_item_dir
+ "Target directory %s already exists. Specify "
+ "--upgrade to force replacement.",
+ target_item_dir,
)
continue
if os.path.islink(target_item_dir):
logger.warning(
- 'Target directory %s already exists and is '
- 'a link. pip will not automatically replace '
- 'links, please remove if replacement is '
- 'desired.',
- target_item_dir
+ "Target directory %s already exists and is "
+ "a link. pip will not automatically replace "
+ "links, please remove if replacement is "
+ "desired.",
+ target_item_dir,
)
continue
if os.path.isdir(target_item_dir):
@@ -502,10 +583,7 @@ class InstallCommand(RequirementCommand):
else:
os.remove(target_item_dir)
- shutil.move(
- os.path.join(lib_dir, item),
- target_item_dir
- )
+ shutil.move(os.path.join(lib_dir, item), target_item_dir)
def _determine_conflicts(
self, to_install: List[InstallRequirement]
@@ -567,7 +645,7 @@ class InstallCommand(RequirementCommand):
requirement=req,
dep_name=dep_name,
dep_version=dep_version,
- you=("you" if resolver_variant == "2020-resolver" else "you'll")
+ you=("you" if resolver_variant == "2020-resolver" else "you'll"),
)
parts.append(message)
@@ -575,14 +653,14 @@ class InstallCommand(RequirementCommand):
def get_lib_location_guesses(
- user: bool = False,
- home: Optional[str] = None,
- root: Optional[str] = None,
- isolated: bool = False,
- prefix: Optional[str] = None
+ user: bool = False,
+ home: Optional[str] = None,
+ root: Optional[str] = None,
+ isolated: bool = False,
+ prefix: Optional[str] = None,
) -> List[str]:
scheme = get_scheme(
- '',
+ "",
user=user,
home=home,
root=root,
@@ -594,8 +672,8 @@ def get_lib_location_guesses(
def site_packages_writable(root: Optional[str], isolated: bool) -> bool:
return all(
- test_writable_dir(d) for d in set(
- get_lib_location_guesses(root=root, isolated=isolated))
+ test_writable_dir(d)
+ for d in set(get_lib_location_guesses(root=root, isolated=isolated))
)
@@ -653,8 +731,10 @@ def decide_user_install(
logger.debug("Non-user install because site-packages writeable")
return False
- logger.info("Defaulting to user installation because normal site-packages "
- "is not writeable")
+ logger.info(
+ "Defaulting to user installation because normal site-packages "
+ "is not writeable"
+ )
return True
@@ -664,6 +744,7 @@ def reject_location_related_install_options(
"""If any location-changing --install-option arguments were passed for
requirements or on the command-line, then show a deprecation warning.
"""
+
def format_options(option_names: Iterable[str]) -> List[str]:
return ["--{}".format(name.replace("_", "-")) for name in option_names]
@@ -683,9 +764,7 @@ def reject_location_related_install_options(
location_options = parse_distutils_args(options)
if location_options:
offenders.append(
- "{!r} from command line".format(
- format_options(location_options.keys())
- )
+ "{!r} from command line".format(format_options(location_options.keys()))
)
if not offenders:
@@ -694,9 +773,7 @@ def reject_location_related_install_options(
raise CommandError(
"Location-changing options found in --install-option: {}."
" This is unsupported, use pip-level options like --user,"
- " --prefix, --root, and --target instead.".format(
- "; ".join(offenders)
- )
+ " --prefix, --root, and --target instead.".format("; ".join(offenders))
)
@@ -727,18 +804,25 @@ def create_os_error_message(
permissions_part = "Check the permissions"
if not running_under_virtualenv() and not using_user_site:
- parts.extend([
- user_option_part, " or ",
- permissions_part.lower(),
- ])
+ parts.extend(
+ [
+ user_option_part,
+ " or ",
+ permissions_part.lower(),
+ ]
+ )
else:
parts.append(permissions_part)
parts.append(".\n")
# Suggest the user to enable Long Paths if path length is
# more than 260
- if (WINDOWS and error.errno == errno.ENOENT and error.filename and
- len(error.filename) > 260):
+ if (
+ WINDOWS
+ and error.errno == errno.ENOENT
+ and error.filename
+ and len(error.filename) > 260
+ ):
parts.append(
"HINT: This error might have occurred since "
"this system does not have Windows Long Path "
diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py
index 828889f49..a9b08a0bc 100644
--- a/src/pip/_internal/commands/list.py
+++ b/src/pip/_internal/commands/list.py
@@ -1,7 +1,7 @@
import json
import logging
from optparse import Values
-from typing import TYPE_CHECKING, Iterator, List, Optional, Sequence, Tuple, cast
+from typing import TYPE_CHECKING, Generator, List, Optional, Sequence, Tuple, cast
from pip._vendor.packaging.utils import canonicalize_name
@@ -14,8 +14,8 @@ from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution, get_environment
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.network.session import PipSession
-from pip._internal.utils.misc import stdlib_pkgs, tabulate, write_output
-from pip._internal.utils.parallel import map_multithread
+from pip._internal.utils.compat import stdlib_pkgs
+from pip._internal.utils.misc import tabulate, write_output
if TYPE_CHECKING:
from pip._internal.metadata.base import DistributionVersion
@@ -26,6 +26,7 @@ if TYPE_CHECKING:
These will be populated during ``get_outdated()``. This is dirty but
makes the rest of the code much cleaner.
"""
+
latest_version: DistributionVersion
latest_filetype: str
@@ -48,77 +49,85 @@ class ListCommand(IndexGroupCommand):
def add_options(self) -> None:
self.cmd_opts.add_option(
- '-o', '--outdated',
- action='store_true',
+ "-o",
+ "--outdated",
+ action="store_true",
default=False,
- help='List outdated packages')
+ help="List outdated packages",
+ )
self.cmd_opts.add_option(
- '-u', '--uptodate',
- action='store_true',
+ "-u",
+ "--uptodate",
+ action="store_true",
default=False,
- help='List uptodate packages')
+ help="List uptodate packages",
+ )
self.cmd_opts.add_option(
- '-e', '--editable',
- action='store_true',
+ "-e",
+ "--editable",
+ action="store_true",
default=False,
- help='List editable projects.')
+ help="List editable projects.",
+ )
self.cmd_opts.add_option(
- '-l', '--local',
- action='store_true',
+ "-l",
+ "--local",
+ action="store_true",
default=False,
- help=('If in a virtualenv that has global access, do not list '
- 'globally-installed packages.'),
+ help=(
+ "If in a virtualenv that has global access, do not list "
+ "globally-installed packages."
+ ),
)
self.cmd_opts.add_option(
- '--user',
- dest='user',
- action='store_true',
+ "--user",
+ dest="user",
+ action="store_true",
default=False,
- help='Only output packages installed in user-site.')
+ help="Only output packages installed in user-site.",
+ )
self.cmd_opts.add_option(cmdoptions.list_path())
self.cmd_opts.add_option(
- '--pre',
- action='store_true',
+ "--pre",
+ action="store_true",
default=False,
- help=("Include pre-release and development versions. By default, "
- "pip only finds stable versions."),
+ help=(
+ "Include pre-release and development versions. By default, "
+ "pip only finds stable versions."
+ ),
)
self.cmd_opts.add_option(
- '--format',
- action='store',
- dest='list_format',
+ "--format",
+ action="store",
+ dest="list_format",
default="columns",
- choices=('columns', 'freeze', 'json'),
- help="Select the output format among: columns (default), freeze, "
- "or json",
+ choices=("columns", "freeze", "json"),
+ help="Select the output format among: columns (default), freeze, or json",
)
self.cmd_opts.add_option(
- '--not-required',
- action='store_true',
- dest='not_required',
- help="List packages that are not dependencies of "
- "installed packages.",
+ "--not-required",
+ action="store_true",
+ dest="not_required",
+ help="List packages that are not dependencies of installed packages.",
)
self.cmd_opts.add_option(
- '--exclude-editable',
- action='store_false',
- dest='include_editable',
- help='Exclude editable package from output.',
+ "--exclude-editable",
+ action="store_false",
+ dest="include_editable",
+ help="Exclude editable package from output.",
)
self.cmd_opts.add_option(
- '--include-editable',
- action='store_true',
- dest='include_editable',
- help='Include editable package from output.',
+ "--include-editable",
+ action="store_true",
+ dest="include_editable",
+ help="Include editable package from output.",
default=True,
)
self.cmd_opts.add_option(cmdoptions.list_exclude())
- index_opts = cmdoptions.make_option_group(
- cmdoptions.index_group, self.parser
- )
+ index_opts = cmdoptions.make_option_group(cmdoptions.index_group, self.parser)
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, self.cmd_opts)
@@ -144,8 +153,7 @@ class ListCommand(IndexGroupCommand):
def run(self, options: Values, args: List[str]) -> int:
if options.outdated and options.uptodate:
- raise CommandError(
- "Options --outdated and --uptodate cannot be combined.")
+ raise CommandError("Options --outdated and --uptodate cannot be combined.")
cmdoptions.check_list_path_option(options)
@@ -183,7 +191,8 @@ class ListCommand(IndexGroupCommand):
self, packages: "_ProcessedDists", options: Values
) -> "_ProcessedDists":
return [
- dist for dist in self.iter_packages_latest_infos(packages, options)
+ dist
+ for dist in self.iter_packages_latest_infos(packages, options)
if dist.latest_version > dist.version
]
@@ -191,7 +200,8 @@ class ListCommand(IndexGroupCommand):
self, packages: "_ProcessedDists", options: Values
) -> "_ProcessedDists":
return [
- dist for dist in self.iter_packages_latest_infos(packages, options)
+ dist
+ for dist in self.iter_packages_latest_infos(packages, options)
if dist.latest_version == dist.version
]
@@ -211,18 +221,21 @@ class ListCommand(IndexGroupCommand):
def iter_packages_latest_infos(
self, packages: "_ProcessedDists", options: Values
- ) -> Iterator["_DistWithLatestInfo"]:
+ ) -> Generator["_DistWithLatestInfo", None, None]:
with self._build_session(options) as session:
finder = self._build_package_finder(options, session)
def latest_info(
- dist: "_DistWithLatestInfo"
+ dist: "_DistWithLatestInfo",
) -> Optional["_DistWithLatestInfo"]:
all_candidates = finder.find_all_candidates(dist.canonical_name)
if not options.pre:
# Remove prereleases
- all_candidates = [candidate for candidate in all_candidates
- if not candidate.version.is_prerelease]
+ all_candidates = [
+ candidate
+ for candidate in all_candidates
+ if not candidate.version.is_prerelease
+ ]
evaluator = finder.make_candidate_evaluator(
project_name=dist.canonical_name,
@@ -233,14 +246,14 @@ class ListCommand(IndexGroupCommand):
remote_version = best_candidate.version
if best_candidate.link.is_wheel:
- typ = 'wheel'
+ typ = "wheel"
else:
- typ = 'sdist'
+ typ = "sdist"
dist.latest_version = remote_version
dist.latest_filetype = typ
return dist
- for dist in map_multithread(latest_info, packages):
+ for dist in map(latest_info, packages):
if dist is not None:
yield dist
@@ -251,17 +264,18 @@ class ListCommand(IndexGroupCommand):
packages,
key=lambda dist: dist.canonical_name,
)
- if options.list_format == 'columns' and packages:
+ if options.list_format == "columns" and packages:
data, header = format_for_columns(packages, options)
self.output_package_listing_columns(data, header)
- elif options.list_format == 'freeze':
+ elif options.list_format == "freeze":
for dist in packages:
if options.verbose >= 1:
- write_output("%s==%s (%s)", dist.raw_name,
- dist.version, dist.location)
+ write_output(
+ "%s==%s (%s)", dist.raw_name, dist.version, dist.location
+ )
else:
write_output("%s==%s", dist.raw_name, dist.version)
- elif options.list_format == 'json':
+ elif options.list_format == "json":
write_output(format_for_json(packages, options))
def output_package_listing_columns(
@@ -275,7 +289,7 @@ class ListCommand(IndexGroupCommand):
# Create and add a separator.
if len(data) > 0:
- pkg_strings.insert(1, " ".join(map(lambda x: '-' * x, sizes)))
+ pkg_strings.insert(1, " ".join(map(lambda x: "-" * x, sizes)))
for val in pkg_strings:
write_output(val)
@@ -288,19 +302,22 @@ def format_for_columns(
Convert the package data into something usable
by output_package_listing_columns.
"""
+ header = ["Package", "Version"]
+
running_outdated = options.outdated
- # Adjust the header for the `pip list --outdated` case.
if running_outdated:
- header = ["Package", "Version", "Latest", "Type"]
- else:
- header = ["Package", "Version"]
+ header.extend(["Latest", "Type"])
- data = []
- if options.verbose >= 1 or any(x.editable for x in pkgs):
+ has_editables = any(x.editable for x in pkgs)
+ if has_editables:
+ header.append("Editable project location")
+
+ if options.verbose >= 1:
header.append("Location")
if options.verbose >= 1:
header.append("Installer")
+ data = []
for proj in pkgs:
# if we're working on the 'outdated' list, separate out the
# latest_version and type
@@ -310,7 +327,10 @@ def format_for_columns(
row.append(str(proj.latest_version))
row.append(proj.latest_filetype)
- if options.verbose >= 1 or proj.editable:
+ if has_editables:
+ row.append(proj.editable_project_location or "")
+
+ if options.verbose >= 1:
row.append(proj.location or "")
if options.verbose >= 1:
row.append(proj.installer)
@@ -324,14 +344,17 @@ def format_for_json(packages: "_ProcessedDists", options: Values) -> str:
data = []
for dist in packages:
info = {
- 'name': dist.raw_name,
- 'version': str(dist.version),
+ "name": dist.raw_name,
+ "version": str(dist.version),
}
if options.verbose >= 1:
- info['location'] = dist.location or ""
- info['installer'] = dist.installer
+ info["location"] = dist.location or ""
+ info["installer"] = dist.installer
if options.outdated:
- info['latest_version'] = str(dist.latest_version)
- info['latest_filetype'] = dist.latest_filetype
+ info["latest_version"] = str(dist.latest_version)
+ info["latest_filetype"] = dist.latest_filetype
+ editable_project_location = dist.editable_project_location
+ if editable_project_location:
+ info["editable_project_location"] = editable_project_location
data.append(info)
return json.dumps(data)
diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py
index 7a20ba1e4..03ed925b2 100644
--- a/src/pip/_internal/commands/search.py
+++ b/src/pip/_internal/commands/search.py
@@ -27,6 +27,7 @@ if TYPE_CHECKING:
summary: str
versions: List[str]
+
logger = logging.getLogger(__name__)
@@ -39,17 +40,19 @@ class SearchCommand(Command, SessionCommandMixin):
def add_options(self) -> None:
self.cmd_opts.add_option(
- '-i', '--index',
- dest='index',
- metavar='URL',
+ "-i",
+ "--index",
+ dest="index",
+ metavar="URL",
default=PyPI.pypi_url,
- help='Base URL of Python Package Index (default %default)')
+ help="Base URL of Python Package Index (default %default)",
+ )
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
if not args:
- raise CommandError('Missing required argument (search query).')
+ raise CommandError("Missing required argument (search query).")
query = args
pypi_hits = self.search(query, options)
hits = transform_hits(pypi_hits)
@@ -71,7 +74,7 @@ class SearchCommand(Command, SessionCommandMixin):
transport = PipXmlrpcTransport(index_url, session)
pypi = xmlrpc.client.ServerProxy(index_url, transport)
try:
- hits = pypi.search({'name': query, 'summary': query}, 'or')
+ hits = pypi.search({"name": query, "summary": query}, "or")
except xmlrpc.client.Fault as fault:
message = "XMLRPC request failed [code: {code}]\n{string}".format(
code=fault.faultCode,
@@ -90,22 +93,22 @@ def transform_hits(hits: List[Dict[str, str]]) -> List["TransformedHit"]:
"""
packages: Dict[str, "TransformedHit"] = OrderedDict()
for hit in hits:
- name = hit['name']
- summary = hit['summary']
- version = hit['version']
+ name = hit["name"]
+ summary = hit["summary"]
+ version = hit["version"]
if name not in packages.keys():
packages[name] = {
- 'name': name,
- 'summary': summary,
- 'versions': [version],
+ "name": name,
+ "summary": summary,
+ "versions": [version],
}
else:
- packages[name]['versions'].append(version)
+ packages[name]["versions"].append(version)
# if this is the highest version, replace summary and score
- if version == highest_version(packages[name]['versions']):
- packages[name]['summary'] = summary
+ if version == highest_version(packages[name]["versions"]):
+ packages[name]["summary"] = summary
return list(packages.values())
@@ -116,14 +119,17 @@ def print_dist_installation_info(name: str, latest: str) -> None:
if dist is not None:
with indent_log():
if dist.version == latest:
- write_output('INSTALLED: %s (latest)', dist.version)
+ write_output("INSTALLED: %s (latest)", dist.version)
else:
- write_output('INSTALLED: %s', dist.version)
+ write_output("INSTALLED: %s", dist.version)
if parse_version(latest).pre:
- write_output('LATEST: %s (pre-release; install'
- ' with "pip install --pre")', latest)
+ write_output(
+ "LATEST: %s (pre-release; install"
+ " with `pip install --pre`)",
+ latest,
+ )
else:
- write_output('LATEST: %s', latest)
+ write_output("LATEST: %s", latest)
def print_results(
@@ -134,25 +140,29 @@ def print_results(
if not hits:
return
if name_column_width is None:
- name_column_width = max([
- len(hit['name']) + len(highest_version(hit.get('versions', ['-'])))
- for hit in hits
- ]) + 4
+ name_column_width = (
+ max(
+ [
+ len(hit["name"]) + len(highest_version(hit.get("versions", ["-"])))
+ for hit in hits
+ ]
+ )
+ + 4
+ )
for hit in hits:
- name = hit['name']
- summary = hit['summary'] or ''
- latest = highest_version(hit.get('versions', ['-']))
+ name = hit["name"]
+ summary = hit["summary"] or ""
+ latest = highest_version(hit.get("versions", ["-"]))
if terminal_width is not None:
target_width = terminal_width - name_column_width - 5
if target_width > 10:
# wrap and indent summary to fit terminal
summary_lines = textwrap.wrap(summary, target_width)
- summary = ('\n' + ' ' * (name_column_width + 3)).join(
- summary_lines)
+ summary = ("\n" + " " * (name_column_width + 3)).join(summary_lines)
- name_latest = f'{name} ({latest})'
- line = f'{name_latest:{name_column_width}} - {summary}'
+ name_latest = f"{name} ({latest})"
+ line = f"{name_latest:{name_column_width}} - {summary}"
try:
write_output(line)
print_dist_installation_info(name, latest)
diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py
index 5b2de39e5..212167c9d 100644
--- a/src/pip/_internal/commands/show.py
+++ b/src/pip/_internal/commands/show.py
@@ -1,8 +1,6 @@
-import csv
import logging
-import pathlib
from optparse import Values
-from typing import Iterator, List, NamedTuple, Optional, Tuple
+from typing import Generator, Iterable, Iterator, List, NamedTuple, Optional
from pip._vendor.packaging.utils import canonicalize_name
@@ -27,23 +25,26 @@ class ShowCommand(Command):
def add_options(self) -> None:
self.cmd_opts.add_option(
- '-f', '--files',
- dest='files',
- action='store_true',
+ "-f",
+ "--files",
+ dest="files",
+ action="store_true",
default=False,
- help='Show the full list of installed files for each package.')
+ help="Show the full list of installed files for each package.",
+ )
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
if not args:
- logger.warning('ERROR: Please provide a package name or names.')
+ logger.warning("ERROR: Please provide a package name or names.")
return ERROR
query = args
results = search_packages_info(query)
if not print_results(
- results, list_files=options.files, verbose=options.verbose):
+ results, list_files=options.files, verbose=options.verbose
+ ):
return ERROR
return SUCCESS
@@ -59,6 +60,7 @@ class _PackageInfo(NamedTuple):
classifiers: List[str]
summary: str
homepage: str
+ project_urls: List[str]
author: str
author_email: str
license: str
@@ -66,34 +68,7 @@ class _PackageInfo(NamedTuple):
files: Optional[List[str]]
-def _covert_legacy_entry(entry: Tuple[str, ...], info: Tuple[str, ...]) -> str:
- """Convert a legacy installed-files.txt path into modern RECORD path.
-
- The legacy format stores paths relative to the info directory, while the
- modern format stores paths relative to the package root, e.g. the
- site-packages directory.
-
- :param entry: Path parts of the installed-files.txt entry.
- :param info: Path parts of the egg-info directory relative to package root.
- :returns: The converted entry.
-
- For best compatibility with symlinks, this does not use ``abspath()`` or
- ``Path.resolve()``, but tries to work with path parts:
-
- 1. While ``entry`` starts with ``..``, remove the equal amounts of parts
- from ``info``; if ``info`` is empty, start appending ``..`` instead.
- 2. Join the two directly.
- """
- while entry and entry[0] == "..":
- if not info or info[-1] == "..":
- info += ("..",)
- else:
- info = info[:-1]
- entry = entry[1:]
- return str(pathlib.Path(*info, *entry))
-
-
-def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
+def search_packages_info(query: List[str]) -> Generator[_PackageInfo, None, None]:
"""
Gather details from installed distributions. Print distribution name,
version, location, and installed files. Installed files requires a
@@ -102,53 +77,20 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
"""
env = get_default_environment()
- installed = {
- dist.canonical_name: dist
- for dist in env.iter_distributions()
- }
+ installed = {dist.canonical_name: dist for dist in env.iter_all_distributions()}
query_names = [canonicalize_name(name) for name in query]
missing = sorted(
[name for name, pkg in zip(query, query_names) if pkg not in installed]
)
if missing:
- logger.warning('Package(s) not found: %s', ', '.join(missing))
+ logger.warning("Package(s) not found: %s", ", ".join(missing))
- def _get_requiring_packages(current_dist: BaseDistribution) -> List[str]:
- return [
+ def _get_requiring_packages(current_dist: BaseDistribution) -> Iterator[str]:
+ return (
dist.metadata["Name"] or "UNKNOWN"
for dist in installed.values()
- if current_dist.canonical_name in {
- canonicalize_name(d.name) for d in dist.iter_dependencies()
- }
- ]
-
- def _files_from_record(dist: BaseDistribution) -> Optional[Iterator[str]]:
- try:
- text = dist.read_text('RECORD')
- except FileNotFoundError:
- return None
- # This extra Path-str cast normalizes entries.
- return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
-
- def _files_from_legacy(dist: BaseDistribution) -> Optional[Iterator[str]]:
- try:
- text = dist.read_text('installed-files.txt')
- except FileNotFoundError:
- return None
- paths = (p for p in text.splitlines(keepends=False) if p)
- root = dist.location
- info = dist.info_directory
- if root is None or info is None:
- return paths
- try:
- info_rel = pathlib.Path(info).relative_to(root)
- except ValueError: # info is not relative to root.
- return paths
- if not info_rel.parts: # info *is* root.
- return paths
- return (
- _covert_legacy_entry(pathlib.Path(p).parts, info_rel.parts)
- for p in paths
+ if current_dist.canonical_name
+ in {canonicalize_name(d.name) for d in dist.iter_dependencies()}
)
for query_name in query_names:
@@ -157,13 +99,16 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
except KeyError:
continue
+ requires = sorted((req.name for req in dist.iter_dependencies()), key=str.lower)
+ required_by = sorted(_get_requiring_packages(dist), key=str.lower)
+
try:
- entry_points_text = dist.read_text('entry_points.txt')
+ entry_points_text = dist.read_text("entry_points.txt")
entry_points = entry_points_text.splitlines(keepends=False)
except FileNotFoundError:
entry_points = []
- files_iter = _files_from_record(dist) or _files_from_legacy(dist)
+ files_iter = dist.iter_declared_entries()
if files_iter is None:
files: Optional[List[str]] = None
else:
@@ -175,13 +120,14 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
name=dist.raw_name,
version=str(dist.version),
location=dist.location or "",
- requires=[req.name for req in dist.iter_dependencies()],
- required_by=_get_requiring_packages(dist),
+ requires=requires,
+ required_by=required_by,
installer=dist.installer,
metadata_version=dist.metadata_version or "",
classifiers=metadata.get_all("Classifier", []),
summary=metadata.get("Summary", ""),
homepage=metadata.get("Home-page", ""),
+ project_urls=metadata.get_all("Project-URL", []),
author=metadata.get("Author", ""),
author_email=metadata.get("Author-email", ""),
license=metadata.get("License", ""),
@@ -191,7 +137,7 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
def print_results(
- distributions: Iterator[_PackageInfo],
+ distributions: Iterable[_PackageInfo],
list_files: bool,
verbose: bool,
) -> bool:
@@ -212,8 +158,8 @@ def print_results(
write_output("Author-email: %s", dist.author_email)
write_output("License: %s", dist.license)
write_output("Location: %s", dist.location)
- write_output("Requires: %s", ', '.join(dist.requires))
- write_output("Required-by: %s", ', '.join(dist.required_by))
+ write_output("Requires: %s", ", ".join(dist.requires))
+ write_output("Required-by: %s", ", ".join(dist.required_by))
if verbose:
write_output("Metadata-Version: %s", dist.metadata_version)
@@ -224,6 +170,9 @@ def print_results(
write_output("Entry-points:")
for entry in dist.entry_points:
write_output(" %s", entry.strip())
+ write_output("Project-URLs:")
+ for project_url in dist.project_urls:
+ write_output(" %s", project_url)
if list_files:
write_output("Files:")
if dist.files is None:
diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py
index c590627ea..dea8077e7 100644
--- a/src/pip/_internal/commands/uninstall.py
+++ b/src/pip/_internal/commands/uninstall.py
@@ -4,6 +4,7 @@ from typing import List
from pip._vendor.packaging.utils import canonicalize_name
+from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import Command
from pip._internal.cli.req_command import SessionCommandMixin, warn_if_run_as_root
from pip._internal.cli.status_codes import SUCCESS
@@ -35,20 +36,25 @@ class UninstallCommand(Command, SessionCommandMixin):
def add_options(self) -> None:
self.cmd_opts.add_option(
- '-r', '--requirement',
- dest='requirements',
- action='append',
+ "-r",
+ "--requirement",
+ dest="requirements",
+ action="append",
default=[],
- metavar='file',
- help='Uninstall all the packages listed in the given requirements '
- 'file. This option can be used multiple times.',
+ metavar="file",
+ help=(
+ "Uninstall all the packages listed in the given requirements "
+ "file. This option can be used multiple times."
+ ),
)
self.cmd_opts.add_option(
- '-y', '--yes',
- dest='yes',
- action='store_true',
- help="Don't ask for confirmation of uninstall deletions.")
-
+ "-y",
+ "--yes",
+ dest="yes",
+ action="store_true",
+ help="Don't ask for confirmation of uninstall deletions.",
+ )
+ self.cmd_opts.add_option(cmdoptions.root_user_action())
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options: Values, args: List[str]) -> int:
@@ -57,7 +63,8 @@ class UninstallCommand(Command, SessionCommandMixin):
reqs_to_uninstall = {}
for name in args:
req = install_req_from_line(
- name, isolated=options.isolated_mode,
+ name,
+ isolated=options.isolated_mode,
)
if req.name:
reqs_to_uninstall[canonicalize_name(req.name)] = req
@@ -70,18 +77,16 @@ class UninstallCommand(Command, SessionCommandMixin):
)
for filename in options.requirements:
for parsed_req in parse_requirements(
- filename,
- options=options,
- session=session):
+ filename, options=options, session=session
+ ):
req = install_req_from_parsed_requirement(
- parsed_req,
- isolated=options.isolated_mode
+ parsed_req, isolated=options.isolated_mode
)
if req.name:
reqs_to_uninstall[canonicalize_name(req.name)] = req
if not reqs_to_uninstall:
raise InstallationError(
- f'You must give at least one requirement to {self.name} (see '
+ f"You must give at least one requirement to {self.name} (see "
f'"pip help {self.name}")'
)
@@ -91,10 +96,11 @@ class UninstallCommand(Command, SessionCommandMixin):
for req in reqs_to_uninstall.values():
uninstall_pathset = req.uninstall(
- auto_confirm=options.yes, verbose=self.verbosity > 0,
+ auto_confirm=options.yes,
+ verbose=self.verbosity > 0,
)
if uninstall_pathset:
uninstall_pathset.commit()
-
- warn_if_run_as_root()
+ if options.root_user_action == "warn":
+ warn_if_run_as_root()
return SUCCESS
diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py
index c8bf4e25d..9dd6c82f2 100644
--- a/src/pip/_internal/commands/wheel.py
+++ b/src/pip/_internal/commands/wheel.py
@@ -9,8 +9,8 @@ from pip._internal.cli import cmdoptions
from pip._internal.cli.req_command import RequirementCommand, with_cleanup
from pip._internal.cli.status_codes import SUCCESS
from pip._internal.exceptions import CommandError
+from pip._internal.operations.build.build_tracker import get_build_tracker
from pip._internal.req.req_install import InstallRequirement
-from pip._internal.req.req_tracker import get_requirement_tracker
from pip._internal.utils.misc import ensure_dir, normalize_path
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.wheel_builder import build, should_build_for_wheel_command
@@ -26,10 +26,8 @@ class WheelCommand(RequirementCommand):
recompiling your software during every install. For more details, see the
wheel docs: https://wheel.readthedocs.io/en/latest/
- Requirements: setuptools>=0.8, and wheel.
-
- 'pip wheel' uses the bdist_wheel setuptools extension from the wheel
- package to build individual wheels.
+ 'pip wheel' uses the build system interface as described here:
+ https://pip.pypa.io/en/stable/reference/build-system/
"""
@@ -43,12 +41,15 @@ class WheelCommand(RequirementCommand):
def add_options(self) -> None:
self.cmd_opts.add_option(
- '-w', '--wheel-dir',
- dest='wheel_dir',
- metavar='dir',
+ "-w",
+ "--wheel-dir",
+ dest="wheel_dir",
+ metavar="dir",
default=os.curdir,
- help=("Build wheels into <dir>, where the default is the "
- "current working directory."),
+ help=(
+ "Build wheels into <dir>, where the default is the "
+ "current working directory."
+ ),
)
self.cmd_opts.add_option(cmdoptions.no_binary())
self.cmd_opts.add_option(cmdoptions.only_binary())
@@ -56,32 +57,35 @@ class WheelCommand(RequirementCommand):
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
self.cmd_opts.add_option(cmdoptions.use_pep517())
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
+ self.cmd_opts.add_option(cmdoptions.check_build_deps())
self.cmd_opts.add_option(cmdoptions.constraints())
self.cmd_opts.add_option(cmdoptions.editable())
self.cmd_opts.add_option(cmdoptions.requirements())
self.cmd_opts.add_option(cmdoptions.src())
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.no_deps())
- self.cmd_opts.add_option(cmdoptions.build_dir())
self.cmd_opts.add_option(cmdoptions.progress_bar())
self.cmd_opts.add_option(
- '--no-verify',
- dest='no_verify',
- action='store_true',
+ "--no-verify",
+ dest="no_verify",
+ action="store_true",
default=False,
help="Don't verify if built wheel is valid.",
)
+ self.cmd_opts.add_option(cmdoptions.config_settings())
self.cmd_opts.add_option(cmdoptions.build_options())
self.cmd_opts.add_option(cmdoptions.global_options())
self.cmd_opts.add_option(
- '--pre',
- action='store_true',
+ "--pre",
+ action="store_true",
default=False,
- help=("Include pre-release and development versions. By default, "
- "pip only finds stable versions."),
+ help=(
+ "Include pre-release and development versions. By default, "
+ "pip only finds stable versions."
+ ),
)
self.cmd_opts.add_option(cmdoptions.require_hashes())
@@ -106,7 +110,7 @@ class WheelCommand(RequirementCommand):
options.wheel_dir = normalize_path(options.wheel_dir)
ensure_dir(options.wheel_dir)
- req_tracker = self.enter_context(get_requirement_tracker())
+ build_tracker = self.enter_context(get_build_tracker())
directory = TempDirectory(
delete=not options.no_clean,
@@ -119,11 +123,12 @@ class WheelCommand(RequirementCommand):
preparer = self.make_requirement_preparer(
temp_build_dir=directory,
options=options,
- req_tracker=req_tracker,
+ build_tracker=build_tracker,
session=session,
finder=finder,
download_dir=options.wheel_dir,
use_user_site=False,
+ verbosity=self.verbosity,
)
resolver = self.make_resolver(
@@ -137,9 +142,7 @@ class WheelCommand(RequirementCommand):
self.trace_basic_info(finder)
- requirement_set = resolver.resolve(
- reqs, check_supported_wheels=True
- )
+ requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
reqs_to_build: List[InstallRequirement] = []
for req in requirement_set.requirements.values():
@@ -165,12 +168,11 @@ class WheelCommand(RequirementCommand):
except OSError as e:
logger.warning(
"Building wheel for %s failed: %s",
- req.name, e,
+ req.name,
+ e,
)
build_failures.append(req)
if len(build_failures) != 0:
- raise CommandError(
- "Failed to build one or more wheels"
- )
+ raise CommandError("Failed to build one or more wheels")
return SUCCESS
diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py
index a4698ec1d..8fd46c9b8 100644
--- a/src/pip/_internal/configuration.py
+++ b/src/pip/_internal/configuration.py
@@ -13,7 +13,6 @@ Some terminology:
import configparser
import locale
-import logging
import os
import sys
from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple
@@ -24,41 +23,39 @@ from pip._internal.exceptions import (
)
from pip._internal.utils import appdirs
from pip._internal.utils.compat import WINDOWS
+from pip._internal.utils.logging import getLogger
from pip._internal.utils.misc import ensure_dir, enum
RawConfigParser = configparser.RawConfigParser # Shorthand
Kind = NewType("Kind", str)
-CONFIG_BASENAME = 'pip.ini' if WINDOWS else 'pip.conf'
+CONFIG_BASENAME = "pip.ini" if WINDOWS else "pip.conf"
ENV_NAMES_IGNORED = "version", "help"
# The kinds of configurations there are.
kinds = enum(
- USER="user", # User Specific
- GLOBAL="global", # System Wide
- SITE="site", # [Virtual] Environment Specific
- ENV="env", # from PIP_CONFIG_FILE
+ USER="user", # User Specific
+ GLOBAL="global", # System Wide
+ SITE="site", # [Virtual] Environment Specific
+ ENV="env", # from PIP_CONFIG_FILE
ENV_VAR="env-var", # from Environment Variables
)
OVERRIDE_ORDER = kinds.GLOBAL, kinds.USER, kinds.SITE, kinds.ENV, kinds.ENV_VAR
VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.SITE
-logger = logging.getLogger(__name__)
+logger = getLogger(__name__)
# NOTE: Maybe use the optionx attribute to normalize keynames.
-def _normalize_name(name):
- # type: (str) -> str
- """Make a name consistent regardless of source (environment or file)
- """
- name = name.lower().replace('_', '-')
- if name.startswith('--'):
+def _normalize_name(name: str) -> str:
+ """Make a name consistent regardless of source (environment or file)"""
+ name = name.lower().replace("_", "-")
+ if name.startswith("--"):
name = name[2:] # only prefer long opts
return name
-def _disassemble_key(name):
- # type: (str) -> List[str]
+def _disassemble_key(name: str) -> List[str]:
if "." not in name:
error_message = (
"Key does not contain dot separated section and key. "
@@ -68,22 +65,18 @@ def _disassemble_key(name):
return name.split(".", 1)
-def get_configuration_files():
- # type: () -> Dict[Kind, List[str]]
+def get_configuration_files() -> Dict[Kind, List[str]]:
global_config_files = [
- os.path.join(path, CONFIG_BASENAME)
- for path in appdirs.site_config_dirs('pip')
+ os.path.join(path, CONFIG_BASENAME) for path in appdirs.site_config_dirs("pip")
]
site_config_file = os.path.join(sys.prefix, CONFIG_BASENAME)
legacy_config_file = os.path.join(
- os.path.expanduser('~'),
- 'pip' if WINDOWS else '.pip',
+ os.path.expanduser("~"),
+ "pip" if WINDOWS else ".pip",
CONFIG_BASENAME,
)
- new_config_file = os.path.join(
- appdirs.user_config_dir("pip"), CONFIG_BASENAME
- )
+ new_config_file = os.path.join(appdirs.user_config_dir("pip"), CONFIG_BASENAME)
return {
kinds.GLOBAL: global_config_files,
kinds.SITE: [site_config_file],
@@ -105,8 +98,7 @@ class Configuration:
and the data stored is also nice.
"""
- def __init__(self, isolated, load_only=None):
- # type: (bool, Optional[Kind]) -> None
+ def __init__(self, isolated: bool, load_only: Optional[Kind] = None) -> None:
super().__init__()
if load_only is not None and load_only not in VALID_LOAD_ONLY:
@@ -119,54 +111,50 @@ class Configuration:
self.load_only = load_only
# Because we keep track of where we got the data from
- self._parsers = {
+ self._parsers: Dict[Kind, List[Tuple[str, RawConfigParser]]] = {
variant: [] for variant in OVERRIDE_ORDER
- } # type: Dict[Kind, List[Tuple[str, RawConfigParser]]]
- self._config = {
+ }
+ self._config: Dict[Kind, Dict[str, Any]] = {
variant: {} for variant in OVERRIDE_ORDER
- } # type: Dict[Kind, Dict[str, Any]]
- self._modified_parsers = [] # type: List[Tuple[str, RawConfigParser]]
+ }
+ self._modified_parsers: List[Tuple[str, RawConfigParser]] = []
- def load(self):
- # type: () -> None
- """Loads configuration from configuration files and environment
- """
+ def load(self) -> None:
+ """Loads configuration from configuration files and environment"""
self._load_config_files()
if not self.isolated:
self._load_environment_vars()
- def get_file_to_edit(self):
- # type: () -> Optional[str]
- """Returns the file with highest priority in configuration
- """
- assert self.load_only is not None, \
- "Need to be specified a file to be editing"
+ def get_file_to_edit(self) -> Optional[str]:
+ """Returns the file with highest priority in configuration"""
+ assert self.load_only is not None, "Need to be specified a file to be editing"
try:
return self._get_parser_to_modify()[0]
except IndexError:
return None
- def items(self):
- # type: () -> Iterable[Tuple[str, Any]]
+ def items(self) -> Iterable[Tuple[str, Any]]:
"""Returns key-value pairs like dict.items() representing the loaded
configuration
"""
return self._dictionary.items()
- def get_value(self, key):
- # type: (str) -> Any
- """Get a value from the configuration.
- """
+ def get_value(self, key: str) -> Any:
+ """Get a value from the configuration."""
+ orig_key = key
+ key = _normalize_name(key)
try:
return self._dictionary[key]
except KeyError:
- raise ConfigurationError(f"No such key - {key}")
-
- def set_value(self, key, value):
- # type: (str, Any) -> None
- """Modify a value in the configuration.
- """
+ # disassembling triggers a more useful error message than simply
+ # "No such key" in the case that the key isn't in the form command.option
+ _disassemble_key(key)
+ raise ConfigurationError(f"No such key - {orig_key}")
+
+ def set_value(self, key: str, value: Any) -> None:
+ """Modify a value in the configuration."""
+ key = _normalize_name(key)
self._ensure_have_load_only()
assert self.load_only
@@ -183,21 +171,23 @@ class Configuration:
self._config[self.load_only][key] = value
self._mark_as_modified(fname, parser)
- def unset_value(self, key):
- # type: (str) -> None
+ def unset_value(self, key: str) -> None:
"""Unset a value in the configuration."""
+ orig_key = key
+ key = _normalize_name(key)
self._ensure_have_load_only()
assert self.load_only
if key not in self._config[self.load_only]:
- raise ConfigurationError(f"No such key - {key}")
+ raise ConfigurationError(f"No such key - {orig_key}")
fname, parser = self._get_parser_to_modify()
if parser is not None:
section, name = _disassemble_key(key)
- if not (parser.has_section(section)
- and parser.remove_option(section, name)):
+ if not (
+ parser.has_section(section) and parser.remove_option(section, name)
+ ):
# The option was not removed.
raise ConfigurationError(
"Fatal Internal error [id=1]. Please report as a bug."
@@ -210,10 +200,8 @@ class Configuration:
del self._config[self.load_only][key]
- def save(self):
- # type: () -> None
- """Save the current in-memory state.
- """
+ def save(self) -> None:
+ """Save the current in-memory state."""
self._ensure_have_load_only()
for fname, parser in self._modified_parsers:
@@ -229,17 +217,14 @@ class Configuration:
# Private routines
#
- def _ensure_have_load_only(self):
- # type: () -> None
+ def _ensure_have_load_only(self) -> None:
if self.load_only is None:
raise ConfigurationError("Needed a specific file to be modifying.")
logger.debug("Will be working with %s variant only", self.load_only)
@property
- def _dictionary(self):
- # type: () -> Dict[str, Any]
- """A dictionary representing the loaded configuration.
- """
+ def _dictionary(self) -> Dict[str, Any]:
+ """A dictionary representing the loaded configuration."""
# NOTE: Dictionaries are not populated if not loaded. So, conditionals
# are not needed here.
retval = {}
@@ -249,10 +234,8 @@ class Configuration:
return retval
- def _load_config_files(self):
- # type: () -> None
- """Loads configuration from configuration files
- """
+ def _load_config_files(self) -> None:
+ """Loads configuration from configuration files"""
config_files = dict(self.iter_config_files())
if config_files[kinds.ENV][0:1] == [os.devnull]:
logger.debug(
@@ -266,9 +249,7 @@ class Configuration:
# If there's specific variant set in `load_only`, load only
# that variant, not the others.
if self.load_only is not None and variant != self.load_only:
- logger.debug(
- "Skipping file '%s' (variant: %s)", fname, variant
- )
+ logger.debug("Skipping file '%s' (variant: %s)", fname, variant)
continue
parser = self._load_file(variant, fname)
@@ -276,9 +257,8 @@ class Configuration:
# Keeping track of the parsers used
self._parsers[variant].append((fname, parser))
- def _load_file(self, variant, fname):
- # type: (Kind, str) -> RawConfigParser
- logger.debug("For variant '%s', will try loading '%s'", variant, fname)
+ def _load_file(self, variant: Kind, fname: str) -> RawConfigParser:
+ logger.verbose("For variant '%s', will try loading '%s'", variant, fname)
parser = self._construct_parser(fname)
for section in parser.sections():
@@ -287,22 +267,20 @@ class Configuration:
return parser
- def _construct_parser(self, fname):
- # type: (str) -> RawConfigParser
+ def _construct_parser(self, fname: str) -> RawConfigParser:
parser = configparser.RawConfigParser()
# If there is no such file, don't bother reading it but create the
# parser anyway, to hold the data.
# Doing this is useful when modifying and saving files, where we don't
# need to construct a parser.
if os.path.exists(fname):
+ locale_encoding = locale.getpreferredencoding(False)
try:
- parser.read(fname)
+ parser.read(fname, encoding=locale_encoding)
except UnicodeDecodeError:
# See https://github.com/pypa/pip/issues/4963
raise ConfigurationFileCouldNotBeLoaded(
- reason="contains invalid {} characters".format(
- locale.getpreferredencoding(False)
- ),
+ reason=f"contains invalid {locale_encoding} characters",
fname=fname,
)
except configparser.Error as error:
@@ -310,16 +288,15 @@ class Configuration:
raise ConfigurationFileCouldNotBeLoaded(error=error)
return parser
- def _load_environment_vars(self):
- # type: () -> None
- """Loads configuration from environment variables
- """
+ def _load_environment_vars(self) -> None:
+ """Loads configuration from environment variables"""
self._config[kinds.ENV_VAR].update(
self._normalized_keys(":env:", self.get_environ_vars())
)
- def _normalized_keys(self, section, items):
- # type: (str, Iterable[Tuple[str, Any]]) -> Dict[str, Any]
+ def _normalized_keys(
+ self, section: str, items: Iterable[Tuple[str, Any]]
+ ) -> Dict[str, Any]:
"""Normalizes items to construct a dictionary with normalized keys.
This routine is where the names become keys and are made the same
@@ -331,8 +308,7 @@ class Configuration:
normalized[key] = val
return normalized
- def get_environ_vars(self):
- # type: () -> Iterable[Tuple[str, str]]
+ def get_environ_vars(self) -> Iterable[Tuple[str, str]]:
"""Returns a generator with all environmental vars with prefix PIP_"""
for key, val in os.environ.items():
if key.startswith("PIP_"):
@@ -341,8 +317,7 @@ class Configuration:
yield name, val
# XXX: This is patched in the tests.
- def iter_config_files(self):
- # type: () -> Iterable[Tuple[Kind, List[str]]]
+ def iter_config_files(self) -> Iterable[Tuple[Kind, List[str]]]:
"""Yields variant and configuration files associated with it.
This should be treated like items of a dictionary.
@@ -350,7 +325,7 @@ class Configuration:
# SMELL: Move the conditions out of this function
# environment variables have the lowest priority
- config_file = os.environ.get('PIP_CONFIG_FILE', None)
+ config_file = os.environ.get("PIP_CONFIG_FILE", None)
if config_file is not None:
yield kinds.ENV, [config_file]
else:
@@ -372,13 +347,11 @@ class Configuration:
# finally virtualenv configuration first trumping others
yield kinds.SITE, config_files[kinds.SITE]
- def get_values_in_config(self, variant):
- # type: (Kind) -> Dict[str, Any]
+ def get_values_in_config(self, variant: Kind) -> Dict[str, Any]:
"""Get values present in a config file"""
return self._config[variant]
- def _get_parser_to_modify(self):
- # type: () -> Tuple[str, RawConfigParser]
+ def _get_parser_to_modify(self) -> Tuple[str, RawConfigParser]:
# Determine which parser to modify
assert self.load_only
parsers = self._parsers[self.load_only]
@@ -392,12 +365,10 @@ class Configuration:
return parsers[-1]
# XXX: This is patched in the tests.
- def _mark_as_modified(self, fname, parser):
- # type: (str, RawConfigParser) -> None
+ def _mark_as_modified(self, fname: str, parser: RawConfigParser) -> None:
file_parser_tuple = (fname, parser)
if file_parser_tuple not in self._modified_parsers:
self._modified_parsers.append(file_parser_tuple)
- def __repr__(self):
- # type: () -> str
+ def __repr__(self) -> str:
return f"{self.__class__.__name__}({self._dictionary!r})"
diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py
index fbdd5e411..75ce2dc90 100644
--- a/src/pip/_internal/distributions/base.py
+++ b/src/pip/_internal/distributions/base.py
@@ -1,9 +1,7 @@
import abc
-from typing import Optional
-
-from pip._vendor.pkg_resources import Distribution
from pip._internal.index.package_finder import PackageFinder
+from pip._internal.metadata.base import BaseDistribution
from pip._internal.req import InstallRequirement
@@ -28,11 +26,14 @@ class AbstractDistribution(metaclass=abc.ABCMeta):
self.req = req
@abc.abstractmethod
- def get_pkg_resources_distribution(self) -> Optional[Distribution]:
+ def get_metadata_distribution(self) -> BaseDistribution:
raise NotImplementedError()
@abc.abstractmethod
def prepare_distribution_metadata(
- self, finder: PackageFinder, build_isolation: bool
+ self,
+ finder: PackageFinder,
+ build_isolation: bool,
+ check_build_deps: bool,
) -> None:
raise NotImplementedError()
diff --git a/src/pip/_internal/distributions/installed.py b/src/pip/_internal/distributions/installed.py
index 0d452e27f..edb38aa1a 100644
--- a/src/pip/_internal/distributions/installed.py
+++ b/src/pip/_internal/distributions/installed.py
@@ -1,9 +1,6 @@
-from typing import Optional
-
-from pip._vendor.pkg_resources import Distribution
-
from pip._internal.distributions.base import AbstractDistribution
from pip._internal.index.package_finder import PackageFinder
+from pip._internal.metadata import BaseDistribution
class InstalledDistribution(AbstractDistribution):
@@ -13,10 +10,14 @@ class InstalledDistribution(AbstractDistribution):
been computed.
"""
- def get_pkg_resources_distribution(self) -> Optional[Distribution]:
+ def get_metadata_distribution(self) -> BaseDistribution:
+ assert self.req.satisfied_by is not None, "not actually installed"
return self.req.satisfied_by
def prepare_distribution_metadata(
- self, finder: PackageFinder, build_isolation: bool
+ self,
+ finder: PackageFinder,
+ build_isolation: bool,
+ check_build_deps: bool,
) -> None:
pass
diff --git a/src/pip/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py
index 596b516a5..4c2564793 100644
--- a/src/pip/_internal/distributions/sdist.py
+++ b/src/pip/_internal/distributions/sdist.py
@@ -1,12 +1,11 @@
import logging
-from typing import Set, Tuple
-
-from pip._vendor.pkg_resources import Distribution
+from typing import Iterable, Set, Tuple
from pip._internal.build_env import BuildEnvironment
from pip._internal.distributions.base import AbstractDistribution
from pip._internal.exceptions import InstallationError
from pip._internal.index.package_finder import PackageFinder
+from pip._internal.metadata import BaseDistribution
from pip._internal.utils.subprocess import runner_with_spinner_message
logger = logging.getLogger(__name__)
@@ -19,11 +18,14 @@ class SourceDistribution(AbstractDistribution):
generated, either using PEP 517 or using the legacy `setup.py egg_info`.
"""
- def get_pkg_resources_distribution(self) -> Distribution:
+ def get_metadata_distribution(self) -> BaseDistribution:
return self.req.get_dist()
def prepare_distribution_metadata(
- self, finder: PackageFinder, build_isolation: bool
+ self,
+ finder: PackageFinder,
+ build_isolation: bool,
+ check_build_deps: bool,
) -> None:
# Load pyproject.toml, to determine whether PEP 517 is to be used
self.req.load_pyproject_toml()
@@ -31,28 +33,34 @@ class SourceDistribution(AbstractDistribution):
# Set up the build isolation, if this requirement should be isolated
should_isolate = self.req.use_pep517 and build_isolation
if should_isolate:
- self._setup_isolation(finder)
-
- self.req.prepare_metadata()
-
- def _setup_isolation(self, finder: PackageFinder) -> None:
- def _raise_conflicts(
- conflicting_with: str, conflicting_reqs: Set[Tuple[str, str]]
- ) -> None:
- format_string = (
- "Some build dependencies for {requirement} "
- "conflict with {conflicting_with}: {description}."
+ # Setup an isolated environment and install the build backend static
+ # requirements in it.
+ self._prepare_build_backend(finder)
+ # Check that if the requirement is editable, it either supports PEP 660 or
+ # has a setup.py or a setup.cfg. This cannot be done earlier because we need
+ # to setup the build backend to verify it supports build_editable, nor can
+ # it be done later, because we want to avoid installing build requirements
+ # needlessly. Doing it here also works around setuptools generating
+ # UNKNOWN.egg-info when running get_requires_for_build_wheel on a directory
+ # without setup.py nor setup.cfg.
+ self.req.isolated_editable_sanity_check()
+ # Install the dynamic build requirements.
+ self._install_build_reqs(finder)
+ # Check if the current environment provides build dependencies
+ should_check_deps = self.req.use_pep517 and check_build_deps
+ if should_check_deps:
+ pyproject_requires = self.req.pyproject_requires
+ assert pyproject_requires is not None
+ conflicting, missing = self.req.build_env.check_requirements(
+ pyproject_requires
)
- error_message = format_string.format(
- requirement=self.req,
- conflicting_with=conflicting_with,
- description=", ".join(
- f"{installed} is incompatible with {wanted}"
- for installed, wanted in sorted(conflicting)
- ),
- )
- raise InstallationError(error_message)
+ if conflicting:
+ self._raise_conflicts("the backend dependencies", conflicting)
+ if missing:
+ self._raise_missing_reqs(missing)
+ self.req.prepare_metadata()
+ def _prepare_build_backend(self, finder: PackageFinder) -> None:
# Isolate in a BuildEnvironment and install the build-time
# requirements.
pyproject_requires = self.req.pyproject_requires
@@ -60,13 +68,13 @@ class SourceDistribution(AbstractDistribution):
self.req.build_env = BuildEnvironment()
self.req.build_env.install_requirements(
- finder, pyproject_requires, "overlay", "Installing build dependencies"
+ finder, pyproject_requires, "overlay", kind="build dependencies"
)
conflicting, missing = self.req.build_env.check_requirements(
self.req.requirements_to_check
)
if conflicting:
- _raise_conflicts("PEP 517/518 supported requirements", conflicting)
+ self._raise_conflicts("PEP 517/518 supported requirements", conflicting)
if missing:
logger.warning(
"Missing build requirements in pyproject.toml for %s.",
@@ -77,19 +85,66 @@ class SourceDistribution(AbstractDistribution):
"pip cannot fall back to setuptools without %s.",
" and ".join(map(repr, sorted(missing))),
)
- # Install any extra build dependencies that the backend requests.
- # This must be done in a second pass, as the pyproject.toml
- # dependencies must be installed before we can call the backend.
+
+ def _get_build_requires_wheel(self) -> Iterable[str]:
with self.req.build_env:
runner = runner_with_spinner_message("Getting requirements to build wheel")
backend = self.req.pep517_backend
assert backend is not None
with backend.subprocess_runner(runner):
- reqs = backend.get_requires_for_build_wheel()
+ return backend.get_requires_for_build_wheel()
+
+ def _get_build_requires_editable(self) -> Iterable[str]:
+ with self.req.build_env:
+ runner = runner_with_spinner_message(
+ "Getting requirements to build editable"
+ )
+ backend = self.req.pep517_backend
+ assert backend is not None
+ with backend.subprocess_runner(runner):
+ return backend.get_requires_for_build_editable()
- conflicting, missing = self.req.build_env.check_requirements(reqs)
+ def _install_build_reqs(self, finder: PackageFinder) -> None:
+ # Install any extra build dependencies that the backend requests.
+ # This must be done in a second pass, as the pyproject.toml
+ # dependencies must be installed before we can call the backend.
+ if (
+ self.req.editable
+ and self.req.permit_editable_wheels
+ and self.req.supports_pyproject_editable()
+ ):
+ build_reqs = self._get_build_requires_editable()
+ else:
+ build_reqs = self._get_build_requires_wheel()
+ conflicting, missing = self.req.build_env.check_requirements(build_reqs)
if conflicting:
- _raise_conflicts("the backend dependencies", conflicting)
+ self._raise_conflicts("the backend dependencies", conflicting)
self.req.build_env.install_requirements(
- finder, missing, "normal", "Installing backend dependencies"
+ finder, missing, "normal", kind="backend dependencies"
+ )
+
+ def _raise_conflicts(
+ self, conflicting_with: str, conflicting_reqs: Set[Tuple[str, str]]
+ ) -> None:
+ format_string = (
+ "Some build dependencies for {requirement} "
+ "conflict with {conflicting_with}: {description}."
+ )
+ error_message = format_string.format(
+ requirement=self.req,
+ conflicting_with=conflicting_with,
+ description=", ".join(
+ f"{installed} is incompatible with {wanted}"
+ for installed, wanted in sorted(conflicting_reqs)
+ ),
+ )
+ raise InstallationError(error_message)
+
+ def _raise_missing_reqs(self, missing: Set[str]) -> None:
+ format_string = (
+ "Some build dependencies for {requirement} are missing: {missing}."
+ )
+ error_message = format_string.format(
+ requirement=self.req, missing=", ".join(map(repr, sorted(missing)))
)
+ raise InstallationError(error_message)
diff --git a/src/pip/_internal/distributions/wheel.py b/src/pip/_internal/distributions/wheel.py
index 00a70b02d..03aac775b 100644
--- a/src/pip/_internal/distributions/wheel.py
+++ b/src/pip/_internal/distributions/wheel.py
@@ -1,10 +1,12 @@
-from zipfile import ZipFile
-
-from pip._vendor.pkg_resources import Distribution
+from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.distributions.base import AbstractDistribution
from pip._internal.index.package_finder import PackageFinder
-from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel
+from pip._internal.metadata import (
+ BaseDistribution,
+ FilesystemWheel,
+ get_wheel_distribution,
+)
class WheelDistribution(AbstractDistribution):
@@ -13,22 +15,20 @@ class WheelDistribution(AbstractDistribution):
This does not need any preparation as wheels can be directly unpacked.
"""
- def get_pkg_resources_distribution(self) -> Distribution:
+ def get_metadata_distribution(self) -> BaseDistribution:
"""Loads the metadata from the wheel file into memory and returns a
Distribution that uses it, not relying on the wheel file or
requirement.
"""
- # Set as part of preparation during download.
- assert self.req.local_file_path
- # Wheels are never unnamed.
- assert self.req.name
-
- with ZipFile(self.req.local_file_path, allowZip64=True) as z:
- return pkg_resources_distribution_for_wheel(
- z, self.req.name, self.req.local_file_path
- )
+ assert self.req.local_file_path, "Set as part of preparation during download"
+ assert self.req.name, "Wheels are never unnamed"
+ wheel = FilesystemWheel(self.req.local_file_path)
+ return get_wheel_distribution(wheel, canonicalize_name(self.req.name))
def prepare_distribution_metadata(
- self, finder: PackageFinder, build_isolation: bool
+ self,
+ finder: PackageFinder,
+ build_isolation: bool,
+ check_build_deps: bool,
) -> None:
pass
diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py
index 8aacf8120..2ab1f591f 100644
--- a/src/pip/_internal/exceptions.py
+++ b/src/pip/_internal/exceptions.py
@@ -1,22 +1,174 @@
-"""Exceptions used throughout package"""
+"""Exceptions used throughout package.
+
+This module MUST NOT try to import from anything within `pip._internal` to
+operate. This is expected to be importable from any/all files within the
+subpackage and, thus, should not depend on them.
+"""
import configparser
+import re
from itertools import chain, groupby, repeat
-from typing import TYPE_CHECKING, Dict, List, Optional
+from typing import TYPE_CHECKING, Dict, List, Optional, Union
-from pip._vendor.pkg_resources import Distribution
from pip._vendor.requests.models import Request, Response
+from pip._vendor.rich.console import Console, ConsoleOptions, RenderResult
+from pip._vendor.rich.markup import escape
+from pip._vendor.rich.text import Text
if TYPE_CHECKING:
from hashlib import _Hash
+ from typing import Literal
+ from pip._internal.metadata import BaseDistribution
from pip._internal.req.req_install import InstallRequirement
+#
+# Scaffolding
+#
+def _is_kebab_case(s: str) -> bool:
+ return re.match(r"^[a-z]+(-[a-z]+)*$", s) is not None
+
+
+def _prefix_with_indent(
+ s: Union[Text, str],
+ console: Console,
+ *,
+ prefix: str,
+ indent: str,
+) -> Text:
+ if isinstance(s, Text):
+ text = s
+ else:
+ text = console.render_str(s)
+
+ return console.render_str(prefix, overflow="ignore") + console.render_str(
+ f"\n{indent}", overflow="ignore"
+ ).join(text.split(allow_blank=True))
+
+
class PipError(Exception):
- """Base pip exception"""
+ """The base pip error."""
+
+
+class DiagnosticPipError(PipError):
+ """An error, that presents diagnostic information to the user.
+
+ This contains a bunch of logic, to enable pretty presentation of our error
+ messages. Each error gets a unique reference. Each error can also include
+ additional context, a hint and/or a note -- which are presented with the
+ main error message in a consistent style.
+
+ This is adapted from the error output styling in `sphinx-theme-builder`.
+ """
+
+ reference: str
+
+ def __init__(
+ self,
+ *,
+ kind: 'Literal["error", "warning"]' = "error",
+ reference: Optional[str] = None,
+ message: Union[str, Text],
+ context: Optional[Union[str, Text]],
+ hint_stmt: Optional[Union[str, Text]],
+ note_stmt: Optional[Union[str, Text]] = None,
+ link: Optional[str] = None,
+ ) -> None:
+ # Ensure a proper reference is provided.
+ if reference is None:
+ assert hasattr(self, "reference"), "error reference not provided!"
+ reference = self.reference
+ assert _is_kebab_case(reference), "error reference must be kebab-case!"
+
+ self.kind = kind
+ self.reference = reference
+
+ self.message = message
+ self.context = context
+
+ self.note_stmt = note_stmt
+ self.hint_stmt = hint_stmt
+
+ self.link = link
+
+ super().__init__(f"<{self.__class__.__name__}: {self.reference}>")
+
+ def __repr__(self) -> str:
+ return (
+ f"<{self.__class__.__name__}("
+ f"reference={self.reference!r}, "
+ f"message={self.message!r}, "
+ f"context={self.context!r}, "
+ f"note_stmt={self.note_stmt!r}, "
+ f"hint_stmt={self.hint_stmt!r}"
+ ")>"
+ )
+
+ def __rich_console__(
+ self,
+ console: Console,
+ options: ConsoleOptions,
+ ) -> RenderResult:
+ colour = "red" if self.kind == "error" else "yellow"
+
+ yield f"[{colour} bold]{self.kind}[/]: [bold]{self.reference}[/]"
+ yield ""
+
+ if not options.ascii_only:
+ # Present the main message, with relevant context indented.
+ if self.context is not None:
+ yield _prefix_with_indent(
+ self.message,
+ console,
+ prefix=f"[{colour}]×[/] ",
+ indent=f"[{colour}]│[/] ",
+ )
+ yield _prefix_with_indent(
+ self.context,
+ console,
+ prefix=f"[{colour}]╰─>[/] ",
+ indent=f"[{colour}] [/] ",
+ )
+ else:
+ yield _prefix_with_indent(
+ self.message,
+ console,
+ prefix="[red]×[/] ",
+ indent=" ",
+ )
+ else:
+ yield self.message
+ if self.context is not None:
+ yield ""
+ yield self.context
+
+ if self.note_stmt is not None or self.hint_stmt is not None:
+ yield ""
+
+ if self.note_stmt is not None:
+ yield _prefix_with_indent(
+ self.note_stmt,
+ console,
+ prefix="[magenta bold]note[/]: ",
+ indent=" ",
+ )
+ if self.hint_stmt is not None:
+ yield _prefix_with_indent(
+ self.hint_stmt,
+ console,
+ prefix="[cyan bold]hint[/]: ",
+ indent=" ",
+ )
+
+ if self.link is not None:
+ yield ""
+ yield f"Link: {self.link}"
+#
+# Actual Errors
+#
class ConfigurationError(PipError):
"""General exception in configuration"""
@@ -29,17 +181,54 @@ class UninstallationError(PipError):
"""General exception during uninstallation"""
+class MissingPyProjectBuildRequires(DiagnosticPipError):
+ """Raised when pyproject.toml has `build-system`, but no `build-system.requires`."""
+
+ reference = "missing-pyproject-build-system-requires"
+
+ def __init__(self, *, package: str) -> None:
+ super().__init__(
+ message=f"Can not process {escape(package)}",
+ context=Text(
+ "This package has an invalid pyproject.toml file.\n"
+ "The [build-system] table is missing the mandatory `requires` key."
+ ),
+ note_stmt="This is an issue with the package mentioned above, not pip.",
+ hint_stmt=Text("See PEP 518 for the detailed specification."),
+ )
+
+
+class InvalidPyProjectBuildRequires(DiagnosticPipError):
+ """Raised when pyproject.toml an invalid `build-system.requires`."""
+
+ reference = "invalid-pyproject-build-system-requires"
+
+ def __init__(self, *, package: str, reason: str) -> None:
+ super().__init__(
+ message=f"Can not process {escape(package)}",
+ context=Text(
+ "This package has an invalid `build-system.requires` key in "
+ f"pyproject.toml.\n{reason}"
+ ),
+ note_stmt="This is an issue with the package mentioned above, not pip.",
+ hint_stmt=Text("See PEP 518 for the detailed specification."),
+ )
+
+
class NoneMetadataError(PipError):
- """
- Raised when accessing "METADATA" or "PKG-INFO" metadata for a
- pip._vendor.pkg_resources.Distribution object and
- `dist.has_metadata('METADATA')` returns True but
- `dist.get_metadata('METADATA')` returns None (and similarly for
- "PKG-INFO").
+ """Raised when accessing a Distribution's "METADATA" or "PKG-INFO".
+
+ This signifies an inconsistency, when the Distribution claims to have
+ the metadata file (if not, raise ``FileNotFoundError`` instead), but is
+ not actually able to produce its content. This may be due to permission
+ errors.
"""
- def __init__(self, dist, metadata_name):
- # type: (Distribution, str) -> None
+ def __init__(
+ self,
+ dist: "BaseDistribution",
+ metadata_name: str,
+ ) -> None:
"""
:param dist: A Distribution object.
:param metadata_name: The name of the metadata being accessed
@@ -48,28 +237,24 @@ class NoneMetadataError(PipError):
self.dist = dist
self.metadata_name = metadata_name
- def __str__(self):
- # type: () -> str
+ def __str__(self) -> str:
# Use `dist` in the error message because its stringification
# includes more information, like the version and location.
- return (
- 'None {} metadata found for distribution: {}'.format(
- self.metadata_name, self.dist,
- )
+ return "None {} metadata found for distribution: {}".format(
+ self.metadata_name,
+ self.dist,
)
class UserInstallationInvalid(InstallationError):
"""A --user install is requested on an environment without user site."""
- def __str__(self):
- # type: () -> str
+ def __str__(self) -> str:
return "User base directory is not specified"
class InvalidSchemeCombination(InstallationError):
- def __str__(self):
- # type: () -> str
+ def __str__(self) -> str:
before = ", ".join(str(a) for a in self.args[:-1])
return f"Cannot set {before} and {self.args[-1]} together"
@@ -102,8 +287,12 @@ class PreviousBuildDirError(PipError):
class NetworkConnectionError(PipError):
"""HTTP connection error"""
- def __init__(self, error_msg, response=None, request=None):
- # type: (str, Response, Request) -> None
+ def __init__(
+ self,
+ error_msg: str,
+ response: Optional[Response] = None,
+ request: Optional[Request] = None,
+ ) -> None:
"""
Initialize NetworkConnectionError with `request` and `response`
objects.
@@ -111,13 +300,15 @@ class NetworkConnectionError(PipError):
self.response = response
self.request = request
self.error_msg = error_msg
- if (self.response is not None and not self.request and
- hasattr(response, 'request')):
+ if (
+ self.response is not None
+ and not self.request
+ and hasattr(response, "request")
+ ):
self.request = self.response.request
super().__init__(error_msg, response, request)
- def __str__(self):
- # type: () -> str
+ def __str__(self) -> str:
return str(self.error_msg)
@@ -129,74 +320,136 @@ class UnsupportedWheel(InstallationError):
"""Unsupported wheel."""
+class InvalidWheel(InstallationError):
+ """Invalid (e.g. corrupt) wheel."""
+
+ def __init__(self, location: str, name: str):
+ self.location = location
+ self.name = name
+
+ def __str__(self) -> str:
+ return f"Wheel '{self.name}' located at {self.location} is invalid."
+
+
class MetadataInconsistent(InstallationError):
"""Built metadata contains inconsistent information.
This is raised when the metadata contains values (e.g. name and version)
- that do not match the information previously obtained from sdist filename
- or user-supplied ``#egg=`` value.
+ that do not match the information previously obtained from sdist filename,
+ user-supplied ``#egg=`` value, or an install requirement name.
"""
- def __init__(self, ireq, field, f_val, m_val):
- # type: (InstallRequirement, str, str, str) -> None
+
+ def __init__(
+ self, ireq: "InstallRequirement", field: str, f_val: str, m_val: str
+ ) -> None:
self.ireq = ireq
self.field = field
self.f_val = f_val
self.m_val = m_val
- def __str__(self):
- # type: () -> str
- template = (
- "Requested {} has inconsistent {}: "
- "filename has {!r}, but metadata has {!r}"
+ def __str__(self) -> str:
+ return (
+ f"Requested {self.ireq} has inconsistent {self.field}: "
+ f"expected {self.f_val!r}, but metadata has {self.m_val!r}"
)
- return template.format(self.ireq, self.field, self.f_val, self.m_val)
-class InstallationSubprocessError(InstallationError):
- """A subprocess call failed during installation."""
- def __init__(self, returncode, description):
- # type: (int, str) -> None
- self.returncode = returncode
- self.description = description
+class LegacyInstallFailure(DiagnosticPipError):
+ """Error occurred while executing `setup.py install`"""
- def __str__(self):
- # type: () -> str
- return (
- "Command errored out with exit status {}: {} "
- "Check the logs for full command output."
- ).format(self.returncode, self.description)
+ reference = "legacy-install-failure"
+
+ def __init__(self, package_details: str) -> None:
+ super().__init__(
+ message="Encountered error while trying to install package.",
+ context=package_details,
+ hint_stmt="See above for output from the failure.",
+ note_stmt="This is an issue with the package mentioned above, not pip.",
+ )
+
+
+class InstallationSubprocessError(DiagnosticPipError, InstallationError):
+ """A subprocess call failed."""
+
+ reference = "subprocess-exited-with-error"
+
+ def __init__(
+ self,
+ *,
+ command_description: str,
+ exit_code: int,
+ output_lines: Optional[List[str]],
+ ) -> None:
+ if output_lines is None:
+ output_prompt = Text("See above for output.")
+ else:
+ output_prompt = (
+ Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n")
+ + Text("".join(output_lines))
+ + Text.from_markup(R"[red]\[end of output][/]")
+ )
+
+ super().__init__(
+ message=(
+ f"[green]{escape(command_description)}[/] did not run successfully.\n"
+ f"exit code: {exit_code}"
+ ),
+ context=output_prompt,
+ hint_stmt=None,
+ note_stmt=(
+ "This error originates from a subprocess, and is likely not a "
+ "problem with pip."
+ ),
+ )
+
+ self.command_description = command_description
+ self.exit_code = exit_code
+
+ def __str__(self) -> str:
+ return f"{self.command_description} exited with {self.exit_code}"
+
+
+class MetadataGenerationFailed(InstallationSubprocessError, InstallationError):
+ reference = "metadata-generation-failed"
+
+ def __init__(
+ self,
+ *,
+ package_details: str,
+ ) -> None:
+ super(InstallationSubprocessError, self).__init__(
+ message="Encountered error while generating package metadata.",
+ context=escape(package_details),
+ hint_stmt="See above for details.",
+ note_stmt="This is an issue with the package mentioned above, not pip.",
+ )
+
+ def __str__(self) -> str:
+ return "metadata generation failed"
class HashErrors(InstallationError):
"""Multiple HashError instances rolled into one for reporting"""
- def __init__(self):
- # type: () -> None
- self.errors = [] # type: List[HashError]
+ def __init__(self) -> None:
+ self.errors: List["HashError"] = []
- def append(self, error):
- # type: (HashError) -> None
+ def append(self, error: "HashError") -> None:
self.errors.append(error)
- def __str__(self):
- # type: () -> str
+ def __str__(self) -> str:
lines = []
self.errors.sort(key=lambda e: e.order)
for cls, errors_of_cls in groupby(self.errors, lambda e: e.__class__):
lines.append(cls.head)
lines.extend(e.body() for e in errors_of_cls)
if lines:
- return '\n'.join(lines)
- return ''
+ return "\n".join(lines)
+ return ""
- def __nonzero__(self):
- # type: () -> bool
+ def __bool__(self) -> bool:
return bool(self.errors)
- def __bool__(self):
- # type: () -> bool
- return self.__nonzero__()
-
class HashError(InstallationError):
"""
@@ -214,12 +467,12 @@ class HashError(InstallationError):
typically available earlier.
"""
- req = None # type: Optional[InstallRequirement]
- head = ''
- order = -1 # type: int
- def body(self):
- # type: () -> str
+ req: Optional["InstallRequirement"] = None
+ head = ""
+ order: int = -1
+
+ def body(self) -> str:
"""Return a summary of me for display under the heading.
This default implementation simply prints a description of the
@@ -229,21 +482,19 @@ class HashError(InstallationError):
its link already populated by the resolver's _populate_link().
"""
- return f' {self._requirement_name()}'
+ return f" {self._requirement_name()}"
- def __str__(self):
- # type: () -> str
- return f'{self.head}\n{self.body()}'
+ def __str__(self) -> str:
+ return f"{self.head}\n{self.body()}"
- def _requirement_name(self):
- # type: () -> str
+ def _requirement_name(self) -> str:
"""Return a description of the requirement that triggered me.
This default implementation returns long description of the req, with
line numbers
"""
- return str(self.req) if self.req else 'unknown package'
+ return str(self.req) if self.req else "unknown package"
class VcsHashUnsupported(HashError):
@@ -251,8 +502,10 @@ class VcsHashUnsupported(HashError):
we don't have a method for hashing those."""
order = 0
- head = ("Can't verify hashes for these requirements because we don't "
- "have a way to hash version control repositories:")
+ head = (
+ "Can't verify hashes for these requirements because we don't "
+ "have a way to hash version control repositories:"
+ )
class DirectoryUrlHashUnsupported(HashError):
@@ -260,32 +513,34 @@ class DirectoryUrlHashUnsupported(HashError):
we don't have a method for hashing those."""
order = 1
- head = ("Can't verify hashes for these file:// requirements because they "
- "point to directories:")
+ head = (
+ "Can't verify hashes for these file:// requirements because they "
+ "point to directories:"
+ )
class HashMissing(HashError):
"""A hash was needed for a requirement but is absent."""
order = 2
- head = ('Hashes are required in --require-hashes mode, but they are '
- 'missing from some requirements. Here is a list of those '
- 'requirements along with the hashes their downloaded archives '
- 'actually had. Add lines like these to your requirements files to '
- 'prevent tampering. (If you did not enable --require-hashes '
- 'manually, note that it turns on automatically when any package '
- 'has a hash.)')
-
- def __init__(self, gotten_hash):
- # type: (str) -> None
+ head = (
+ "Hashes are required in --require-hashes mode, but they are "
+ "missing from some requirements. Here is a list of those "
+ "requirements along with the hashes their downloaded archives "
+ "actually had. Add lines like these to your requirements files to "
+ "prevent tampering. (If you did not enable --require-hashes "
+ "manually, note that it turns on automatically when any package "
+ "has a hash.)"
+ )
+
+ def __init__(self, gotten_hash: str) -> None:
"""
:param gotten_hash: The hash of the (possibly malicious) archive we
just downloaded
"""
self.gotten_hash = gotten_hash
- def body(self):
- # type: () -> str
+ def body(self) -> str:
# Dodge circular import.
from pip._internal.utils.hashes import FAVORITE_HASH
@@ -294,13 +549,16 @@ class HashMissing(HashError):
# In the case of URL-based requirements, display the original URL
# seen in the requirements file rather than the package name,
# so the output can be directly copied into the requirements file.
- package = (self.req.original_link if self.req.original_link
- # In case someone feeds something downright stupid
- # to InstallRequirement's constructor.
- else getattr(self.req, 'req', None))
- return ' {} --hash={}:{}'.format(package or 'unknown package',
- FAVORITE_HASH,
- self.gotten_hash)
+ package = (
+ self.req.original_link
+ if self.req.original_link
+ # In case someone feeds something downright stupid
+ # to InstallRequirement's constructor.
+ else getattr(self.req, "req", None)
+ )
+ return " {} --hash={}:{}".format(
+ package or "unknown package", FAVORITE_HASH, self.gotten_hash
+ )
class HashUnpinned(HashError):
@@ -308,8 +566,10 @@ class HashUnpinned(HashError):
version."""
order = 3
- head = ('In --require-hashes mode, all requirements must have their '
- 'versions pinned with ==. These do not:')
+ head = (
+ "In --require-hashes mode, all requirements must have their "
+ "versions pinned with ==. These do not:"
+ )
class HashMismatch(HashError):
@@ -321,14 +581,16 @@ class HashMismatch(HashError):
improve its error message.
"""
- order = 4
- head = ('THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS '
- 'FILE. If you have updated the package versions, please update '
- 'the hashes. Otherwise, examine the package contents carefully; '
- 'someone may have tampered with them.')
- def __init__(self, allowed, gots):
- # type: (Dict[str, List[str]], Dict[str, _Hash]) -> None
+ order = 4
+ head = (
+ "THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS "
+ "FILE. If you have updated the package versions, please update "
+ "the hashes. Otherwise, examine the package contents carefully; "
+ "someone may have tampered with them."
+ )
+
+ def __init__(self, allowed: Dict[str, List[str]], gots: Dict[str, "_Hash"]) -> None:
"""
:param allowed: A dict of algorithm names pointing to lists of allowed
hex digests
@@ -338,13 +600,10 @@ class HashMismatch(HashError):
self.allowed = allowed
self.gots = gots
- def body(self):
- # type: () -> str
- return ' {}:\n{}'.format(self._requirement_name(),
- self._hash_comparison())
+ def body(self) -> str:
+ return " {}:\n{}".format(self._requirement_name(), self._hash_comparison())
- def _hash_comparison(self):
- # type: () -> str
+ def _hash_comparison(self) -> str:
"""
Return a comparison of actual and expected hash values.
@@ -355,20 +614,22 @@ class HashMismatch(HashError):
Got bcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdefbcdef
"""
- def hash_then_or(hash_name):
- # type: (str) -> chain[str]
+
+ def hash_then_or(hash_name: str) -> "chain[str]":
# For now, all the decent hashes have 6-char names, so we can get
# away with hard-coding space literals.
- return chain([hash_name], repeat(' or'))
+ return chain([hash_name], repeat(" or"))
- lines = [] # type: List[str]
+ lines: List[str] = []
for hash_name, expecteds in self.allowed.items():
prefix = hash_then_or(hash_name)
- lines.extend((' Expected {} {}'.format(next(prefix), e))
- for e in expecteds)
- lines.append(' Got {}\n'.format(
- self.gots[hash_name].hexdigest()))
- return '\n'.join(lines)
+ lines.extend(
+ (" Expected {} {}".format(next(prefix), e)) for e in expecteds
+ )
+ lines.append(
+ " Got {}\n".format(self.gots[hash_name].hexdigest())
+ )
+ return "\n".join(lines)
class UnsupportedPythonVersion(InstallationError):
@@ -377,18 +638,20 @@ class UnsupportedPythonVersion(InstallationError):
class ConfigurationFileCouldNotBeLoaded(ConfigurationError):
- """When there are errors while loading a configuration file
- """
-
- def __init__(self, reason="could not be loaded", fname=None, error=None):
- # type: (str, Optional[str], Optional[configparser.Error]) -> None
+ """When there are errors while loading a configuration file"""
+
+ def __init__(
+ self,
+ reason: str = "could not be loaded",
+ fname: Optional[str] = None,
+ error: Optional[configparser.Error] = None,
+ ) -> None:
super().__init__(error)
self.reason = reason
self.fname = fname
self.error = error
- def __str__(self):
- # type: () -> str
+ def __str__(self) -> str:
if self.fname is not None:
message_part = f" in {self.fname}."
else:
diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py
index 14d745eef..0120610c7 100644
--- a/src/pip/_internal/index/collector.py
+++ b/src/pip/_internal/index/collector.py
@@ -2,30 +2,32 @@
The main purpose of this module is to expose LinkCollector.collect_sources().
"""
-import cgi
import collections
+import email.message
import functools
-import html
import itertools
+import json
import logging
import os
-import re
import urllib.parse
import urllib.request
-import xml.etree.ElementTree
+from html.parser import HTMLParser
from optparse import Values
from typing import (
+ TYPE_CHECKING,
Callable,
+ Dict,
Iterable,
List,
MutableMapping,
NamedTuple,
Optional,
Sequence,
+ Tuple,
Union,
)
-from pip._vendor import html5lib, requests
+from pip._vendor import requests
from pip._vendor.requests import Response
from pip._vendor.requests.exceptions import RetryError, SSLError
@@ -35,14 +37,18 @@ from pip._internal.models.search_scope import SearchScope
from pip._internal.network.session import PipSession
from pip._internal.network.utils import raise_for_status
from pip._internal.utils.filetypes import is_archive_file
-from pip._internal.utils.misc import pairwise, redact_auth_from_url
+from pip._internal.utils.misc import redact_auth_from_url
from pip._internal.vcs import vcs
from .sources import CandidatesFromPage, LinkSource, build_source
+if TYPE_CHECKING:
+ from typing import Protocol
+else:
+ Protocol = object
+
logger = logging.getLogger(__name__)
-HTMLElement = xml.etree.ElementTree.Element
ResponseHeaders = MutableMapping[str, str]
@@ -52,70 +58,90 @@ def _match_vcs_scheme(url: str) -> Optional[str]:
Returns the matched VCS scheme, or None if there's no match.
"""
for scheme in vcs.schemes:
- if url.lower().startswith(scheme) and url[len(scheme)] in '+:':
+ if url.lower().startswith(scheme) and url[len(scheme)] in "+:":
return scheme
return None
-class _NotHTML(Exception):
+class _NotAPIContent(Exception):
def __init__(self, content_type: str, request_desc: str) -> None:
super().__init__(content_type, request_desc)
self.content_type = content_type
self.request_desc = request_desc
-def _ensure_html_header(response: Response) -> None:
- """Check the Content-Type header to ensure the response contains HTML.
+def _ensure_api_header(response: Response) -> None:
+ """
+ Check the Content-Type header to ensure the response contains a Simple
+ API Response.
- Raises `_NotHTML` if the content type is not text/html.
+ Raises `_NotAPIContent` if the content type is not a valid content-type.
"""
- content_type = response.headers.get("Content-Type", "")
- if not content_type.lower().startswith("text/html"):
- raise _NotHTML(content_type, response.request.method)
+ content_type = response.headers.get("Content-Type", "Unknown")
+
+ content_type_l = content_type.lower()
+ if content_type_l.startswith(
+ (
+ "text/html",
+ "application/vnd.pypi.simple.v1+html",
+ "application/vnd.pypi.simple.v1+json",
+ )
+ ):
+ return
+
+ raise _NotAPIContent(content_type, response.request.method)
class _NotHTTP(Exception):
pass
-def _ensure_html_response(url: str, session: PipSession) -> None:
- """Send a HEAD request to the URL, and ensure the response contains HTML.
+def _ensure_api_response(url: str, session: PipSession) -> None:
+ """
+ Send a HEAD request to the URL, and ensure the response contains a simple
+ API Response.
Raises `_NotHTTP` if the URL is not available for a HEAD request, or
- `_NotHTML` if the content type is not text/html.
+ `_NotAPIContent` if the content type is not a valid content type.
"""
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
- if scheme not in {'http', 'https'}:
+ if scheme not in {"http", "https"}:
raise _NotHTTP()
resp = session.head(url, allow_redirects=True)
raise_for_status(resp)
- _ensure_html_header(resp)
+ _ensure_api_header(resp)
-def _get_html_response(url: str, session: PipSession) -> Response:
- """Access an HTML page with GET, and return the response.
+def _get_simple_response(url: str, session: PipSession) -> Response:
+ """Access an Simple API response with GET, and return the response.
This consists of three parts:
1. If the URL looks suspiciously like an archive, send a HEAD first to
- check the Content-Type is HTML, to avoid downloading a large file.
- Raise `_NotHTTP` if the content type cannot be determined, or
- `_NotHTML` if it is not HTML.
+ check the Content-Type is HTML or Simple API, to avoid downloading a
+ large file. Raise `_NotHTTP` if the content type cannot be determined, or
+ `_NotAPIContent` if it is not HTML or a Simple API.
2. Actually perform the request. Raise HTTP exceptions on network failures.
- 3. Check the Content-Type header to make sure we got HTML, and raise
- `_NotHTML` otherwise.
+ 3. Check the Content-Type header to make sure we got a Simple API response,
+ and raise `_NotAPIContent` otherwise.
"""
if is_archive_file(Link(url).filename):
- _ensure_html_response(url, session=session)
+ _ensure_api_response(url, session=session)
- logger.debug('Getting page %s', redact_auth_from_url(url))
+ logger.debug("Getting page %s", redact_auth_from_url(url))
resp = session.get(
url,
headers={
- "Accept": "text/html",
+ "Accept": ", ".join(
+ [
+ "application/vnd.pypi.simple.v1+json",
+ "application/vnd.pypi.simple.v1+html; q=0.1",
+ "text/html; q=0.01",
+ ]
+ ),
# We don't want to blindly returned cached data for
# /simple/, because authors generally expecting that
# twine upload && pip install will function, but if
@@ -137,153 +163,52 @@ def _get_html_response(url: str, session: PipSession) -> Response:
# The check for archives above only works if the url ends with
# something that looks like an archive. However that is not a
# requirement of an url. Unless we issue a HEAD request on every
- # url we cannot know ahead of time for sure if something is HTML
- # or not. However we can check after we've downloaded it.
- _ensure_html_header(resp)
+ # url we cannot know ahead of time for sure if something is a
+ # Simple API response or not. However we can check after we've
+ # downloaded it.
+ _ensure_api_header(resp)
+
+ logger.debug(
+ "Fetched page %s as %s",
+ redact_auth_from_url(url),
+ resp.headers.get("Content-Type", "Unknown"),
+ )
return resp
def _get_encoding_from_headers(headers: ResponseHeaders) -> Optional[str]:
- """Determine if we have any encoding information in our headers.
- """
+ """Determine if we have any encoding information in our headers."""
if headers and "Content-Type" in headers:
- content_type, params = cgi.parse_header(headers["Content-Type"])
- if "charset" in params:
- return params['charset']
+ m = email.message.Message()
+ m["content-type"] = headers["Content-Type"]
+ charset = m.get_param("charset")
+ if charset:
+ return str(charset)
return None
-def _determine_base_url(document: HTMLElement, page_url: str) -> str:
- """Determine the HTML document's base URL.
-
- This looks for a ``<base>`` tag in the HTML document. If present, its href
- attribute denotes the base URL of anchor tags in the document. If there is
- no such tag (or if it does not have a valid href attribute), the HTML
- file's URL is used as the base URL.
-
- :param document: An HTML document representation. The current
- implementation expects the result of ``html5lib.parse()``.
- :param page_url: The URL of the HTML document.
- """
- for base in document.findall(".//base"):
- href = base.get("href")
- if href is not None:
- return href
- return page_url
-
-
-def _clean_url_path_part(part: str) -> str:
- """
- Clean a "part" of a URL path (i.e. after splitting on "@" characters).
- """
- # We unquote prior to quoting to make sure nothing is double quoted.
- return urllib.parse.quote(urllib.parse.unquote(part))
-
-
-def _clean_file_url_path(part: str) -> str:
- """
- Clean the first part of a URL path that corresponds to a local
- filesystem path (i.e. the first part after splitting on "@" characters).
- """
- # We unquote prior to quoting to make sure nothing is double quoted.
- # Also, on Windows the path part might contain a drive letter which
- # should not be quoted. On Linux where drive letters do not
- # exist, the colon should be quoted. We rely on urllib.request
- # to do the right thing here.
- return urllib.request.pathname2url(urllib.request.url2pathname(part))
-
-
-# percent-encoded: /
-_reserved_chars_re = re.compile('(@|%2F)', re.IGNORECASE)
-
-
-def _clean_url_path(path: str, is_local_path: bool) -> str:
- """
- Clean the path portion of a URL.
- """
- if is_local_path:
- clean_func = _clean_file_url_path
- else:
- clean_func = _clean_url_path_part
-
- # Split on the reserved characters prior to cleaning so that
- # revision strings in VCS URLs are properly preserved.
- parts = _reserved_chars_re.split(path)
-
- cleaned_parts = []
- for to_clean, reserved in pairwise(itertools.chain(parts, [''])):
- cleaned_parts.append(clean_func(to_clean))
- # Normalize %xx escapes (e.g. %2f -> %2F)
- cleaned_parts.append(reserved.upper())
-
- return ''.join(cleaned_parts)
-
-
-def _clean_link(url: str) -> str:
- """
- Make sure a link is fully quoted.
- For example, if ' ' occurs in the URL, it will be replaced with "%20",
- and without double-quoting other characters.
- """
- # Split the URL into parts according to the general structure
- # `scheme://netloc/path;parameters?query#fragment`.
- result = urllib.parse.urlparse(url)
- # If the netloc is empty, then the URL refers to a local filesystem path.
- is_local_path = not result.netloc
- path = _clean_url_path(result.path, is_local_path=is_local_path)
- return urllib.parse.urlunparse(result._replace(path=path))
-
-
-def _create_link_from_element(
- anchor: HTMLElement,
- page_url: str,
- base_url: str,
-) -> Optional[Link]:
- """
- Convert an anchor element in a simple repository page to a Link.
- """
- href = anchor.get("href")
- if not href:
- return None
-
- url = _clean_link(urllib.parse.urljoin(base_url, href))
- pyrequire = anchor.get('data-requires-python')
- pyrequire = html.unescape(pyrequire) if pyrequire else None
-
- yanked_reason = anchor.get('data-yanked')
- if yanked_reason:
- yanked_reason = html.unescape(yanked_reason)
-
- link = Link(
- url,
- comes_from=page_url,
- requires_python=pyrequire,
- yanked_reason=yanked_reason,
- )
-
- return link
-
-
class CacheablePageContent:
- def __init__(self, page: "HTMLPage") -> None:
+ def __init__(self, page: "IndexContent") -> None:
assert page.cache_link_parsing
self.page = page
def __eq__(self, other: object) -> bool:
- return (isinstance(other, type(self)) and
- self.page.url == other.page.url)
+ return isinstance(other, type(self)) and self.page.url == other.page.url
def __hash__(self) -> int:
return hash(self.page.url)
-def with_cached_html_pages(
- fn: Callable[["HTMLPage"], Iterable[Link]],
-) -> Callable[["HTMLPage"], List[Link]]:
+class ParseLinks(Protocol):
+ def __call__(self, page: "IndexContent") -> Iterable[Link]:
+ ...
+
+
+def with_cached_index_content(fn: ParseLinks) -> ParseLinks:
"""
- Given a function that parses an Iterable[Link] from an HTMLPage, cache the
- function's result (keyed by CacheablePageContent), unless the HTMLPage
+ Given a function that parses an Iterable[Link] from an IndexContent, cache the
+ function's result (keyed by CacheablePageContent), unless the IndexContent
`page` has `page.cache_link_parsing == False`.
"""
@@ -292,7 +217,7 @@ def with_cached_html_pages(
return list(fn(cacheable_page.page))
@functools.wraps(fn)
- def wrapper_wrapper(page: "HTMLPage") -> List[Link]:
+ def wrapper_wrapper(page: "IndexContent") -> List[Link]:
if page.cache_link_parsing:
return wrapper(CacheablePageContent(page))
return list(fn(page))
@@ -300,36 +225,42 @@ def with_cached_html_pages(
return wrapper_wrapper
-@with_cached_html_pages
-def parse_links(page: "HTMLPage") -> Iterable[Link]:
+@with_cached_index_content
+def parse_links(page: "IndexContent") -> Iterable[Link]:
"""
- Parse an HTML document, and yield its anchor elements as Link objects.
+ Parse a Simple API's Index Content, and yield its anchor elements as Link objects.
"""
- document = html5lib.parse(
- page.content,
- transport_encoding=page.encoding,
- namespaceHTMLElements=False,
- )
+
+ content_type_l = page.content_type.lower()
+ if content_type_l.startswith("application/vnd.pypi.simple.v1+json"):
+ data = json.loads(page.content)
+ for file in data.get("files", []):
+ link = Link.from_json(file, page.url)
+ if link is None:
+ continue
+ yield link
+ return
+
+ parser = HTMLLinkParser(page.url)
+ encoding = page.encoding or "utf-8"
+ parser.feed(page.content.decode(encoding))
url = page.url
- base_url = _determine_base_url(document, url)
- for anchor in document.findall(".//a"):
- link = _create_link_from_element(
- anchor,
- page_url=url,
- base_url=base_url,
- )
+ base_url = parser.base_url or url
+ for anchor in parser.anchors:
+ link = Link.from_element(anchor, page_url=url, base_url=base_url)
if link is None:
continue
yield link
-class HTMLPage:
- """Represents one page, along with its URL"""
+class IndexContent:
+ """Represents one response (or page), along with its URL"""
def __init__(
self,
content: bytes,
+ content_type: str,
encoding: Optional[str],
url: str,
cache_link_parsing: bool = True,
@@ -342,6 +273,7 @@ class HTMLPage:
have this set to False, for example.
"""
self.content = content
+ self.content_type = content_type
self.encoding = encoding
self.url = url
self.cache_link_parsing = cache_link_parsing
@@ -350,80 +282,115 @@ class HTMLPage:
return redact_auth_from_url(self.url)
-def _handle_get_page_fail(
+class HTMLLinkParser(HTMLParser):
+ """
+ HTMLParser that keeps the first base HREF and a list of all anchor
+ elements' attributes.
+ """
+
+ def __init__(self, url: str) -> None:
+ super().__init__(convert_charrefs=True)
+
+ self.url: str = url
+ self.base_url: Optional[str] = None
+ self.anchors: List[Dict[str, Optional[str]]] = []
+
+ def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None:
+ if tag == "base" and self.base_url is None:
+ href = self.get_href(attrs)
+ if href is not None:
+ self.base_url = href
+ elif tag == "a":
+ self.anchors.append(dict(attrs))
+
+ def get_href(self, attrs: List[Tuple[str, Optional[str]]]) -> Optional[str]:
+ for name, value in attrs:
+ if name == "href":
+ return value
+ return None
+
+
+def _handle_get_simple_fail(
link: Link,
reason: Union[str, Exception],
- meth: Optional[Callable[..., None]] = None
+ meth: Optional[Callable[..., None]] = None,
) -> None:
if meth is None:
meth = logger.debug
meth("Could not fetch URL %s: %s - skipping", link, reason)
-def _make_html_page(response: Response, cache_link_parsing: bool = True) -> HTMLPage:
+def _make_index_content(
+ response: Response, cache_link_parsing: bool = True
+) -> IndexContent:
encoding = _get_encoding_from_headers(response.headers)
- return HTMLPage(
+ return IndexContent(
response.content,
+ response.headers["Content-Type"],
encoding=encoding,
url=response.url,
- cache_link_parsing=cache_link_parsing)
-
+ cache_link_parsing=cache_link_parsing,
+ )
-def _get_html_page(
- link: Link, session: Optional[PipSession] = None
-) -> Optional["HTMLPage"]:
- if session is None:
- raise TypeError(
- "_get_html_page() missing 1 required keyword argument: 'session'"
- )
- url = link.url.split('#', 1)[0]
+def _get_index_content(link: Link, *, session: PipSession) -> Optional["IndexContent"]:
+ url = link.url.split("#", 1)[0]
# Check for VCS schemes that do not support lookup as web pages.
vcs_scheme = _match_vcs_scheme(url)
if vcs_scheme:
- logger.warning('Cannot look at %s URL %s because it does not support '
- 'lookup as web pages.', vcs_scheme, link)
+ logger.warning(
+ "Cannot look at %s URL %s because it does not support lookup as web pages.",
+ vcs_scheme,
+ link,
+ )
return None
# Tack index.html onto file:// URLs that point to directories
scheme, _, path, _, _, _ = urllib.parse.urlparse(url)
- if (scheme == 'file' and os.path.isdir(urllib.request.url2pathname(path))):
+ if scheme == "file" and os.path.isdir(urllib.request.url2pathname(path)):
# add trailing slash if not present so urljoin doesn't trim
# final segment
- if not url.endswith('/'):
- url += '/'
- url = urllib.parse.urljoin(url, 'index.html')
- logger.debug(' file: URL is directory, getting %s', url)
+ if not url.endswith("/"):
+ url += "/"
+ # TODO: In the future, it would be nice if pip supported PEP 691
+ # style respones in the file:// URLs, however there's no
+ # standard file extension for application/vnd.pypi.simple.v1+json
+ # so we'll need to come up with something on our own.
+ url = urllib.parse.urljoin(url, "index.html")
+ logger.debug(" file: URL is directory, getting %s", url)
try:
- resp = _get_html_response(url, session=session)
+ resp = _get_simple_response(url, session=session)
except _NotHTTP:
logger.warning(
- 'Skipping page %s because it looks like an archive, and cannot '
- 'be checked by a HTTP HEAD request.', link,
+ "Skipping page %s because it looks like an archive, and cannot "
+ "be checked by a HTTP HEAD request.",
+ link,
)
- except _NotHTML as exc:
+ except _NotAPIContent as exc:
logger.warning(
- 'Skipping page %s because the %s request got Content-Type: %s.'
- 'The only supported Content-Type is text/html',
- link, exc.request_desc, exc.content_type,
+ "Skipping page %s because the %s request got Content-Type: %s. "
+ "The only supported Content-Types are application/vnd.pypi.simple.v1+json, "
+ "application/vnd.pypi.simple.v1+html, and text/html",
+ link,
+ exc.request_desc,
+ exc.content_type,
)
except NetworkConnectionError as exc:
- _handle_get_page_fail(link, exc)
+ _handle_get_simple_fail(link, exc)
except RetryError as exc:
- _handle_get_page_fail(link, exc)
+ _handle_get_simple_fail(link, exc)
except SSLError as exc:
reason = "There was a problem confirming the ssl certificate: "
reason += str(exc)
- _handle_get_page_fail(link, reason, meth=logger.info)
+ _handle_get_simple_fail(link, reason, meth=logger.info)
except requests.ConnectionError as exc:
- _handle_get_page_fail(link, f"connection error: {exc}")
+ _handle_get_simple_fail(link, f"connection error: {exc}")
except requests.Timeout:
- _handle_get_page_fail(link, "timed out")
+ _handle_get_simple_fail(link, "timed out")
else:
- return _make_html_page(resp,
- cache_link_parsing=link.cache_link_parsing)
+ return _make_index_content(resp, cache_link_parsing=link.cache_link_parsing)
return None
@@ -451,9 +418,10 @@ class LinkCollector:
@classmethod
def create(
- cls, session: PipSession,
+ cls,
+ session: PipSession,
options: Values,
- suppress_no_index: bool = False
+ suppress_no_index: bool = False,
) -> "LinkCollector":
"""
:param session: The Session to use to make requests.
@@ -463,8 +431,8 @@ class LinkCollector:
index_urls = [options.index_url] + options.extra_index_urls
if options.no_index and not suppress_no_index:
logger.debug(
- 'Ignoring indexes: %s',
- ','.join(redact_auth_from_url(url) for url in index_urls),
+ "Ignoring indexes: %s",
+ ",".join(redact_auth_from_url(url) for url in index_urls),
)
index_urls = []
@@ -472,10 +440,13 @@ class LinkCollector:
find_links = options.find_links or []
search_scope = SearchScope.create(
- find_links=find_links, index_urls=index_urls,
+ find_links=find_links,
+ index_urls=index_urls,
+ no_index=options.no_index,
)
link_collector = LinkCollector(
- session=session, search_scope=search_scope,
+ session=session,
+ search_scope=search_scope,
)
return link_collector
@@ -483,11 +454,11 @@ class LinkCollector:
def find_links(self) -> List[str]:
return self.search_scope.find_links
- def fetch_page(self, location: Link) -> Optional[HTMLPage]:
+ def fetch_response(self, location: Link) -> Optional[IndexContent]:
"""
Fetch an HTML page containing package links.
"""
- return _get_html_page(location, session=self.session)
+ return _get_index_content(location, session=self.session)
def collect_sources(
self,
diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py
index 2dadb5aef..9bf247f02 100644
--- a/src/pip/_internal/index/package_finder.py
+++ b/src/pip/_internal/index/package_finder.py
@@ -3,6 +3,7 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
+import enum
import functools
import itertools
import logging
@@ -37,17 +38,14 @@ from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import build_netloc
from pip._internal.utils.packaging import check_requires_python
from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS
-from pip._internal.utils.urls import url_to_path
-__all__ = ['FormatControl', 'BestCandidateResult', 'PackageFinder']
+__all__ = ["FormatControl", "BestCandidateResult", "PackageFinder"]
logger = getLogger(__name__)
BuildTag = Union[Tuple[()], Tuple[int, str]]
-CandidateSortingKey = (
- Tuple[int, int, int, _BaseVersion, Optional[int], BuildTag]
-)
+CandidateSortingKey = Tuple[int, int, int, _BaseVersion, Optional[int], BuildTag]
def _check_link_requires_python(
@@ -66,39 +64,54 @@ def _check_link_requires_python(
"""
try:
is_compatible = check_requires_python(
- link.requires_python, version_info=version_info,
+ link.requires_python,
+ version_info=version_info,
)
except specifiers.InvalidSpecifier:
logger.debug(
"Ignoring invalid Requires-Python (%r) for link: %s",
- link.requires_python, link,
+ link.requires_python,
+ link,
)
else:
if not is_compatible:
- version = '.'.join(map(str, version_info))
+ version = ".".join(map(str, version_info))
if not ignore_requires_python:
logger.verbose(
- 'Link requires a different Python (%s not in: %r): %s',
- version, link.requires_python, link,
+ "Link requires a different Python (%s not in: %r): %s",
+ version,
+ link.requires_python,
+ link,
)
return False
logger.debug(
- 'Ignoring failed Requires-Python check (%s not in: %r) '
- 'for link: %s',
- version, link.requires_python, link,
+ "Ignoring failed Requires-Python check (%s not in: %r) for link: %s",
+ version,
+ link.requires_python,
+ link,
)
return True
+class LinkType(enum.Enum):
+ candidate = enum.auto()
+ different_project = enum.auto()
+ yanked = enum.auto()
+ format_unsupported = enum.auto()
+ format_invalid = enum.auto()
+ platform_mismatch = enum.auto()
+ requires_python_mismatch = enum.auto()
+
+
class LinkEvaluator:
"""
Responsible for evaluating links for a particular project.
"""
- _py_version_re = re.compile(r'-py([123]\.?[0-9]?)$')
+ _py_version_re = re.compile(r"-py([123]\.?[0-9]?)$")
# Don't include an allow_yanked default value to make sure each call
# site considers whether yanked releases are allowed. This also causes
@@ -141,19 +154,20 @@ class LinkEvaluator:
self.project_name = project_name
- def evaluate_link(self, link: Link) -> Tuple[bool, Optional[str]]:
+ def evaluate_link(self, link: Link) -> Tuple[LinkType, str]:
"""
Determine whether a link is a candidate for installation.
- :return: A tuple (is_candidate, result), where `result` is (1) a
- version string if `is_candidate` is True, and (2) if
- `is_candidate` is False, an optional string to log the reason
- the link fails to qualify.
+ :return: A tuple (result, detail), where *result* is an enum
+ representing whether the evaluation found a candidate, or the reason
+ why one is not found. If a candidate is found, *detail* will be the
+ candidate's version string; if one is not found, it contains the
+ reason the link fails to qualify.
"""
version = None
if link.is_yanked and not self._allow_yanked:
- reason = link.yanked_reason or '<none given>'
- return (False, f'yanked for reason: {reason}')
+ reason = link.yanked_reason or "<none given>"
+ return (LinkType.yanked, f"yanked for reason: {reason}")
if link.egg_fragment:
egg_info = link.egg_fragment
@@ -161,72 +175,78 @@ class LinkEvaluator:
else:
egg_info, ext = link.splitext()
if not ext:
- return (False, 'not a file')
+ return (LinkType.format_unsupported, "not a file")
if ext not in SUPPORTED_EXTENSIONS:
- return (False, f'unsupported archive format: {ext}')
+ return (
+ LinkType.format_unsupported,
+ f"unsupported archive format: {ext}",
+ )
if "binary" not in self._formats and ext == WHEEL_EXTENSION:
- reason = 'No binaries permitted for {}'.format(
- self.project_name)
- return (False, reason)
- if "macosx10" in link.path and ext == '.zip':
- return (False, 'macosx10 one')
+ reason = f"No binaries permitted for {self.project_name}"
+ return (LinkType.format_unsupported, reason)
+ if "macosx10" in link.path and ext == ".zip":
+ return (LinkType.format_unsupported, "macosx10 one")
if ext == WHEEL_EXTENSION:
try:
wheel = Wheel(link.filename)
except InvalidWheelFilename:
- return (False, 'invalid wheel filename')
+ return (
+ LinkType.format_invalid,
+ "invalid wheel filename",
+ )
if canonicalize_name(wheel.name) != self._canonical_name:
- reason = 'wrong project name (not {})'.format(
- self.project_name)
- return (False, reason)
+ reason = f"wrong project name (not {self.project_name})"
+ return (LinkType.different_project, reason)
supported_tags = self._target_python.get_tags()
if not wheel.supported(supported_tags):
# Include the wheel's tags in the reason string to
# simplify troubleshooting compatibility issues.
- file_tags = wheel.get_formatted_file_tags()
+ file_tags = ", ".join(wheel.get_formatted_file_tags())
reason = (
- "none of the wheel's tags ({}) are compatible "
- "(run pip debug --verbose to show compatible tags)".format(
- ', '.join(file_tags)
- )
+ f"none of the wheel's tags ({file_tags}) are compatible "
+ f"(run pip debug --verbose to show compatible tags)"
)
- return (False, reason)
+ return (LinkType.platform_mismatch, reason)
version = wheel.version
# This should be up by the self.ok_binary check, but see issue 2700.
if "source" not in self._formats and ext != WHEEL_EXTENSION:
- reason = f'No sources permitted for {self.project_name}'
- return (False, reason)
+ reason = f"No sources permitted for {self.project_name}"
+ return (LinkType.format_unsupported, reason)
if not version:
version = _extract_version_from_fragment(
- egg_info, self._canonical_name,
+ egg_info,
+ self._canonical_name,
)
if not version:
- reason = f'Missing project version for {self.project_name}'
- return (False, reason)
+ reason = f"Missing project version for {self.project_name}"
+ return (LinkType.format_invalid, reason)
match = self._py_version_re.search(version)
if match:
- version = version[:match.start()]
+ version = version[: match.start()]
py_version = match.group(1)
if py_version != self._target_python.py_version:
- return (False, 'Python version is incorrect')
+ return (
+ LinkType.platform_mismatch,
+ "Python version is incorrect",
+ )
supports_python = _check_link_requires_python(
- link, version_info=self._target_python.py_version_info,
+ link,
+ version_info=self._target_python.py_version_info,
ignore_requires_python=self._ignore_requires_python,
)
if not supports_python:
- # Return None for the reason text to suppress calling
- # _log_skipped_link().
- return (False, None)
+ reason = f"{version} Requires-Python {link.requires_python}"
+ return (LinkType.requires_python_mismatch, reason)
- logger.debug('Found link %s, version: %s', link, version)
+ logger.debug("Found link %s, version: %s", link, version)
- return (True, version)
+ return (LinkType.candidate, version)
def filter_unallowed_hashes(
@@ -251,8 +271,8 @@ def filter_unallowed_hashes(
"""
if not hashes:
logger.debug(
- 'Given no hashes to check %s links for project %r: '
- 'discarding no candidates',
+ "Given no hashes to check %s links for project %r: "
+ "discarding no candidates",
len(candidates),
project_name,
)
@@ -282,22 +302,22 @@ def filter_unallowed_hashes(
filtered = list(candidates)
if len(filtered) == len(candidates):
- discard_message = 'discarding no candidates'
+ discard_message = "discarding no candidates"
else:
- discard_message = 'discarding {} non-matches:\n {}'.format(
+ discard_message = "discarding {} non-matches:\n {}".format(
len(non_matches),
- '\n '.join(str(candidate.link) for candidate in non_matches)
+ "\n ".join(str(candidate.link) for candidate in non_matches),
)
logger.debug(
- 'Checked %s links for project %r against %s hashes '
- '(%s matches, %s no digest): %s',
+ "Checked %s links for project %r against %s hashes "
+ "(%s matches, %s no digest): %s",
len(candidates),
project_name,
hashes.digest_count,
match_count,
len(matches_or_no_digest) - match_count,
- discard_message
+ discard_message,
)
return filtered
@@ -354,13 +374,11 @@ class BestCandidateResult:
self.best_candidate = best_candidate
def iter_all(self) -> Iterable[InstallationCandidate]:
- """Iterate through all candidates.
- """
+ """Iterate through all candidates."""
return iter(self._candidates)
def iter_applicable(self) -> Iterable[InstallationCandidate]:
- """Iterate through the applicable candidates.
- """
+ """Iterate through the applicable candidates."""
return iter(self._applicable_candidates)
@@ -444,7 +462,8 @@ class CandidateEvaluator:
allow_prereleases = self._allow_all_prereleases or None
specifier = self._specifier
versions = {
- str(v) for v in specifier.filter(
+ str(v)
+ for v in specifier.filter(
# We turn the version object into a str here because otherwise
# when we're debundled but setuptools isn't, Python will see
# packaging.version.Version and
@@ -458,9 +477,7 @@ class CandidateEvaluator:
}
# Again, converting version to str to deal with debundling.
- applicable_candidates = [
- c for c in candidates if str(c.version) in versions
- ]
+ applicable_candidates = [c for c in candidates if str(c.version) in versions]
filtered_applicable_candidates = filter_unallowed_hashes(
candidates=applicable_candidates,
@@ -509,9 +526,11 @@ class CandidateEvaluator:
# can raise InvalidWheelFilename
wheel = Wheel(link.filename)
try:
- pri = -(wheel.find_most_preferred_tag(
- valid_tags, self._wheel_tag_preferences
- ))
+ pri = -(
+ wheel.find_most_preferred_tag(
+ valid_tags, self._wheel_tag_preferences
+ )
+ )
except ValueError:
raise UnsupportedWheel(
"{} is not a supported wheel for this platform. It "
@@ -520,7 +539,7 @@ class CandidateEvaluator:
if self._prefer_binary:
binary_preference = 1
if wheel.build_tag is not None:
- match = re.match(r'^(\d+)(.*)$', wheel.build_tag)
+ match = re.match(r"^(\d+)(.*)$", wheel.build_tag)
build_tag_groups = match.groups()
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
else: # sdist
@@ -528,8 +547,12 @@ class CandidateEvaluator:
has_allowed_hash = int(link.is_hash_allowed(self._hashes))
yank_value = -1 * int(link.is_yanked) # -1 for yanked.
return (
- has_allowed_hash, yank_value, binary_preference, candidate.version,
- pri, build_tag,
+ has_allowed_hash,
+ yank_value,
+ binary_preference,
+ candidate.version,
+ pri,
+ build_tag,
)
def sort_best_candidate(
@@ -603,7 +626,7 @@ class PackageFinder:
self.format_control = format_control
# These are boring links that have already been logged somehow.
- self._logged_links: Set[Link] = set()
+ self._logged_links: Set[Tuple[Link, LinkType, str]] = set()
# Don't include an allow_yanked default value to make sure each call
# site considers whether yanked releases are allowed. This also causes
@@ -680,6 +703,14 @@ class PackageFinder:
def set_prefer_binary(self) -> None:
self._candidate_prefs.prefer_binary = True
+ def requires_python_skipped_reasons(self) -> List[str]:
+ reasons = {
+ detail
+ for _, result, detail in self._logged_links
+ if result == LinkType.requires_python_mismatch
+ }
+ return sorted(reasons)
+
def make_link_evaluator(self, project_name: str) -> LinkEvaluator:
canonical_name = canonicalize_name(project_name)
formats = self.format_control.get_allowed_formats(canonical_name)
@@ -709,12 +740,13 @@ class PackageFinder:
no_eggs.append(link)
return no_eggs + eggs
- def _log_skipped_link(self, link: Link, reason: str) -> None:
- if link not in self._logged_links:
+ def _log_skipped_link(self, link: Link, result: LinkType, detail: str) -> None:
+ entry = (link, result, detail)
+ if entry not in self._logged_links:
# Put the link at the end so the reason is more visible and because
# the link string is usually very long.
- logger.debug('Skipping link: %s: %s', reason, link)
- self._logged_links.add(link)
+ logger.debug("Skipping link: %s: %s", detail, link)
+ self._logged_links.add(entry)
def get_install_candidate(
self, link_evaluator: LinkEvaluator, link: Link
@@ -723,16 +755,15 @@ class PackageFinder:
If the link is a candidate for install, convert it to an
InstallationCandidate and return it. Otherwise, return None.
"""
- is_candidate, result = link_evaluator.evaluate_link(link)
- if not is_candidate:
- if result:
- self._log_skipped_link(link, reason=result)
+ result, detail = link_evaluator.evaluate_link(link)
+ if result != LinkType.candidate:
+ self._log_skipped_link(link, result, detail)
return None
return InstallationCandidate(
name=link_evaluator.project_name,
link=link,
- version=result,
+ version=detail,
)
def evaluate_links(
@@ -753,13 +784,14 @@ class PackageFinder:
self, project_url: Link, link_evaluator: LinkEvaluator
) -> List[InstallationCandidate]:
logger.debug(
- 'Fetching project page and analyzing links: %s', project_url,
+ "Fetching project page and analyzing links: %s",
+ project_url,
)
- html_page = self._link_collector.fetch_page(project_url)
- if html_page is None:
+ index_response = self._link_collector.fetch_response(project_url)
+ if index_response is None:
return []
- page_links = list(parse_links(html_page))
+ page_links = list(parse_links(index_response))
with indent_log():
package_links = self.evaluate_links(
@@ -809,7 +841,14 @@ class PackageFinder:
)
if logger.isEnabledFor(logging.DEBUG) and file_candidates:
- paths = [url_to_path(c.link.url) for c in file_candidates]
+ paths = []
+ for candidate in file_candidates:
+ assert candidate.link.url # we need to have a URL
+ try:
+ paths.append(candidate.link.file_path)
+ except Exception:
+ paths.append(candidate.link.url) # it's not a local file
+
logger.debug("Local files found: %s", ", ".join(paths))
# This is an intentional priority ordering
@@ -821,8 +860,7 @@ class PackageFinder:
specifier: Optional[specifiers.BaseSpecifier] = None,
hashes: Optional[Hashes] = None,
) -> CandidateEvaluator:
- """Create a CandidateEvaluator object to use.
- """
+ """Create a CandidateEvaluator object to use."""
candidate_prefs = self._candidate_prefs
return CandidateEvaluator.create(
project_name=project_name,
@@ -867,54 +905,60 @@ class PackageFinder:
"""
hashes = req.hashes(trust_internet=False)
best_candidate_result = self.find_best_candidate(
- req.name, specifier=req.specifier, hashes=hashes,
+ req.name,
+ specifier=req.specifier,
+ hashes=hashes,
)
best_candidate = best_candidate_result.best_candidate
installed_version: Optional[_BaseVersion] = None
if req.satisfied_by is not None:
- installed_version = parse_version(req.satisfied_by.version)
+ installed_version = req.satisfied_by.version
def _format_versions(cand_iter: Iterable[InstallationCandidate]) -> str:
# This repeated parse_version and str() conversion is needed to
# handle different vendoring sources from pip and pkg_resources.
# If we stop using the pkg_resources provided specifier and start
# using our own, we can drop the cast to str().
- return ", ".join(sorted(
- {str(c.version) for c in cand_iter},
- key=parse_version,
- )) or "none"
+ return (
+ ", ".join(
+ sorted(
+ {str(c.version) for c in cand_iter},
+ key=parse_version,
+ )
+ )
+ or "none"
+ )
if installed_version is None and best_candidate is None:
logger.critical(
- 'Could not find a version that satisfies the requirement %s '
- '(from versions: %s)',
+ "Could not find a version that satisfies the requirement %s "
+ "(from versions: %s)",
req,
_format_versions(best_candidate_result.iter_all()),
)
raise DistributionNotFound(
- 'No matching distribution found for {}'.format(
- req)
+ "No matching distribution found for {}".format(req)
)
best_installed = False
if installed_version and (
- best_candidate is None or
- best_candidate.version <= installed_version):
+ best_candidate is None or best_candidate.version <= installed_version
+ ):
best_installed = True
if not upgrade and installed_version is not None:
if best_installed:
logger.debug(
- 'Existing installed version (%s) is most up-to-date and '
- 'satisfies requirement',
+ "Existing installed version (%s) is most up-to-date and "
+ "satisfies requirement",
installed_version,
)
else:
logger.debug(
- 'Existing installed version (%s) satisfies requirement '
- '(most up-to-date version is %s)',
+ "Existing installed version (%s) satisfies requirement "
+ "(most up-to-date version is %s)",
installed_version,
best_candidate.version,
)
@@ -923,15 +967,14 @@ class PackageFinder:
if best_installed:
# We have an existing version, and its the best version
logger.debug(
- 'Installed version (%s) is most up-to-date (past versions: '
- '%s)',
+ "Installed version (%s) is most up-to-date (past versions: %s)",
installed_version,
_format_versions(best_candidate_result.iter_applicable()),
)
raise BestVersionAlreadyInstalled
logger.debug(
- 'Using version %s (newest of versions: %s)',
+ "Using version %s (newest of versions: %s)",
best_candidate.version,
_format_versions(best_candidate_result.iter_applicable()),
)
diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py
index be18f21bb..60afe0a73 100644
--- a/src/pip/_internal/locations/__init__.py
+++ b/src/pip/_internal/locations/__init__.py
@@ -4,13 +4,14 @@ import os
import pathlib
import sys
import sysconfig
-from typing import Dict, Iterator, List, Optional, Tuple
+from typing import Any, Dict, Generator, List, Optional, Tuple
from pip._internal.models.scheme import SCHEME_KEYS, Scheme
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.deprecation import deprecated
+from pip._internal.utils.virtualenv import running_under_virtualenv
-from . import _distutils, _sysconfig
+from . import _sysconfig
from .base import (
USER_CACHE_DIR,
get_major_minor_version,
@@ -37,14 +38,60 @@ __all__ = [
logger = logging.getLogger(__name__)
-if os.environ.get("_PIP_LOCATIONS_NO_WARN_ON_MISMATCH"):
- _MISMATCH_LEVEL = logging.DEBUG
-else:
+
+_PLATLIBDIR: str = getattr(sys, "platlibdir", "lib")
+
+_USE_SYSCONFIG_DEFAULT = sys.version_info >= (3, 10)
+
+
+def _should_use_sysconfig() -> bool:
+ """This function determines the value of _USE_SYSCONFIG.
+
+ By default, pip uses sysconfig on Python 3.10+.
+ But Python distributors can override this decision by setting:
+ sysconfig._PIP_USE_SYSCONFIG = True / False
+ Rationale in https://github.com/pypa/pip/issues/10647
+
+ This is a function for testability, but should be constant during any one
+ run.
+ """
+ return bool(getattr(sysconfig, "_PIP_USE_SYSCONFIG", _USE_SYSCONFIG_DEFAULT))
+
+
+_USE_SYSCONFIG = _should_use_sysconfig()
+
+if not _USE_SYSCONFIG:
+ # Import distutils lazily to avoid deprecation warnings,
+ # but import it soon enough that it is in memory and available during
+ # a pip reinstall.
+ from . import _distutils
+
+# Be noisy about incompatibilities if this platforms "should" be using
+# sysconfig, but is explicitly opting out and using distutils instead.
+if _USE_SYSCONFIG_DEFAULT and not _USE_SYSCONFIG:
_MISMATCH_LEVEL = logging.WARNING
+else:
+ _MISMATCH_LEVEL = logging.DEBUG
+
+
+def _looks_like_bpo_44860() -> bool:
+ """The resolution to bpo-44860 will change this incorrect platlib.
+
+ See <https://bugs.python.org/issue44860>.
+ """
+ from distutils.command.install import INSTALL_SCHEMES
+
+ try:
+ unix_user_platlib = INSTALL_SCHEMES["unix_user"]["platlib"]
+ except KeyError:
+ return False
+ return unix_user_platlib == "$usersite"
def _looks_like_red_hat_patched_platlib_purelib(scheme: Dict[str, str]) -> bool:
platlib = scheme["platlib"]
+ if "/$platlibdir/" in platlib:
+ platlib = platlib.replace("/$platlibdir/", f"/{_PLATLIBDIR}/")
if "/lib64/" not in platlib:
return False
unpatched = platlib.replace("/lib64/", "/lib/")
@@ -52,12 +99,12 @@ def _looks_like_red_hat_patched_platlib_purelib(scheme: Dict[str, str]) -> bool:
@functools.lru_cache(maxsize=None)
-def _looks_like_red_hat_patched() -> bool:
+def _looks_like_red_hat_lib() -> bool:
"""Red Hat patches platlib in unix_prefix and unix_home, but not purelib.
This is the only way I can see to tell a Red Hat-patched Python.
"""
- from distutils.command.install import INSTALL_SCHEMES # type: ignore
+ from distutils.command.install import INSTALL_SCHEMES
return all(
k in INSTALL_SCHEMES
@@ -67,16 +114,70 @@ def _looks_like_red_hat_patched() -> bool:
@functools.lru_cache(maxsize=None)
-def _looks_like_debian_patched() -> bool:
+def _looks_like_debian_scheme() -> bool:
"""Debian adds two additional schemes."""
- from distutils.command.install import INSTALL_SCHEMES # type: ignore
+ from distutils.command.install import INSTALL_SCHEMES
return "deb_system" in INSTALL_SCHEMES and "unix_local" in INSTALL_SCHEMES
-def _fix_abiflags(parts: Tuple[str]) -> Iterator[str]:
+@functools.lru_cache(maxsize=None)
+def _looks_like_red_hat_scheme() -> bool:
+ """Red Hat patches ``sys.prefix`` and ``sys.exec_prefix``.
+
+ Red Hat's ``00251-change-user-install-location.patch`` changes the install
+ command's ``prefix`` and ``exec_prefix`` to append ``"/local"``. This is
+ (fortunately?) done quite unconditionally, so we create a default command
+ object without any configuration to detect this.
+ """
+ from distutils.command.install import install
+ from distutils.dist import Distribution
+
+ cmd: Any = install(Distribution())
+ cmd.finalize_options()
+ return (
+ cmd.exec_prefix == f"{os.path.normpath(sys.exec_prefix)}/local"
+ and cmd.prefix == f"{os.path.normpath(sys.prefix)}/local"
+ )
+
+
+@functools.lru_cache(maxsize=None)
+def _looks_like_slackware_scheme() -> bool:
+ """Slackware patches sysconfig but fails to patch distutils and site.
+
+ Slackware changes sysconfig's user scheme to use ``"lib64"`` for the lib
+ path, but does not do the same to the site module.
+ """
+ if user_site is None: # User-site not available.
+ return False
+ try:
+ paths = sysconfig.get_paths(scheme="posix_user", expand=False)
+ except KeyError: # User-site not available.
+ return False
+ return "/lib64/" in paths["purelib"] and "/lib64/" not in user_site
+
+
+@functools.lru_cache(maxsize=None)
+def _looks_like_msys2_mingw_scheme() -> bool:
+ """MSYS2 patches distutils and sysconfig to use a UNIX-like scheme.
+
+ However, MSYS2 incorrectly patches sysconfig ``nt`` scheme. The fix is
+ likely going to be included in their 3.10 release, so we ignore the warning.
+ See msys2/MINGW-packages#9319.
+
+ MSYS2 MINGW's patch uses lowercase ``"lib"`` instead of the usual uppercase,
+ and is missing the final ``"site-packages"``.
+ """
+ paths = sysconfig.get_paths("nt", expand=False)
+ return all(
+ "Lib" not in p and "lib" in p and not p.endswith("site-packages")
+ for p in (paths[key] for key in ("platlib", "purelib"))
+ )
+
+
+def _fix_abiflags(parts: Tuple[str]) -> Generator[str, None, None]:
ldversion = sysconfig.get_config_var("LDVERSION")
- abiflags: str = getattr(sys, "abiflags", None)
+ abiflags = getattr(sys, "abiflags", None)
# LDVERSION does not end with sys.abiflags. Just return the path unchanged.
if not ldversion or not abiflags or not ldversion.endswith(abiflags):
@@ -90,15 +191,6 @@ def _fix_abiflags(parts: Tuple[str]) -> Iterator[str]:
yield part
-def _default_base(*, user: bool) -> str:
- if user:
- base = sysconfig.get_config_var("userbase")
- else:
- base = sysconfig.get_config_var("base")
- assert base is not None
- return base
-
-
@functools.lru_cache(maxsize=None)
def _warn_mismatched(old: pathlib.Path, new: pathlib.Path, *, key: str) -> None:
issue_url = "https://github.com/pypa/pip/issues/10151"
@@ -144,7 +236,7 @@ def get_scheme(
isolated: bool = False,
prefix: Optional[str] = None,
) -> Scheme:
- old = _distutils.get_scheme(
+ new = _sysconfig.get_scheme(
dist_name,
user=user,
home=home,
@@ -152,7 +244,10 @@ def get_scheme(
isolated=isolated,
prefix=prefix,
)
- new = _sysconfig.get_scheme(
+ if _USE_SYSCONFIG:
+ return new
+
+ old = _distutils.get_scheme(
dist_name,
user=user,
home=home,
@@ -161,11 +256,9 @@ def get_scheme(
prefix=prefix,
)
- base = prefix or home or _default_base(user=user)
warning_contexts = []
for k in SCHEME_KEYS:
- # Extra join because distutils can return relative paths.
- old_v = pathlib.Path(base, getattr(old, k))
+ old_v = pathlib.Path(getattr(old, k))
new_v = pathlib.Path(getattr(new, k))
if old_v == new_v:
@@ -201,19 +294,45 @@ def get_scheme(
# On Red Hat and derived Linux distributions, distutils is patched to
# use "lib64" instead of "lib" for platlib.
- if k == "platlib" and _looks_like_red_hat_patched():
+ if k == "platlib" and _looks_like_red_hat_lib():
+ continue
+
+ # On Python 3.9+, sysconfig's posix_user scheme sets platlib against
+ # sys.platlibdir, but distutils's unix_user incorrectly coninutes
+ # using the same $usersite for both platlib and purelib. This creates a
+ # mismatch when sys.platlibdir is not "lib".
+ skip_bpo_44860 = (
+ user
+ and k == "platlib"
+ and not WINDOWS
+ and sys.version_info >= (3, 9)
+ and _PLATLIBDIR != "lib"
+ and _looks_like_bpo_44860()
+ )
+ if skip_bpo_44860:
+ continue
+
+ # Slackware incorrectly patches posix_user to use lib64 instead of lib,
+ # but not usersite to match the location.
+ skip_slackware_user_scheme = (
+ user
+ and k in ("platlib", "purelib")
+ and not WINDOWS
+ and _looks_like_slackware_scheme()
+ )
+ if skip_slackware_user_scheme:
continue
# Both Debian and Red Hat patch Python to place the system site under
# /usr/local instead of /usr. Debian also places lib in dist-packages
# instead of site-packages, but the /usr/local check should cover it.
skip_linux_system_special_case = (
- not (user or home or prefix)
+ not (user or home or prefix or running_under_virtualenv())
and old_v.parts[1:3] == ("usr", "local")
and len(new_v.parts) > 1
and new_v.parts[1] == "usr"
and (len(new_v.parts) < 3 or new_v.parts[2] != "local")
- and (_looks_like_red_hat_patched() or _looks_like_debian_patched())
+ and (_looks_like_red_hat_scheme() or _looks_like_debian_scheme())
)
if skip_linux_system_special_case:
continue
@@ -229,6 +348,26 @@ def get_scheme(
if skip_sysconfig_abiflag_bug:
continue
+ # MSYS2 MINGW's sysconfig patch does not include the "site-packages"
+ # part of the path. This is incorrect and will be fixed in MSYS.
+ skip_msys2_mingw_bug = (
+ WINDOWS and k in ("platlib", "purelib") and _looks_like_msys2_mingw_scheme()
+ )
+ if skip_msys2_mingw_bug:
+ continue
+
+ # CPython's POSIX install script invokes pip (via ensurepip) against the
+ # interpreter located in the source tree, not the install site. This
+ # triggers special logic in sysconfig that's not present in distutils.
+ # https://github.com/python/cpython/blob/8c21941ddaf/Lib/sysconfig.py#L178-L194
+ skip_cpython_build = (
+ sysconfig.is_python_build(check_home=True)
+ and not WINDOWS
+ and k in ("headers", "include", "platinclude")
+ )
+ if skip_cpython_build:
+ continue
+
warning_contexts.append((old_v, new_v, f"scheme.{k}"))
if not warning_contexts:
@@ -248,10 +387,12 @@ def get_scheme(
)
if any(default_old[k] != getattr(old, k) for k in SCHEME_KEYS):
deprecated(
- "Configuring installation scheme with distutils config files "
- "is deprecated and will no longer work in the near future. If you "
- "are using a Homebrew or Linuxbrew Python, please see discussion "
- "at https://github.com/Homebrew/homebrew-core/issues/76621",
+ reason=(
+ "Configuring installation scheme with distutils config files "
+ "is deprecated and will no longer work in the near future. If you "
+ "are using a Homebrew or Linuxbrew Python, please see discussion "
+ "at https://github.com/Homebrew/homebrew-core/issues/76621"
+ ),
replacement=None,
gone_in=None,
)
@@ -266,8 +407,11 @@ def get_scheme(
def get_bin_prefix() -> str:
- old = _distutils.get_bin_prefix()
new = _sysconfig.get_bin_prefix()
+ if _USE_SYSCONFIG:
+ return new
+
+ old = _distutils.get_bin_prefix()
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix"):
_log_context()
return old
@@ -277,10 +421,32 @@ def get_bin_user() -> str:
return _sysconfig.get_scheme("", user=True).scripts
+def _looks_like_deb_system_dist_packages(value: str) -> bool:
+ """Check if the value is Debian's APT-controlled dist-packages.
+
+ Debian's ``distutils.sysconfig.get_python_lib()`` implementation returns the
+ default package path controlled by APT, but does not patch ``sysconfig`` to
+ do the same. This is similar to the bug worked around in ``get_scheme()``,
+ but here the default is ``deb_system`` instead of ``unix_local``. Ultimately
+ we can't do anything about this Debian bug, and this detection allows us to
+ skip the warning when needed.
+ """
+ if not _looks_like_debian_scheme():
+ return False
+ if value == "/usr/lib/python3/dist-packages":
+ return True
+ return False
+
+
def get_purelib() -> str:
"""Return the default pure-Python lib location."""
- old = _distutils.get_purelib()
new = _sysconfig.get_purelib()
+ if _USE_SYSCONFIG:
+ return new
+
+ old = _distutils.get_purelib()
+ if _looks_like_deb_system_dist_packages(old):
+ return old
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib"):
_log_context()
return old
@@ -288,17 +454,61 @@ def get_purelib() -> str:
def get_platlib() -> str:
"""Return the default platform-shared lib location."""
- old = _distutils.get_platlib()
new = _sysconfig.get_platlib()
+ if _USE_SYSCONFIG:
+ return new
+
+ from . import _distutils
+
+ old = _distutils.get_platlib()
+ if _looks_like_deb_system_dist_packages(old):
+ return old
if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib"):
_log_context()
return old
+def _deduplicated(v1: str, v2: str) -> List[str]:
+ """Deduplicate values from a list."""
+ if v1 == v2:
+ return [v1]
+ return [v1, v2]
+
+
+def _looks_like_apple_library(path: str) -> bool:
+ """Apple patches sysconfig to *always* look under */Library/Python*."""
+ if sys.platform[:6] != "darwin":
+ return False
+ return path == f"/Library/Python/{get_major_minor_version()}/site-packages"
+
+
def get_prefixed_libs(prefix: str) -> List[str]:
"""Return the lib locations under ``prefix``."""
- old_pure, old_plat = _distutils.get_prefixed_libs(prefix)
new_pure, new_plat = _sysconfig.get_prefixed_libs(prefix)
+ if _USE_SYSCONFIG:
+ return _deduplicated(new_pure, new_plat)
+
+ old_pure, old_plat = _distutils.get_prefixed_libs(prefix)
+ old_lib_paths = _deduplicated(old_pure, old_plat)
+
+ # Apple's Python (shipped with Xcode and Command Line Tools) hard-code
+ # platlib and purelib to '/Library/Python/X.Y/site-packages'. This will
+ # cause serious build isolation bugs when Apple starts shipping 3.10 because
+ # pip will install build backends to the wrong location. This tells users
+ # who is at fault so Apple may notice it and fix the issue in time.
+ if all(_looks_like_apple_library(p) for p in old_lib_paths):
+ deprecated(
+ reason=(
+ "Python distributed by Apple's Command Line Tools incorrectly "
+ "patches sysconfig to always point to '/Library/Python'. This "
+ "will cause build isolation to operate incorrectly on Python "
+ "3.10 or later. Please help report this to Apple so they can "
+ "fix this. https://developer.apple.com/bug-reporting/"
+ ),
+ replacement=None,
+ gone_in=None,
+ )
+ return old_lib_paths
warned = [
_warn_if_mismatch(
@@ -315,6 +525,4 @@ def get_prefixed_libs(prefix: str) -> List[str]:
if any(warned):
_log_context(prefix=prefix)
- if old_pure == old_plat:
- return [old_pure]
- return [old_pure, old_plat]
+ return old_lib_paths
diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py
index 6d7c09dd0..c7712f016 100644
--- a/src/pip/_internal/locations/_distutils.py
+++ b/src/pip/_internal/locations/_distutils.py
@@ -3,6 +3,17 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
+# If pip's going to use distutils, it should not be using the copy that setuptools
+# might have injected into the environment. This is done by removing the injected
+# shim, if it's injected.
+#
+# See https://github.com/pypa/pip/issues/8761 for the original discussion and
+# rationale for why this is done within pip.
+try:
+ __import__("_distutils_hack").remove_shim()
+except (ImportError, AttributeError):
+ pass
+
import logging
import os
import sys
@@ -24,10 +35,10 @@ logger = logging.getLogger(__name__)
def distutils_scheme(
dist_name: str,
user: bool = False,
- home: str = None,
- root: str = None,
+ home: Optional[str] = None,
+ root: Optional[str] = None,
isolated: bool = False,
- prefix: str = None,
+ prefix: Optional[str] = None,
*,
ignore_config_files: bool = False,
) -> Dict[str, str]:
@@ -81,8 +92,14 @@ def distutils_scheme(
scheme.update(dict(purelib=i.install_lib, platlib=i.install_lib))
if running_under_virtualenv():
+ if home:
+ prefix = home
+ elif user:
+ prefix = i.install_userbase
+ else:
+ prefix = i.prefix
scheme["headers"] = os.path.join(
- i.prefix,
+ prefix,
"include",
"site",
f"python{get_major_minor_version()}",
@@ -91,10 +108,7 @@ def distutils_scheme(
if root is not None:
path_no_drive = os.path.splitdrive(os.path.abspath(scheme["headers"]))[1]
- scheme["headers"] = os.path.join(
- root,
- path_no_drive[1:],
- )
+ scheme["headers"] = os.path.join(root, path_no_drive[1:])
return scheme
@@ -135,17 +149,20 @@ def get_scheme(
def get_bin_prefix() -> str:
+ # XXX: In old virtualenv versions, sys.prefix can contain '..' components,
+ # so we need to call normpath to eliminate them.
+ prefix = os.path.normpath(sys.prefix)
if WINDOWS:
- bin_py = os.path.join(sys.prefix, "Scripts")
+ bin_py = os.path.join(prefix, "Scripts")
# buildout uses 'bin' on Windows too?
if not os.path.exists(bin_py):
- bin_py = os.path.join(sys.prefix, "bin")
+ bin_py = os.path.join(prefix, "bin")
return bin_py
# Forcing to use /usr/local/bin for standard macOS framework installs
# Also log to ~/Library/Logs/ for use with the Console.app log viewer
- if sys.platform[:6] == "darwin" and sys.prefix[:16] == "/System/Library/":
+ if sys.platform[:6] == "darwin" and prefix[:16] == "/System/Library/":
return "/usr/local/bin"
- return os.path.join(sys.prefix, "bin")
+ return os.path.join(prefix, "bin")
def get_purelib() -> str:
diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py
index 0fc67843f..0bbc9283d 100644
--- a/src/pip/_internal/locations/_sysconfig.py
+++ b/src/pip/_internal/locations/_sysconfig.py
@@ -1,4 +1,3 @@
-import distutils.util # FIXME: For change_root.
import logging
import os
import sys
@@ -9,14 +8,14 @@ from pip._internal.exceptions import InvalidSchemeCombination, UserInstallationI
from pip._internal.models.scheme import SCHEME_KEYS, Scheme
from pip._internal.utils.virtualenv import running_under_virtualenv
-from .base import get_major_minor_version, is_osx_framework
+from .base import change_root, get_major_minor_version, is_osx_framework
logger = logging.getLogger(__name__)
# Notes on _infer_* functions.
-# Unfortunately ``_get_default_scheme()`` is private, so there's no way to
-# ask things like "what is the '_prefix' scheme on this platform". These
+# Unfortunately ``get_default_scheme()`` didn't exist before 3.10, so there's no
+# way to ask things like "what is the '_prefix' scheme on this platform". These
# functions try to answer that with some heuristics while accounting for ad-hoc
# platforms not covered by CPython's default sysconfig implementation. If the
# ad-hoc implementation does not fully implement sysconfig, we'll fall back to
@@ -24,7 +23,33 @@ logger = logging.getLogger(__name__)
_AVAILABLE_SCHEMES = set(sysconfig.get_scheme_names())
-_HAS_PREFERRED_SCHEME_API = sys.version_info >= (3, 10)
+_PREFERRED_SCHEME_API = getattr(sysconfig, "get_preferred_scheme", None)
+
+
+def _should_use_osx_framework_prefix() -> bool:
+ """Check for Apple's ``osx_framework_library`` scheme.
+
+ Python distributed by Apple's Command Line Tools has this special scheme
+ that's used when:
+
+ * This is a framework build.
+ * We are installing into the system prefix.
+
+ This does not account for ``pip install --prefix`` (also means we're not
+ installing to the system prefix), which should use ``posix_prefix``, but
+ logic here means ``_infer_prefix()`` outputs ``osx_framework_library``. But
+ since ``prefix`` is not available for ``sysconfig.get_default_scheme()``,
+ which is the stdlib replacement for ``_infer_prefix()``, presumably Apple
+ wouldn't be able to magically switch between ``osx_framework_library`` and
+ ``posix_prefix``. ``_infer_prefix()`` returning ``osx_framework_library``
+ means its behavior is consistent whether we use the stdlib implementation
+ or our own, and we deal with this special case in ``get_scheme()`` instead.
+ """
+ return (
+ "osx_framework_library" in _AVAILABLE_SCHEMES
+ and not running_under_virtualenv()
+ and is_osx_framework()
+ )
def _infer_prefix() -> str:
@@ -41,10 +66,9 @@ def _infer_prefix() -> str:
If none of the above works, fall back to ``posix_prefix``.
"""
- if _HAS_PREFERRED_SCHEME_API:
- return sysconfig.get_preferred_scheme("prefix") # type: ignore
- os_framework_global = is_osx_framework() and not running_under_virtualenv()
- if os_framework_global and "osx_framework_library" in _AVAILABLE_SCHEMES:
+ if _PREFERRED_SCHEME_API:
+ return _PREFERRED_SCHEME_API("prefix")
+ if _should_use_osx_framework_prefix():
return "osx_framework_library"
implementation_suffixed = f"{sys.implementation.name}_{os.name}"
if implementation_suffixed in _AVAILABLE_SCHEMES:
@@ -61,8 +85,8 @@ def _infer_prefix() -> str:
def _infer_user() -> str:
"""Try to find a user scheme for the current platform."""
- if _HAS_PREFERRED_SCHEME_API:
- return sysconfig.get_preferred_scheme("user") # type: ignore
+ if _PREFERRED_SCHEME_API:
+ return _PREFERRED_SCHEME_API("user")
if is_osx_framework() and not running_under_virtualenv():
suffixed = "osx_framework_user"
else:
@@ -76,8 +100,8 @@ def _infer_user() -> str:
def _infer_home() -> str:
"""Try to find a home for the current platform."""
- if _HAS_PREFERRED_SCHEME_API:
- return sysconfig.get_preferred_scheme("home") # type: ignore
+ if _PREFERRED_SCHEME_API:
+ return _PREFERRED_SCHEME_API("home")
suffixed = f"{os.name}_home"
if suffixed in _AVAILABLE_SCHEMES:
return suffixed
@@ -130,6 +154,12 @@ def get_scheme(
else:
scheme_name = _infer_prefix()
+ # Special case: When installing into a custom prefix, use posix_prefix
+ # instead of osx_framework_library. See _should_use_osx_framework_prefix()
+ # docstring for details.
+ if prefix is not None and scheme_name == "osx_framework_library":
+ scheme_name = "posix_prefix"
+
if home is not None:
variables = {k: home for k in _HOME_KEYS}
elif prefix is not None:
@@ -163,7 +193,7 @@ def get_scheme(
)
if root is not None:
for key in SCHEME_KEYS:
- value = distutils.util.change_root(root, getattr(scheme, key))
+ value = change_root(root, getattr(scheme, key))
setattr(scheme, key, value)
return scheme
diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py
index 86dad4a3a..3f7de0061 100644
--- a/src/pip/_internal/locations/base.py
+++ b/src/pip/_internal/locations/base.py
@@ -5,6 +5,7 @@ import sys
import sysconfig
import typing
+from pip._internal.exceptions import InstallationError
from pip._internal.utils import appdirs
from pip._internal.utils.virtualenv import running_under_virtualenv
@@ -23,6 +24,34 @@ def get_major_minor_version() -> str:
return "{}.{}".format(*sys.version_info)
+def change_root(new_root: str, pathname: str) -> str:
+ """Return 'pathname' with 'new_root' prepended.
+
+ If 'pathname' is relative, this is equivalent to os.path.join(new_root, pathname).
+ Otherwise, it requires making 'pathname' relative and then joining the
+ two, which is tricky on DOS/Windows and Mac OS.
+
+ This is borrowed from Python's standard library's distutils module.
+ """
+ if os.name == "posix":
+ if not os.path.isabs(pathname):
+ return os.path.join(new_root, pathname)
+ else:
+ return os.path.join(new_root, pathname[1:])
+
+ elif os.name == "nt":
+ (drive, path) = os.path.splitdrive(pathname)
+ if path[0] == "\\":
+ path = path[1:]
+ return os.path.join(new_root, path)
+
+ else:
+ raise InstallationError(
+ f"Unknown platform: {os.name}\n"
+ "Can not change root path prefix on unknown platform."
+ )
+
+
def get_src_prefix() -> str:
if running_under_virtualenv():
src_prefix = os.path.join(sys.prefix, "src")
diff --git a/src/pip/_internal/main.py b/src/pip/_internal/main.py
index 51eee1588..33c6d24cd 100644
--- a/src/pip/_internal/main.py
+++ b/src/pip/_internal/main.py
@@ -1,8 +1,7 @@
from typing import List, Optional
-def main(args=None):
- # type: (Optional[List[str]]) -> int
+def main(args: Optional[List[str]] = None) -> int:
"""This is preserved for old console scripts that may still be referencing
it.
diff --git a/src/pip/_internal/metadata/__init__.py b/src/pip/_internal/metadata/__init__.py
index e3429d255..9f73ca710 100644
--- a/src/pip/_internal/metadata/__init__.py
+++ b/src/pip/_internal/metadata/__init__.py
@@ -1,16 +1,70 @@
-from typing import List, Optional
+import contextlib
+import functools
+import os
+import sys
+from typing import TYPE_CHECKING, List, Optional, Type, cast
-from .base import BaseDistribution, BaseEnvironment
+from pip._internal.utils.misc import strtobool
+
+from .base import BaseDistribution, BaseEnvironment, FilesystemWheel, MemoryWheel, Wheel
+
+if TYPE_CHECKING:
+ from typing import Protocol
+else:
+ Protocol = object
__all__ = [
"BaseDistribution",
"BaseEnvironment",
+ "FilesystemWheel",
+ "MemoryWheel",
+ "Wheel",
"get_default_environment",
"get_environment",
"get_wheel_distribution",
+ "select_backend",
]
+def _should_use_importlib_metadata() -> bool:
+ """Whether to use the ``importlib.metadata`` or ``pkg_resources`` backend.
+
+ By default, pip uses ``importlib.metadata`` on Python 3.11+, and
+ ``pkg_resourcess`` otherwise. This can be overridden by a couple of ways:
+
+ * If environment variable ``_PIP_USE_IMPORTLIB_METADATA`` is set, it
+ dictates whether ``importlib.metadata`` is used, regardless of Python
+ version.
+ * On Python 3.11+, Python distributors can patch ``importlib.metadata``
+ to add a global constant ``_PIP_USE_IMPORTLIB_METADATA = False``. This
+ makes pip use ``pkg_resources`` (unless the user set the aforementioned
+ environment variable to *True*).
+ """
+ with contextlib.suppress(KeyError, ValueError):
+ return bool(strtobool(os.environ["_PIP_USE_IMPORTLIB_METADATA"]))
+ if sys.version_info < (3, 11):
+ return False
+ import importlib.metadata
+
+ return bool(getattr(importlib.metadata, "_PIP_USE_IMPORTLIB_METADATA", True))
+
+
+class Backend(Protocol):
+ Distribution: Type[BaseDistribution]
+ Environment: Type[BaseEnvironment]
+
+
+@functools.lru_cache(maxsize=None)
+def select_backend() -> Backend:
+ if _should_use_importlib_metadata():
+ from . import importlib
+
+ return cast(Backend, importlib)
+ from . import pkg_resources
+
+ return cast(Backend, pkg_resources)
+
+
def get_default_environment() -> BaseEnvironment:
"""Get the default representation for the current environment.
@@ -18,9 +72,7 @@ def get_default_environment() -> BaseEnvironment:
Environment instance should be built from ``sys.path`` and may use caching
to share instance state accorss calls.
"""
- from .pkg_resources import Environment
-
- return Environment.default()
+ return select_backend().Environment.default()
def get_environment(paths: Optional[List[str]]) -> BaseEnvironment:
@@ -30,12 +82,19 @@ def get_environment(paths: Optional[List[str]]) -> BaseEnvironment:
given import paths. The backend must build a fresh instance representing
the state of installed distributions when this function is called.
"""
- from .pkg_resources import Environment
+ return select_backend().Environment.from_paths(paths)
- return Environment.from_paths(paths)
+def get_directory_distribution(directory: str) -> BaseDistribution:
+ """Get the distribution metadata representation in the specified directory.
+
+ This returns a Distribution instance from the chosen backend based on
+ the given on-disk ``.dist-info`` directory.
+ """
+ return select_backend().Distribution.from_directory(directory)
-def get_wheel_distribution(wheel_path: str, canonical_name: str) -> BaseDistribution:
+
+def get_wheel_distribution(wheel: Wheel, canonical_name: str) -> BaseDistribution:
"""Get the representation of the specified wheel's distribution metadata.
This returns a Distribution instance from the chosen backend based on
@@ -43,6 +102,26 @@ def get_wheel_distribution(wheel_path: str, canonical_name: str) -> BaseDistribu
:param canonical_name: Normalized project name of the given wheel.
"""
- from .pkg_resources import Distribution
+ return select_backend().Distribution.from_wheel(wheel, canonical_name)
+
+
+def get_metadata_distribution(
+ metadata_contents: bytes,
+ filename: str,
+ canonical_name: str,
+) -> BaseDistribution:
+ """Get the dist representation of the specified METADATA file contents.
- return Distribution.from_wheel(wheel_path, canonical_name)
+ This returns a Distribution instance from the chosen backend sourced from the data
+ in `metadata_contents`.
+
+ :param metadata_contents: Contents of a METADATA file within a dist, or one served
+ via PEP 658.
+ :param filename: Filename for the dist this metadata represents.
+ :param canonical_name: Normalized project name of the given dist.
+ """
+ return select_backend().Distribution.from_metadata_file_contents(
+ metadata_contents,
+ filename,
+ canonical_name,
+ )
diff --git a/src/pip/_internal/metadata/_json.py b/src/pip/_internal/metadata/_json.py
new file mode 100644
index 000000000..336b52f1e
--- /dev/null
+++ b/src/pip/_internal/metadata/_json.py
@@ -0,0 +1,84 @@
+# Extracted from https://github.com/pfmoore/pkg_metadata
+
+from email.header import Header, decode_header, make_header
+from email.message import Message
+from typing import Any, Dict, List, Union
+
+METADATA_FIELDS = [
+ # Name, Multiple-Use
+ ("Metadata-Version", False),
+ ("Name", False),
+ ("Version", False),
+ ("Dynamic", True),
+ ("Platform", True),
+ ("Supported-Platform", True),
+ ("Summary", False),
+ ("Description", False),
+ ("Description-Content-Type", False),
+ ("Keywords", False),
+ ("Home-page", False),
+ ("Download-URL", False),
+ ("Author", False),
+ ("Author-email", False),
+ ("Maintainer", False),
+ ("Maintainer-email", False),
+ ("License", False),
+ ("Classifier", True),
+ ("Requires-Dist", True),
+ ("Requires-Python", False),
+ ("Requires-External", True),
+ ("Project-URL", True),
+ ("Provides-Extra", True),
+ ("Provides-Dist", True),
+ ("Obsoletes-Dist", True),
+]
+
+
+def json_name(field: str) -> str:
+ return field.lower().replace("-", "_")
+
+
+def msg_to_json(msg: Message) -> Dict[str, Any]:
+ """Convert a Message object into a JSON-compatible dictionary."""
+
+ def sanitise_header(h: Union[Header, str]) -> str:
+ if isinstance(h, Header):
+ chunks = []
+ for bytes, encoding in decode_header(h):
+ if encoding == "unknown-8bit":
+ try:
+ # See if UTF-8 works
+ bytes.decode("utf-8")
+ encoding = "utf-8"
+ except UnicodeDecodeError:
+ # If not, latin1 at least won't fail
+ encoding = "latin1"
+ chunks.append((bytes, encoding))
+ return str(make_header(chunks))
+ return str(h)
+
+ result = {}
+ for field, multi in METADATA_FIELDS:
+ if field not in msg:
+ continue
+ key = json_name(field)
+ if multi:
+ value: Union[str, List[str]] = [
+ sanitise_header(v) for v in msg.get_all(field)
+ ]
+ else:
+ value = sanitise_header(msg.get(field))
+ if key == "keywords":
+ # Accept both comma-separated and space-separated
+ # forms, for better compatibility with old data.
+ if "," in value:
+ value = [v.strip() for v in value.split(",")]
+ else:
+ value = value.split()
+ result[key] = value
+
+ payload = msg.get_payload()
+ if payload:
+ result["description"] = payload
+
+ return result
diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py
index 9fdc123ce..cafb79fb3 100644
--- a/src/pip/_internal/metadata/base.py
+++ b/src/pip/_internal/metadata/base.py
@@ -1,37 +1,56 @@
+import csv
import email.message
+import functools
import json
import logging
+import pathlib
import re
+import zipfile
from typing import (
+ IO,
TYPE_CHECKING,
+ Any,
Collection,
Container,
+ Dict,
Iterable,
Iterator,
List,
+ NamedTuple,
Optional,
+ Tuple,
Union,
)
from pip._vendor.packaging.requirements import Requirement
+from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
+from pip._vendor.packaging.utils import NormalizedName
from pip._vendor.packaging.version import LegacyVersion, Version
+from pip._internal.exceptions import NoneMetadataError
+from pip._internal.locations import site_packages, user_site
from pip._internal.models.direct_url import (
DIRECT_URL_METADATA_NAME,
DirectUrl,
DirectUrlValidationError,
)
-from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here.
+from pip._internal.utils.compat import stdlib_pkgs # TODO: Move definition here.
+from pip._internal.utils.egg_link import egg_link_path_from_sys_path
+from pip._internal.utils.misc import is_local, normalize_path
+from pip._internal.utils.packaging import safe_extra
+from pip._internal.utils.urls import url_to_path
+
+from ._json import msg_to_json
if TYPE_CHECKING:
from typing import Protocol
-
- from pip._vendor.packaging.utils import NormalizedName
else:
Protocol = object
DistributionVersion = Union[LegacyVersion, Version]
+InfoPath = Union[str, pathlib.PurePath]
+
logger = logging.getLogger(__name__)
@@ -49,7 +68,89 @@ class BaseEntryPoint(Protocol):
raise NotImplementedError()
+def _convert_installed_files_path(
+ entry: Tuple[str, ...],
+ info: Tuple[str, ...],
+) -> str:
+ """Convert a legacy installed-files.txt path into modern RECORD path.
+
+ The legacy format stores paths relative to the info directory, while the
+ modern format stores paths relative to the package root, e.g. the
+ site-packages directory.
+
+ :param entry: Path parts of the installed-files.txt entry.
+ :param info: Path parts of the egg-info directory relative to package root.
+ :returns: The converted entry.
+
+ For best compatibility with symlinks, this does not use ``abspath()`` or
+ ``Path.resolve()``, but tries to work with path parts:
+
+ 1. While ``entry`` starts with ``..``, remove the equal amounts of parts
+ from ``info``; if ``info`` is empty, start appending ``..`` instead.
+ 2. Join the two directly.
+ """
+ while entry and entry[0] == "..":
+ if not info or info[-1] == "..":
+ info += ("..",)
+ else:
+ info = info[:-1]
+ entry = entry[1:]
+ return str(pathlib.Path(*info, *entry))
+
+
+class RequiresEntry(NamedTuple):
+ requirement: str
+ extra: str
+ marker: str
+
+
class BaseDistribution(Protocol):
+ @classmethod
+ def from_directory(cls, directory: str) -> "BaseDistribution":
+ """Load the distribution from a metadata directory.
+
+ :param directory: Path to a metadata directory, e.g. ``.dist-info``.
+ """
+ raise NotImplementedError()
+
+ @classmethod
+ def from_metadata_file_contents(
+ cls,
+ metadata_contents: bytes,
+ filename: str,
+ project_name: str,
+ ) -> "BaseDistribution":
+ """Load the distribution from the contents of a METADATA file.
+
+ This is used to implement PEP 658 by generating a "shallow" dist object that can
+ be used for resolution without downloading or building the actual dist yet.
+
+ :param metadata_contents: The contents of a METADATA file.
+ :param filename: File name for the dist with this metadata.
+ :param project_name: Name of the project this dist represents.
+ """
+ raise NotImplementedError()
+
+ @classmethod
+ def from_wheel(cls, wheel: "Wheel", name: str) -> "BaseDistribution":
+ """Load the distribution from a given wheel.
+
+ :param wheel: A concrete wheel definition.
+ :param name: File name of the wheel.
+
+ :raises InvalidWheel: Whenever loading of the wheel causes a
+ :py:exc:`zipfile.BadZipFile` exception to be thrown.
+ :raises UnsupportedWheel: If the wheel is a valid zip, but malformed
+ internally.
+ """
+ raise NotImplementedError()
+
+ def __repr__(self) -> str:
+ return f"{self.raw_name} {self.version} ({self.location})"
+
+ def __str__(self) -> str:
+ return f"{self.raw_name} {self.version}"
+
@property
def location(self) -> Optional[str]:
"""Where the distribution is loaded from.
@@ -65,8 +166,43 @@ class BaseDistribution(Protocol):
raise NotImplementedError()
@property
- def info_directory(self) -> Optional[str]:
- """Location of the .[egg|dist]-info directory.
+ def editable_project_location(self) -> Optional[str]:
+ """The project location for editable distributions.
+
+ This is the directory where pyproject.toml or setup.py is located.
+ None if the distribution is not installed in editable mode.
+ """
+ # TODO: this property is relatively costly to compute, memoize it ?
+ direct_url = self.direct_url
+ if direct_url:
+ if direct_url.is_local_editable():
+ return url_to_path(direct_url.url)
+ else:
+ # Search for an .egg-link file by walking sys.path, as it was
+ # done before by dist_is_editable().
+ egg_link_path = egg_link_path_from_sys_path(self.raw_name)
+ if egg_link_path:
+ # TODO: get project location from second line of egg_link file
+ # (https://github.com/pypa/pip/issues/10243)
+ return self.location
+ return None
+
+ @property
+ def installed_location(self) -> Optional[str]:
+ """The distribution's "installed" location.
+
+ This should generally be a ``site-packages`` directory. This is
+ usually ``dist.location``, except for legacy develop-installed packages,
+ where ``dist.location`` is the source code location, and this is where
+ the ``.egg-link`` file is.
+
+ The returned location is normalized (in particular, with symlinks removed).
+ """
+ raise NotImplementedError()
+
+ @property
+ def info_location(self) -> Optional[str]:
+ """Location of the .[egg|dist]-info directory or file.
Similarly to ``location``, a string value is not necessarily a
filesystem path. ``None`` means the distribution is created in-memory.
@@ -81,7 +217,66 @@ class BaseDistribution(Protocol):
raise NotImplementedError()
@property
- def canonical_name(self) -> "NormalizedName":
+ def installed_by_distutils(self) -> bool:
+ """Whether this distribution is installed with legacy distutils format.
+
+ A distribution installed with "raw" distutils not patched by setuptools
+ uses one single file at ``info_location`` to store metadata. We need to
+ treat this specially on uninstallation.
+ """
+ info_location = self.info_location
+ if not info_location:
+ return False
+ return pathlib.Path(info_location).is_file()
+
+ @property
+ def installed_as_egg(self) -> bool:
+ """Whether this distribution is installed as an egg.
+
+ This usually indicates the distribution was installed by (older versions
+ of) easy_install.
+ """
+ location = self.location
+ if not location:
+ return False
+ return location.endswith(".egg")
+
+ @property
+ def installed_with_setuptools_egg_info(self) -> bool:
+ """Whether this distribution is installed with the ``.egg-info`` format.
+
+ This usually indicates the distribution was installed with setuptools
+ with an old pip version or with ``single-version-externally-managed``.
+
+ Note that this ensure the metadata store is a directory. distutils can
+ also installs an ``.egg-info``, but as a file, not a directory. This
+ property is *False* for that case. Also see ``installed_by_distutils``.
+ """
+ info_location = self.info_location
+ if not info_location:
+ return False
+ if not info_location.endswith(".egg-info"):
+ return False
+ return pathlib.Path(info_location).is_dir()
+
+ @property
+ def installed_with_dist_info(self) -> bool:
+ """Whether this distribution is installed with the "modern format".
+
+ This indicates a "modern" installation, e.g. storing metadata in the
+ ``.dist-info`` directory. This applies to installations made by
+ setuptools (but through pip, not directly), or anything using the
+ standardized build backend interface (PEP 517).
+ """
+ info_location = self.info_location
+ if not info_location:
+ return False
+ if not info_location.endswith(".dist-info"):
+ return False
+ return pathlib.Path(info_location).is_dir()
+
+ @property
+ def canonical_name(self) -> NormalizedName:
raise NotImplementedError()
@property
@@ -89,6 +284,14 @@ class BaseDistribution(Protocol):
raise NotImplementedError()
@property
+ def setuptools_filename(self) -> str:
+ """Convert a project name to its setuptools-compatible filename.
+
+ This is a copy of ``pkg_resources.to_filename()`` for compatibility.
+ """
+ return self.raw_name.replace("-", "_")
+
+ @property
def direct_url(self) -> Optional[DirectUrl]:
"""Obtain a DirectUrl from this distribution.
@@ -116,39 +319,102 @@ class BaseDistribution(Protocol):
@property
def installer(self) -> str:
- raise NotImplementedError()
+ try:
+ installer_text = self.read_text("INSTALLER")
+ except (OSError, ValueError, NoneMetadataError):
+ return "" # Fail silently if the installer file cannot be read.
+ for line in installer_text.splitlines():
+ cleaned_line = line.strip()
+ if cleaned_line:
+ return cleaned_line
+ return ""
+
+ @property
+ def requested(self) -> bool:
+ return self.is_file("REQUESTED")
@property
def editable(self) -> bool:
- raise NotImplementedError()
+ return bool(self.editable_project_location)
@property
def local(self) -> bool:
- raise NotImplementedError()
+ """If distribution is installed in the current virtual environment.
+
+ Always True if we're not in a virtualenv.
+ """
+ if self.installed_location is None:
+ return False
+ return is_local(self.installed_location)
@property
def in_usersite(self) -> bool:
- raise NotImplementedError()
+ if self.installed_location is None or user_site is None:
+ return False
+ return self.installed_location.startswith(normalize_path(user_site))
@property
def in_site_packages(self) -> bool:
+ if self.installed_location is None or site_packages is None:
+ return False
+ return self.installed_location.startswith(normalize_path(site_packages))
+
+ def is_file(self, path: InfoPath) -> bool:
+ """Check whether an entry in the info directory is a file."""
raise NotImplementedError()
- def read_text(self, name: str) -> str:
- """Read a file in the .dist-info (or .egg-info) directory.
+ def iter_distutils_script_names(self) -> Iterator[str]:
+ """Find distutils 'scripts' entries metadata.
- Should raise ``FileNotFoundError`` if ``name`` does not exist in the
- metadata directory.
+ If 'scripts' is supplied in ``setup.py``, distutils records those in the
+ installed distribution's ``scripts`` directory, a file for each script.
+ """
+ raise NotImplementedError()
+
+ def read_text(self, path: InfoPath) -> str:
+ """Read a file in the info directory.
+
+ :raise FileNotFoundError: If ``path`` does not exist in the directory.
+ :raise NoneMetadataError: If ``path`` exists in the info directory, but
+ cannot be read.
"""
raise NotImplementedError()
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
raise NotImplementedError()
+ def _metadata_impl(self) -> email.message.Message:
+ raise NotImplementedError()
+
+ @functools.lru_cache(maxsize=1)
+ def _metadata_cached(self) -> email.message.Message:
+ # When we drop python 3.7 support, move this to the metadata property and use
+ # functools.cached_property instead of lru_cache.
+ metadata = self._metadata_impl()
+ self._add_egg_info_requires(metadata)
+ return metadata
+
@property
def metadata(self) -> email.message.Message:
- """Metadata of distribution parsed from e.g. METADATA or PKG-INFO."""
- raise NotImplementedError()
+ """Metadata of distribution parsed from e.g. METADATA or PKG-INFO.
+
+ This should return an empty message if the metadata file is unavailable.
+
+ :raises NoneMetadataError: If the metadata file is available, but does
+ not contain valid metadata.
+ """
+ return self._metadata_cached()
+
+ @property
+ def metadata_dict(self) -> Dict[str, Any]:
+ """PEP 566 compliant JSON-serializable representation of METADATA or PKG-INFO.
+
+ This should return an empty dict if the metadata file is unavailable.
+
+ :raises NoneMetadataError: If the metadata file is available, but does
+ not contain valid metadata.
+ """
+ return msg_to_json(self.metadata)
@property
def metadata_version(self) -> Optional[str]:
@@ -159,12 +425,159 @@ class BaseDistribution(Protocol):
def raw_name(self) -> str:
"""Value of "Name:" in distribution metadata."""
# The metadata should NEVER be missing the Name: key, but if it somehow
- # does not, fall back to the known canonical name.
+ # does, fall back to the known canonical name.
return self.metadata.get("Name", self.canonical_name)
+ @property
+ def requires_python(self) -> SpecifierSet:
+ """Value of "Requires-Python:" in distribution metadata.
+
+ If the key does not exist or contains an invalid value, an empty
+ SpecifierSet should be returned.
+ """
+ value = self.metadata.get("Requires-Python")
+ if value is None:
+ return SpecifierSet()
+ try:
+ # Convert to str to satisfy the type checker; this can be a Header object.
+ spec = SpecifierSet(str(value))
+ except InvalidSpecifier as e:
+ message = "Package %r has an invalid Requires-Python: %s"
+ logger.warning(message, self.raw_name, e)
+ return SpecifierSet()
+ return spec
+
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
+ """Dependencies of this distribution.
+
+ For modern .dist-info distributions, this is the collection of
+ "Requires-Dist:" entries in distribution metadata.
+ """
raise NotImplementedError()
+ def iter_provided_extras(self) -> Iterable[str]:
+ """Extras provided by this distribution.
+
+ For modern .dist-info distributions, this is the collection of
+ "Provides-Extra:" entries in distribution metadata.
+ """
+ raise NotImplementedError()
+
+ def _iter_declared_entries_from_record(self) -> Optional[Iterator[str]]:
+ try:
+ text = self.read_text("RECORD")
+ except FileNotFoundError:
+ return None
+ # This extra Path-str cast normalizes entries.
+ return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
+
+ def _iter_declared_entries_from_legacy(self) -> Optional[Iterator[str]]:
+ try:
+ text = self.read_text("installed-files.txt")
+ except FileNotFoundError:
+ return None
+ paths = (p for p in text.splitlines(keepends=False) if p)
+ root = self.location
+ info = self.info_location
+ if root is None or info is None:
+ return paths
+ try:
+ info_rel = pathlib.Path(info).relative_to(root)
+ except ValueError: # info is not relative to root.
+ return paths
+ if not info_rel.parts: # info *is* root.
+ return paths
+ return (
+ _convert_installed_files_path(pathlib.Path(p).parts, info_rel.parts)
+ for p in paths
+ )
+
+ def iter_declared_entries(self) -> Optional[Iterator[str]]:
+ """Iterate through file entries declared in this distribution.
+
+ For modern .dist-info distributions, this is the files listed in the
+ ``RECORD`` metadata file. For legacy setuptools distributions, this
+ comes from ``installed-files.txt``, with entries normalized to be
+ compatible with the format used by ``RECORD``.
+
+ :return: An iterator for listed entries, or None if the distribution
+ contains neither ``RECORD`` nor ``installed-files.txt``.
+ """
+ return (
+ self._iter_declared_entries_from_record()
+ or self._iter_declared_entries_from_legacy()
+ )
+
+ def _iter_requires_txt_entries(self) -> Iterator[RequiresEntry]:
+ """Parse a ``requires.txt`` in an egg-info directory.
+
+ This is an INI-ish format where an egg-info stores dependencies. A
+ section name describes extra other environment markers, while each entry
+ is an arbitrary string (not a key-value pair) representing a dependency
+ as a requirement string (no markers).
+
+ There is a construct in ``importlib.metadata`` called ``Sectioned`` that
+ does mostly the same, but the format is currently considered private.
+ """
+ try:
+ content = self.read_text("requires.txt")
+ except FileNotFoundError:
+ return
+ extra = marker = "" # Section-less entries don't have markers.
+ for line in content.splitlines():
+ line = line.strip()
+ if not line or line.startswith("#"): # Comment; ignored.
+ continue
+ if line.startswith("[") and line.endswith("]"): # A section header.
+ extra, _, marker = line.strip("[]").partition(":")
+ continue
+ yield RequiresEntry(requirement=line, extra=extra, marker=marker)
+
+ def _iter_egg_info_extras(self) -> Iterable[str]:
+ """Get extras from the egg-info directory."""
+ known_extras = {""}
+ for entry in self._iter_requires_txt_entries():
+ if entry.extra in known_extras:
+ continue
+ known_extras.add(entry.extra)
+ yield entry.extra
+
+ def _iter_egg_info_dependencies(self) -> Iterable[str]:
+ """Get distribution dependencies from the egg-info directory.
+
+ To ease parsing, this converts a legacy dependency entry into a PEP 508
+ requirement string. Like ``_iter_requires_txt_entries()``, there is code
+ in ``importlib.metadata`` that does mostly the same, but not do exactly
+ what we need.
+
+ Namely, ``importlib.metadata`` does not normalize the extra name before
+ putting it into the requirement string, which causes marker comparison
+ to fail because the dist-info format do normalize. This is consistent in
+ all currently available PEP 517 backends, although not standardized.
+ """
+ for entry in self._iter_requires_txt_entries():
+ if entry.extra and entry.marker:
+ marker = f'({entry.marker}) and extra == "{safe_extra(entry.extra)}"'
+ elif entry.extra:
+ marker = f'extra == "{safe_extra(entry.extra)}"'
+ elif entry.marker:
+ marker = entry.marker
+ else:
+ marker = ""
+ if marker:
+ yield f"{entry.requirement} ; {marker}"
+ else:
+ yield entry.requirement
+
+ def _add_egg_info_requires(self, metadata: email.message.Message) -> None:
+ """Add egg-info requires.txt information to the metadata."""
+ if not metadata.get_all("Requires-Dist"):
+ for dep in self._iter_egg_info_dependencies():
+ metadata["Requires-Dist"] = dep
+ if not metadata.get_all("Provides-Extra"):
+ for extra in self._iter_egg_info_extras():
+ metadata["Provides-Extra"] = extra
+
class BaseEnvironment:
"""An environment containing distributions to introspect."""
@@ -178,7 +591,11 @@ class BaseEnvironment:
raise NotImplementedError()
def get_distribution(self, name: str) -> Optional["BaseDistribution"]:
- """Given a requirement name, return the installed distributions."""
+ """Given a requirement name, return the installed distributions.
+
+ The name may not be normalized. The implementation must canonicalize
+ it for lookup.
+ """
raise NotImplementedError()
def _iter_distributions(self) -> Iterator["BaseDistribution"]:
@@ -190,8 +607,8 @@ class BaseEnvironment:
"""
raise NotImplementedError()
- def iter_distributions(self) -> Iterator["BaseDistribution"]:
- """Iterate through installed distributions."""
+ def iter_all_distributions(self) -> Iterator[BaseDistribution]:
+ """Iterate through all installed distributions without any filtering."""
for dist in self._iter_distributions():
# Make sure the distribution actually comes from a valid Python
# packaging distribution. Pip's AdjacentTempDirectory leaves folders
@@ -221,6 +638,11 @@ class BaseEnvironment:
) -> Iterator[BaseDistribution]:
"""Return a list of installed distributions.
+ This is based on ``iter_all_distributions()`` with additional filtering
+ options. Note that ``iter_installed_distributions()`` without arguments
+ is *not* equal to ``iter_all_distributions()``, since some of the
+ configurations exclude packages by default.
+
:param local_only: If True (default), only return installations
local to the current virtualenv, if in a virtualenv.
:param skip: An iterable of canonicalized project names to ignore;
@@ -230,7 +652,7 @@ class BaseEnvironment:
:param user_only: If True, only report installations in the user
site directory.
"""
- it = self.iter_distributions()
+ it = self.iter_all_distributions()
if local_only:
it = (d for d in it if d.local)
if not include_editables:
@@ -240,3 +662,27 @@ class BaseEnvironment:
if user_only:
it = (d for d in it if d.in_usersite)
return (d for d in it if d.canonical_name not in skip)
+
+
+class Wheel(Protocol):
+ location: str
+
+ def as_zipfile(self) -> zipfile.ZipFile:
+ raise NotImplementedError()
+
+
+class FilesystemWheel(Wheel):
+ def __init__(self, location: str) -> None:
+ self.location = location
+
+ def as_zipfile(self) -> zipfile.ZipFile:
+ return zipfile.ZipFile(self.location, allowZip64=True)
+
+
+class MemoryWheel(Wheel):
+ def __init__(self, location: str, stream: IO[bytes]) -> None:
+ self.location = location
+ self.stream = stream
+
+ def as_zipfile(self) -> zipfile.ZipFile:
+ return zipfile.ZipFile(self.stream, allowZip64=True)
diff --git a/src/pip/_internal/metadata/importlib/__init__.py b/src/pip/_internal/metadata/importlib/__init__.py
new file mode 100644
index 000000000..5e7af9fe5
--- /dev/null
+++ b/src/pip/_internal/metadata/importlib/__init__.py
@@ -0,0 +1,4 @@
+from ._dists import Distribution
+from ._envs import Environment
+
+__all__ = ["Distribution", "Environment"]
diff --git a/src/pip/_internal/metadata/importlib/_compat.py b/src/pip/_internal/metadata/importlib/_compat.py
new file mode 100644
index 000000000..593bff23e
--- /dev/null
+++ b/src/pip/_internal/metadata/importlib/_compat.py
@@ -0,0 +1,55 @@
+import importlib.metadata
+from typing import Any, Optional, Protocol, cast
+
+
+class BadMetadata(ValueError):
+ def __init__(self, dist: importlib.metadata.Distribution, *, reason: str) -> None:
+ self.dist = dist
+ self.reason = reason
+
+ def __str__(self) -> str:
+ return f"Bad metadata in {self.dist} ({self.reason})"
+
+
+class BasePath(Protocol):
+ """A protocol that various path objects conform.
+
+ This exists because importlib.metadata uses both ``pathlib.Path`` and
+ ``zipfile.Path``, and we need a common base for type hints (Union does not
+ work well since ``zipfile.Path`` is too new for our linter setup).
+
+ This does not mean to be exhaustive, but only contains things that present
+ in both classes *that we need*.
+ """
+
+ @property
+ def name(self) -> str:
+ raise NotImplementedError()
+
+ @property
+ def parent(self) -> "BasePath":
+ raise NotImplementedError()
+
+
+def get_info_location(d: importlib.metadata.Distribution) -> Optional[BasePath]:
+ """Find the path to the distribution's metadata directory.
+
+ HACK: This relies on importlib.metadata's private ``_path`` attribute. Not
+ all distributions exist on disk, so importlib.metadata is correct to not
+ expose the attribute as public. But pip's code base is old and not as clean,
+ so we do this to avoid having to rewrite too many things. Hopefully we can
+ eliminate this some day.
+ """
+ return getattr(d, "_path", None)
+
+
+def get_dist_name(dist: importlib.metadata.Distribution) -> str:
+ """Get the distribution's project name.
+
+ The ``name`` attribute is only available in Python 3.10 or later. We are
+ targeting exactly that, but Mypy does not know this.
+ """
+ name = cast(Any, dist).name
+ if not isinstance(name, str):
+ raise BadMetadata(dist, reason="invalid metadata entry 'name'")
+ return name
diff --git a/src/pip/_internal/metadata/importlib/_dists.py b/src/pip/_internal/metadata/importlib/_dists.py
new file mode 100644
index 000000000..65c043c87
--- /dev/null
+++ b/src/pip/_internal/metadata/importlib/_dists.py
@@ -0,0 +1,224 @@
+import email.message
+import importlib.metadata
+import os
+import pathlib
+import zipfile
+from typing import (
+ Collection,
+ Dict,
+ Iterable,
+ Iterator,
+ Mapping,
+ Optional,
+ Sequence,
+ cast,
+)
+
+from pip._vendor.packaging.requirements import Requirement
+from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
+from pip._vendor.packaging.version import parse as parse_version
+
+from pip._internal.exceptions import InvalidWheel, UnsupportedWheel
+from pip._internal.metadata.base import (
+ BaseDistribution,
+ BaseEntryPoint,
+ DistributionVersion,
+ InfoPath,
+ Wheel,
+)
+from pip._internal.utils.misc import normalize_path
+from pip._internal.utils.packaging import safe_extra
+from pip._internal.utils.temp_dir import TempDirectory
+from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
+
+from ._compat import BasePath, get_dist_name
+
+
+class WheelDistribution(importlib.metadata.Distribution):
+ """An ``importlib.metadata.Distribution`` read from a wheel.
+
+ Although ``importlib.metadata.PathDistribution`` accepts ``zipfile.Path``,
+ its implementation is too "lazy" for pip's needs (we can't keep the ZipFile
+ handle open for the entire lifetime of the distribution object).
+
+ This implementation eagerly reads the entire metadata directory into the
+ memory instead, and operates from that.
+ """
+
+ def __init__(
+ self,
+ files: Mapping[pathlib.PurePosixPath, bytes],
+ info_location: pathlib.PurePosixPath,
+ ) -> None:
+ self._files = files
+ self.info_location = info_location
+
+ @classmethod
+ def from_zipfile(
+ cls,
+ zf: zipfile.ZipFile,
+ name: str,
+ location: str,
+ ) -> "WheelDistribution":
+ info_dir, _ = parse_wheel(zf, name)
+ paths = (
+ (name, pathlib.PurePosixPath(name.split("/", 1)[-1]))
+ for name in zf.namelist()
+ if name.startswith(f"{info_dir}/")
+ )
+ files = {
+ relpath: read_wheel_metadata_file(zf, fullpath)
+ for fullpath, relpath in paths
+ }
+ info_location = pathlib.PurePosixPath(location, info_dir)
+ return cls(files, info_location)
+
+ def iterdir(self, path: InfoPath) -> Iterator[pathlib.PurePosixPath]:
+ # Only allow iterating through the metadata directory.
+ if pathlib.PurePosixPath(str(path)) in self._files:
+ return iter(self._files)
+ raise FileNotFoundError(path)
+
+ def read_text(self, filename: str) -> Optional[str]:
+ try:
+ data = self._files[pathlib.PurePosixPath(filename)]
+ except KeyError:
+ return None
+ try:
+ text = data.decode("utf-8")
+ except UnicodeDecodeError as e:
+ wheel = self.info_location.parent
+ error = f"Error decoding metadata for {wheel}: {e} in {filename} file"
+ raise UnsupportedWheel(error)
+ return text
+
+
+class Distribution(BaseDistribution):
+ def __init__(
+ self,
+ dist: importlib.metadata.Distribution,
+ info_location: Optional[BasePath],
+ installed_location: Optional[BasePath],
+ ) -> None:
+ self._dist = dist
+ self._info_location = info_location
+ self._installed_location = installed_location
+
+ @classmethod
+ def from_directory(cls, directory: str) -> BaseDistribution:
+ info_location = pathlib.Path(directory)
+ dist = importlib.metadata.Distribution.at(info_location)
+ return cls(dist, info_location, info_location.parent)
+
+ @classmethod
+ def from_metadata_file_contents(
+ cls,
+ metadata_contents: bytes,
+ filename: str,
+ project_name: str,
+ ) -> BaseDistribution:
+ # Generate temp dir to contain the metadata file, and write the file contents.
+ temp_dir = pathlib.Path(
+ TempDirectory(kind="metadata", globally_managed=True).path
+ )
+ metadata_path = temp_dir / "METADATA"
+ metadata_path.write_bytes(metadata_contents)
+ # Construct dist pointing to the newly created directory.
+ dist = importlib.metadata.Distribution.at(metadata_path.parent)
+ return cls(dist, metadata_path.parent, None)
+
+ @classmethod
+ def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
+ try:
+ with wheel.as_zipfile() as zf:
+ dist = WheelDistribution.from_zipfile(zf, name, wheel.location)
+ except zipfile.BadZipFile as e:
+ raise InvalidWheel(wheel.location, name) from e
+ except UnsupportedWheel as e:
+ raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
+ return cls(dist, dist.info_location, pathlib.PurePosixPath(wheel.location))
+
+ @property
+ def location(self) -> Optional[str]:
+ if self._info_location is None:
+ return None
+ return str(self._info_location.parent)
+
+ @property
+ def info_location(self) -> Optional[str]:
+ if self._info_location is None:
+ return None
+ return str(self._info_location)
+
+ @property
+ def installed_location(self) -> Optional[str]:
+ if self._installed_location is None:
+ return None
+ return normalize_path(str(self._installed_location))
+
+ def _get_dist_name_from_location(self) -> Optional[str]:
+ """Try to get the name from the metadata directory name.
+
+ This is much faster than reading metadata.
+ """
+ if self._info_location is None:
+ return None
+ stem, suffix = os.path.splitext(self._info_location.name)
+ if suffix not in (".dist-info", ".egg-info"):
+ return None
+ return stem.split("-", 1)[0]
+
+ @property
+ def canonical_name(self) -> NormalizedName:
+ name = self._get_dist_name_from_location() or get_dist_name(self._dist)
+ return canonicalize_name(name)
+
+ @property
+ def version(self) -> DistributionVersion:
+ return parse_version(self._dist.version)
+
+ def is_file(self, path: InfoPath) -> bool:
+ return self._dist.read_text(str(path)) is not None
+
+ def iter_distutils_script_names(self) -> Iterator[str]:
+ # A distutils installation is always "flat" (not in e.g. egg form), so
+ # if this distribution's info location is NOT a pathlib.Path (but e.g.
+ # zipfile.Path), it can never contain any distutils scripts.
+ if not isinstance(self._info_location, pathlib.Path):
+ return
+ for child in self._info_location.joinpath("scripts").iterdir():
+ yield child.name
+
+ def read_text(self, path: InfoPath) -> str:
+ content = self._dist.read_text(str(path))
+ if content is None:
+ raise FileNotFoundError(path)
+ return content
+
+ def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
+ # importlib.metadata's EntryPoint structure sasitfies BaseEntryPoint.
+ return self._dist.entry_points
+
+ def _metadata_impl(self) -> email.message.Message:
+ # From Python 3.10+, importlib.metadata declares PackageMetadata as the
+ # return type. This protocol is unfortunately a disaster now and misses
+ # a ton of fields that we need, including get() and get_payload(). We
+ # rely on the implementation that the object is actually a Message now,
+ # until upstream can improve the protocol. (python/cpython#94952)
+ return cast(email.message.Message, self._dist.metadata)
+
+ def iter_provided_extras(self) -> Iterable[str]:
+ return (
+ safe_extra(extra) for extra in self.metadata.get_all("Provides-Extra", [])
+ )
+
+ def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
+ contexts: Sequence[Dict[str, str]] = [{"extra": safe_extra(e)} for e in extras]
+ for req_string in self.metadata.get_all("Requires-Dist", []):
+ req = Requirement(req_string)
+ if not req.marker:
+ yield req
+ elif not extras and req.marker.evaluate({"extra": ""}):
+ yield req
+ elif any(req.marker.evaluate(context) for context in contexts):
+ yield req
diff --git a/src/pip/_internal/metadata/importlib/_envs.py b/src/pip/_internal/metadata/importlib/_envs.py
new file mode 100644
index 000000000..cbec59e2c
--- /dev/null
+++ b/src/pip/_internal/metadata/importlib/_envs.py
@@ -0,0 +1,188 @@
+import functools
+import importlib.metadata
+import logging
+import os
+import pathlib
+import sys
+import zipfile
+import zipimport
+from typing import Iterator, List, Optional, Sequence, Set, Tuple
+
+from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
+
+from pip._internal.metadata.base import BaseDistribution, BaseEnvironment
+from pip._internal.models.wheel import Wheel
+from pip._internal.utils.deprecation import deprecated
+from pip._internal.utils.filetypes import WHEEL_EXTENSION
+
+from ._compat import BadMetadata, BasePath, get_dist_name, get_info_location
+from ._dists import Distribution
+
+logger = logging.getLogger(__name__)
+
+
+def _looks_like_wheel(location: str) -> bool:
+ if not location.endswith(WHEEL_EXTENSION):
+ return False
+ if not os.path.isfile(location):
+ return False
+ if not Wheel.wheel_file_re.match(os.path.basename(location)):
+ return False
+ return zipfile.is_zipfile(location)
+
+
+class _DistributionFinder:
+ """Finder to locate distributions.
+
+ The main purpose of this class is to memoize found distributions' names, so
+ only one distribution is returned for each package name. At lot of pip code
+ assumes this (because it is setuptools's behavior), and not doing the same
+ can potentially cause a distribution in lower precedence path to override a
+ higher precedence one if the caller is not careful.
+
+ Eventually we probably want to make it possible to see lower precedence
+ installations as well. It's useful feature, after all.
+ """
+
+ FoundResult = Tuple[importlib.metadata.Distribution, Optional[BasePath]]
+
+ def __init__(self) -> None:
+ self._found_names: Set[NormalizedName] = set()
+
+ def _find_impl(self, location: str) -> Iterator[FoundResult]:
+ """Find distributions in a location."""
+ # Skip looking inside a wheel. Since a package inside a wheel is not
+ # always valid (due to .data directories etc.), its .dist-info entry
+ # should not be considered an installed distribution.
+ if _looks_like_wheel(location):
+ return
+ # To know exactly where we find a distribution, we have to feed in the
+ # paths one by one, instead of dumping the list to importlib.metadata.
+ for dist in importlib.metadata.distributions(path=[location]):
+ info_location = get_info_location(dist)
+ try:
+ raw_name = get_dist_name(dist)
+ except BadMetadata as e:
+ logger.warning("Skipping %s due to %s", info_location, e.reason)
+ continue
+ normalized_name = canonicalize_name(raw_name)
+ if normalized_name in self._found_names:
+ continue
+ self._found_names.add(normalized_name)
+ yield dist, info_location
+
+ def find(self, location: str) -> Iterator[BaseDistribution]:
+ """Find distributions in a location.
+
+ The path can be either a directory, or a ZIP archive.
+ """
+ for dist, info_location in self._find_impl(location):
+ if info_location is None:
+ installed_location: Optional[BasePath] = None
+ else:
+ installed_location = info_location.parent
+ yield Distribution(dist, info_location, installed_location)
+
+ def find_linked(self, location: str) -> Iterator[BaseDistribution]:
+ """Read location in egg-link files and return distributions in there.
+
+ The path should be a directory; otherwise this returns nothing. This
+ follows how setuptools does this for compatibility. The first non-empty
+ line in the egg-link is read as a path (resolved against the egg-link's
+ containing directory if relative). Distributions found at that linked
+ location are returned.
+ """
+ path = pathlib.Path(location)
+ if not path.is_dir():
+ return
+ for child in path.iterdir():
+ if child.suffix != ".egg-link":
+ continue
+ with child.open() as f:
+ lines = (line.strip() for line in f)
+ target_rel = next((line for line in lines if line), "")
+ if not target_rel:
+ continue
+ target_location = str(path.joinpath(target_rel))
+ for dist, info_location in self._find_impl(target_location):
+ yield Distribution(dist, info_location, path)
+
+ def _find_eggs_in_dir(self, location: str) -> Iterator[BaseDistribution]:
+ from pip._vendor.pkg_resources import find_distributions
+
+ from pip._internal.metadata import pkg_resources as legacy
+
+ with os.scandir(location) as it:
+ for entry in it:
+ if not entry.name.endswith(".egg"):
+ continue
+ for dist in find_distributions(entry.path):
+ yield legacy.Distribution(dist)
+
+ def _find_eggs_in_zip(self, location: str) -> Iterator[BaseDistribution]:
+ from pip._vendor.pkg_resources import find_eggs_in_zip
+
+ from pip._internal.metadata import pkg_resources as legacy
+
+ try:
+ importer = zipimport.zipimporter(location)
+ except zipimport.ZipImportError:
+ return
+ for dist in find_eggs_in_zip(importer, location):
+ yield legacy.Distribution(dist)
+
+ def find_eggs(self, location: str) -> Iterator[BaseDistribution]:
+ """Find eggs in a location.
+
+ This actually uses the old *pkg_resources* backend. We likely want to
+ deprecate this so we can eventually remove the *pkg_resources*
+ dependency entirely. Before that, this should first emit a deprecation
+ warning for some versions when using the fallback since importing
+ *pkg_resources* is slow for those who don't need it.
+ """
+ if os.path.isdir(location):
+ yield from self._find_eggs_in_dir(location)
+ if zipfile.is_zipfile(location):
+ yield from self._find_eggs_in_zip(location)
+
+
+@functools.lru_cache(maxsize=None) # Warn a distribution exactly once.
+def _emit_egg_deprecation(location: Optional[str]) -> None:
+ deprecated(
+ reason=f"Loading egg at {location} is deprecated.",
+ replacement="to use pip for package installation.",
+ gone_in=None,
+ )
+
+
+class Environment(BaseEnvironment):
+ def __init__(self, paths: Sequence[str]) -> None:
+ self._paths = paths
+
+ @classmethod
+ def default(cls) -> BaseEnvironment:
+ return cls(sys.path)
+
+ @classmethod
+ def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
+ if paths is None:
+ return cls(sys.path)
+ return cls(paths)
+
+ def _iter_distributions(self) -> Iterator[BaseDistribution]:
+ finder = _DistributionFinder()
+ for location in self._paths:
+ yield from finder.find(location)
+ for dist in finder.find_eggs(location):
+ # _emit_egg_deprecation(dist.location) # TODO: Enable this.
+ yield dist
+ # This must go last because that's how pkg_resources tie-breaks.
+ yield from finder.find_linked(location)
+
+ def get_distribution(self, name: str) -> Optional[BaseDistribution]:
+ matches = (
+ distribution
+ for distribution in self.iter_all_distributions()
+ if distribution.canonical_name == canonicalize_name(name)
+ )
+ return next(matches, None)
diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py
index 59460062e..f330ef12a 100644
--- a/src/pip/_internal/metadata/pkg_resources.py
+++ b/src/pip/_internal/metadata/pkg_resources.py
@@ -1,29 +1,28 @@
import email.message
+import email.parser
import logging
+import os
import zipfile
-from typing import (
- TYPE_CHECKING,
- Collection,
- Iterable,
- Iterator,
- List,
- NamedTuple,
- Optional,
-)
+from typing import Collection, Iterable, Iterator, List, Mapping, NamedTuple, Optional
from pip._vendor import pkg_resources
from pip._vendor.packaging.requirements import Requirement
-from pip._vendor.packaging.utils import canonicalize_name
+from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import parse as parse_version
-from pip._internal.utils import misc # TODO: Move definition here.
-from pip._internal.utils.packaging import get_installer, get_metadata
-from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel
-
-from .base import BaseDistribution, BaseEntryPoint, BaseEnvironment, DistributionVersion
-
-if TYPE_CHECKING:
- from pip._vendor.packaging.utils import NormalizedName
+from pip._internal.exceptions import InvalidWheel, NoneMetadataError, UnsupportedWheel
+from pip._internal.utils.egg_link import egg_link_path_from_location
+from pip._internal.utils.misc import display_path, normalize_path
+from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
+
+from .base import (
+ BaseDistribution,
+ BaseEntryPoint,
+ BaseEnvironment,
+ DistributionVersion,
+ InfoPath,
+ Wheel,
+)
logger = logging.getLogger(__name__)
@@ -34,14 +33,101 @@ class EntryPoint(NamedTuple):
group: str
+class InMemoryMetadata:
+ """IMetadataProvider that reads metadata files from a dictionary.
+
+ This also maps metadata decoding exceptions to our internal exception type.
+ """
+
+ def __init__(self, metadata: Mapping[str, bytes], wheel_name: str) -> None:
+ self._metadata = metadata
+ self._wheel_name = wheel_name
+
+ def has_metadata(self, name: str) -> bool:
+ return name in self._metadata
+
+ def get_metadata(self, name: str) -> str:
+ try:
+ return self._metadata[name].decode()
+ except UnicodeDecodeError as e:
+ # Augment the default error with the origin of the file.
+ raise UnsupportedWheel(
+ f"Error decoding metadata for {self._wheel_name}: {e} in {name} file"
+ )
+
+ def get_metadata_lines(self, name: str) -> Iterable[str]:
+ return pkg_resources.yield_lines(self.get_metadata(name))
+
+ def metadata_isdir(self, name: str) -> bool:
+ return False
+
+ def metadata_listdir(self, name: str) -> List[str]:
+ return []
+
+ def run_script(self, script_name: str, namespace: str) -> None:
+ pass
+
+
class Distribution(BaseDistribution):
def __init__(self, dist: pkg_resources.Distribution) -> None:
self._dist = dist
@classmethod
- def from_wheel(cls, path: str, name: str) -> "Distribution":
- with zipfile.ZipFile(path, allowZip64=True) as zf:
- dist = pkg_resources_distribution_for_wheel(zf, name, path)
+ def from_directory(cls, directory: str) -> BaseDistribution:
+ dist_dir = directory.rstrip(os.sep)
+
+ # Build a PathMetadata object, from path to metadata. :wink:
+ base_dir, dist_dir_name = os.path.split(dist_dir)
+ metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
+
+ # Determine the correct Distribution object type.
+ if dist_dir.endswith(".egg-info"):
+ dist_cls = pkg_resources.Distribution
+ dist_name = os.path.splitext(dist_dir_name)[0]
+ else:
+ assert dist_dir.endswith(".dist-info")
+ dist_cls = pkg_resources.DistInfoDistribution
+ dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]
+
+ dist = dist_cls(base_dir, project_name=dist_name, metadata=metadata)
+ return cls(dist)
+
+ @classmethod
+ def from_metadata_file_contents(
+ cls,
+ metadata_contents: bytes,
+ filename: str,
+ project_name: str,
+ ) -> BaseDistribution:
+ metadata_dict = {
+ "METADATA": metadata_contents,
+ }
+ dist = pkg_resources.DistInfoDistribution(
+ location=filename,
+ metadata=InMemoryMetadata(metadata_dict, filename),
+ project_name=project_name,
+ )
+ return cls(dist)
+
+ @classmethod
+ def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
+ try:
+ with wheel.as_zipfile() as zf:
+ info_dir, _ = parse_wheel(zf, name)
+ metadata_dict = {
+ path.split("/", 1)[-1]: read_wheel_metadata_file(zf, path)
+ for path in zf.namelist()
+ if path.startswith(f"{info_dir}/")
+ }
+ except zipfile.BadZipFile as e:
+ raise InvalidWheel(wheel.location, name) from e
+ except UnsupportedWheel as e:
+ raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
+ dist = pkg_resources.DistInfoDistribution(
+ location=wheel.location,
+ metadata=InMemoryMetadata(metadata_dict, wheel.location),
+ project_name=name,
+ )
return cls(dist)
@property
@@ -49,41 +135,52 @@ class Distribution(BaseDistribution):
return self._dist.location
@property
- def info_directory(self) -> Optional[str]:
- return self._dist.egg_info
-
- @property
- def canonical_name(self) -> "NormalizedName":
- return canonicalize_name(self._dist.project_name)
+ def installed_location(self) -> Optional[str]:
+ egg_link = egg_link_path_from_location(self.raw_name)
+ if egg_link:
+ location = egg_link
+ elif self.location:
+ location = self.location
+ else:
+ return None
+ return normalize_path(location)
@property
- def version(self) -> DistributionVersion:
- return parse_version(self._dist.version)
+ def info_location(self) -> Optional[str]:
+ return self._dist.egg_info
@property
- def installer(self) -> str:
- return get_installer(self._dist)
+ def installed_by_distutils(self) -> bool:
+ # A distutils-installed distribution is provided by FileMetadata. This
+ # provider has a "path" attribute not present anywhere else. Not the
+ # best introspection logic, but pip has been doing this for a long time.
+ try:
+ return bool(self._dist._provider.path)
+ except AttributeError:
+ return False
@property
- def editable(self) -> bool:
- return misc.dist_is_editable(self._dist)
+ def canonical_name(self) -> NormalizedName:
+ return canonicalize_name(self._dist.project_name)
@property
- def local(self) -> bool:
- return misc.dist_is_local(self._dist)
+ def version(self) -> DistributionVersion:
+ return parse_version(self._dist.version)
- @property
- def in_usersite(self) -> bool:
- return misc.dist_in_usersite(self._dist)
+ def is_file(self, path: InfoPath) -> bool:
+ return self._dist.has_metadata(str(path))
- @property
- def in_site_packages(self) -> bool:
- return misc.dist_in_site_packages(self._dist)
+ def iter_distutils_script_names(self) -> Iterator[str]:
+ yield from self._dist.metadata_listdir("scripts")
- def read_text(self, name: str) -> str:
+ def read_text(self, path: InfoPath) -> str:
+ name = str(path)
if not self._dist.has_metadata(name):
raise FileNotFoundError(name)
- return self._dist.get_metadata(name)
+ content = self._dist.get_metadata(name)
+ if content is None:
+ raise NoneMetadataError(self, name)
+ return content
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
for group, entries in self._dist.get_entry_map().items():
@@ -91,15 +188,36 @@ class Distribution(BaseDistribution):
name, _, value = str(entry_point).partition("=")
yield EntryPoint(name=name.strip(), value=value.strip(), group=group)
- @property
- def metadata(self) -> email.message.Message:
- return get_metadata(self._dist)
+ def _metadata_impl(self) -> email.message.Message:
+ """
+ :raises NoneMetadataError: if the distribution reports `has_metadata()`
+ True but `get_metadata()` returns None.
+ """
+ if isinstance(self._dist, pkg_resources.DistInfoDistribution):
+ metadata_name = "METADATA"
+ else:
+ metadata_name = "PKG-INFO"
+ try:
+ metadata = self.read_text(metadata_name)
+ except FileNotFoundError:
+ if self.location:
+ displaying_path = display_path(self.location)
+ else:
+ displaying_path = repr(self.location)
+ logger.warning("No metadata found in %s", displaying_path)
+ metadata = ""
+ feed_parser = email.parser.FeedParser()
+ feed_parser.feed(metadata)
+ return feed_parser.close()
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
if extras: # pkg_resources raises on invalid extras, so we sanitize.
extras = frozenset(extras).intersection(self._dist.extras)
return self._dist.requires(extras)
+ def iter_provided_extras(self) -> Iterable[str]:
+ return self._dist.extras
+
class Environment(BaseEnvironment):
def __init__(self, ws: pkg_resources.WorkingSet) -> None:
@@ -113,6 +231,10 @@ class Environment(BaseEnvironment):
def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
return cls(pkg_resources.WorkingSet(paths))
+ def _iter_distributions(self) -> Iterator[BaseDistribution]:
+ for dist in self._ws:
+ yield Distribution(dist)
+
def _search_distribution(self, name: str) -> Optional[BaseDistribution]:
"""Find a distribution matching the ``name`` in the environment.
@@ -120,13 +242,12 @@ class Environment(BaseEnvironment):
match the behavior of ``pkg_resources.get_distribution()``.
"""
canonical_name = canonicalize_name(name)
- for dist in self.iter_distributions():
+ for dist in self.iter_all_distributions():
if dist.canonical_name == canonical_name:
return dist
return None
def get_distribution(self, name: str) -> Optional[BaseDistribution]:
-
# Search the distribution by looking through the working set.
dist = self._search_distribution(name)
if dist:
@@ -147,7 +268,3 @@ class Environment(BaseEnvironment):
except pkg_resources.DistributionNotFound:
return None
return self._search_distribution(name)
-
- def _iter_distributions(self) -> Iterator[BaseDistribution]:
- for dist in self._ws:
- yield Distribution(dist)
diff --git a/src/pip/_internal/models/candidate.py b/src/pip/_internal/models/candidate.py
index c673d8d05..a4963aec6 100644
--- a/src/pip/_internal/models/candidate.py
+++ b/src/pip/_internal/models/candidate.py
@@ -5,8 +5,7 @@ from pip._internal.utils.models import KeyBasedCompareMixin
class InstallationCandidate(KeyBasedCompareMixin):
- """Represents a potential "candidate" for installation.
- """
+ """Represents a potential "candidate" for installation."""
__slots__ = ["name", "version", "link"]
@@ -17,15 +16,19 @@ class InstallationCandidate(KeyBasedCompareMixin):
super().__init__(
key=(self.name, self.version, self.link),
- defining_class=InstallationCandidate
+ defining_class=InstallationCandidate,
)
def __repr__(self) -> str:
return "<InstallationCandidate({!r}, {!r}, {!r})>".format(
- self.name, self.version, self.link,
+ self.name,
+ self.version,
+ self.link,
)
def __str__(self) -> str:
- return '{!r} candidate (version {} at {})'.format(
- self.name, self.version, self.link,
+ return "{!r} candidate (version {} at {})".format(
+ self.name,
+ self.version,
+ self.link,
)
diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py
index 3f9b6993e..e75feda9c 100644
--- a/src/pip/_internal/models/direct_url.py
+++ b/src/pip/_internal/models/direct_url.py
@@ -74,14 +74,10 @@ class VcsInfo:
vcs: str,
commit_id: str,
requested_revision: Optional[str] = None,
- resolved_revision: Optional[str] = None,
- resolved_revision_type: Optional[str] = None,
) -> None:
self.vcs = vcs
self.requested_revision = requested_revision
self.commit_id = commit_id
- self.resolved_revision = resolved_revision
- self.resolved_revision_type = resolved_revision_type
@classmethod
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["VcsInfo"]:
@@ -91,8 +87,6 @@ class VcsInfo:
vcs=_get_required(d, str, "vcs"),
commit_id=_get_required(d, str, "commit_id"),
requested_revision=_get(d, str, "requested_revision"),
- resolved_revision=_get(d, str, "resolved_revision"),
- resolved_revision_type=_get(d, str, "resolved_revision_type"),
)
def _to_dict(self) -> Dict[str, Any]:
@@ -100,8 +94,6 @@ class VcsInfo:
vcs=self.vcs,
requested_revision=self.requested_revision,
commit_id=self.commit_id,
- resolved_revision=self.resolved_revision,
- resolved_revision_type=self.resolved_revision_type,
)
@@ -137,9 +129,7 @@ class DirInfo:
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["DirInfo"]:
if d is None:
return None
- return cls(
- editable=_get_required(d, bool, "editable", default=False)
- )
+ return cls(editable=_get_required(d, bool, "editable", default=False))
def _to_dict(self) -> Dict[str, Any]:
return _filter_none(editable=self.editable or None)
@@ -149,7 +139,6 @@ InfoType = Union[ArchiveInfo, DirInfo, VcsInfo]
class DirectUrl:
-
def __init__(
self,
url: str,
@@ -165,9 +154,9 @@ class DirectUrl:
return netloc
user_pass, netloc_no_user_pass = netloc.split("@", 1)
if (
- isinstance(self.info, VcsInfo) and
- self.info.vcs == "git" and
- user_pass == "git"
+ isinstance(self.info, VcsInfo)
+ and self.info.vcs == "git"
+ and user_pass == "git"
):
return netloc
if ENV_VAR_RE.match(user_pass):
@@ -218,3 +207,6 @@ class DirectUrl:
def to_json(self) -> str:
return json.dumps(self.to_dict(), sort_keys=True)
+
+ def is_local_editable(self) -> bool:
+ return isinstance(self.info, DirInfo) and self.info.editable
diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py
index 010c3620d..db3995eac 100644
--- a/src/pip/_internal/models/format_control.py
+++ b/src/pip/_internal/models/format_control.py
@@ -6,15 +6,14 @@ from pip._internal.exceptions import CommandError
class FormatControl:
- """Helper for managing formats from which a package can be installed.
- """
+ """Helper for managing formats from which a package can be installed."""
__slots__ = ["no_binary", "only_binary"]
def __init__(
self,
no_binary: Optional[Set[str]] = None,
- only_binary: Optional[Set[str]] = None
+ only_binary: Optional[Set[str]] = None,
) -> None:
if no_binary is None:
no_binary = set()
@@ -31,35 +30,30 @@ class FormatControl:
if self.__slots__ != other.__slots__:
return False
- return all(
- getattr(self, k) == getattr(other, k)
- for k in self.__slots__
- )
+ return all(getattr(self, k) == getattr(other, k) for k in self.__slots__)
def __repr__(self) -> str:
return "{}({}, {})".format(
- self.__class__.__name__,
- self.no_binary,
- self.only_binary
+ self.__class__.__name__, self.no_binary, self.only_binary
)
@staticmethod
def handle_mutual_excludes(value: str, target: Set[str], other: Set[str]) -> None:
- if value.startswith('-'):
+ if value.startswith("-"):
raise CommandError(
"--no-binary / --only-binary option requires 1 argument."
)
- new = value.split(',')
- while ':all:' in new:
+ new = value.split(",")
+ while ":all:" in new:
other.clear()
target.clear()
- target.add(':all:')
- del new[:new.index(':all:') + 1]
+ target.add(":all:")
+ del new[: new.index(":all:") + 1]
# Without a none, we want to discard everything as :all: covers it
- if ':none:' not in new:
+ if ":none:" not in new:
return
for name in new:
- if name == ':none:':
+ if name == ":none:":
target.clear()
continue
name = canonicalize_name(name)
@@ -69,16 +63,18 @@ class FormatControl:
def get_allowed_formats(self, canonical_name: str) -> FrozenSet[str]:
result = {"binary", "source"}
if canonical_name in self.only_binary:
- result.discard('source')
+ result.discard("source")
elif canonical_name in self.no_binary:
- result.discard('binary')
- elif ':all:' in self.only_binary:
- result.discard('source')
- elif ':all:' in self.no_binary:
- result.discard('binary')
+ result.discard("binary")
+ elif ":all:" in self.only_binary:
+ result.discard("source")
+ elif ":all:" in self.no_binary:
+ result.discard("binary")
return frozenset(result)
def disallow_binaries(self) -> None:
self.handle_mutual_excludes(
- ':all:', self.no_binary, self.only_binary,
+ ":all:",
+ self.no_binary,
+ self.only_binary,
)
diff --git a/src/pip/_internal/models/index.py b/src/pip/_internal/models/index.py
index 1874a5b60..b94c32511 100644
--- a/src/pip/_internal/models/index.py
+++ b/src/pip/_internal/models/index.py
@@ -2,18 +2,16 @@ import urllib.parse
class PackageIndex:
- """Represents a Package Index and provides easier access to endpoints
- """
+ """Represents a Package Index and provides easier access to endpoints"""
- __slots__ = ['url', 'netloc', 'simple_url', 'pypi_url',
- 'file_storage_domain']
+ __slots__ = ["url", "netloc", "simple_url", "pypi_url", "file_storage_domain"]
def __init__(self, url: str, file_storage_domain: str) -> None:
super().__init__()
self.url = url
self.netloc = urllib.parse.urlsplit(url).netloc
- self.simple_url = self._url_for_path('simple')
- self.pypi_url = self._url_for_path('pypi')
+ self.simple_url = self._url_for_path("simple")
+ self.pypi_url = self._url_for_path("pypi")
# This is part of a temporary hack used to block installs of PyPI
# packages which depend on external urls only necessary until PyPI can
@@ -24,9 +22,7 @@ class PackageIndex:
return urllib.parse.urljoin(self.url, path)
-PyPI = PackageIndex(
- 'https://pypi.org/', file_storage_domain='files.pythonhosted.org'
-)
+PyPI = PackageIndex("https://pypi.org/", file_storage_domain="files.pythonhosted.org")
TestPyPI = PackageIndex(
- 'https://test.pypi.org/', file_storage_domain='test-files.pythonhosted.org'
+ "https://test.pypi.org/", file_storage_domain="test-files.pythonhosted.org"
)
diff --git a/src/pip/_internal/models/installation_report.py b/src/pip/_internal/models/installation_report.py
new file mode 100644
index 000000000..965f09523
--- /dev/null
+++ b/src/pip/_internal/models/installation_report.py
@@ -0,0 +1,53 @@
+from typing import Any, Dict, Sequence
+
+from pip._vendor.packaging.markers import default_environment
+
+from pip import __version__
+from pip._internal.req.req_install import InstallRequirement
+
+
+class InstallationReport:
+ def __init__(self, install_requirements: Sequence[InstallRequirement]):
+ self._install_requirements = install_requirements
+
+ @classmethod
+ def _install_req_to_dict(cls, ireq: InstallRequirement) -> Dict[str, Any]:
+ assert ireq.download_info, f"No download_info for {ireq}"
+ res = {
+ # PEP 610 json for the download URL. download_info.archive_info.hash may
+ # be absent when the requirement was installed from the wheel cache
+ # and the cache entry was populated by an older pip version that did not
+ # record origin.json.
+ "download_info": ireq.download_info.to_dict(),
+ # is_direct is true if the requirement was a direct URL reference (which
+ # includes editable requirements), and false if the requirement was
+ # downloaded from a PEP 503 index or --find-links.
+ "is_direct": bool(ireq.original_link),
+ # requested is true if the requirement was specified by the user (aka
+ # top level requirement), and false if it was installed as a dependency of a
+ # requirement. https://peps.python.org/pep-0376/#requested
+ "requested": ireq.user_supplied,
+ # PEP 566 json encoding for metadata
+ # https://www.python.org/dev/peps/pep-0566/#json-compatible-metadata
+ "metadata": ireq.get_dist().metadata_dict,
+ }
+ if ireq.user_supplied and ireq.extras:
+ # For top level requirements, the list of requested extras, if any.
+ res["requested_extras"] = list(sorted(ireq.extras))
+ return res
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "version": "0",
+ "pip_version": __version__,
+ "install": [
+ self._install_req_to_dict(ireq) for ireq in self._install_requirements
+ ],
+ # https://peps.python.org/pep-0508/#environment-markers
+ # TODO: currently, the resolver uses the default environment to evaluate
+ # environment markers, so that is what we report here. In the future, it
+ # should also take into account options such as --python-version or
+ # --platform, perhaps under the form of an environment_override field?
+ # https://github.com/pypa/pip/issues/11198
+ "environment": default_environment(),
+ }
diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py
index eb656fa01..c792d128b 100644
--- a/src/pip/_internal/models/link.py
+++ b/src/pip/_internal/models/link.py
@@ -1,14 +1,27 @@
import functools
+import itertools
import logging
import os
import posixpath
import re
import urllib.parse
-from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Tuple, Union
+from dataclasses import dataclass
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Dict,
+ List,
+ Mapping,
+ NamedTuple,
+ Optional,
+ Tuple,
+ Union,
+)
from pip._internal.utils.filetypes import WHEEL_EXTENSION
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.misc import (
+ pairwise,
redact_auth_from_url,
split_auth_from_netloc,
splitext,
@@ -17,38 +30,158 @@ from pip._internal.utils.models import KeyBasedCompareMixin
from pip._internal.utils.urls import path_to_url, url_to_path
if TYPE_CHECKING:
- from pip._internal.index.collector import HTMLPage
+ from pip._internal.index.collector import IndexContent
logger = logging.getLogger(__name__)
-_SUPPORTED_HASHES = ("sha1", "sha224", "sha384", "sha256", "sha512", "md5")
+# Order matters, earlier hashes have a precedence over later hashes for what
+# we will pick to use.
+_SUPPORTED_HASHES = ("sha512", "sha384", "sha256", "sha224", "sha1", "md5")
-class Link(KeyBasedCompareMixin):
- """Represents a parsed link from a Package Index's simple URL
+@dataclass(frozen=True)
+class LinkHash:
+ """Links to content may have embedded hash values. This class parses those.
+
+ `name` must be any member of `_SUPPORTED_HASHES`.
+
+ This class can be converted to and from `ArchiveInfo`. While ArchiveInfo intends to
+ be JSON-serializable to conform to PEP 610, this class contains the logic for
+ parsing a hash name and value for correctness, and then checking whether that hash
+ conforms to a schema with `.is_hash_allowed()`."""
+
+ name: str
+ value: str
+
+ _hash_re = re.compile(
+ # NB: we do not validate that the second group (.*) is a valid hex
+ # digest. Instead, we simply keep that string in this class, and then check it
+ # against Hashes when hash-checking is needed. This is easier to debug than
+ # proactively discarding an invalid hex digest, as we handle incorrect hashes
+ # and malformed hashes in the same place.
+ r"({choices})=(.*)".format(
+ choices="|".join(re.escape(hash_name) for hash_name in _SUPPORTED_HASHES)
+ ),
+ )
+
+ def __post_init__(self) -> None:
+ assert self._hash_re.match(f"{self.name}={self.value}")
+
+ @classmethod
+ @functools.lru_cache(maxsize=None)
+ def split_hash_name_and_value(cls, url: str) -> Optional["LinkHash"]:
+ """Search a string for a checksum algorithm name and encoded output value."""
+ match = cls._hash_re.search(url)
+ if match is None:
+ return None
+ name, value = match.groups()
+ return cls(name=name, value=value)
+
+ def as_hashes(self) -> Hashes:
+ """Return a Hashes instance which checks only for the current hash."""
+ return Hashes({self.name: [self.value]})
+
+ def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
+ """
+ Return True if the current hash is allowed by `hashes`.
+ """
+ if hashes is None:
+ return False
+ return hashes.is_hash_allowed(self.name, hex_digest=self.value)
+
+
+def _clean_url_path_part(part: str) -> str:
+ """
+ Clean a "part" of a URL path (i.e. after splitting on "@" characters).
+ """
+ # We unquote prior to quoting to make sure nothing is double quoted.
+ return urllib.parse.quote(urllib.parse.unquote(part))
+
+
+def _clean_file_url_path(part: str) -> str:
+ """
+ Clean the first part of a URL path that corresponds to a local
+ filesystem path (i.e. the first part after splitting on "@" characters).
+ """
+ # We unquote prior to quoting to make sure nothing is double quoted.
+ # Also, on Windows the path part might contain a drive letter which
+ # should not be quoted. On Linux where drive letters do not
+ # exist, the colon should be quoted. We rely on urllib.request
+ # to do the right thing here.
+ return urllib.request.pathname2url(urllib.request.url2pathname(part))
+
+
+# percent-encoded: /
+_reserved_chars_re = re.compile("(@|%2F)", re.IGNORECASE)
+
+
+def _clean_url_path(path: str, is_local_path: bool) -> str:
+ """
+ Clean the path portion of a URL.
+ """
+ if is_local_path:
+ clean_func = _clean_file_url_path
+ else:
+ clean_func = _clean_url_path_part
+
+ # Split on the reserved characters prior to cleaning so that
+ # revision strings in VCS URLs are properly preserved.
+ parts = _reserved_chars_re.split(path)
+
+ cleaned_parts = []
+ for to_clean, reserved in pairwise(itertools.chain(parts, [""])):
+ cleaned_parts.append(clean_func(to_clean))
+ # Normalize %xx escapes (e.g. %2f -> %2F)
+ cleaned_parts.append(reserved.upper())
+
+ return "".join(cleaned_parts)
+
+
+def _ensure_quoted_url(url: str) -> str:
+ """
+ Make sure a link is fully quoted.
+ For example, if ' ' occurs in the URL, it will be replaced with "%20",
+ and without double-quoting other characters.
"""
+ # Split the URL into parts according to the general structure
+ # `scheme://netloc/path;parameters?query#fragment`.
+ result = urllib.parse.urlparse(url)
+ # If the netloc is empty, then the URL refers to a local filesystem path.
+ is_local_path = not result.netloc
+ path = _clean_url_path(result.path, is_local_path=is_local_path)
+ return urllib.parse.urlunparse(result._replace(path=path))
+
+
+class Link(KeyBasedCompareMixin):
+ """Represents a parsed link from a Package Index's simple URL"""
__slots__ = [
"_parsed_url",
"_url",
+ "_hashes",
"comes_from",
"requires_python",
"yanked_reason",
+ "dist_info_metadata",
+ "link_hash",
"cache_link_parsing",
]
def __init__(
self,
url: str,
- comes_from: Optional[Union[str, "HTMLPage"]] = None,
+ comes_from: Optional[Union[str, "IndexContent"]] = None,
requires_python: Optional[str] = None,
yanked_reason: Optional[str] = None,
+ dist_info_metadata: Optional[str] = None,
+ link_hash: Optional[LinkHash] = None,
cache_link_parsing: bool = True,
+ hashes: Optional[Mapping[str, str]] = None,
) -> None:
"""
:param url: url of the resource pointed to (href of the link)
- :param comes_from: instance of HTMLPage where the link was found,
+ :param comes_from: instance of IndexContent where the link was found,
or string.
:param requires_python: String containing the `Requires-Python`
metadata field, specified in PEP 345. This may be specified by
@@ -60,43 +193,119 @@ class Link(KeyBasedCompareMixin):
a simple repository HTML link. If the file has been yanked but
no reason was provided, this should be the empty string. See
PEP 592 for more information and the specification.
+ :param dist_info_metadata: the metadata attached to the file, or None if no such
+ metadata is provided. This is the value of the "data-dist-info-metadata"
+ attribute, if present, in a simple repository HTML link. This may be parsed
+ into its own `Link` by `self.metadata_link()`. See PEP 658 for more
+ information and the specification.
+ :param link_hash: a checksum for the content the link points to. If not
+ provided, this will be extracted from the link URL, if the URL has
+ any checksum.
:param cache_link_parsing: A flag that is used elsewhere to determine
whether resources retrieved from this link
should be cached. PyPI index urls should
generally have this set to False, for
example.
+ :param hashes: A mapping of hash names to digests to allow us to
+ determine the validity of a download.
"""
# url can be a UNC windows share
- if url.startswith('\\\\'):
+ if url.startswith("\\\\"):
url = path_to_url(url)
self._parsed_url = urllib.parse.urlsplit(url)
# Store the url as a private attribute to prevent accidentally
# trying to set a new value.
self._url = url
+ self._hashes = hashes if hashes is not None else {}
self.comes_from = comes_from
self.requires_python = requires_python if requires_python else None
self.yanked_reason = yanked_reason
+ self.dist_info_metadata = dist_info_metadata
+ self.link_hash = link_hash or LinkHash.split_hash_name_and_value(self._url)
super().__init__(key=url, defining_class=Link)
self.cache_link_parsing = cache_link_parsing
+ @classmethod
+ def from_json(
+ cls,
+ file_data: Dict[str, Any],
+ page_url: str,
+ ) -> Optional["Link"]:
+ """
+ Convert an pypi json document from a simple repository page into a Link.
+ """
+ file_url = file_data.get("url")
+ if file_url is None:
+ return None
+
+ url = _ensure_quoted_url(urllib.parse.urljoin(page_url, file_url))
+ pyrequire = file_data.get("requires-python")
+ yanked_reason = file_data.get("yanked")
+ dist_info_metadata = file_data.get("dist-info-metadata")
+ hashes = file_data.get("hashes", {})
+
+ # The Link.yanked_reason expects an empty string instead of a boolean.
+ if yanked_reason and not isinstance(yanked_reason, str):
+ yanked_reason = ""
+ # The Link.yanked_reason expects None instead of False.
+ elif not yanked_reason:
+ yanked_reason = None
+
+ return cls(
+ url,
+ comes_from=page_url,
+ requires_python=pyrequire,
+ yanked_reason=yanked_reason,
+ hashes=hashes,
+ dist_info_metadata=dist_info_metadata,
+ )
+
+ @classmethod
+ def from_element(
+ cls,
+ anchor_attribs: Dict[str, Optional[str]],
+ page_url: str,
+ base_url: str,
+ ) -> Optional["Link"]:
+ """
+ Convert an anchor element's attributes in a simple repository page to a Link.
+ """
+ href = anchor_attribs.get("href")
+ if not href:
+ return None
+
+ url = _ensure_quoted_url(urllib.parse.urljoin(base_url, href))
+ pyrequire = anchor_attribs.get("data-requires-python")
+ yanked_reason = anchor_attribs.get("data-yanked")
+ dist_info_metadata = anchor_attribs.get("data-dist-info-metadata")
+
+ return cls(
+ url,
+ comes_from=page_url,
+ requires_python=pyrequire,
+ yanked_reason=yanked_reason,
+ dist_info_metadata=dist_info_metadata,
+ )
+
def __str__(self) -> str:
if self.requires_python:
- rp = f' (requires-python:{self.requires_python})'
+ rp = f" (requires-python:{self.requires_python})"
else:
- rp = ''
+ rp = ""
if self.comes_from:
- return '{} (from {}){}'.format(
- redact_auth_from_url(self._url), self.comes_from, rp)
+ return "{} (from {}){}".format(
+ redact_auth_from_url(self._url), self.comes_from, rp
+ )
else:
return redact_auth_from_url(str(self._url))
def __repr__(self) -> str:
- return f'<Link {self}>'
+ return f"<Link {self}>"
@property
def url(self) -> str:
@@ -104,7 +313,7 @@ class Link(KeyBasedCompareMixin):
@property
def filename(self) -> str:
- path = self.path.rstrip('/')
+ path = self.path.rstrip("/")
name = posixpath.basename(path)
if not name:
# Make sure we don't leak auth information if the netloc
@@ -113,7 +322,7 @@ class Link(KeyBasedCompareMixin):
return netloc
name = urllib.parse.unquote(name)
- assert name, f'URL {self._url!r} produced no filename'
+ assert name, f"URL {self._url!r} produced no filename"
return name
@property
@@ -136,7 +345,7 @@ class Link(KeyBasedCompareMixin):
return urllib.parse.unquote(self._parsed_url.path)
def splitext(self) -> Tuple[str, str]:
- return splitext(posixpath.basename(self.path.rstrip('/')))
+ return splitext(posixpath.basename(self.path.rstrip("/")))
@property
def ext(self) -> str:
@@ -145,9 +354,9 @@ class Link(KeyBasedCompareMixin):
@property
def url_without_fragment(self) -> str:
scheme, netloc, path, query, fragment = self._parsed_url
- return urllib.parse.urlunsplit((scheme, netloc, path, query, ''))
+ return urllib.parse.urlunsplit((scheme, netloc, path, query, ""))
- _egg_fragment_re = re.compile(r'[#&]egg=([^&]*)')
+ _egg_fragment_re = re.compile(r"[#&]egg=([^&]*)")
@property
def egg_fragment(self) -> Optional[str]:
@@ -156,7 +365,7 @@ class Link(KeyBasedCompareMixin):
return None
return match.group(1)
- _subdirectory_fragment_re = re.compile(r'[#&]subdirectory=([^&]*)')
+ _subdirectory_fragment_re = re.compile(r"[#&]subdirectory=([^&]*)")
@property
def subdirectory_fragment(self) -> Optional[str]:
@@ -165,31 +374,45 @@ class Link(KeyBasedCompareMixin):
return None
return match.group(1)
- _hash_re = re.compile(
- r'({choices})=([a-f0-9]+)'.format(choices="|".join(_SUPPORTED_HASHES))
- )
+ def metadata_link(self) -> Optional["Link"]:
+ """Implementation of PEP 658 parsing."""
+ # Note that Link.from_element() parsing the "data-dist-info-metadata" attribute
+ # from an HTML anchor tag is typically how the Link.dist_info_metadata attribute
+ # gets set.
+ if self.dist_info_metadata is None:
+ return None
+ metadata_url = f"{self.url_without_fragment}.metadata"
+ link_hash: Optional[LinkHash] = None
+ # If data-dist-info-metadata="true" is set, then the metadata file exists,
+ # but there is no information about its checksum or anything else.
+ if self.dist_info_metadata != "true":
+ link_hash = LinkHash.split_hash_name_and_value(self.dist_info_metadata)
+ return Link(metadata_url, link_hash=link_hash)
+
+ def as_hashes(self) -> Optional[Hashes]:
+ if self.link_hash is not None:
+ return self.link_hash.as_hashes()
+ return None
@property
def hash(self) -> Optional[str]:
- match = self._hash_re.search(self._url)
- if match:
- return match.group(2)
+ if self.link_hash is not None:
+ return self.link_hash.value
return None
@property
def hash_name(self) -> Optional[str]:
- match = self._hash_re.search(self._url)
- if match:
- return match.group(1)
+ if self.link_hash is not None:
+ return self.link_hash.name
return None
@property
def show_url(self) -> str:
- return posixpath.basename(self._url.split('#', 1)[0].split('?', 1)[0])
+ return posixpath.basename(self._url.split("#", 1)[0].split("?", 1)[0])
@property
def is_file(self) -> bool:
- return self.scheme == 'file'
+ return self.scheme == "file"
def is_existing_dir(self) -> bool:
return self.is_file and os.path.isdir(self.file_path)
@@ -210,19 +433,15 @@ class Link(KeyBasedCompareMixin):
@property
def has_hash(self) -> bool:
- return self.hash_name is not None
+ return self.link_hash is not None
def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
"""
- Return True if the link has a hash and it is allowed.
+ Return True if the link has a hash and it is allowed by `hashes`.
"""
- if hashes is None or not self.has_hash:
+ if self.link_hash is None:
return False
- # Assert non-None so mypy knows self.hash_name and self.hash are str.
- assert self.hash_name is not None
- assert self.hash is not None
-
- return hashes.is_hash_allowed(self.hash_name, hex_digest=self.hash)
+ return self.link_hash.is_hash_allowed(hashes)
class _CleanResult(NamedTuple):
@@ -256,33 +475,33 @@ class _CleanResult(NamedTuple):
subdirectory: str
hashes: Dict[str, str]
- @classmethod
- def from_link(cls, link: Link) -> "_CleanResult":
- parsed = link._parsed_url
- netloc = parsed.netloc.rsplit("@", 1)[-1]
- # According to RFC 8089, an empty host in file: means localhost.
- if parsed.scheme == "file" and not netloc:
- netloc = "localhost"
- fragment = urllib.parse.parse_qs(parsed.fragment)
- if "egg" in fragment:
- logger.debug("Ignoring egg= fragment in %s", link)
- try:
- # If there are multiple subdirectory values, use the first one.
- # This matches the behavior of Link.subdirectory_fragment.
- subdirectory = fragment["subdirectory"][0]
- except (IndexError, KeyError):
- subdirectory = ""
- # If there are multiple hash values under the same algorithm, use the
- # first one. This matches the behavior of Link.hash_value.
- hashes = {k: fragment[k][0] for k in _SUPPORTED_HASHES if k in fragment}
- return cls(
- parsed=parsed._replace(netloc=netloc, query="", fragment=""),
- query=urllib.parse.parse_qs(parsed.query),
- subdirectory=subdirectory,
- hashes=hashes,
- )
+
+def _clean_link(link: Link) -> _CleanResult:
+ parsed = link._parsed_url
+ netloc = parsed.netloc.rsplit("@", 1)[-1]
+ # According to RFC 8089, an empty host in file: means localhost.
+ if parsed.scheme == "file" and not netloc:
+ netloc = "localhost"
+ fragment = urllib.parse.parse_qs(parsed.fragment)
+ if "egg" in fragment:
+ logger.debug("Ignoring egg= fragment in %s", link)
+ try:
+ # If there are multiple subdirectory values, use the first one.
+ # This matches the behavior of Link.subdirectory_fragment.
+ subdirectory = fragment["subdirectory"][0]
+ except (IndexError, KeyError):
+ subdirectory = ""
+ # If there are multiple hash values under the same algorithm, use the
+ # first one. This matches the behavior of Link.hash_value.
+ hashes = {k: fragment[k][0] for k in _SUPPORTED_HASHES if k in fragment}
+ return _CleanResult(
+ parsed=parsed._replace(netloc=netloc, query="", fragment=""),
+ query=urllib.parse.parse_qs(parsed.query),
+ subdirectory=subdirectory,
+ hashes=hashes,
+ )
@functools.lru_cache(maxsize=None)
def links_equivalent(link1: Link, link2: Link) -> bool:
- return _CleanResult.from_link(link1) == _CleanResult.from_link(link2)
+ return _clean_link(link1) == _clean_link(link2)
diff --git a/src/pip/_internal/models/scheme.py b/src/pip/_internal/models/scheme.py
index 9a8dafba3..f51190ac6 100644
--- a/src/pip/_internal/models/scheme.py
+++ b/src/pip/_internal/models/scheme.py
@@ -6,7 +6,7 @@ https://docs.python.org/3/install/index.html#alternate-installation.
"""
-SCHEME_KEYS = ['platlib', 'purelib', 'headers', 'scripts', 'data']
+SCHEME_KEYS = ["platlib", "purelib", "headers", "scripts", "data"]
class Scheme:
diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py
index 24ec9834d..a64af7389 100644
--- a/src/pip/_internal/models/search_scope.py
+++ b/src/pip/_internal/models/search_scope.py
@@ -20,13 +20,14 @@ class SearchScope:
Encapsulates the locations that pip is configured to search.
"""
- __slots__ = ["find_links", "index_urls"]
+ __slots__ = ["find_links", "index_urls", "no_index"]
@classmethod
def create(
cls,
find_links: List[str],
index_urls: List[str],
+ no_index: bool,
) -> "SearchScope":
"""
Create a SearchScope object after normalizing the `find_links`.
@@ -38,7 +39,7 @@ class SearchScope:
# blindly normalize anything starting with a ~...
built_find_links: List[str] = []
for link in find_links:
- if link.startswith('~'):
+ if link.startswith("~"):
new_link = normalize_path(link)
if os.path.exists(new_link):
link = new_link
@@ -49,26 +50,29 @@ class SearchScope:
if not has_tls():
for link in itertools.chain(index_urls, built_find_links):
parsed = urllib.parse.urlparse(link)
- if parsed.scheme == 'https':
+ if parsed.scheme == "https":
logger.warning(
- 'pip is configured with locations that require '
- 'TLS/SSL, however the ssl module in Python is not '
- 'available.'
+ "pip is configured with locations that require "
+ "TLS/SSL, however the ssl module in Python is not "
+ "available."
)
break
return cls(
find_links=built_find_links,
index_urls=index_urls,
+ no_index=no_index,
)
def __init__(
self,
find_links: List[str],
index_urls: List[str],
+ no_index: bool,
) -> None:
self.find_links = find_links
self.index_urls = index_urls
+ self.no_index = no_index
def get_formatted_locations(self) -> str:
lines = []
@@ -88,20 +92,23 @@ class SearchScope:
# exceptions for malformed URLs
if not purl.scheme and not purl.netloc:
logger.warning(
- 'The index url "%s" seems invalid, '
- 'please provide a scheme.', redacted_index_url)
+ 'The index url "%s" seems invalid, please provide a scheme.',
+ redacted_index_url,
+ )
redacted_index_urls.append(redacted_index_url)
- lines.append('Looking in indexes: {}'.format(
- ', '.join(redacted_index_urls)))
+ lines.append(
+ "Looking in indexes: {}".format(", ".join(redacted_index_urls))
+ )
if self.find_links:
lines.append(
- 'Looking in links: {}'.format(', '.join(
- redact_auth_from_url(url) for url in self.find_links))
+ "Looking in links: {}".format(
+ ", ".join(redact_auth_from_url(url) for url in self.find_links)
+ )
)
- return '\n'.join(lines)
+ return "\n".join(lines)
def get_index_urls_locations(self, project_name: str) -> List[str]:
"""Returns the locations found via self.index_urls
@@ -112,15 +119,15 @@ class SearchScope:
def mkurl_pypi_url(url: str) -> str:
loc = posixpath.join(
- url,
- urllib.parse.quote(canonicalize_name(project_name)))
+ url, urllib.parse.quote(canonicalize_name(project_name))
+ )
# For maximum compatibility with easy_install, ensure the path
# ends in a trailing slash. Although this isn't in the spec
# (and PyPI can handle it without the slash) some other index
# implementations might break if they relied on easy_install's
# behavior.
- if not loc.endswith('/'):
- loc = loc + '/'
+ if not loc.endswith("/"):
+ loc = loc + "/"
return loc
return [mkurl_pypi_url(url) for url in self.index_urls]
diff --git a/src/pip/_internal/models/selection_prefs.py b/src/pip/_internal/models/selection_prefs.py
index 66a563629..977bc4caa 100644
--- a/src/pip/_internal/models/selection_prefs.py
+++ b/src/pip/_internal/models/selection_prefs.py
@@ -9,8 +9,13 @@ class SelectionPreferences:
and installing files.
"""
- __slots__ = ['allow_yanked', 'allow_all_prereleases', 'format_control',
- 'prefer_binary', 'ignore_requires_python']
+ __slots__ = [
+ "allow_yanked",
+ "allow_all_prereleases",
+ "format_control",
+ "prefer_binary",
+ "ignore_requires_python",
+ ]
# Don't include an allow_yanked default value to make sure each call
# site considers whether yanked releases are allowed. This also causes
diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py
index 11b259170..744bd7ef5 100644
--- a/src/pip/_internal/models/target_python.py
+++ b/src/pip/_internal/models/target_python.py
@@ -53,7 +53,7 @@ class TargetPython:
else:
py_version_info = normalize_version_info(py_version_info)
- py_version = '.'.join(map(str, py_version_info[:2]))
+ py_version = ".".join(map(str, py_version_info[:2]))
self.abis = abis
self.implementation = implementation
@@ -70,19 +70,18 @@ class TargetPython:
"""
display_version = None
if self._given_py_version_info is not None:
- display_version = '.'.join(
+ display_version = ".".join(
str(part) for part in self._given_py_version_info
)
key_values = [
- ('platforms', self.platforms),
- ('version_info', display_version),
- ('abis', self.abis),
- ('implementation', self.implementation),
+ ("platforms", self.platforms),
+ ("version_info", display_version),
+ ("abis", self.abis),
+ ("implementation", self.implementation),
]
- return ' '.join(
- f'{key}={value!r}' for key, value in key_values
- if value is not None
+ return " ".join(
+ f"{key}={value!r}" for key, value in key_values if value is not None
)
def get_tags(self) -> List[Tag]:
diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py
index a79a86106..35c703755 100644
--- a/src/pip/_internal/models/wheel.py
+++ b/src/pip/_internal/models/wheel.py
@@ -16,7 +16,7 @@ class Wheel:
r"""^(?P<namever>(?P<name>.+?)-(?P<ver>.*?))
((-(?P<build>\d[^-]*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)
\.whl|\.dist-info)$""",
- re.VERBOSE
+ re.VERBOSE,
)
def __init__(self, filename: str) -> None:
@@ -25,23 +25,20 @@ class Wheel:
"""
wheel_info = self.wheel_file_re.match(filename)
if not wheel_info:
- raise InvalidWheelFilename(
- f"{filename} is not a valid wheel filename."
- )
+ raise InvalidWheelFilename(f"{filename} is not a valid wheel filename.")
self.filename = filename
- self.name = wheel_info.group('name').replace('_', '-')
+ self.name = wheel_info.group("name").replace("_", "-")
# we'll assume "_" means "-" due to wheel naming scheme
# (https://github.com/pypa/pip/issues/1150)
- 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.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(".")
# All the tag combinations from this file
self.file_tags = {
- Tag(x, y, z) for x in self.pyversions
- for y in self.abis for z in self.plats
+ Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats
}
def get_formatted_file_tags(self) -> List[str]:
@@ -61,7 +58,10 @@ class Wheel:
:raises ValueError: If none of the wheel's file tags match one of
the supported tags.
"""
- return min(tags.index(tag) for tag in self.file_tags if tag in tags)
+ try:
+ return next(i for i, t in enumerate(tags) if t in self.file_tags)
+ except StopIteration:
+ raise ValueError()
def find_most_preferred_tag(
self, tags: List[Tag], tag_to_priority: Dict[Tag, int]
diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py
index 74d225472..ca42798bd 100644
--- a/src/pip/_internal/network/auth.py
+++ b/src/pip/_internal/network/auth.py
@@ -28,13 +28,13 @@ Credentials = Tuple[str, str, str]
try:
import keyring
except ImportError:
- keyring = None
+ keyring = None # type: ignore[assignment]
except Exception as exc:
logger.warning(
"Keyring is skipped due to an exception: %s",
str(exc),
)
- keyring = None
+ keyring = None # type: ignore[assignment]
def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[AuthInfo]:
@@ -66,7 +66,7 @@ def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[Au
"Keyring is skipped due to an exception: %s",
str(exc),
)
- keyring = None
+ keyring = None # type: ignore[assignment]
return None
@@ -179,9 +179,16 @@ class MultiDomainBasicAuth(AuthBase):
# Try to get credentials from original url
username, password = self._get_new_credentials(original_url)
- # If credentials not found, use any stored credentials for this netloc
- if username is None and password is None:
- username, password = self.passwords.get(netloc, (None, None))
+ # If credentials not found, use any stored credentials for this netloc.
+ # Do this if either the username or the password is missing.
+ # This accounts for the situation in which the user has specified
+ # the username in the index url, but the password comes from keyring.
+ if (username is None or password is None) and netloc in self.passwords:
+ un, pw = self.passwords[netloc]
+ # It is possible that the cached credentials are for a different username,
+ # in which case the cache should be ignored.
+ if username is None or username == un:
+ username, password = un, pw
if username is not None or password is not None:
# Convert the username and password if they're None, so that
diff --git a/src/pip/_internal/network/cache.py b/src/pip/_internal/network/cache.py
index 2d915e6fc..a81a23985 100644
--- a/src/pip/_internal/network/cache.py
+++ b/src/pip/_internal/network/cache.py
@@ -3,7 +3,7 @@
import os
from contextlib import contextmanager
-from typing import Iterator, Optional
+from typing import Generator, Optional
from pip._vendor.cachecontrol.cache import BaseCache
from pip._vendor.cachecontrol.caches import FileCache
@@ -18,7 +18,7 @@ def is_from_cache(response: Response) -> bool:
@contextmanager
-def suppressed_cache_errors() -> Iterator[None]:
+def suppressed_cache_errors() -> Generator[None, None, None]:
"""If we can't access the cache then we can just skip caching and process
requests as if caching wasn't enabled.
"""
@@ -53,7 +53,7 @@ class SafeFileCache(BaseCache):
with open(path, "rb") as f:
return f.read()
- def set(self, key: str, value: bytes) -> None:
+ def set(self, key: str, value: bytes, expires: Optional[int] = None) -> None:
path = self._get_cache_path(key)
with suppressed_cache_errors():
ensure_dir(os.path.dirname(path))
diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py
index 47af547d6..79b82a570 100644
--- a/src/pip/_internal/network/download.py
+++ b/src/pip/_internal/network/download.py
@@ -1,6 +1,6 @@
"""Download files with progress indicators.
"""
-import cgi
+import email.message
import logging
import mimetypes
import os
@@ -8,7 +8,7 @@ from typing import Iterable, Optional, Tuple
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
-from pip._internal.cli.progress_bars import DownloadProgressProvider
+from pip._internal.cli.progress_bars import get_download_progress_renderer
from pip._internal.exceptions import NetworkConnectionError
from pip._internal.models.index import PyPI
from pip._internal.models.link import Link
@@ -65,7 +65,8 @@ def _prepare_download(
if not show_progress:
return chunks
- return DownloadProgressProvider(progress_bar, max=total_length)(chunks)
+ renderer = get_download_progress_renderer(bar_type=progress_bar, size=total_length)
+ return renderer(chunks)
def sanitize_content_filename(filename: str) -> str:
@@ -80,12 +81,13 @@ def parse_content_disposition(content_disposition: str, default_filename: str) -
Parse the "filename" value from a Content-Disposition header, and
return the default filename if the result is empty.
"""
- _type, params = cgi.parse_header(content_disposition)
- filename = params.get("filename")
+ m = email.message.Message()
+ m["content-type"] = content_disposition
+ filename = m.get_param("filename")
if filename:
# We need to sanitize the filename to prevent directory traversal
# in case the filename contains ".." path parts.
- filename = sanitize_content_filename(filename)
+ filename = sanitize_content_filename(str(filename))
return filename or default_filename
diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py
index 249bd0587..854a6fa1f 100644
--- a/src/pip/_internal/network/lazy_wheel.py
+++ b/src/pip/_internal/network/lazy_wheel.py
@@ -5,36 +5,36 @@ __all__ = ["HTTPRangeRequestUnsupported", "dist_from_wheel_url"]
from bisect import bisect_left, bisect_right
from contextlib import contextmanager
from tempfile import NamedTemporaryFile
-from typing import Any, Dict, Iterator, List, Optional, Tuple
+from typing import Any, Dict, Generator, List, Optional, Tuple
from zipfile import BadZipfile, ZipFile
-from pip._vendor.pkg_resources import Distribution
+from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
+from pip._internal.metadata import BaseDistribution, MemoryWheel, get_wheel_distribution
from pip._internal.network.session import PipSession
from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks
-from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel
class HTTPRangeRequestUnsupported(Exception):
pass
-def dist_from_wheel_url(name: str, url: str, session: PipSession) -> Distribution:
- """Return a pkg_resources.Distribution from the given wheel URL.
+def dist_from_wheel_url(name: str, url: str, session: PipSession) -> BaseDistribution:
+ """Return a distribution object from the given wheel URL.
- This uses HTTP range requests to only fetch the potion of the wheel
+ This uses HTTP range requests to only fetch the portion of the wheel
containing metadata, just enough for the object to be constructed.
If such requests are not supported, HTTPRangeRequestUnsupported
is raised.
"""
- with LazyZipOverHTTP(url, session) as wheel:
+ with LazyZipOverHTTP(url, session) as zf:
# For read-only ZIP files, ZipFile only needs methods read,
# seek, seekable and tell, not the whole IO protocol.
- zip_file = ZipFile(wheel) # type: ignore
+ wheel = MemoryWheel(zf.name, zf) # type: ignore
# After context manager exit, wheel.name
# is an invalid file by intention.
- return pkg_resources_distribution_for_wheel(zip_file, name, wheel.name)
+ return get_wheel_distribution(wheel, canonicalize_name(name))
class LazyZipOverHTTP:
@@ -135,11 +135,11 @@ class LazyZipOverHTTP:
self._file.__enter__()
return self
- def __exit__(self, *exc: Any) -> Optional[bool]:
- return self._file.__exit__(*exc)
+ def __exit__(self, *exc: Any) -> None:
+ self._file.__exit__(*exc)
@contextmanager
- def _stay(self) -> Iterator[None]:
+ def _stay(self) -> Generator[None, None, None]:
"""Return a context manager keeping the position.
At the end of the block, seek back to original position.
@@ -177,8 +177,8 @@ class LazyZipOverHTTP:
def _merge(
self, start: int, end: int, left: int, right: int
- ) -> Iterator[Tuple[int, int]]:
- """Return an iterator of intervals to be fetched.
+ ) -> Generator[Tuple[int, int], None, None]:
+ """Return a generator of intervals to be fetched.
Args:
start (int): Start of needed interval
diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py
index faaae4059..e512ac784 100644
--- a/src/pip/_internal/network/session.py
+++ b/src/pip/_internal/network/session.py
@@ -2,17 +2,8 @@
network request configuration and behavior.
"""
-# When mypy runs on Windows the call to distro.linux_distribution() is skipped
-# resulting in the failure:
-#
-# error: unused 'type: ignore' comment
-#
-# If the upstream module adds typing, this comment should be removed. See
-# https://github.com/nir0s/distro/pull/269
-#
-# mypy: warn-unused-ignores=False
-
import email.utils
+import io
import ipaddress
import json
import logging
@@ -24,11 +15,23 @@ import subprocess
import sys
import urllib.parse
import warnings
-from typing import Any, Dict, Iterator, List, Mapping, Optional, Sequence, Tuple, Union
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Dict,
+ Generator,
+ List,
+ Mapping,
+ Optional,
+ Sequence,
+ Tuple,
+ Union,
+)
from pip._vendor import requests, urllib3
-from pip._vendor.cachecontrol import CacheControlAdapter
-from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter
+from pip._vendor.cachecontrol import CacheControlAdapter as _BaseCacheControlAdapter
+from pip._vendor.requests.adapters import DEFAULT_POOLBLOCK, BaseAdapter
+from pip._vendor.requests.adapters import HTTPAdapter as _BaseHTTPAdapter
from pip._vendor.requests.models import PreparedRequest, Response
from pip._vendor.requests.structures import CaseInsensitiveDict
from pip._vendor.urllib3.connectionpool import ConnectionPool
@@ -46,6 +49,12 @@ from pip._internal.utils.glibc import libc_ver
from pip._internal.utils.misc import build_url_from_netloc, parse_netloc
from pip._internal.utils.urls import url_to_path
+if TYPE_CHECKING:
+ from ssl import SSLContext
+
+ from pip._vendor.urllib3.poolmanager import PoolManager
+
+
logger = logging.getLogger(__name__)
SecureOrigin = Tuple[str, str, Optional[Union[int, str]]]
@@ -128,9 +137,8 @@ def user_agent() -> str:
if sys.platform.startswith("linux"):
from pip._vendor import distro
- # https://github.com/nir0s/distro/pull/269
- linux_distribution = distro.linux_distribution() # type: ignore
- distro_infos = dict(
+ linux_distribution = distro.name(), distro.version(), distro.codename()
+ distro_infos: Dict[str, Any] = dict(
filter(
lambda x: x[1],
zip(["name", "version", "id"], linux_distribution),
@@ -218,8 +226,11 @@ class LocalFSAdapter(BaseAdapter):
try:
stats = os.stat(pathname)
except OSError as exc:
+ # format the exception raised as a io.BytesIO object,
+ # to return a better error message:
resp.status_code = 404
- resp.raw = exc
+ resp.reason = type(exc).__name__
+ resp.raw = io.BytesIO(f"{resp.reason}: {exc}".encode("utf8"))
else:
modified = email.utils.formatdate(stats.st_mtime, usegmt=True)
content_type = mimetypes.guess_type(pathname)[0] or "text/plain"
@@ -240,6 +251,48 @@ class LocalFSAdapter(BaseAdapter):
pass
+class _SSLContextAdapterMixin:
+ """Mixin to add the ``ssl_context`` constructor argument to HTTP adapters.
+
+ The additional argument is forwarded directly to the pool manager. This allows us
+ to dynamically decide what SSL store to use at runtime, which is used to implement
+ the optional ``truststore`` backend.
+ """
+
+ def __init__(
+ self,
+ *,
+ ssl_context: Optional["SSLContext"] = None,
+ **kwargs: Any,
+ ) -> None:
+ self._ssl_context = ssl_context
+ super().__init__(**kwargs)
+
+ def init_poolmanager(
+ self,
+ connections: int,
+ maxsize: int,
+ block: bool = DEFAULT_POOLBLOCK,
+ **pool_kwargs: Any,
+ ) -> "PoolManager":
+ if self._ssl_context is not None:
+ pool_kwargs.setdefault("ssl_context", self._ssl_context)
+ return super().init_poolmanager( # type: ignore[misc]
+ connections=connections,
+ maxsize=maxsize,
+ block=block,
+ **pool_kwargs,
+ )
+
+
+class HTTPAdapter(_SSLContextAdapterMixin, _BaseHTTPAdapter):
+ pass
+
+
+class CacheControlAdapter(_SSLContextAdapterMixin, _BaseCacheControlAdapter):
+ pass
+
+
class InsecureHTTPAdapter(HTTPAdapter):
def cert_verify(
self,
@@ -273,6 +326,7 @@ class PipSession(requests.Session):
cache: Optional[str] = None,
trusted_hosts: Sequence[str] = (),
index_urls: Optional[List[str]] = None,
+ ssl_context: Optional["SSLContext"] = None,
**kwargs: Any,
) -> None:
"""
@@ -325,13 +379,14 @@ class PipSession(requests.Session):
secure_adapter = CacheControlAdapter(
cache=SafeFileCache(cache),
max_retries=retries,
+ ssl_context=ssl_context,
)
self._trusted_host_adapter = InsecureCacheControlAdapter(
cache=SafeFileCache(cache),
max_retries=retries,
)
else:
- secure_adapter = HTTPAdapter(max_retries=retries)
+ secure_adapter = HTTPAdapter(max_retries=retries, ssl_context=ssl_context)
self._trusted_host_adapter = insecure_adapter
self.mount("https://", secure_adapter)
@@ -369,12 +424,19 @@ class PipSession(requests.Session):
if host_port not in self.pip_trusted_origins:
self.pip_trusted_origins.append(host_port)
+ self.mount(
+ build_url_from_netloc(host, scheme="http") + "/", self._trusted_host_adapter
+ )
self.mount(build_url_from_netloc(host) + "/", self._trusted_host_adapter)
if not host_port[1]:
+ self.mount(
+ build_url_from_netloc(host, scheme="http") + ":",
+ self._trusted_host_adapter,
+ )
# Mount wildcard ports for the same host.
self.mount(build_url_from_netloc(host) + ":", self._trusted_host_adapter)
- def iter_secure_origins(self) -> Iterator[SecureOrigin]:
+ def iter_secure_origins(self) -> Generator[SecureOrigin, None, None]:
yield from SECURE_ORIGINS
for host, port in self.pip_trusted_origins:
yield ("*", host, "*" if port is None else port)
@@ -403,7 +465,7 @@ class PipSession(requests.Session):
continue
try:
- addr = ipaddress.ip_address(origin_host)
+ addr = ipaddress.ip_address(origin_host or "")
network = ipaddress.ip_network(secure_host)
except ValueError:
# We don't have both a valid address or a valid network, so
@@ -449,6 +511,8 @@ class PipSession(requests.Session):
def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> Response:
# Allow setting a default timeout on a session
kwargs.setdefault("timeout", self.timeout)
+ # Allow setting a default proxies on a session
+ kwargs.setdefault("proxies", self.proxies)
# Dispatch the actual request
return super().request(method, url, *args, **kwargs)
diff --git a/src/pip/_internal/network/utils.py b/src/pip/_internal/network/utils.py
index 094cf1b4a..134848ae5 100644
--- a/src/pip/_internal/network/utils.py
+++ b/src/pip/_internal/network/utils.py
@@ -1,4 +1,4 @@
-from typing import Dict, Iterator
+from typing import Dict, Generator
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
@@ -56,7 +56,7 @@ def raise_for_status(resp: Response) -> None:
def response_chunks(
response: Response, chunk_size: int = CONTENT_CHUNK_SIZE
-) -> Iterator[bytes]:
+) -> Generator[bytes, None, None]:
"""Given a requests Response, provide the data chunks."""
try:
# Special case for urllib3.
diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/operations/build/build_tracker.py
index 24d3c5303..6621549b8 100644
--- a/src/pip/_internal/req/req_tracker.py
+++ b/src/pip/_internal/operations/build/build_tracker.py
@@ -3,7 +3,7 @@ import hashlib
import logging
import os
from types import TracebackType
-from typing import Dict, Iterator, Optional, Set, Type, Union
+from typing import Dict, Generator, Optional, Set, Type, Union
from pip._internal.models.link import Link
from pip._internal.req.req_install import InstallRequirement
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
@contextlib.contextmanager
-def update_env_context_manager(**changes: str) -> Iterator[None]:
+def update_env_context_manager(**changes: str) -> Generator[None, None, None]:
target = os.environ
# Save values from the target and change them.
@@ -39,25 +39,25 @@ def update_env_context_manager(**changes: str) -> Iterator[None]:
@contextlib.contextmanager
-def get_requirement_tracker() -> Iterator["RequirementTracker"]:
- root = os.environ.get("PIP_REQ_TRACKER")
+def get_build_tracker() -> Generator["BuildTracker", None, None]:
+ root = os.environ.get("PIP_BUILD_TRACKER")
with contextlib.ExitStack() as ctx:
if root is None:
- root = ctx.enter_context(TempDirectory(kind="req-tracker")).path
- ctx.enter_context(update_env_context_manager(PIP_REQ_TRACKER=root))
+ root = ctx.enter_context(TempDirectory(kind="build-tracker")).path
+ ctx.enter_context(update_env_context_manager(PIP_BUILD_TRACKER=root))
logger.debug("Initialized build tracking at %s", root)
- with RequirementTracker(root) as tracker:
+ with BuildTracker(root) as tracker:
yield tracker
-class RequirementTracker:
+class BuildTracker:
def __init__(self, root: str) -> None:
self._root = root
self._entries: Set[InstallRequirement] = set()
logger.debug("Created build tracker: %s", self._root)
- def __enter__(self) -> "RequirementTracker":
+ def __enter__(self) -> "BuildTracker":
logger.debug("Entered build tracker: %s", self._root)
return self
@@ -118,7 +118,7 @@ class RequirementTracker:
logger.debug("Removed build tracker: %r", self._root)
@contextlib.contextmanager
- def track(self, req: InstallRequirement) -> Iterator[None]:
+ def track(self, req: InstallRequirement) -> Generator[None, None, None]:
self.add(req)
yield
self.remove(req)
diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py
index 1c826835b..e2b7b4445 100644
--- a/src/pip/_internal/operations/build/metadata.py
+++ b/src/pip/_internal/operations/build/metadata.py
@@ -6,19 +6,22 @@ import os
from pip._vendor.pep517.wrappers import Pep517HookCaller
from pip._internal.build_env import BuildEnvironment
+from pip._internal.exceptions import (
+ InstallationSubprocessError,
+ MetadataGenerationFailed,
+)
from pip._internal.utils.subprocess import runner_with_spinner_message
from pip._internal.utils.temp_dir import TempDirectory
-def generate_metadata(build_env, backend):
- # type: (BuildEnvironment, Pep517HookCaller) -> str
+def generate_metadata(
+ build_env: BuildEnvironment, backend: Pep517HookCaller, details: str
+) -> str:
"""Generate metadata using mechanisms described in PEP 517.
Returns the generated metadata directory.
"""
- metadata_tmpdir = TempDirectory(
- kind="modern-metadata", globally_managed=True
- )
+ metadata_tmpdir = TempDirectory(kind="modern-metadata", globally_managed=True)
metadata_dir = metadata_tmpdir.path
@@ -26,10 +29,11 @@ def generate_metadata(build_env, backend):
# Note that Pep517HookCaller implements a fallback for
# prepare_metadata_for_build_wheel, so we don't have to
# consider the possibility that this hook doesn't exist.
- runner = runner_with_spinner_message("Preparing wheel metadata")
+ runner = runner_with_spinner_message("Preparing metadata (pyproject.toml)")
with backend.subprocess_runner(runner):
- distinfo_dir = backend.prepare_metadata_for_build_wheel(
- metadata_dir
- )
+ try:
+ distinfo_dir = backend.prepare_metadata_for_build_wheel(metadata_dir)
+ except InstallationSubprocessError as error:
+ raise MetadataGenerationFailed(package_details=details) from error
return os.path.join(metadata_dir, distinfo_dir)
diff --git a/src/pip/_internal/operations/build/metadata_editable.py b/src/pip/_internal/operations/build/metadata_editable.py
new file mode 100644
index 000000000..4c3f48b6c
--- /dev/null
+++ b/src/pip/_internal/operations/build/metadata_editable.py
@@ -0,0 +1,41 @@
+"""Metadata generation logic for source distributions.
+"""
+
+import os
+
+from pip._vendor.pep517.wrappers import Pep517HookCaller
+
+from pip._internal.build_env import BuildEnvironment
+from pip._internal.exceptions import (
+ InstallationSubprocessError,
+ MetadataGenerationFailed,
+)
+from pip._internal.utils.subprocess import runner_with_spinner_message
+from pip._internal.utils.temp_dir import TempDirectory
+
+
+def generate_editable_metadata(
+ build_env: BuildEnvironment, backend: Pep517HookCaller, details: str
+) -> str:
+ """Generate metadata using mechanisms described in PEP 660.
+
+ Returns the generated metadata directory.
+ """
+ metadata_tmpdir = TempDirectory(kind="modern-metadata", globally_managed=True)
+
+ metadata_dir = metadata_tmpdir.path
+
+ with build_env:
+ # Note that Pep517HookCaller implements a fallback for
+ # prepare_metadata_for_build_wheel/editable, so we don't have to
+ # consider the possibility that this hook doesn't exist.
+ runner = runner_with_spinner_message(
+ "Preparing editable metadata (pyproject.toml)"
+ )
+ with backend.subprocess_runner(runner):
+ try:
+ distinfo_dir = backend.prepare_metadata_for_build_editable(metadata_dir)
+ except InstallationSubprocessError as error:
+ raise MetadataGenerationFailed(package_details=details) from error
+
+ return os.path.join(metadata_dir, distinfo_dir)
diff --git a/src/pip/_internal/operations/build/metadata_legacy.py b/src/pip/_internal/operations/build/metadata_legacy.py
index f46538a07..e60988d64 100644
--- a/src/pip/_internal/operations/build/metadata_legacy.py
+++ b/src/pip/_internal/operations/build/metadata_legacy.py
@@ -5,7 +5,12 @@ import logging
import os
from pip._internal.build_env import BuildEnvironment
-from pip._internal.exceptions import InstallationError
+from pip._internal.cli.spinners import open_spinner
+from pip._internal.exceptions import (
+ InstallationError,
+ InstallationSubprocessError,
+ MetadataGenerationFailed,
+)
from pip._internal.utils.setuptools_build import make_setuptools_egg_info_args
from pip._internal.utils.subprocess import call_subprocess
from pip._internal.utils.temp_dir import TempDirectory
@@ -13,49 +18,39 @@ from pip._internal.utils.temp_dir import TempDirectory
logger = logging.getLogger(__name__)
-def _find_egg_info(directory):
- # type: (str) -> str
- """Find an .egg-info subdirectory in `directory`.
- """
- filenames = [
- f for f in os.listdir(directory) if f.endswith(".egg-info")
- ]
+def _find_egg_info(directory: str) -> str:
+ """Find an .egg-info subdirectory in `directory`."""
+ filenames = [f for f in os.listdir(directory) if f.endswith(".egg-info")]
if not filenames:
- raise InstallationError(
- f"No .egg-info directory found in {directory}"
- )
+ raise InstallationError(f"No .egg-info directory found in {directory}")
if len(filenames) > 1:
raise InstallationError(
- "More than one .egg-info directory found in {}".format(
- directory
- )
+ "More than one .egg-info directory found in {}".format(directory)
)
return os.path.join(directory, filenames[0])
def generate_metadata(
- build_env, # type: BuildEnvironment
- setup_py_path, # type: str
- source_dir, # type: str
- isolated, # type: bool
- details, # type: str
-):
- # type: (...) -> str
+ build_env: BuildEnvironment,
+ setup_py_path: str,
+ source_dir: str,
+ isolated: bool,
+ details: str,
+) -> str:
"""Generate metadata using setup.py-based defacto mechanisms.
Returns the generated metadata directory.
"""
logger.debug(
- 'Running setup.py (path:%s) egg_info for package %s',
- setup_py_path, details,
+ "Running setup.py (path:%s) egg_info for package %s",
+ setup_py_path,
+ details,
)
- egg_info_dir = TempDirectory(
- kind="pip-egg-info", globally_managed=True
- ).path
+ egg_info_dir = TempDirectory(kind="pip-egg-info", globally_managed=True).path
args = make_setuptools_egg_info_args(
setup_py_path,
@@ -64,11 +59,16 @@ def generate_metadata(
)
with build_env:
- call_subprocess(
- args,
- cwd=source_dir,
- command_desc='python setup.py egg_info',
- )
+ with open_spinner("Preparing metadata (setup.py)") as spinner:
+ try:
+ call_subprocess(
+ args,
+ cwd=source_dir,
+ command_desc="python setup.py egg_info",
+ spinner=spinner,
+ )
+ except InstallationSubprocessError as error:
+ raise MetadataGenerationFailed(package_details=details) from error
# Return the .egg-info directory.
return _find_egg_info(egg_info_dir)
diff --git a/src/pip/_internal/operations/build/wheel.py b/src/pip/_internal/operations/build/wheel.py
index 903bd7a05..b0d2fc9ea 100644
--- a/src/pip/_internal/operations/build/wheel.py
+++ b/src/pip/_internal/operations/build/wheel.py
@@ -10,22 +10,21 @@ logger = logging.getLogger(__name__)
def build_wheel_pep517(
- name, # type: str
- backend, # type: Pep517HookCaller
- metadata_directory, # type: str
- tempd, # type: str
-):
- # type: (...) -> Optional[str]
+ name: str,
+ backend: Pep517HookCaller,
+ metadata_directory: str,
+ tempd: str,
+) -> Optional[str]:
"""Build one InstallRequirement using the PEP 517 build process.
Returns path to wheel if successfully built. Otherwise, returns None.
"""
assert metadata_directory is not None
try:
- logger.debug('Destination directory: %s', tempd)
+ logger.debug("Destination directory: %s", tempd)
runner = runner_with_spinner_message(
- f'Building wheel for {name} (PEP 517)'
+ f"Building wheel for {name} (pyproject.toml)"
)
with backend.subprocess_runner(runner):
wheel_name = backend.build_wheel(
@@ -33,6 +32,6 @@ def build_wheel_pep517(
metadata_directory=metadata_directory,
)
except Exception:
- logger.error('Failed building wheel for %s', name)
+ logger.error("Failed building wheel for %s", name)
return None
return os.path.join(tempd, wheel_name)
diff --git a/src/pip/_internal/operations/build/wheel_editable.py b/src/pip/_internal/operations/build/wheel_editable.py
new file mode 100644
index 000000000..cf7b01aed
--- /dev/null
+++ b/src/pip/_internal/operations/build/wheel_editable.py
@@ -0,0 +1,46 @@
+import logging
+import os
+from typing import Optional
+
+from pip._vendor.pep517.wrappers import HookMissing, Pep517HookCaller
+
+from pip._internal.utils.subprocess import runner_with_spinner_message
+
+logger = logging.getLogger(__name__)
+
+
+def build_wheel_editable(
+ name: str,
+ backend: Pep517HookCaller,
+ metadata_directory: str,
+ tempd: str,
+) -> Optional[str]:
+ """Build one InstallRequirement using the PEP 660 build process.
+
+ Returns path to wheel if successfully built. Otherwise, returns None.
+ """
+ assert metadata_directory is not None
+ try:
+ logger.debug("Destination directory: %s", tempd)
+
+ runner = runner_with_spinner_message(
+ f"Building editable for {name} (pyproject.toml)"
+ )
+ with backend.subprocess_runner(runner):
+ try:
+ wheel_name = backend.build_editable(
+ tempd,
+ metadata_directory=metadata_directory,
+ )
+ except HookMissing as e:
+ logger.error(
+ "Cannot build editable %s because the build "
+ "backend does not have the %s hook",
+ name,
+ e,
+ )
+ return None
+ except Exception:
+ logger.error("Failed building editable for %s", name)
+ return None
+ return os.path.join(tempd, wheel_name)
diff --git a/src/pip/_internal/operations/build/wheel_legacy.py b/src/pip/_internal/operations/build/wheel_legacy.py
index 755c3bc83..c5f0492cc 100644
--- a/src/pip/_internal/operations/build/wheel_legacy.py
+++ b/src/pip/_internal/operations/build/wheel_legacy.py
@@ -4,59 +4,51 @@ from typing import List, Optional
from pip._internal.cli.spinners import open_spinner
from pip._internal.utils.setuptools_build import make_setuptools_bdist_wheel_args
-from pip._internal.utils.subprocess import (
- LOG_DIVIDER,
- call_subprocess,
- format_command_args,
-)
+from pip._internal.utils.subprocess import call_subprocess, format_command_args
logger = logging.getLogger(__name__)
def format_command_result(
- command_args, # type: List[str]
- command_output, # type: str
-):
- # type: (...) -> str
+ command_args: List[str],
+ command_output: str,
+) -> str:
"""Format command information for logging."""
command_desc = format_command_args(command_args)
- text = f'Command arguments: {command_desc}\n'
+ text = f"Command arguments: {command_desc}\n"
if not command_output:
- text += 'Command output: None'
+ text += "Command output: None"
elif logger.getEffectiveLevel() > logging.DEBUG:
- text += 'Command output: [use --verbose to show]'
+ text += "Command output: [use --verbose to show]"
else:
- if not command_output.endswith('\n'):
- command_output += '\n'
- text += f'Command output:\n{command_output}{LOG_DIVIDER}'
+ if not command_output.endswith("\n"):
+ command_output += "\n"
+ text += f"Command output:\n{command_output}"
return text
def get_legacy_build_wheel_path(
- names, # type: List[str]
- temp_dir, # type: str
- name, # type: str
- command_args, # type: List[str]
- command_output, # type: str
-):
- # type: (...) -> Optional[str]
+ names: List[str],
+ temp_dir: str,
+ name: str,
+ command_args: List[str],
+ command_output: str,
+) -> Optional[str]:
"""Return the path to the wheel in the temporary build directory."""
# Sort for determinism.
names = sorted(names)
if not names:
- msg = (
- 'Legacy build of wheel for {!r} created no files.\n'
- ).format(name)
+ msg = ("Legacy build of wheel for {!r} created no files.\n").format(name)
msg += format_command_result(command_args, command_output)
logger.warning(msg)
return None
if len(names) > 1:
msg = (
- 'Legacy build of wheel for {!r} created more than one file.\n'
- 'Filenames (choosing first): {}\n'
+ "Legacy build of wheel for {!r} created more than one file.\n"
+ "Filenames (choosing first): {}\n"
).format(name, names)
msg += format_command_result(command_args, command_output)
logger.warning(msg)
@@ -65,14 +57,13 @@ def get_legacy_build_wheel_path(
def build_wheel_legacy(
- name, # type: str
- setup_py_path, # type: str
- source_dir, # type: str
- global_options, # type: List[str]
- build_options, # type: List[str]
- tempd, # type: str
-):
- # type: (...) -> Optional[str]
+ name: str,
+ setup_py_path: str,
+ source_dir: str,
+ global_options: List[str],
+ build_options: List[str],
+ tempd: str,
+) -> Optional[str]:
"""Build one unpacked package using the "legacy" build process.
Returns path to wheel if successfully built. Otherwise, returns None.
@@ -84,19 +75,20 @@ def build_wheel_legacy(
destination_dir=tempd,
)
- spin_message = f'Building wheel for {name} (setup.py)'
+ spin_message = f"Building wheel for {name} (setup.py)"
with open_spinner(spin_message) as spinner:
- logger.debug('Destination directory: %s', tempd)
+ logger.debug("Destination directory: %s", tempd)
try:
output = call_subprocess(
wheel_args,
+ command_desc="python setup.py bdist_wheel",
cwd=source_dir,
spinner=spinner,
)
except Exception:
spinner.finish("error")
- logger.error('Failed building wheel for %s', name)
+ logger.error("Failed building wheel for %s", name)
return None
names = os.listdir(tempd)
diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py
index f3963fb33..fb3ac8b9c 100644
--- a/src/pip/_internal/operations/check.py
+++ b/src/pip/_internal/operations/check.py
@@ -2,19 +2,16 @@
"""
import logging
-from typing import TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Set, Tuple
+from typing import Callable, Dict, List, NamedTuple, Optional, Set, Tuple
from pip._vendor.packaging.requirements import Requirement
-from pip._vendor.packaging.utils import canonicalize_name
+from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._internal.distributions import make_distribution_for_install_requirement
from pip._internal.metadata import get_default_environment
from pip._internal.metadata.base import DistributionVersion
from pip._internal.req.req_install import InstallRequirement
-if TYPE_CHECKING:
- from pip._vendor.packaging.utils import NormalizedName
-
logger = logging.getLogger(__name__)
@@ -24,12 +21,12 @@ class PackageDetails(NamedTuple):
# Shorthands
-PackageSet = Dict['NormalizedName', PackageDetails]
-Missing = Tuple['NormalizedName', Requirement]
-Conflicting = Tuple['NormalizedName', DistributionVersion, Requirement]
+PackageSet = Dict[NormalizedName, PackageDetails]
+Missing = Tuple[NormalizedName, Requirement]
+Conflicting = Tuple[NormalizedName, DistributionVersion, Requirement]
-MissingDict = Dict['NormalizedName', List[Missing]]
-ConflictingDict = Dict['NormalizedName', List[Conflicting]]
+MissingDict = Dict[NormalizedName, List[Missing]]
+ConflictingDict = Dict[NormalizedName, List[Conflicting]]
CheckResult = Tuple[MissingDict, ConflictingDict]
ConflictDetails = Tuple[PackageSet, CheckResult]
@@ -51,8 +48,9 @@ def create_package_set_from_installed() -> Tuple[PackageSet, bool]:
return package_set, problems
-def check_package_set(package_set, should_ignore=None):
- # type: (PackageSet, Optional[Callable[[str], bool]]) -> CheckResult
+def check_package_set(
+ package_set: PackageSet, should_ignore: Optional[Callable[[str], bool]] = None
+) -> CheckResult:
"""Check if a package set is consistent
If should_ignore is passed, it should be a callable that takes a
@@ -64,8 +62,8 @@ def check_package_set(package_set, should_ignore=None):
for package_name, package_detail in package_set.items():
# Info about dependencies of package_name
- missing_deps = set() # type: Set[Missing]
- conflicting_deps = set() # type: Set[Conflicting]
+ missing_deps: Set[Missing] = set()
+ conflicting_deps: Set[Conflicting] = set()
if should_ignore and should_ignore(package_name):
continue
@@ -95,8 +93,7 @@ def check_package_set(package_set, should_ignore=None):
return missing, conflicting
-def check_install_conflicts(to_install):
- # type: (List[InstallRequirement]) -> ConflictDetails
+def check_install_conflicts(to_install: List[InstallRequirement]) -> ConflictDetails:
"""For checking if the dependency graph would be consistent after \
installing given requirements
"""
@@ -112,33 +109,32 @@ def check_install_conflicts(to_install):
package_set,
check_package_set(
package_set, should_ignore=lambda name: name not in whitelist
- )
+ ),
)
-def _simulate_installation_of(to_install, package_set):
- # type: (List[InstallRequirement], PackageSet) -> Set[NormalizedName]
- """Computes the version of packages after installing to_install.
- """
+def _simulate_installation_of(
+ to_install: List[InstallRequirement], package_set: PackageSet
+) -> Set[NormalizedName]:
+ """Computes the version of packages after installing to_install."""
# Keep track of packages that were installed
installed = set()
# Modify it as installing requirement_set would (assuming no errors)
for inst_req in to_install:
abstract_dist = make_distribution_for_install_requirement(inst_req)
- dist = abstract_dist.get_pkg_resources_distribution()
-
- assert dist is not None
- name = canonicalize_name(dist.project_name)
- package_set[name] = PackageDetails(dist.parsed_version, dist.requires())
+ dist = abstract_dist.get_metadata_distribution()
+ name = dist.canonical_name
+ package_set[name] = PackageDetails(dist.version, list(dist.iter_dependencies()))
installed.add(name)
return installed
-def _create_whitelist(would_be_installed, package_set):
- # type: (Set[NormalizedName], PackageSet) -> Set[NormalizedName]
+def _create_whitelist(
+ would_be_installed: Set[NormalizedName], package_set: PackageSet
+) -> Set[NormalizedName]:
packages_affected = set(would_be_installed)
for package_name in package_set:
diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py
index defb20c7c..930d4c600 100644
--- a/src/pip/_internal/operations/freeze.py
+++ b/src/pip/_internal/operations/freeze.py
@@ -1,19 +1,8 @@
import collections
import logging
import os
-from typing import (
- Container,
- Dict,
- Iterable,
- Iterator,
- List,
- NamedTuple,
- Optional,
- Set,
- Union,
-)
+from typing import Container, Dict, Generator, Iterable, List, NamedTuple, Optional, Set
-from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import Version
@@ -30,22 +19,20 @@ logger = logging.getLogger(__name__)
class _EditableInfo(NamedTuple):
- requirement: Optional[str]
- editable: bool
+ requirement: str
comments: List[str]
def freeze(
- requirement=None, # type: Optional[List[str]]
- local_only=False, # type: bool
- user_only=False, # type: bool
- paths=None, # type: Optional[List[str]]
- isolated=False, # type: bool
- exclude_editable=False, # type: bool
- skip=() # type: Container[str]
-):
- # type: (...) -> Iterator[str]
- installations = {} # type: Dict[str, FrozenRequirement]
+ requirement: Optional[List[str]] = None,
+ local_only: bool = False,
+ user_only: bool = False,
+ paths: Optional[List[str]] = None,
+ isolated: bool = False,
+ exclude_editable: bool = False,
+ skip: Container[str] = (),
+) -> Generator[str, None, None]:
+ installations: Dict[str, FrozenRequirement] = {}
dists = get_environment(paths).iter_installed_distributions(
local_only=local_only,
@@ -63,42 +50,50 @@ def freeze(
# should only be emitted once, even if the same option is in multiple
# requirements files, so we need to keep track of what has been emitted
# so that we don't emit it again if it's seen again
- emitted_options = set() # type: Set[str]
+ emitted_options: Set[str] = set()
# keep track of which files a requirement is in so that we can
# give an accurate warning if a requirement appears multiple times.
- req_files = collections.defaultdict(list) # type: Dict[str, List[str]]
+ req_files: Dict[str, List[str]] = collections.defaultdict(list)
for req_file_path in requirement:
with open(req_file_path) as req_file:
for line in req_file:
- if (not line.strip() or
- line.strip().startswith('#') or
- line.startswith((
- '-r', '--requirement',
- '-f', '--find-links',
- '-i', '--index-url',
- '--pre',
- '--trusted-host',
- '--process-dependency-links',
- '--extra-index-url',
- '--use-feature'))):
+ if (
+ not line.strip()
+ or line.strip().startswith("#")
+ or line.startswith(
+ (
+ "-r",
+ "--requirement",
+ "-f",
+ "--find-links",
+ "-i",
+ "--index-url",
+ "--pre",
+ "--trusted-host",
+ "--process-dependency-links",
+ "--extra-index-url",
+ "--use-feature",
+ )
+ )
+ ):
line = line.rstrip()
if line not in emitted_options:
emitted_options.add(line)
yield line
continue
- if line.startswith('-e') or line.startswith('--editable'):
- if line.startswith('-e'):
+ if line.startswith("-e") or line.startswith("--editable"):
+ if line.startswith("-e"):
line = line[2:].strip()
else:
- line = line[len('--editable'):].strip().lstrip('=')
+ line = line[len("--editable") :].strip().lstrip("=")
line_req = install_req_from_editable(
line,
isolated=isolated,
)
else:
line_req = install_req_from_line(
- COMMENT_RE.sub('', line).strip(),
+ COMMENT_RE.sub("", line).strip(),
isolated=isolated,
)
@@ -106,15 +101,15 @@ def freeze(
logger.info(
"Skipping line in requirement file [%s] because "
"it's not clear what it would install: %s",
- req_file_path, line.strip(),
+ req_file_path,
+ line.strip(),
)
logger.info(
" (add #egg=PackageName to the URL to avoid"
" this warning)"
)
else:
- line_req_canonical_name = canonicalize_name(
- line_req.name)
+ line_req_canonical_name = canonicalize_name(line_req.name)
if line_req_canonical_name not in installations:
# either it's not installed, or it is installed
# but has been processed already
@@ -123,14 +118,13 @@ def freeze(
"Requirement file [%s] contains %s, but "
"package %r is not installed",
req_file_path,
- COMMENT_RE.sub('', line).strip(),
- line_req.name
+ COMMENT_RE.sub("", line).strip(),
+ line_req.name,
)
else:
req_files[line_req.name].append(req_file_path)
else:
- yield str(installations[
- line_req_canonical_name]).rstrip()
+ yield str(installations[line_req_canonical_name]).rstrip()
del installations[line_req_canonical_name]
req_files[line_req.name].append(req_file_path)
@@ -138,15 +132,14 @@ def freeze(
# single requirements file or in different requirements files).
for name, files in req_files.items():
if len(files) > 1:
- logger.warning("Requirement %s included multiple times [%s]",
- name, ', '.join(sorted(set(files))))
+ logger.warning(
+ "Requirement %s included multiple times [%s]",
+ name,
+ ", ".join(sorted(set(files))),
+ )
- yield(
- '## The following requirements were added by '
- 'pip freeze:'
- )
- for installation in sorted(
- installations.values(), key=lambda x: x.name.lower()):
+ yield ("## The following requirements were added by pip freeze:")
+ for installation in sorted(installations.values(), key=lambda x: x.name.lower()):
if installation.canonical_name not in skip:
yield str(installation).rstrip()
@@ -159,21 +152,12 @@ def _format_as_name_version(dist: BaseDistribution) -> str:
def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
"""
- Compute and return values (req, editable, comments) for use in
+ Compute and return values (req, comments) for use in
FrozenRequirement.from_dist().
"""
- if not dist.editable:
- return _EditableInfo(requirement=None, editable=False, comments=[])
- if dist.location is None:
- display = _format_as_name_version(dist)
- logger.warning("Editable requirement not found on disk: %s", display)
- return _EditableInfo(
- requirement=None,
- editable=True,
- comments=[f"# Editable install not found ({display})"],
- )
-
- location = os.path.normcase(os.path.abspath(dist.location))
+ editable_project_location = dist.editable_project_location
+ assert editable_project_location
+ location = os.path.normcase(os.path.abspath(editable_project_location))
from pip._internal.vcs import RemoteNotFoundError, RemoteNotValidError, vcs
@@ -182,13 +166,13 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
if vcs_backend is None:
display = _format_as_name_version(dist)
logger.debug(
- 'No VCS found for editable requirement "%s" in: %r', display,
+ 'No VCS found for editable requirement "%s" in: %r',
+ display,
location,
)
return _EditableInfo(
requirement=location,
- editable=True,
- comments=[f'# Editable install with no version control ({display})'],
+ comments=[f"# Editable install with no version control ({display})"],
)
vcs_name = type(vcs_backend).__name__
@@ -199,50 +183,47 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
display = _format_as_name_version(dist)
return _EditableInfo(
requirement=location,
- editable=True,
- comments=[f'# Editable {vcs_name} install with no remote ({display})'],
+ comments=[f"# Editable {vcs_name} install with no remote ({display})"],
)
except RemoteNotValidError as ex:
display = _format_as_name_version(dist)
return _EditableInfo(
requirement=location,
- editable=True,
comments=[
f"# Editable {vcs_name} install ({display}) with either a deleted "
f"local remote or invalid URI:",
f"# '{ex.url}'",
],
)
-
except BadCommand:
logger.warning(
- 'cannot determine version of editable source in %s '
- '(%s command not found in path)',
+ "cannot determine version of editable source in %s "
+ "(%s command not found in path)",
location,
vcs_backend.name,
)
- return _EditableInfo(requirement=None, editable=True, comments=[])
-
+ return _EditableInfo(requirement=location, comments=[])
except InstallationError as exc:
- logger.warning(
- "Error when trying to get requirement for VCS system %s, "
- "falling back to uneditable format", exc
- )
+ logger.warning("Error when trying to get requirement for VCS system %s", exc)
else:
- return _EditableInfo(requirement=req, editable=True, comments=[])
+ return _EditableInfo(requirement=req, comments=[])
- logger.warning('Could not determine repository location of %s', location)
+ logger.warning("Could not determine repository location of %s", location)
return _EditableInfo(
- requirement=None,
- editable=False,
- comments=['## !! Could not determine repository location'],
+ requirement=location,
+ comments=["## !! Could not determine repository location"],
)
class FrozenRequirement:
- def __init__(self, name, req, editable, comments=()):
- # type: (str, Union[str, Requirement], bool, Iterable[str]) -> None
+ def __init__(
+ self,
+ name: str,
+ req: str,
+ editable: bool,
+ comments: Iterable[str] = (),
+ ) -> None:
self.name = name
self.canonical_name = canonicalize_name(name)
self.req = req
@@ -251,27 +232,23 @@ class FrozenRequirement:
@classmethod
def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement":
- # TODO `get_requirement_info` is taking care of editable requirements.
- # TODO This should be refactored when we will add detection of
- # editable that provide .dist-info metadata.
- req, editable, comments = _get_editable_info(dist)
- if req is None and not editable:
- # if PEP 610 metadata is present, attempt to use it
+ editable = dist.editable
+ if editable:
+ req, comments = _get_editable_info(dist)
+ else:
+ comments = []
direct_url = dist.direct_url
if direct_url:
- req = direct_url_as_pep440_direct_reference(
- direct_url, dist.raw_name
- )
- comments = []
- if req is None:
- # name==version requirement
- req = _format_as_name_version(dist)
+ # if PEP 610 metadata is present, use it
+ req = direct_url_as_pep440_direct_reference(direct_url, dist.raw_name)
+ else:
+ # name==version requirement
+ req = _format_as_name_version(dist)
return cls(dist.raw_name, req, editable, comments=comments)
- def __str__(self):
- # type: () -> str
+ def __str__(self) -> str:
req = self.req
if self.editable:
- req = f'-e {req}'
- return '\n'.join(list(self.comments) + [str(req)]) + '\n'
+ req = f"-e {req}"
+ return "\n".join(list(self.comments) + [str(req)]) + "\n"
diff --git a/src/pip/_internal/operations/install/editable_legacy.py b/src/pip/_internal/operations/install/editable_legacy.py
index 6882c475c..bb548cdca 100644
--- a/src/pip/_internal/operations/install/editable_legacy.py
+++ b/src/pip/_internal/operations/install/editable_legacy.py
@@ -12,22 +12,21 @@ logger = logging.getLogger(__name__)
def install_editable(
- install_options, # type: List[str]
- global_options, # type: Sequence[str]
- prefix, # type: Optional[str]
- home, # type: Optional[str]
- use_user_site, # type: bool
- name, # type: str
- setup_py_path, # type: str
- isolated, # type: bool
- build_env, # type: BuildEnvironment
- unpacked_source_directory, # type: str
-):
- # type: (...) -> None
+ install_options: List[str],
+ global_options: Sequence[str],
+ prefix: Optional[str],
+ home: Optional[str],
+ use_user_site: bool,
+ name: str,
+ setup_py_path: str,
+ isolated: bool,
+ build_env: BuildEnvironment,
+ unpacked_source_directory: str,
+) -> None:
"""Install a package in editable mode. Most arguments are pass-through
to setuptools.
"""
- logger.info('Running setup.py develop for %s', name)
+ logger.info("Running setup.py develop for %s", name)
args = make_setuptools_develop_args(
setup_py_path,
@@ -43,5 +42,6 @@ def install_editable(
with build_env:
call_subprocess(
args,
+ command_desc="python setup.py develop",
cwd=unpacked_source_directory,
)
diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py
index 4cb24fe1a..290967dd6 100644
--- a/src/pip/_internal/operations/install/legacy.py
+++ b/src/pip/_internal/operations/install/legacy.py
@@ -3,14 +3,12 @@
import logging
import os
-import sys
-from distutils.util import change_root
from typing import List, Optional, Sequence
from pip._internal.build_env import BuildEnvironment
-from pip._internal.exceptions import InstallationError
+from pip._internal.exceptions import InstallationError, LegacyInstallFailure
+from pip._internal.locations.base import change_root
from pip._internal.models.scheme import Scheme
-from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import ensure_dir
from pip._internal.utils.setuptools_build import make_setuptools_install_args
from pip._internal.utils.subprocess import runner_with_spinner_message
@@ -19,19 +17,12 @@ from pip._internal.utils.temp_dir import TempDirectory
logger = logging.getLogger(__name__)
-class LegacyInstallFailure(Exception):
- def __init__(self):
- # type: () -> None
- self.parent = sys.exc_info()
-
-
def write_installed_files_from_setuptools_record(
record_lines: List[str],
root: Optional[str],
req_description: str,
) -> None:
- def prepend_root(path):
- # type: (str) -> str
+ def prepend_root(path: str) -> str:
if root is None or not os.path.isabs(path):
return path
else:
@@ -39,7 +30,7 @@ def write_installed_files_from_setuptools_record(
for line in record_lines:
directory = os.path.dirname(line)
- if directory.endswith('.egg-info'):
+ if directory.endswith(".egg-info"):
egg_info_dir = prepend_root(directory)
break
else:
@@ -55,39 +46,36 @@ def write_installed_files_from_setuptools_record(
filename = line.strip()
if os.path.isdir(filename):
filename += os.path.sep
- new_lines.append(
- os.path.relpath(prepend_root(filename), egg_info_dir)
- )
+ new_lines.append(os.path.relpath(prepend_root(filename), egg_info_dir))
new_lines.sort()
ensure_dir(egg_info_dir)
- inst_files_path = os.path.join(egg_info_dir, 'installed-files.txt')
- with open(inst_files_path, 'w') as f:
- f.write('\n'.join(new_lines) + '\n')
+ inst_files_path = os.path.join(egg_info_dir, "installed-files.txt")
+ with open(inst_files_path, "w") as f:
+ f.write("\n".join(new_lines) + "\n")
def install(
- install_options, # type: List[str]
- global_options, # type: Sequence[str]
- root, # type: Optional[str]
- home, # type: Optional[str]
- prefix, # type: Optional[str]
- use_user_site, # type: bool
- pycompile, # type: bool
- scheme, # type: Scheme
- setup_py_path, # type: str
- isolated, # type: bool
- req_name, # type: str
- build_env, # type: BuildEnvironment
- unpacked_source_directory, # type: str
- req_description, # type: str
-):
- # type: (...) -> bool
+ install_options: List[str],
+ global_options: Sequence[str],
+ root: Optional[str],
+ home: Optional[str],
+ prefix: Optional[str],
+ use_user_site: bool,
+ pycompile: bool,
+ scheme: Scheme,
+ setup_py_path: str,
+ isolated: bool,
+ req_name: str,
+ build_env: BuildEnvironment,
+ unpacked_source_directory: str,
+ req_description: str,
+) -> bool:
header_dir = scheme.headers
with TempDirectory(kind="record") as temp_dir:
try:
- record_filename = os.path.join(temp_dir.path, 'install-record.txt')
+ record_filename = os.path.join(temp_dir.path, "install-record.txt")
install_args = make_setuptools_install_args(
setup_py_path,
global_options=global_options,
@@ -105,20 +93,20 @@ def install(
runner = runner_with_spinner_message(
f"Running setup.py install for {req_name}"
)
- with indent_log(), build_env:
+ with build_env:
runner(
cmd=install_args,
cwd=unpacked_source_directory,
)
if not os.path.exists(record_filename):
- logger.debug('Record file %s not found', record_filename)
+ logger.debug("Record file %s not found", record_filename)
# Signal to the caller that we didn't install the new package
return False
- except Exception:
+ except Exception as e:
# Signal to the caller that we didn't install the new package
- raise LegacyInstallFailure
+ raise LegacyInstallFailure(package_details=req_name) from e
# At this point, we have successfully installed the requirement.
diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py
index b5eafda98..1650d59a3 100644
--- a/src/pip/_internal/operations/install/wheel.py
+++ b/src/pip/_internal/operations/install/wheel.py
@@ -22,6 +22,7 @@ from typing import (
BinaryIO,
Callable,
Dict,
+ Generator,
Iterable,
Iterator,
List,
@@ -38,11 +39,14 @@ from zipfile import ZipFile, ZipInfo
from pip._vendor.distlib.scripts import ScriptMaker
from pip._vendor.distlib.util import get_export_entry
from pip._vendor.packaging.utils import canonicalize_name
-from pip._vendor.six import ensure_str, ensure_text, reraise
from pip._internal.exceptions import InstallationError
from pip._internal.locations import get_major_minor_version
-from pip._internal.metadata import BaseDistribution, get_wheel_distribution
+from pip._internal.metadata import (
+ BaseDistribution,
+ FilesystemWheel,
+ get_wheel_distribution,
+)
from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl
from pip._internal.models.scheme import SCHEME_KEYS, Scheme
from pip._internal.utils.filesystem import adjacent_tmp_file, replace
@@ -59,62 +63,55 @@ if TYPE_CHECKING:
from typing import Protocol
class File(Protocol):
- src_record_path = None # type: RecordPath
- dest_path = None # type: str
- changed = None # type: bool
+ src_record_path: "RecordPath"
+ dest_path: str
+ changed: bool
- def save(self):
- # type: () -> None
+ def save(self) -> None:
pass
logger = logging.getLogger(__name__)
-RecordPath = NewType('RecordPath', str)
+RecordPath = NewType("RecordPath", str)
InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]]
-def rehash(path, blocksize=1 << 20):
- # type: (str, int) -> Tuple[str, str]
+def rehash(path: str, blocksize: int = 1 << 20) -> Tuple[str, str]:
"""Return (encoded_digest, length) for path using hashlib.sha256()"""
h, length = hash_file(path, blocksize)
- digest = 'sha256=' + urlsafe_b64encode(
- h.digest()
- ).decode('latin1').rstrip('=')
+ digest = "sha256=" + urlsafe_b64encode(h.digest()).decode("latin1").rstrip("=")
return (digest, str(length))
-def csv_io_kwargs(mode):
- # type: (str) -> Dict[str, Any]
+def csv_io_kwargs(mode: str) -> Dict[str, Any]:
"""Return keyword arguments to properly open a CSV file
in the given mode.
"""
- return {'mode': mode, 'newline': '', 'encoding': 'utf-8'}
+ return {"mode": mode, "newline": "", "encoding": "utf-8"}
-def fix_script(path):
- # type: (str) -> bool
+def fix_script(path: str) -> bool:
"""Replace #!python with #!/path/to/python
Return True if file was changed.
"""
# XXX RECORD hashes will need to be updated
assert os.path.isfile(path)
- with open(path, 'rb') as script:
+ with open(path, "rb") as script:
firstline = script.readline()
- if not firstline.startswith(b'#!python'):
+ if not firstline.startswith(b"#!python"):
return False
exename = sys.executable.encode(sys.getfilesystemencoding())
- firstline = b'#!' + exename + os.linesep.encode("ascii")
+ firstline = b"#!" + exename + os.linesep.encode("ascii")
rest = script.read()
- with open(path, 'wb') as script:
+ with open(path, "wb") as script:
script.write(firstline)
script.write(rest)
return True
-def wheel_root_is_purelib(metadata):
- # type: (Message) -> bool
+def wheel_root_is_purelib(metadata: Message) -> bool:
return metadata.get("Root-Is-Purelib", "").lower() == "true"
@@ -129,8 +126,7 @@ def get_entrypoints(dist: BaseDistribution) -> Tuple[Dict[str, str], Dict[str, s
return console_scripts, gui_scripts
-def message_about_scripts_not_on_PATH(scripts):
- # type: (Sequence[str]) -> Optional[str]
+def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> Optional[str]:
"""Determine if any scripts are not on PATH and format a warning.
Returns a warning message if one or more scripts are not on PATH,
otherwise None.
@@ -139,7 +135,7 @@ def message_about_scripts_not_on_PATH(scripts):
return None
# Group scripts by the path they were installed in
- grouped_by_dir = collections.defaultdict(set) # type: Dict[str, Set[str]]
+ grouped_by_dir: Dict[str, Set[str]] = collections.defaultdict(set)
for destfile in scripts:
parent_dir = os.path.dirname(destfile)
script_name = os.path.basename(destfile)
@@ -147,23 +143,24 @@ def message_about_scripts_not_on_PATH(scripts):
# We don't want to warn for directories that are on PATH.
not_warn_dirs = [
- os.path.normcase(i).rstrip(os.sep) for i in
- os.environ.get("PATH", "").split(os.pathsep)
+ os.path.normcase(i).rstrip(os.sep)
+ for i in os.environ.get("PATH", "").split(os.pathsep)
]
# If an executable sits with sys.executable, we don't warn for it.
# This covers the case of venv invocations without activating the venv.
not_warn_dirs.append(os.path.normcase(os.path.dirname(sys.executable)))
- warn_for = {
- parent_dir: scripts for parent_dir, scripts in grouped_by_dir.items()
+ warn_for: Dict[str, Set[str]] = {
+ parent_dir: scripts
+ for parent_dir, scripts in grouped_by_dir.items()
if os.path.normcase(parent_dir) not in not_warn_dirs
- } # type: Dict[str, Set[str]]
+ }
if not warn_for:
return None
# Format a message
msg_lines = []
for parent_dir, dir_scripts in warn_for.items():
- sorted_scripts = sorted(dir_scripts) # type: List[str]
+ sorted_scripts: List[str] = sorted(dir_scripts)
if len(sorted_scripts) == 1:
start_text = "script {} is".format(sorted_scripts[0])
else:
@@ -172,8 +169,9 @@ def message_about_scripts_not_on_PATH(scripts):
)
msg_lines.append(
- "The {} installed in '{}' which is not on PATH."
- .format(start_text, parent_dir)
+ "The {} installed in '{}' which is not on PATH.".format(
+ start_text, parent_dir
+ )
)
last_line_fmt = (
@@ -200,8 +198,9 @@ def message_about_scripts_not_on_PATH(scripts):
return "\n".join(msg_lines)
-def _normalized_outrows(outrows):
- # type: (Iterable[InstalledCSVRow]) -> List[Tuple[str, str, str]]
+def _normalized_outrows(
+ outrows: Iterable[InstalledCSVRow],
+) -> List[Tuple[str, str, str]]:
"""Normalize the given rows of a RECORD file.
Items in each row are converted into str. Rows are then sorted to make
@@ -221,69 +220,57 @@ def _normalized_outrows(outrows):
# For additional background, see--
# https://github.com/pypa/pip/issues/5868
return sorted(
- (ensure_str(record_path, encoding='utf-8'), hash_, str(size))
- for record_path, hash_, size in outrows
+ (record_path, hash_, str(size)) for record_path, hash_, size in outrows
)
-def _record_to_fs_path(record_path):
- # type: (RecordPath) -> str
- return record_path
+def _record_to_fs_path(record_path: RecordPath, lib_dir: str) -> str:
+ return os.path.join(lib_dir, record_path)
-def _fs_to_record_path(path, relative_to=None):
- # type: (str, Optional[str]) -> RecordPath
- if relative_to is not None:
- # On Windows, do not handle relative paths if they belong to different
- # logical disks
- if os.path.splitdrive(path)[0].lower() == \
- os.path.splitdrive(relative_to)[0].lower():
- path = os.path.relpath(path, relative_to)
- path = path.replace(os.path.sep, '/')
- return cast('RecordPath', path)
+def _fs_to_record_path(path: str, lib_dir: str) -> RecordPath:
+ # On Windows, do not handle relative paths if they belong to different
+ # logical disks
+ if os.path.splitdrive(path)[0].lower() == os.path.splitdrive(lib_dir)[0].lower():
+ path = os.path.relpath(path, lib_dir)
-
-def _parse_record_path(record_column):
- # type: (str) -> RecordPath
- p = ensure_text(record_column, encoding='utf-8')
- return cast('RecordPath', p)
+ path = path.replace(os.path.sep, "/")
+ return cast("RecordPath", path)
def get_csv_rows_for_installed(
- old_csv_rows, # type: List[List[str]]
- installed, # type: Dict[RecordPath, RecordPath]
- changed, # type: Set[RecordPath]
- generated, # type: List[str]
- lib_dir, # type: str
-):
- # type: (...) -> List[InstalledCSVRow]
+ old_csv_rows: List[List[str]],
+ installed: Dict[RecordPath, RecordPath],
+ changed: Set[RecordPath],
+ generated: List[str],
+ lib_dir: str,
+) -> List[InstalledCSVRow]:
"""
:param installed: A map from archive RECORD path to installation RECORD
path.
"""
- installed_rows = [] # type: List[InstalledCSVRow]
+ installed_rows: List[InstalledCSVRow] = []
for row in old_csv_rows:
if len(row) > 3:
- logger.warning('RECORD line has more than three elements: %s', row)
- old_record_path = _parse_record_path(row[0])
+ logger.warning("RECORD line has more than three elements: %s", row)
+ old_record_path = cast("RecordPath", row[0])
new_record_path = installed.pop(old_record_path, old_record_path)
if new_record_path in changed:
- digest, length = rehash(_record_to_fs_path(new_record_path))
+ digest, length = rehash(_record_to_fs_path(new_record_path, lib_dir))
else:
- digest = row[1] if len(row) > 1 else ''
- length = row[2] if len(row) > 2 else ''
+ digest = row[1] if len(row) > 1 else ""
+ length = row[2] if len(row) > 2 else ""
installed_rows.append((new_record_path, digest, length))
for f in generated:
path = _fs_to_record_path(f, lib_dir)
digest, length = rehash(f)
installed_rows.append((path, digest, length))
for installed_record_path in installed.values():
- installed_rows.append((installed_record_path, '', ''))
+ installed_rows.append((installed_record_path, "", ""))
return installed_rows
-def get_console_script_specs(console):
- # type: (Dict[str, str]) -> List[str]
+def get_console_script_specs(console: Dict[str, str]) -> List[str]:
"""
Given the mapping from entrypoint name to callable, return the relevant
console script specs.
@@ -326,62 +313,57 @@ def get_console_script_specs(console):
# DEFAULT
# - The default behavior is to install pip, pipX, pipX.Y, easy_install
# and easy_install-X.Y.
- pip_script = console.pop('pip', None)
+ pip_script = console.pop("pip", None)
if pip_script:
if "ENSUREPIP_OPTIONS" not in os.environ:
- scripts_to_generate.append('pip = ' + pip_script)
+ scripts_to_generate.append("pip = " + pip_script)
if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall":
scripts_to_generate.append(
- 'pip{} = {}'.format(sys.version_info[0], pip_script)
+ "pip{} = {}".format(sys.version_info[0], pip_script)
)
- scripts_to_generate.append(
- f'pip{get_major_minor_version()} = {pip_script}'
- )
+ scripts_to_generate.append(f"pip{get_major_minor_version()} = {pip_script}")
# Delete any other versioned pip entry points
- pip_ep = [k for k in console if re.match(r'pip(\d(\.\d)?)?$', k)]
+ pip_ep = [k for k in console if re.match(r"pip(\d(\.\d)?)?$", k)]
for k in pip_ep:
del console[k]
- easy_install_script = console.pop('easy_install', None)
+ easy_install_script = console.pop("easy_install", None)
if easy_install_script:
if "ENSUREPIP_OPTIONS" not in os.environ:
- scripts_to_generate.append(
- 'easy_install = ' + easy_install_script
- )
+ scripts_to_generate.append("easy_install = " + easy_install_script)
scripts_to_generate.append(
- 'easy_install-{} = {}'.format(
+ "easy_install-{} = {}".format(
get_major_minor_version(), easy_install_script
)
)
# Delete any other versioned easy_install entry points
easy_install_ep = [
- k for k in console if re.match(r'easy_install(-\d\.\d)?$', k)
+ k for k in console if re.match(r"easy_install(-\d\.\d)?$", k)
]
for k in easy_install_ep:
del console[k]
# Generate the console entry points specified in the wheel
- scripts_to_generate.extend(starmap('{} = {}'.format, console.items()))
+ scripts_to_generate.extend(starmap("{} = {}".format, console.items()))
return scripts_to_generate
class ZipBackedFile:
- def __init__(self, src_record_path, dest_path, zip_file):
- # type: (RecordPath, str, ZipFile) -> None
+ def __init__(
+ self, src_record_path: RecordPath, dest_path: str, zip_file: ZipFile
+ ) -> None:
self.src_record_path = src_record_path
self.dest_path = dest_path
self._zip_file = zip_file
self.changed = False
- def _getinfo(self):
- # type: () -> ZipInfo
+ def _getinfo(self) -> ZipInfo:
return self._zip_file.getinfo(self.src_record_path)
- def save(self):
- # type: () -> None
+ def save(self) -> None:
# directory creation is lazy and after file filtering
# to ensure we don't install empty dirs; empty dirs can't be
# uninstalled.
@@ -410,22 +392,19 @@ class ZipBackedFile:
class ScriptFile:
- def __init__(self, file):
- # type: (File) -> None
+ def __init__(self, file: "File") -> None:
self._file = file
self.src_record_path = self._file.src_record_path
self.dest_path = self._file.dest_path
self.changed = False
- def save(self):
- # type: () -> None
+ def save(self) -> None:
self._file.save()
self.changed = fix_script(self.dest_path)
class MissingCallableSuffix(InstallationError):
- def __init__(self, entry_point):
- # type: (str) -> None
+ def __init__(self, entry_point: str) -> None:
super().__init__(
"Invalid script entry point: {} - A callable "
"suffix is required. Cf https://packaging.python.org/"
@@ -434,31 +413,30 @@ class MissingCallableSuffix(InstallationError):
)
-def _raise_for_invalid_entrypoint(specification):
- # type: (str) -> None
+def _raise_for_invalid_entrypoint(specification: str) -> None:
entry = get_export_entry(specification)
if entry is not None and entry.suffix is None:
raise MissingCallableSuffix(str(entry))
class PipScriptMaker(ScriptMaker):
- def make(self, specification, options=None):
- # type: (str, Dict[str, Any]) -> List[str]
+ def make(
+ self, specification: str, options: Optional[Dict[str, Any]] = None
+ ) -> List[str]:
_raise_for_invalid_entrypoint(specification)
return super().make(specification, options)
def _install_wheel(
- name, # type: str
- wheel_zip, # type: ZipFile
- wheel_path, # type: str
- scheme, # type: Scheme
- pycompile=True, # type: bool
- warn_script_location=True, # type: bool
- direct_url=None, # type: Optional[DirectUrl]
- requested=False, # type: bool
-):
- # type: (...) -> None
+ name: str,
+ wheel_zip: ZipFile,
+ wheel_path: str,
+ scheme: Scheme,
+ pycompile: bool = True,
+ warn_script_location: bool = True,
+ direct_url: Optional[DirectUrl] = None,
+ requested: bool = False,
+) -> None:
"""Install a wheel.
:param name: Name of the project to install
@@ -485,33 +463,23 @@ def _install_wheel(
# installed = files copied from the wheel to the destination
# changed = files changed while installing (scripts #! line typically)
# generated = files newly generated during the install (script wrappers)
- installed = {} # type: Dict[RecordPath, RecordPath]
- changed = set() # type: Set[RecordPath]
- generated = [] # type: List[str]
+ installed: Dict[RecordPath, RecordPath] = {}
+ changed: Set[RecordPath] = set()
+ generated: List[str] = []
- def record_installed(srcfile, destfile, modified=False):
- # type: (RecordPath, str, bool) -> None
+ def record_installed(
+ srcfile: RecordPath, destfile: str, modified: bool = False
+ ) -> None:
"""Map archive RECORD paths to installation RECORD paths."""
newpath = _fs_to_record_path(destfile, lib_dir)
installed[srcfile] = newpath
if modified:
- changed.add(_fs_to_record_path(destfile))
-
- def all_paths():
- # type: () -> Iterable[RecordPath]
- names = wheel_zip.namelist()
- # If a flag is set, names may be unicode in Python 2. We convert to
- # text explicitly so these are valid for lookup in RECORD.
- decoded_names = map(ensure_text, names)
- for name in decoded_names:
- yield cast("RecordPath", name)
-
- def is_dir_path(path):
- # type: (RecordPath) -> bool
+ changed.add(newpath)
+
+ def is_dir_path(path: RecordPath) -> bool:
return path.endswith("/")
- def assert_no_path_traversal(dest_dir_path, target_path):
- # type: (str, str) -> None
+ def assert_no_path_traversal(dest_dir_path: str, target_path: str) -> None:
if not is_within_directory(dest_dir_path, target_path):
message = (
"The wheel {!r} has a file {!r} trying to install"
@@ -521,10 +489,10 @@ def _install_wheel(
message.format(wheel_path, target_path, dest_dir_path)
)
- def root_scheme_file_maker(zip_file, dest):
- # type: (ZipFile, str) -> Callable[[RecordPath], File]
- def make_root_scheme_file(record_path):
- # type: (RecordPath) -> File
+ def root_scheme_file_maker(
+ zip_file: ZipFile, dest: str
+ ) -> Callable[[RecordPath], "File"]:
+ def make_root_scheme_file(record_path: RecordPath) -> "File":
normed_path = os.path.normpath(record_path)
dest_path = os.path.join(dest, normed_path)
assert_no_path_traversal(dest, dest_path)
@@ -532,17 +500,12 @@ def _install_wheel(
return make_root_scheme_file
- def data_scheme_file_maker(zip_file, scheme):
- # type: (ZipFile, Scheme) -> Callable[[RecordPath], File]
- scheme_paths = {}
- for key in SCHEME_KEYS:
- encoded_key = ensure_text(key)
- scheme_paths[encoded_key] = ensure_text(
- getattr(scheme, key), encoding=sys.getfilesystemencoding()
- )
+ def data_scheme_file_maker(
+ zip_file: ZipFile, scheme: Scheme
+ ) -> Callable[[RecordPath], "File"]:
+ scheme_paths = {key: getattr(scheme, key) for key in SCHEME_KEYS}
- def make_data_scheme_file(record_path):
- # type: (RecordPath) -> File
+ def make_data_scheme_file(record_path: RecordPath) -> "File":
normed_path = os.path.normpath(record_path)
try:
_, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2)
@@ -561,9 +524,7 @@ def _install_wheel(
"Unknown scheme key used in {}: {} (for file {!r}). .data"
" directory contents should be in subdirectories named"
" with a valid scheme key ({})"
- ).format(
- wheel_path, scheme_key, record_path, valid_scheme_keys
- )
+ ).format(wheel_path, scheme_key, record_path, valid_scheme_keys)
raise InstallationError(message)
dest_path = os.path.join(scheme_path, dest_subpath)
@@ -572,30 +533,19 @@ def _install_wheel(
return make_data_scheme_file
- def is_data_scheme_path(path):
- # type: (RecordPath) -> bool
+ def is_data_scheme_path(path: RecordPath) -> bool:
return path.split("/", 1)[0].endswith(".data")
- paths = all_paths()
+ paths = cast(List[RecordPath], wheel_zip.namelist())
file_paths = filterfalse(is_dir_path, paths)
- root_scheme_paths, data_scheme_paths = partition(
- is_data_scheme_path, file_paths
- )
+ root_scheme_paths, data_scheme_paths = partition(is_data_scheme_path, file_paths)
- make_root_scheme_file = root_scheme_file_maker(
- wheel_zip,
- ensure_text(lib_dir, encoding=sys.getfilesystemencoding()),
- )
- files = map(make_root_scheme_file, root_scheme_paths)
+ make_root_scheme_file = root_scheme_file_maker(wheel_zip, lib_dir)
+ files: Iterator[File] = map(make_root_scheme_file, root_scheme_paths)
- def is_script_scheme_path(path):
- # type: (RecordPath) -> bool
+ def is_script_scheme_path(path: RecordPath) -> bool:
parts = path.split("/", 2)
- return (
- len(parts) > 2 and
- parts[0].endswith(".data") and
- parts[1] == "scripts"
- )
+ return len(parts) > 2 and parts[0].endswith(".data") and parts[1] == "scripts"
other_scheme_paths, script_scheme_paths = partition(
is_script_scheme_path, data_scheme_paths
@@ -606,30 +556,32 @@ def _install_wheel(
files = chain(files, other_scheme_files)
# Get the defined entry points
- distribution = get_wheel_distribution(wheel_path, canonicalize_name(name))
+ distribution = get_wheel_distribution(
+ FilesystemWheel(wheel_path),
+ canonicalize_name(name),
+ )
console, gui = get_entrypoints(distribution)
- def is_entrypoint_wrapper(file):
- # type: (File) -> bool
+ def is_entrypoint_wrapper(file: "File") -> bool:
# EP, EP.exe and EP-script.py are scripts generated for
# entry point EP by setuptools
path = file.dest_path
name = os.path.basename(path)
- if name.lower().endswith('.exe'):
+ if name.lower().endswith(".exe"):
matchname = name[:-4]
- elif name.lower().endswith('-script.py'):
+ elif name.lower().endswith("-script.py"):
matchname = name[:-10]
elif name.lower().endswith(".pya"):
matchname = name[:-4]
else:
matchname = name
# Ignore setuptools-generated scripts
- return (matchname in console or matchname in gui)
+ return matchname in console or matchname in gui
- script_scheme_files = map(make_data_scheme_file, script_scheme_paths)
- script_scheme_files = filterfalse(
- is_entrypoint_wrapper, script_scheme_files
+ script_scheme_files: Iterator[File] = map(
+ make_data_scheme_file, script_scheme_paths
)
+ script_scheme_files = filterfalse(is_entrypoint_wrapper, script_scheme_files)
script_scheme_files = map(ScriptFile, script_scheme_files)
files = chain(files, script_scheme_files)
@@ -637,8 +589,7 @@ def _install_wheel(
file.save()
record_installed(file.src_record_path, file.dest_path, file.changed)
- def pyc_source_file_paths():
- # type: () -> Iterator[str]
+ def pyc_source_file_paths() -> Generator[str, None, None]:
# We de-duplicate installation paths, since there can be overlap (e.g.
# file in .data maps to same location as file in wheel root).
# Sorting installation paths makes it easier to reproduce and debug
@@ -647,30 +598,21 @@ def _install_wheel(
full_installed_path = os.path.join(lib_dir, installed_path)
if not os.path.isfile(full_installed_path):
continue
- if not full_installed_path.endswith('.py'):
+ if not full_installed_path.endswith(".py"):
continue
yield full_installed_path
- def pyc_output_path(path):
- # type: (str) -> str
- """Return the path the pyc file would have been written to.
- """
+ def pyc_output_path(path: str) -> str:
+ """Return the path the pyc file would have been written to."""
return importlib.util.cache_from_source(path)
# Compile all of the pyc files for the installed files
if pycompile:
with captured_stdout() as stdout:
with warnings.catch_warnings():
- warnings.filterwarnings('ignore')
+ warnings.filterwarnings("ignore")
for path in pyc_source_file_paths():
- # Python 2's `compileall.compile_file` requires a str in
- # error cases, so we must convert to the native type.
- path_arg = ensure_str(
- path, encoding=sys.getfilesystemencoding()
- )
- success = compileall.compile_file(
- path_arg, force=True, quiet=True
- )
+ success = compileall.compile_file(path, force=True, quiet=True)
if success:
pyc_path = pyc_output_path(path)
assert os.path.exists(pyc_path)
@@ -689,7 +631,7 @@ def _install_wheel(
# Ensure we don't generate any variants for scripts because this is almost
# never what somebody wants.
# See https://bitbucket.org/pypa/distlib/issue/35/
- maker.variants = {''}
+ maker.variants = {""}
# This is required because otherwise distlib creates scripts that are not
# executable.
@@ -699,14 +641,12 @@ def _install_wheel(
# Generate the console and GUI entry points specified in the wheel
scripts_to_generate = get_console_script_specs(console)
- gui_scripts_to_generate = list(starmap('{} = {}'.format, gui.items()))
+ gui_scripts_to_generate = list(starmap("{} = {}".format, gui.items()))
generated_console_scripts = maker.make_multiple(scripts_to_generate)
generated.extend(generated_console_scripts)
- generated.extend(
- maker.make_multiple(gui_scripts_to_generate, {'gui': True})
- )
+ generated.extend(maker.make_multiple(gui_scripts_to_generate, {"gui": True}))
if warn_script_location:
msg = message_about_scripts_not_on_PATH(generated_console_scripts)
@@ -716,8 +656,7 @@ def _install_wheel(
generated_file_mode = 0o666 & ~current_umask()
@contextlib.contextmanager
- def _generate_file(path, **kwargs):
- # type: (str, **Any) -> Iterator[BinaryIO]
+ def _generate_file(path: str, **kwargs: Any) -> Generator[BinaryIO, None, None]:
with adjacent_tmp_file(path, **kwargs) as f:
yield f
os.chmod(f.name, generated_file_mode)
@@ -726,9 +665,9 @@ def _install_wheel(
dest_info_dir = os.path.join(lib_dir, info_dir)
# Record pip as the installer
- installer_path = os.path.join(dest_info_dir, 'INSTALLER')
+ installer_path = os.path.join(dest_info_dir, "INSTALLER")
with _generate_file(installer_path) as installer_file:
- installer_file.write(b'pip\n')
+ installer_file.write(b"pip\n")
generated.append(installer_path)
# Record the PEP 610 direct URL reference
@@ -740,12 +679,12 @@ def _install_wheel(
# Record the REQUESTED file
if requested:
- requested_path = os.path.join(dest_info_dir, 'REQUESTED')
+ requested_path = os.path.join(dest_info_dir, "REQUESTED")
with open(requested_path, "wb"):
pass
generated.append(requested_path)
- record_text = distribution.read_text('RECORD')
+ record_text = distribution.read_text("RECORD")
record_rows = list(csv.reader(record_text.splitlines()))
rows = get_csv_rows_for_installed(
@@ -753,42 +692,38 @@ def _install_wheel(
installed=installed,
changed=changed,
generated=generated,
- lib_dir=lib_dir)
+ lib_dir=lib_dir,
+ )
# Record details of all files installed
- record_path = os.path.join(dest_info_dir, 'RECORD')
+ record_path = os.path.join(dest_info_dir, "RECORD")
- with _generate_file(record_path, **csv_io_kwargs('w')) as record_file:
- # The type mypy infers for record_file is different for Python 3
- # (typing.IO[Any]) and Python 2 (typing.BinaryIO). We explicitly
- # cast to typing.IO[str] as a workaround.
- writer = csv.writer(cast('IO[str]', record_file))
+ with _generate_file(record_path, **csv_io_kwargs("w")) as record_file:
+ # Explicitly cast to typing.IO[str] as a workaround for the mypy error:
+ # "writer" has incompatible type "BinaryIO"; expected "_Writer"
+ writer = csv.writer(cast("IO[str]", record_file))
writer.writerows(_normalized_outrows(rows))
@contextlib.contextmanager
-def req_error_context(req_description):
- # type: (str) -> Iterator[None]
+def req_error_context(req_description: str) -> Generator[None, None, None]:
try:
yield
except InstallationError as e:
message = "For req: {}. {}".format(req_description, e.args[0])
- reraise(
- InstallationError, InstallationError(message), sys.exc_info()[2]
- )
+ raise InstallationError(message) from e
def install_wheel(
- name, # type: str
- wheel_path, # type: str
- scheme, # type: Scheme
- req_description, # type: str
- pycompile=True, # type: bool
- warn_script_location=True, # type: bool
- direct_url=None, # type: Optional[DirectUrl]
- requested=False, # type: bool
-):
- # type: (...) -> None
+ name: str,
+ wheel_path: str,
+ scheme: Scheme,
+ req_description: str,
+ pycompile: bool = True,
+ warn_script_location: bool = True,
+ direct_url: Optional[DirectUrl] = None,
+ requested: bool = False,
+) -> None:
with ZipFile(wheel_path, allowZip64=True) as z:
with req_error_context(req_description):
_install_wheel(
diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py
index 8bb5a6843..4bf414cb0 100644
--- a/src/pip/_internal/operations/prepare.py
+++ b/src/pip/_internal/operations/prepare.py
@@ -8,10 +8,9 @@ import logging
import mimetypes
import os
import shutil
-from typing import Dict, Iterable, List, Optional, Tuple
+from typing import Dict, Iterable, List, Optional
from pip._vendor.packaging.utils import canonicalize_name
-from pip._vendor.pkg_resources import Distribution
from pip._internal.distributions import make_distribution_for_install_requirement
from pip._internal.distributions.installed import InstalledDistribution
@@ -20,11 +19,14 @@ from pip._internal.exceptions import (
HashMismatch,
HashUnpinned,
InstallationError,
+ MetadataInconsistent,
NetworkConnectionError,
PreviousBuildDirError,
VcsHashUnsupported,
)
from pip._internal.index.package_finder import PackageFinder
+from pip._internal.metadata import BaseDistribution, get_metadata_distribution
+from pip._internal.models.direct_url import ArchiveInfo
from pip._internal.models.link import Link
from pip._internal.models.wheel import Wheel
from pip._internal.network.download import BatchDownloader, Downloader
@@ -33,13 +35,20 @@ from pip._internal.network.lazy_wheel import (
dist_from_wheel_url,
)
from pip._internal.network.session import PipSession
+from pip._internal.operations.build.build_tracker import BuildTracker
from pip._internal.req.req_install import InstallRequirement
-from pip._internal.req.req_tracker import RequirementTracker
-from pip._internal.utils.deprecation import deprecated
-from pip._internal.utils.filesystem import copy2_fixed
+from pip._internal.utils.direct_url_helpers import (
+ direct_url_for_editable,
+ direct_url_from_link,
+)
from pip._internal.utils.hashes import Hashes, MissingHashes
from pip._internal.utils.logging import indent_log
-from pip._internal.utils.misc import display_path, hide_url, is_installable_dir, rmtree
+from pip._internal.utils.misc import (
+ display_path,
+ hash_file,
+ hide_url,
+ is_installable_dir,
+)
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.unpacking import unpack_file
from pip._internal.vcs import vcs
@@ -48,30 +57,29 @@ logger = logging.getLogger(__name__)
def _get_prepared_distribution(
- req, # type: InstallRequirement
- req_tracker, # type: RequirementTracker
- finder, # type: PackageFinder
- build_isolation, # type: bool
-):
- # type: (...) -> Distribution
+ req: InstallRequirement,
+ build_tracker: BuildTracker,
+ finder: PackageFinder,
+ build_isolation: bool,
+ check_build_deps: bool,
+) -> BaseDistribution:
"""Prepare a distribution for installation."""
abstract_dist = make_distribution_for_install_requirement(req)
- with req_tracker.track(req):
- abstract_dist.prepare_distribution_metadata(finder, build_isolation)
- return abstract_dist.get_pkg_resources_distribution()
+ with build_tracker.track(req):
+ abstract_dist.prepare_distribution_metadata(
+ finder, build_isolation, check_build_deps
+ )
+ return abstract_dist.get_metadata_distribution()
-def unpack_vcs_link(link, location):
- # type: (Link, str) -> None
+def unpack_vcs_link(link: Link, location: str, verbosity: int) -> None:
vcs_backend = vcs.get_backend_for_scheme(link.scheme)
assert vcs_backend is not None
- vcs_backend.unpack(location, url=hide_url(link.url))
+ vcs_backend.unpack(location, url=hide_url(link.url), verbosity=verbosity)
class File:
-
- def __init__(self, path, content_type):
- # type: (str, Optional[str]) -> None
+ def __init__(self, path: str, content_type: Optional[str]) -> None:
self.path = path
if content_type is None:
self.content_type = mimetypes.guess_type(path)[0]
@@ -80,19 +88,16 @@ class File:
def get_http_url(
- link, # type: Link
- download, # type: Downloader
- download_dir=None, # type: Optional[str]
- hashes=None, # type: Optional[Hashes]
-):
- # type: (...) -> File
+ link: Link,
+ download: Downloader,
+ download_dir: Optional[str] = None,
+ hashes: Optional[Hashes] = None,
+) -> File:
temp_dir = TempDirectory(kind="unpack", globally_managed=True)
# If a download dir is specified, is the file already downloaded there?
already_downloaded_path = None
if download_dir:
- already_downloaded_path = _check_download_dir(
- link, download_dir, hashes
- )
+ already_downloaded_path = _check_download_dir(link, download_dir, hashes)
if already_downloaded_path:
from_path = already_downloaded_path
@@ -106,72 +111,14 @@ def get_http_url(
return File(from_path, content_type)
-def _copy2_ignoring_special_files(src, dest):
- # type: (str, str) -> None
- """Copying special files is not supported, but as a convenience to users
- we skip errors copying them. This supports tools that may create e.g.
- socket files in the project source directory.
- """
- try:
- copy2_fixed(src, dest)
- except shutil.SpecialFileError as e:
- # SpecialFileError may be raised due to either the source or
- # destination. If the destination was the cause then we would actually
- # care, but since the destination directory is deleted prior to
- # copy we ignore all of them assuming it is caused by the source.
- logger.warning(
- "Ignoring special file error '%s' encountered copying %s to %s.",
- str(e),
- src,
- dest,
- )
-
-
-def _copy_source_tree(source, target):
- # type: (str, str) -> None
- target_abspath = os.path.abspath(target)
- target_basename = os.path.basename(target_abspath)
- target_dirname = os.path.dirname(target_abspath)
-
- def ignore(d, names):
- # type: (str, List[str]) -> List[str]
- skipped = [] # type: List[str]
- if d == source:
- # Pulling in those directories can potentially be very slow,
- # exclude the following directories if they appear in the top
- # level dir (and only it).
- # See discussion at https://github.com/pypa/pip/pull/6770
- skipped += ['.tox', '.nox']
- if os.path.abspath(d) == target_dirname:
- # Prevent an infinite recursion if the target is in source.
- # This can happen when TMPDIR is set to ${PWD}/...
- # and we copy PWD to TMPDIR.
- skipped += [target_basename]
- return skipped
-
- shutil.copytree(
- source,
- target,
- ignore=ignore,
- symlinks=True,
- copy_function=_copy2_ignoring_special_files,
- )
-
-
def get_file_url(
- link, # type: Link
- download_dir=None, # type: Optional[str]
- hashes=None # type: Optional[Hashes]
-):
- # type: (...) -> File
- """Get file and optionally check its hash.
- """
+ link: Link, download_dir: Optional[str] = None, hashes: Optional[Hashes] = None
+) -> File:
+ """Get file and optionally check its hash."""
# If a download dir is specified, is the file already there and valid?
already_downloaded_path = None
if download_dir:
- already_downloaded_path = _check_download_dir(
- link, download_dir, hashes
- )
+ already_downloaded_path = _check_download_dir(link, download_dir, hashes)
if already_downloaded_path:
from_path = already_downloaded_path
@@ -189,13 +136,13 @@ def get_file_url(
def unpack_url(
- link, # type: Link
- location, # type: str
- download, # type: Downloader
- download_dir=None, # type: Optional[str]
- hashes=None, # type: Optional[Hashes]
-):
- # type: (...) -> Optional[File]
+ link: Link,
+ location: str,
+ download: Downloader,
+ verbosity: int,
+ download_dir: Optional[str] = None,
+ hashes: Optional[Hashes] = None,
+) -> Optional[File]:
"""Unpack link into location, downloading if required.
:param hashes: A Hashes object, one of whose embedded hashes must match,
@@ -205,32 +152,10 @@ def unpack_url(
"""
# non-editable vcs urls
if link.is_vcs:
- unpack_vcs_link(link, location)
+ unpack_vcs_link(link, location, verbosity=verbosity)
return None
- # Once out-of-tree-builds are no longer supported, could potentially
- # replace the below condition with `assert not link.is_existing_dir`
- # - unpack_url does not need to be called for in-tree-builds.
- #
- # As further cleanup, _copy_source_tree and accompanying tests can
- # be removed.
- if link.is_existing_dir():
- deprecated(
- reason=(
- "pip copied the source tree into a temporary directory "
- "before building it. This is changing so that packages "
- "are built in-place "
- 'within the original source tree ("in-tree build").'
- ),
- replacement=None,
- gone_in="21.3",
- feature_flag="in-tree-build",
- issue=7555,
- )
- if os.path.isdir(location):
- rmtree(location)
- _copy_source_tree(link.file_path, location)
- return None
+ assert not link.is_existing_dir()
# file urls
if link.is_file:
@@ -253,10 +178,11 @@ def unpack_url(
return file
-def _check_download_dir(link, download_dir, hashes):
- # type: (Link, str, Optional[Hashes]) -> Optional[str]
- """ Check download_dir for previously downloaded file with correct hash
- If a correct file is found return its path else None
+def _check_download_dir(
+ link: Link, download_dir: str, hashes: Optional[Hashes]
+) -> Optional[str]:
+ """Check download_dir for previously downloaded file with correct hash
+ If a correct file is found return its path else None
"""
download_path = os.path.join(download_dir, link.filename)
@@ -264,15 +190,14 @@ def _check_download_dir(link, download_dir, hashes):
return None
# If already downloaded, does its hash match?
- logger.info('File was already downloaded %s', download_path)
+ logger.info("File was already downloaded %s", download_path)
if hashes:
try:
hashes.check_against_path(download_path)
except HashMismatch:
logger.warning(
- 'Previously-downloaded file %s has bad hash. '
- 'Re-downloading.',
- download_path
+ "Previously-downloaded file %s has bad hash. Re-downloading.",
+ download_path,
)
os.unlink(download_path)
return None
@@ -280,30 +205,29 @@ def _check_download_dir(link, download_dir, hashes):
class RequirementPreparer:
- """Prepares a Requirement
- """
+ """Prepares a Requirement"""
def __init__(
self,
- build_dir, # type: str
- download_dir, # type: Optional[str]
- src_dir, # type: str
- build_isolation, # type: bool
- req_tracker, # type: RequirementTracker
- session, # type: PipSession
- progress_bar, # type: str
- finder, # type: PackageFinder
- require_hashes, # type: bool
- use_user_site, # type: bool
- lazy_wheel, # type: bool
- in_tree_build, # type: bool
- ):
- # type: (...) -> None
+ build_dir: str,
+ download_dir: Optional[str],
+ src_dir: str,
+ build_isolation: bool,
+ check_build_deps: bool,
+ build_tracker: BuildTracker,
+ session: PipSession,
+ progress_bar: str,
+ finder: PackageFinder,
+ require_hashes: bool,
+ use_user_site: bool,
+ lazy_wheel: bool,
+ verbosity: int,
+ ) -> None:
super().__init__()
self.src_dir = src_dir
self.build_dir = build_dir
- self.req_tracker = req_tracker
+ self.build_tracker = build_tracker
self._session = session
self._download = Downloader(session, progress_bar)
self._batch_download = BatchDownloader(session, progress_bar)
@@ -316,6 +240,9 @@ class RequirementPreparer:
# Is build isolation allowed?
self.build_isolation = build_isolation
+ # Should check build dependencies?
+ self.check_build_deps = check_build_deps
+
# Should hash-checking be required?
self.require_hashes = require_hashes
@@ -325,17 +252,16 @@ class RequirementPreparer:
# Should wheels be downloaded lazily?
self.use_lazy_wheel = lazy_wheel
- # Should in-tree builds be used for local paths?
- self.in_tree_build = in_tree_build
+ # How verbose should underlying tooling be?
+ self.verbosity = verbosity
- # Memoized downloaded files, as mapping of url: (path, mime type)
- self._downloaded = {} # type: Dict[str, Tuple[str, str]]
+ # Memoized downloaded files, as mapping of url: path.
+ self._downloaded: Dict[str, str] = {}
# Previous "header" printed for a link-based InstallRequirement
self._previous_requirement_header = ("", "")
- def _log_preparing_link(self, req):
- # type: (InstallRequirement) -> None
+ def _log_preparing_link(self, req: InstallRequirement) -> None:
"""Provide context for the requirement being prepared."""
if req.link.is_file and not req.original_link_is_in_wheel_cache:
message = "Processing %s"
@@ -352,8 +278,9 @@ class RequirementPreparer:
with indent_log():
logger.info("Using cached %s", req.link.filename)
- def _ensure_link_req_src_dir(self, req, parallel_builds):
- # type: (InstallRequirement, bool) -> None
+ def _ensure_link_req_src_dir(
+ self, req: InstallRequirement, parallel_builds: bool
+ ) -> None:
"""Ensure source_dir of a linked InstallRequirement."""
# Since source_dir is only set for editable requirements.
if req.link.is_wheel:
@@ -361,7 +288,7 @@ class RequirementPreparer:
# directory.
return
assert req.source_dir is None
- if req.link.is_existing_dir() and self.in_tree_build:
+ if req.link.is_existing_dir():
# build local directories in-tree
req.source_dir = req.link.file_path
return
@@ -378,6 +305,7 @@ class RequirementPreparer:
# installation.
# FIXME: this won't upgrade when there's an existing
# package unpacked in `req.source_dir`
+ # TODO: this check is now probably dead code
if is_installable_dir(req.source_dir):
raise PreviousBuildDirError(
"pip can't proceed with requirements '{}' due to a"
@@ -387,8 +315,7 @@ class RequirementPreparer:
"Please delete it and try again.".format(req, req.source_dir)
)
- def _get_linked_req_hashes(self, req):
- # type: (InstallRequirement) -> Hashes
+ def _get_linked_req_hashes(self, req: InstallRequirement) -> Hashes:
# By the time this is called, the requirement's link should have
# been checked so we can tell what kind of requirements req is
# and raise some more informative errors than otherwise.
@@ -420,18 +347,72 @@ class RequirementPreparer:
# showing the user what the hash should be.
return req.hashes(trust_internet=False) or MissingHashes()
- def _fetch_metadata_using_lazy_wheel(self, link):
- # type: (Link) -> Optional[Distribution]
+ def _fetch_metadata_only(
+ self,
+ req: InstallRequirement,
+ ) -> Optional[BaseDistribution]:
+ if self.require_hashes:
+ logger.debug(
+ "Metadata-only fetching is not used as hash checking is required",
+ )
+ return None
+ # Try PEP 658 metadata first, then fall back to lazy wheel if unavailable.
+ return self._fetch_metadata_using_link_data_attr(
+ req
+ ) or self._fetch_metadata_using_lazy_wheel(req.link)
+
+ def _fetch_metadata_using_link_data_attr(
+ self,
+ req: InstallRequirement,
+ ) -> Optional[BaseDistribution]:
+ """Fetch metadata from the data-dist-info-metadata attribute, if possible."""
+ # (1) Get the link to the metadata file, if provided by the backend.
+ metadata_link = req.link.metadata_link()
+ if metadata_link is None:
+ return None
+ assert req.req is not None
+ logger.info(
+ "Obtaining dependency information for %s from %s",
+ req.req,
+ metadata_link,
+ )
+ # (2) Download the contents of the METADATA file, separate from the dist itself.
+ metadata_file = get_http_url(
+ metadata_link,
+ self._download,
+ hashes=metadata_link.as_hashes(),
+ )
+ with open(metadata_file.path, "rb") as f:
+ metadata_contents = f.read()
+ # (3) Generate a dist just from those file contents.
+ metadata_dist = get_metadata_distribution(
+ metadata_contents,
+ req.link.filename,
+ req.req.name,
+ )
+ # (4) Ensure the Name: field from the METADATA file matches the name from the
+ # install requirement.
+ #
+ # NB: raw_name will fall back to the name from the install requirement if
+ # the Name: field is not present, but it's noted in the raw_name docstring
+ # that that should NEVER happen anyway.
+ if metadata_dist.raw_name != req.req.name:
+ raise MetadataInconsistent(
+ req, "Name", req.req.name, metadata_dist.raw_name
+ )
+ return metadata_dist
+
+ def _fetch_metadata_using_lazy_wheel(
+ self,
+ link: Link,
+ ) -> Optional[BaseDistribution]:
"""Fetch metadata using lazy wheel, if possible."""
+ # --use-feature=fast-deps must be provided.
if not self.use_lazy_wheel:
return None
- if self.require_hashes:
- logger.debug('Lazy wheel is not used as hash checking is required')
- return None
if link.is_file or not link.is_wheel:
logger.debug(
- 'Lazy wheel is not used as '
- '%r does not points to a remote wheel',
+ "Lazy wheel is not used as %r does not point to a remote wheel",
link,
)
return None
@@ -439,22 +420,22 @@ class RequirementPreparer:
wheel = Wheel(link.filename)
name = canonicalize_name(wheel.name)
logger.info(
- 'Obtaining dependency information from %s %s',
- name, wheel.version,
+ "Obtaining dependency information from %s %s",
+ name,
+ wheel.version,
)
- url = link.url.split('#', 1)[0]
+ url = link.url.split("#", 1)[0]
try:
return dist_from_wheel_url(name, url, self._session)
except HTTPRangeRequestUnsupported:
- logger.debug('%s does not support range requests', url)
+ logger.debug("%s does not support range requests", url)
return None
def _complete_partial_requirements(
self,
- partially_downloaded_reqs, # type: Iterable[InstallRequirement]
- parallel_builds=False, # type: bool
- ):
- # type: (...) -> None
+ partially_downloaded_reqs: Iterable[InstallRequirement],
+ parallel_builds: bool = False,
+ ) -> None:
"""Download any requirements which were only fetched by metadata."""
# Download to a temporary directory. These will be copied over as
# needed for downstream 'download', 'wheel', and 'install' commands.
@@ -463,7 +444,7 @@ class RequirementPreparer:
# Map each link to the requirement that owns it. This allows us to set
# `req.local_file_path` on the appropriate requirement after passing
# all the links at once into BatchDownloader.
- links_to_fully_download = {} # type: Dict[Link, InstallRequirement]
+ links_to_fully_download: Dict[Link, InstallRequirement] = {}
for req in partially_downloaded_reqs:
assert req.link
links_to_fully_download[req.link] = req
@@ -482,35 +463,36 @@ class RequirementPreparer:
for req in partially_downloaded_reqs:
self._prepare_linked_requirement(req, parallel_builds)
- def prepare_linked_requirement(self, req, parallel_builds=False):
- # type: (InstallRequirement, bool) -> Distribution
+ def prepare_linked_requirement(
+ self, req: InstallRequirement, parallel_builds: bool = False
+ ) -> BaseDistribution:
"""Prepare a requirement to be obtained from req.link."""
assert req.link
- link = req.link
self._log_preparing_link(req)
with indent_log():
# Check if the relevant file is already available
# in the download directory
file_path = None
- if self.download_dir is not None and link.is_wheel:
+ if self.download_dir is not None and req.link.is_wheel:
hashes = self._get_linked_req_hashes(req)
file_path = _check_download_dir(req.link, self.download_dir, hashes)
if file_path is not None:
# The file is already available, so mark it as downloaded
- self._downloaded[req.link.url] = file_path, None
+ self._downloaded[req.link.url] = file_path
else:
# The file is not available, attempt to fetch only metadata
- wheel_dist = self._fetch_metadata_using_lazy_wheel(link)
- if wheel_dist is not None:
+ metadata_dist = self._fetch_metadata_only(req)
+ if metadata_dist is not None:
req.needs_more_preparation = True
- return wheel_dist
+ return metadata_dist
# None of the optimizations worked, fully prepare the requirement
return self._prepare_linked_requirement(req, parallel_builds)
- def prepare_linked_requirements_more(self, reqs, parallel_builds=False):
- # type: (Iterable[InstallRequirement], bool) -> None
+ def prepare_linked_requirements_more(
+ self, reqs: Iterable[InstallRequirement], parallel_builds: bool = False
+ ) -> None:
"""Prepare linked requirements more, if needed."""
reqs = [req for req in reqs if req.needs_more_preparation]
for req in reqs:
@@ -519,12 +501,12 @@ class RequirementPreparer:
hashes = self._get_linked_req_hashes(req)
file_path = _check_download_dir(req.link, self.download_dir, hashes)
if file_path is not None:
- self._downloaded[req.link.url] = file_path, None
+ self._downloaded[req.link.url] = file_path
req.needs_more_preparation = False
# Prepare requirements we found were already downloaded for some
# reason. The other downloads will be completed separately.
- partially_downloaded_reqs = [] # type: List[InstallRequirement]
+ partially_downloaded_reqs: List[InstallRequirement] = []
for req in reqs:
if req.needs_more_preparation:
partially_downloaded_reqs.append(req)
@@ -534,35 +516,58 @@ class RequirementPreparer:
# TODO: separate this part out from RequirementPreparer when the v1
# resolver can be removed!
self._complete_partial_requirements(
- partially_downloaded_reqs, parallel_builds=parallel_builds,
+ partially_downloaded_reqs,
+ parallel_builds=parallel_builds,
)
- def _prepare_linked_requirement(self, req, parallel_builds):
- # type: (InstallRequirement, bool) -> Distribution
+ def _prepare_linked_requirement(
+ self, req: InstallRequirement, parallel_builds: bool
+ ) -> BaseDistribution:
assert req.link
link = req.link
self._ensure_link_req_src_dir(req, parallel_builds)
hashes = self._get_linked_req_hashes(req)
- if link.is_existing_dir() and self.in_tree_build:
+ if link.is_existing_dir():
local_file = None
elif link.url not in self._downloaded:
try:
local_file = unpack_url(
- link, req.source_dir, self._download,
- self.download_dir, hashes
+ link,
+ req.source_dir,
+ self._download,
+ self.verbosity,
+ self.download_dir,
+ hashes,
)
except NetworkConnectionError as exc:
raise InstallationError(
- 'Could not install requirement {} because of HTTP '
- 'error {} for URL {}'.format(req, exc, link)
+ "Could not install requirement {} because of HTTP "
+ "error {} for URL {}".format(req, exc, link)
)
else:
- file_path, content_type = self._downloaded[link.url]
+ file_path = self._downloaded[link.url]
if hashes:
hashes.check_against_path(file_path)
- local_file = File(file_path, content_type)
+ local_file = File(file_path, content_type=None)
+
+ # If download_info is set, we got it from the wheel cache.
+ if req.download_info is None:
+ # Editables don't go through this function (see
+ # prepare_editable_requirement).
+ assert not req.editable
+ req.download_info = direct_url_from_link(link, req.source_dir)
+ # Make sure we have a hash in download_info. If we got it as part of the
+ # URL, it will have been verified and we can rely on it. Otherwise we
+ # compute it from the downloaded file.
+ if (
+ isinstance(req.download_info.info, ArchiveInfo)
+ and not req.download_info.info.hash
+ and local_file
+ ):
+ hash = hash_file(local_file.path)[0].hexdigest()
+ req.download_info.info.hash = f"sha256={hash}"
# For use in later processing,
# preserve the file path on the requirement.
@@ -570,12 +575,15 @@ class RequirementPreparer:
req.local_file_path = local_file.path
dist = _get_prepared_distribution(
- req, self.req_tracker, self.finder, self.build_isolation,
+ req,
+ self.build_tracker,
+ self.finder,
+ self.build_isolation,
+ self.check_build_deps,
)
return dist
- def save_linked_requirement(self, req):
- # type: (InstallRequirement) -> None
+ def save_linked_requirement(self, req: InstallRequirement) -> None:
assert self.download_dir is not None
assert req.link is not None
link = req.link
@@ -586,8 +594,9 @@ class RequirementPreparer:
if link.is_existing_dir():
logger.debug(
- 'Not copying link to destination directory '
- 'since it is a directory: %s', link,
+ "Not copying link to destination directory "
+ "since it is a directory: %s",
+ link,
)
return
if req.local_file_path is None:
@@ -598,31 +607,35 @@ class RequirementPreparer:
if not os.path.exists(download_location):
shutil.copy(req.local_file_path, download_location)
download_path = display_path(download_location)
- logger.info('Saved %s', download_path)
+ logger.info("Saved %s", download_path)
def prepare_editable_requirement(
self,
- req, # type: InstallRequirement
- ):
- # type: (...) -> Distribution
- """Prepare an editable requirement
- """
+ req: InstallRequirement,
+ ) -> BaseDistribution:
+ """Prepare an editable requirement."""
assert req.editable, "cannot prepare a non-editable req as editable"
- logger.info('Obtaining %s', req)
+ logger.info("Obtaining %s", req)
with indent_log():
if self.require_hashes:
raise InstallationError(
- 'The editable requirement {} cannot be installed when '
- 'requiring hashes, because there is no single file to '
- 'hash.'.format(req)
+ "The editable requirement {} cannot be installed when "
+ "requiring hashes, because there is no single file to "
+ "hash.".format(req)
)
req.ensure_has_source_dir(self.src_dir)
req.update_editable()
+ assert req.source_dir
+ req.download_info = direct_url_for_editable(req.unpacked_source_directory)
dist = _get_prepared_distribution(
- req, self.req_tracker, self.finder, self.build_isolation,
+ req,
+ self.build_tracker,
+ self.finder,
+ self.build_isolation,
+ self.check_build_deps,
)
req.check_if_exists(self.use_user_site)
@@ -631,27 +644,24 @@ class RequirementPreparer:
def prepare_installed_requirement(
self,
- req, # type: InstallRequirement
- skip_reason # type: str
- ):
- # type: (...) -> Distribution
- """Prepare an already-installed requirement
- """
+ req: InstallRequirement,
+ skip_reason: str,
+ ) -> BaseDistribution:
+ """Prepare an already-installed requirement."""
assert req.satisfied_by, "req should have been satisfied but isn't"
assert skip_reason is not None, (
"did not get skip reason skipped but req.satisfied_by "
"is set to {}".format(req.satisfied_by)
)
logger.info(
- 'Requirement %s: %s (%s)',
- skip_reason, req, req.satisfied_by.version
+ "Requirement %s: %s (%s)", skip_reason, req, req.satisfied_by.version
)
with indent_log():
if self.require_hashes:
logger.debug(
- 'Since it is already installed, we are trusting this '
- 'package without checking its hash. To ensure a '
- 'completely repeatable environment, install into an '
- 'empty virtualenv.'
+ "Since it is already installed, we are trusting this "
+ "package without checking its hash. To ensure a "
+ "completely repeatable environment, install into an "
+ "empty virtualenv."
)
- return InstalledDistribution(req).get_pkg_resources_distribution()
+ return InstalledDistribution(req).get_metadata_distribution()
diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py
index 5aa6160b4..1e9119f3e 100644
--- a/src/pip/_internal/pyproject.py
+++ b/src/pip/_internal/pyproject.py
@@ -1,3 +1,4 @@
+import importlib.util
import os
from collections import namedtuple
from typing import Any, List, Optional
@@ -5,34 +6,29 @@ from typing import Any, List, Optional
from pip._vendor import tomli
from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
-from pip._internal.exceptions import InstallationError
+from pip._internal.exceptions import (
+ InstallationError,
+ InvalidPyProjectBuildRequires,
+ MissingPyProjectBuildRequires,
+)
-def _is_list_of_str(obj):
- # type: (Any) -> bool
- return (
- isinstance(obj, list) and
- all(isinstance(item, str) for item in obj)
- )
+def _is_list_of_str(obj: Any) -> bool:
+ return isinstance(obj, list) and all(isinstance(item, str) for item in obj)
-def make_pyproject_path(unpacked_source_directory):
- # type: (str) -> str
- return os.path.join(unpacked_source_directory, 'pyproject.toml')
+def make_pyproject_path(unpacked_source_directory: str) -> str:
+ return os.path.join(unpacked_source_directory, "pyproject.toml")
-BuildSystemDetails = namedtuple('BuildSystemDetails', [
- 'requires', 'backend', 'check', 'backend_path'
-])
+BuildSystemDetails = namedtuple(
+ "BuildSystemDetails", ["requires", "backend", "check", "backend_path"]
+)
def load_pyproject_toml(
- use_pep517, # type: Optional[bool]
- pyproject_toml, # type: str
- setup_py, # type: str
- req_name # type: str
-):
- # type: (...) -> Optional[BuildSystemDetails]
+ use_pep517: Optional[bool], pyproject_toml: str, setup_py: str, req_name: str
+) -> Optional[BuildSystemDetails]:
"""Load the pyproject.toml file.
Parameters:
@@ -57,9 +53,15 @@ def load_pyproject_toml(
has_pyproject = os.path.isfile(pyproject_toml)
has_setup = os.path.isfile(setup_py)
+ if not has_pyproject and not has_setup:
+ raise InstallationError(
+ f"{req_name} does not appear to be a Python project: "
+ f"neither 'setup.py' nor 'pyproject.toml' found."
+ )
+
if has_pyproject:
with open(pyproject_toml, encoding="utf-8") as f:
- pp_toml = tomli.load(f)
+ pp_toml = tomli.loads(f.read())
build_system = pp_toml.get("build-system")
else:
build_system = None
@@ -82,17 +84,21 @@ def load_pyproject_toml(
raise InstallationError(
"Disabling PEP 517 processing is invalid: "
"project specifies a build backend of {} "
- "in pyproject.toml".format(
- build_system["build-backend"]
- )
+ "in pyproject.toml".format(build_system["build-backend"])
)
use_pep517 = True
# If we haven't worked out whether to use PEP 517 yet,
# and the user hasn't explicitly stated a preference,
- # we do so if the project has a pyproject.toml file.
+ # we do so if the project has a pyproject.toml file
+ # or if we cannot import setuptools.
+
+ # We fallback to PEP 517 when without setuptools,
+ # so setuptools can be installed as a default build backend.
+ # For more info see:
+ # https://discuss.python.org/t/pip-without-setuptools-could-the-experience-be-improved/11810/9
elif use_pep517 is None:
- use_pep517 = has_pyproject
+ use_pep517 = has_pyproject or not importlib.util.find_spec("setuptools")
# At this point, we know whether we're going to use PEP 517.
assert use_pep517 is not None
@@ -124,46 +130,32 @@ def load_pyproject_toml(
# Ensure that the build-system section in pyproject.toml conforms
# to PEP 518.
- error_template = (
- "{package} has a pyproject.toml file that does not comply "
- "with PEP 518: {reason}"
- )
# Specifying the build-system table but not the requires key is invalid
if "requires" not in build_system:
- raise InstallationError(
- error_template.format(package=req_name, reason=(
- "it has a 'build-system' table but not "
- "'build-system.requires' which is mandatory in the table"
- ))
- )
+ raise MissingPyProjectBuildRequires(package=req_name)
# Error out if requires is not a list of strings
requires = build_system["requires"]
if not _is_list_of_str(requires):
- raise InstallationError(error_template.format(
+ raise InvalidPyProjectBuildRequires(
package=req_name,
- reason="'build-system.requires' is not a list of strings.",
- ))
+ reason="It is not a list of strings.",
+ )
# Each requirement must be valid as per PEP 508
for requirement in requires:
try:
Requirement(requirement)
- except InvalidRequirement:
- raise InstallationError(
- error_template.format(
- package=req_name,
- reason=(
- "'build-system.requires' contains an invalid "
- "requirement: {!r}".format(requirement)
- ),
- )
- )
+ except InvalidRequirement as error:
+ raise InvalidPyProjectBuildRequires(
+ package=req_name,
+ reason=f"It contains an invalid requirement: {requirement!r}",
+ ) from error
backend = build_system.get("build-backend")
backend_path = build_system.get("backend-path", [])
- check = [] # type: List[str]
+ check: List[str] = []
if backend is None:
# If the user didn't specify a backend, we assume they want to use
# the setuptools backend. But we can't be sure they have included
diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py
index 70dea27a6..8d5635966 100644
--- a/src/pip/_internal/req/__init__.py
+++ b/src/pip/_internal/req/__init__.py
@@ -1,6 +1,6 @@
import collections
import logging
-from typing import Iterator, List, Optional, Sequence, Tuple
+from typing import Generator, List, Optional, Sequence, Tuple
from pip._internal.utils.logging import indent_log
@@ -28,7 +28,7 @@ class InstallationResult:
def _validate_requirements(
requirements: List[InstallRequirement],
-) -> Iterator[Tuple[str, InstallRequirement]]:
+) -> Generator[Tuple[str, InstallRequirement], None, None]:
for req in requirements:
assert req.name, f"invalid to-be-installed requirement: {req}"
yield req.name, req
diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py
index b9b18139a..dea7c3b01 100644
--- a/src/pip/_internal/req/constructors.py
+++ b/src/pip/_internal/req/constructors.py
@@ -16,17 +16,16 @@ from typing import Any, Dict, Optional, Set, Tuple, Union
from pip._vendor.packaging.markers import Marker
from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
from pip._vendor.packaging.specifiers import Specifier
-from pip._vendor.pkg_resources import RequirementParseError, parse_requirements
from pip._internal.exceptions import InstallationError
from pip._internal.models.index import PyPI, TestPyPI
from pip._internal.models.link import Link
from pip._internal.models.wheel import Wheel
-from pip._internal.pyproject import make_pyproject_path
from pip._internal.req.req_file import ParsedRequirement
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.filetypes import is_archive_file
from pip._internal.utils.misc import is_installable_dir
+from pip._internal.utils.packaging import get_requirement
from pip._internal.utils.urls import path_to_url
from pip._internal.vcs import is_url, vcs
@@ -55,7 +54,7 @@ def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
def convert_extras(extras: Optional[str]) -> Set[str]:
if not extras:
return set()
- return Requirement("placeholder" + extras.lower()).extras
+ return get_requirement("placeholder" + extras.lower()).extras
def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
@@ -75,21 +74,6 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
url_no_extras, extras = _strip_extras(url)
if os.path.isdir(url_no_extras):
- setup_py = os.path.join(url_no_extras, "setup.py")
- setup_cfg = os.path.join(url_no_extras, "setup.cfg")
- if not os.path.exists(setup_py) and not os.path.exists(setup_cfg):
- msg = (
- 'File "setup.py" or "setup.cfg" not found. Directory cannot be '
- "installed in editable mode: {}".format(os.path.abspath(url_no_extras))
- )
- pyproject_path = make_pyproject_path(url_no_extras)
- if os.path.isfile(pyproject_path):
- msg += (
- '\n(A "pyproject.toml" file was found, but editable '
- "mode currently requires a setuptools-based build.)"
- )
- raise InstallationError(msg)
-
# Treating it as code that has already been checked out
url_no_extras = path_to_url(url_no_extras)
@@ -99,7 +83,7 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
return (
package_name,
url_no_extras,
- Requirement("placeholder" + extras.lower()).extras,
+ get_requirement("placeholder" + extras.lower()).extras,
)
else:
return package_name, url_no_extras, set()
@@ -128,31 +112,56 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
return package_name, url, set()
+def check_first_requirement_in_file(filename: str) -> None:
+ """Check if file is parsable as a requirements file.
+
+ This is heavily based on ``pkg_resources.parse_requirements``, but
+ simplified to just check the first meaningful line.
+
+ :raises InvalidRequirement: If the first meaningful line cannot be parsed
+ as an requirement.
+ """
+ with open(filename, encoding="utf-8", errors="ignore") as f:
+ # Create a steppable iterator, so we can handle \-continuations.
+ lines = (
+ line
+ for line in (line.strip() for line in f)
+ if line and not line.startswith("#") # Skip blank lines/comments.
+ )
+
+ for line in lines:
+ # Drop comments -- a hash without a space may be in a URL.
+ if " #" in line:
+ line = line[: line.find(" #")]
+ # If there is a line continuation, drop it, and append the next line.
+ if line.endswith("\\"):
+ line = line[:-2].strip() + next(lines, "")
+ Requirement(line)
+ return
+
+
def deduce_helpful_msg(req: str) -> str:
"""Returns helpful msg in case requirements file does not exist,
or cannot be parsed.
:params req: Requirements file path
"""
- msg = ""
- if os.path.exists(req):
- msg = " The path does exist. "
- # Try to parse and check if it is a requirements file.
- try:
- with open(req) as fp:
- # parse first line only
- next(parse_requirements(fp.read()))
- msg += (
- "The argument you provided "
- "({}) appears to be a"
- " requirements file. If that is the"
- " case, use the '-r' flag to install"
- " the packages specified within it."
- ).format(req)
- except RequirementParseError:
- logger.debug("Cannot parse '%s' as requirements file", req, exc_info=True)
+ if not os.path.exists(req):
+ return f" File '{req}' does not exist."
+ msg = " The path does exist. "
+ # Try to parse and check if it is a requirements file.
+ try:
+ check_first_requirement_in_file(req)
+ except InvalidRequirement:
+ logger.debug("Cannot parse '%s' as requirements file", req)
else:
- msg += f" File '{req}' does not exist."
+ msg += (
+ f"The argument you provided "
+ f"({req}) appears to be a"
+ f" requirements file. If that is the"
+ f" case, use the '-r' flag to install"
+ f" the packages specified within it."
+ )
return msg
@@ -197,6 +206,8 @@ def install_req_from_editable(
options: Optional[Dict[str, Any]] = None,
constraint: bool = False,
user_supplied: bool = False,
+ permit_editable_wheels: bool = False,
+ config_settings: Optional[Dict[str, str]] = None,
) -> InstallRequirement:
parts = parse_req_from_editable(editable_req)
@@ -206,6 +217,7 @@ def install_req_from_editable(
comes_from=comes_from,
user_supplied=user_supplied,
editable=True,
+ permit_editable_wheels=permit_editable_wheels,
link=parts.link,
constraint=constraint,
use_pep517=use_pep517,
@@ -213,6 +225,7 @@ def install_req_from_editable(
install_options=options.get("install_options", []) if options else [],
global_options=options.get("global_options", []) if options else [],
hash_options=options.get("hashes", {}) if options else {},
+ config_settings=config_settings,
extras=parts.extras,
)
@@ -248,6 +261,8 @@ def _get_url_from_path(path: str, name: str) -> Optional[str]:
if _looks_like_path(name) and os.path.isdir(path):
if is_installable_dir(path):
return path_to_url(path)
+ # TODO: The is_installable_dir test here might not be necessary
+ # now that it is done in load_pyproject_toml too.
raise InstallationError(
f"Directory {name!r} is not installable. Neither 'setup.py' "
"nor 'pyproject.toml' found."
@@ -323,7 +338,7 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
def _parse_req_string(req_as_string: str) -> Requirement:
try:
- req = Requirement(req_as_string)
+ req = get_requirement(req_as_string)
except InvalidRequirement:
if os.path.sep in req_as_string:
add_msg = "It looks like a path."
@@ -367,6 +382,7 @@ def install_req_from_line(
constraint: bool = False,
line_source: Optional[str] = None,
user_supplied: bool = False,
+ config_settings: Optional[Dict[str, str]] = None,
) -> InstallRequirement:
"""Creates an InstallRequirement from a name, which might be a
requirement, directory containing 'setup.py', filename, or URL.
@@ -386,6 +402,7 @@ def install_req_from_line(
install_options=options.get("install_options", []) if options else [],
global_options=options.get("global_options", []) if options else [],
hash_options=options.get("hashes", {}) if options else {},
+ config_settings=config_settings,
constraint=constraint,
extras=parts.extras,
user_supplied=user_supplied,
@@ -398,9 +415,10 @@ def install_req_from_req_string(
isolated: bool = False,
use_pep517: Optional[bool] = None,
user_supplied: bool = False,
+ config_settings: Optional[Dict[str, str]] = None,
) -> InstallRequirement:
try:
- req = Requirement(req_string)
+ req = get_requirement(req_string)
except InvalidRequirement:
raise InstallationError(f"Invalid requirement: '{req_string}'")
@@ -427,6 +445,7 @@ def install_req_from_req_string(
isolated=isolated,
use_pep517=use_pep517,
user_supplied=user_supplied,
+ config_settings=config_settings,
)
@@ -435,6 +454,7 @@ def install_req_from_parsed_requirement(
isolated: bool = False,
use_pep517: Optional[bool] = None,
user_supplied: bool = False,
+ config_settings: Optional[Dict[str, str]] = None,
) -> InstallRequirement:
if parsed_req.is_editable:
req = install_req_from_editable(
@@ -444,6 +464,7 @@ def install_req_from_parsed_requirement(
constraint=parsed_req.constraint,
isolated=isolated,
user_supplied=user_supplied,
+ config_settings=config_settings,
)
else:
@@ -456,6 +477,7 @@ def install_req_from_parsed_requirement(
constraint=parsed_req.constraint,
line_source=parsed_req.line_source,
user_supplied=user_supplied,
+ config_settings=config_settings,
)
return req
@@ -474,4 +496,6 @@ def install_req_from_link_and_ireq(
install_options=ireq.install_options,
global_options=ireq.global_options,
hash_options=ireq.hash_options,
+ config_settings=ireq.config_settings,
+ user_supplied=ireq.user_supplied,
)
diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py
index b392989bf..06ea6f277 100644
--- a/src/pip/_internal/req/req_file.py
+++ b/src/pip/_internal/req/req_file.py
@@ -8,7 +8,17 @@ import re
import shlex
import urllib.parse
from optparse import Values
-from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Tuple
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Dict,
+ Generator,
+ Iterable,
+ List,
+ Optional,
+ Tuple,
+)
from pip._internal.cli import cmdoptions
from pip._internal.exceptions import InstallationError, RequirementsFileParseError
@@ -27,7 +37,7 @@ if TYPE_CHECKING:
__all__ = ["parse_requirements"]
-ReqFileLines = Iterator[Tuple[int, str]]
+ReqFileLines = Iterable[Tuple[int, str]]
LineParser = Callable[[str], Tuple[str, Values]]
@@ -119,7 +129,7 @@ def parse_requirements(
finder: Optional["PackageFinder"] = None,
options: Optional[optparse.Values] = None,
constraint: bool = False,
-) -> Iterator[ParsedRequirement]:
+) -> Generator[ParsedRequirement, None, None]:
"""Parse a requirements file and yield ParsedRequirement instances.
:param filename: Path or url of requirements file.
@@ -219,11 +229,13 @@ def handle_option_line(
if finder:
find_links = finder.find_links
index_urls = finder.index_urls
- if opts.index_url:
- index_urls = [opts.index_url]
+ no_index = finder.search_scope.no_index
if opts.no_index is True:
+ no_index = True
index_urls = []
- if opts.extra_index_urls:
+ if opts.index_url and not no_index:
+ index_urls = [opts.index_url]
+ if opts.extra_index_urls and not no_index:
index_urls.extend(opts.extra_index_urls)
if opts.find_links:
# FIXME: it would be nice to keep track of the source
@@ -243,6 +255,7 @@ def handle_option_line(
search_scope = SearchScope(
find_links=find_links,
index_urls=index_urls,
+ no_index=no_index,
)
finder.search_scope = search_scope
@@ -311,13 +324,15 @@ class RequirementsFileParser:
self._session = session
self._line_parser = line_parser
- def parse(self, filename: str, constraint: bool) -> Iterator[ParsedLine]:
+ def parse(
+ self, filename: str, constraint: bool
+ ) -> Generator[ParsedLine, None, None]:
"""Parse a given file, yielding parsed lines."""
yield from self._parse_and_recurse(filename, constraint)
def _parse_and_recurse(
self, filename: str, constraint: bool
- ) -> Iterator[ParsedLine]:
+ ) -> Generator[ParsedLine, None, None]:
for line in self._parse_file(filename, constraint):
if not line.is_requirement and (
line.opts.requirements or line.opts.constraints
@@ -346,7 +361,9 @@ class RequirementsFileParser:
else:
yield line
- def _parse_file(self, filename: str, constraint: bool) -> Iterator[ParsedLine]:
+ def _parse_file(
+ self, filename: str, constraint: bool
+ ) -> Generator[ParsedLine, None, None]:
_, content = get_file_content(filename, self._session)
lines_enum = preprocess(content)
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index 0d93941a3..88d481dfe 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -1,15 +1,15 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
+import functools
import logging
import os
import shutil
import sys
import uuid
import zipfile
-from typing import Any, Dict, Iterable, List, Optional, Sequence, Union
+from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
-from pip._vendor import pkg_resources, six
from pip._vendor.packaging.markers import Marker
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.specifiers import SpecifierSet
@@ -17,39 +17,47 @@ from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import Version
from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.pep517.wrappers import Pep517HookCaller
-from pip._vendor.pkg_resources import Distribution
from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
-from pip._internal.exceptions import InstallationError
+from pip._internal.exceptions import InstallationError, LegacyInstallFailure
from pip._internal.locations import get_scheme
+from pip._internal.metadata import (
+ BaseDistribution,
+ get_default_environment,
+ get_directory_distribution,
+ get_wheel_distribution,
+)
+from pip._internal.metadata.base import FilesystemWheel
+from pip._internal.models.direct_url import DirectUrl
from pip._internal.models.link import Link
from pip._internal.operations.build.metadata import generate_metadata
+from pip._internal.operations.build.metadata_editable import generate_editable_metadata
from pip._internal.operations.build.metadata_legacy import (
generate_metadata as generate_metadata_legacy,
)
from pip._internal.operations.install.editable_legacy import (
install_editable as install_editable_legacy,
)
-from pip._internal.operations.install.legacy import LegacyInstallFailure
from pip._internal.operations.install.legacy import install as install_legacy
from pip._internal.operations.install.wheel import install_wheel
from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path
from pip._internal.req.req_uninstall import UninstallPathSet
-from pip._internal.utils.deprecation import deprecated
-from pip._internal.utils.direct_url_helpers import direct_url_from_link
+from pip._internal.utils.deprecation import LegacyInstallReason, deprecated
+from pip._internal.utils.direct_url_helpers import (
+ direct_url_for_editable,
+ direct_url_from_link,
+)
from pip._internal.utils.hashes import Hashes
-from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import (
+ ConfiguredPep517HookCaller,
ask_path_exists,
backup_dir,
display_path,
- dist_in_site_packages,
- dist_in_usersite,
- get_distribution,
hide_url,
redact_auth_from_url,
)
-from pip._internal.utils.packaging import get_metadata
+from pip._internal.utils.packaging import safe_extra
+from pip._internal.utils.subprocess import runner_with_spinner_message
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
from pip._internal.utils.virtualenv import running_under_virtualenv
from pip._internal.vcs import vcs
@@ -57,32 +65,6 @@ from pip._internal.vcs import vcs
logger = logging.getLogger(__name__)
-def _get_dist(metadata_directory: str) -> Distribution:
- """Return a pkg_resources.Distribution for the provided
- metadata directory.
- """
- dist_dir = metadata_directory.rstrip(os.sep)
-
- # Build a PathMetadata object, from path to metadata. :wink:
- base_dir, dist_dir_name = os.path.split(dist_dir)
- metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
-
- # Determine the correct Distribution object type.
- if dist_dir.endswith(".egg-info"):
- dist_cls = pkg_resources.Distribution
- dist_name = os.path.splitext(dist_dir_name)[0]
- else:
- assert dist_dir.endswith(".dist-info")
- dist_cls = pkg_resources.DistInfoDistribution
- dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]
-
- return dist_cls(
- base_dir,
- project_name=dist_name,
- metadata=metadata,
- )
-
-
class InstallRequirement:
"""
Represents something that may be installed later on, may have information
@@ -102,16 +84,19 @@ class InstallRequirement:
install_options: Optional[List[str]] = None,
global_options: Optional[List[str]] = None,
hash_options: Optional[Dict[str, List[str]]] = None,
+ config_settings: Optional[Dict[str, str]] = None,
constraint: bool = False,
- extras: Iterable[str] = (),
+ extras: Collection[str] = (),
user_supplied: bool = False,
+ permit_editable_wheels: bool = False,
) -> None:
assert req is None or isinstance(req, Requirement), req
self.req = req
self.comes_from = comes_from
self.constraint = constraint
self.editable = editable
- self.legacy_install_reason: Optional[int] = None
+ self.permit_editable_wheels = permit_editable_wheels
+ self.legacy_install_reason: Optional[LegacyInstallReason] = None
# source_dir is the local directory where the linked requirement is
# located, or unpacked. In case unpacking is needed, creating and
@@ -130,6 +115,10 @@ class InstallRequirement:
self.link = self.original_link = link
self.original_link_is_in_wheel_cache = False
+ # Information about the location of the artifact that was downloaded . This
+ # property is guaranteed to be set in resolver results.
+ self.download_info: Optional[DirectUrl] = None
+
# Path to any downloaded or already-existing package.
self.local_file_path: Optional[str] = None
if self.link and self.link.is_file:
@@ -138,16 +127,15 @@ class InstallRequirement:
if extras:
self.extras = extras
elif req:
- self.extras = {pkg_resources.safe_extra(extra) for extra in req.extras}
+ self.extras = {safe_extra(extra) for extra in req.extras}
else:
self.extras = set()
if markers is None and req:
markers = req.marker
self.markers = markers
- # This holds the pkg_resources.Distribution object if this requirement
- # is already available:
- self.satisfied_by: Optional[Distribution] = None
+ # This holds the Distribution object if this requirement is already installed.
+ self.satisfied_by: Optional[BaseDistribution] = None
# Whether the installation process should try to uninstall an existing
# distribution before installing this requirement.
self.should_reinstall = False
@@ -159,6 +147,7 @@ class InstallRequirement:
self.install_options = install_options if install_options else []
self.global_options = global_options if global_options else []
self.hash_options = hash_options if hash_options else {}
+ self.config_settings = config_settings
# Set to True after successful preparation of this requirement
self.prepared = False
# User supplied requirement are explicitly requested for installation
@@ -235,7 +224,19 @@ class InstallRequirement:
def name(self) -> Optional[str]:
if self.req is None:
return None
- return pkg_resources.safe_name(self.req.name)
+ return self.req.name
+
+ @functools.lru_cache() # use cached_property in python 3.8+
+ def supports_pyproject_editable(self) -> bool:
+ if not self.use_pep517:
+ return False
+ assert self.pep517_backend
+ with self.build_env:
+ runner = runner_with_spinner_message(
+ "Checking if build backend supports build_editable"
+ )
+ with self.pep517_backend.subprocess_runner(runner):
+ return "build_editable" in self.pep517_backend._supported_features()
@property
def specifier(self) -> SpecifierSet:
@@ -396,32 +397,24 @@ class InstallRequirement:
"""
if self.req is None:
return
- existing_dist = get_distribution(self.req.name)
+ existing_dist = get_default_environment().get_distribution(self.req.name)
if not existing_dist:
return
- # pkg_resouces may contain a different copy of packaging.version from
- # pip in if the downstream distributor does a poor job debundling pip.
- # We avoid existing_dist.parsed_version and let SpecifierSet.contains
- # parses the version instead.
- existing_version = existing_dist.version
- version_compatible = (
- existing_version is not None
- and self.req.specifier.contains(existing_version, prereleases=True)
+ version_compatible = self.req.specifier.contains(
+ existing_dist.version,
+ prereleases=True,
)
if not version_compatible:
self.satisfied_by = None
if use_user_site:
- if dist_in_usersite(existing_dist):
+ if existing_dist.in_usersite:
self.should_reinstall = True
- elif running_under_virtualenv() and dist_in_site_packages(
- existing_dist
- ):
+ elif running_under_virtualenv() and existing_dist.in_site_packages:
raise InstallationError(
- "Will not install to the user site because it will "
- "lack sys.path precedence to {} in {}".format(
- existing_dist.project_name, existing_dist.location
- )
+ f"Will not install to the user site because it will "
+ f"lack sys.path precedence to {existing_dist.raw_name} "
+ f"in {existing_dist.location}"
)
else:
self.should_reinstall = True
@@ -456,6 +449,13 @@ class InstallRequirement:
return setup_py
@property
+ def setup_cfg_path(self) -> str:
+ assert self.source_dir, f"No source dir for {self}"
+ setup_cfg = os.path.join(self.unpacked_source_directory, "setup.cfg")
+
+ return setup_cfg
+
+ @property
def pyproject_toml_path(self) -> str:
assert self.source_dir, f"No source dir for {self}"
return make_pyproject_path(self.unpacked_source_directory)
@@ -480,47 +480,69 @@ class InstallRequirement:
requires, backend, check, backend_path = pyproject_toml_data
self.requirements_to_check = check
self.pyproject_requires = requires
- self.pep517_backend = Pep517HookCaller(
+ self.pep517_backend = ConfiguredPep517HookCaller(
+ self,
self.unpacked_source_directory,
backend,
backend_path=backend_path,
)
- def _generate_metadata(self) -> str:
- """Invokes metadata generator functions, with the required arguments."""
- if not self.use_pep517:
- assert self.unpacked_source_directory
-
- if not os.path.exists(self.setup_py_path):
- raise InstallationError(
- f'File "setup.py" not found for legacy project {self}.'
- )
+ def isolated_editable_sanity_check(self) -> None:
+ """Check that an editable requirement if valid for use with PEP 517/518.
- return generate_metadata_legacy(
- build_env=self.build_env,
- setup_py_path=self.setup_py_path,
- source_dir=self.unpacked_source_directory,
- isolated=self.isolated,
- details=self.name or f"from {self.link}",
+ This verifies that an editable that has a pyproject.toml either supports PEP 660
+ or as a setup.py or a setup.cfg
+ """
+ if (
+ self.editable
+ and self.use_pep517
+ and not self.supports_pyproject_editable()
+ and not os.path.isfile(self.setup_py_path)
+ and not os.path.isfile(self.setup_cfg_path)
+ ):
+ raise InstallationError(
+ f"Project {self} has a 'pyproject.toml' and its build "
+ f"backend is missing the 'build_editable' hook. Since it does not "
+ f"have a 'setup.py' nor a 'setup.cfg', "
+ f"it cannot be installed in editable mode. "
+ f"Consider using a build backend that supports PEP 660."
)
- assert self.pep517_backend is not None
-
- return generate_metadata(
- build_env=self.build_env,
- backend=self.pep517_backend,
- )
-
def prepare_metadata(self) -> None:
"""Ensure that project metadata is available.
- Under PEP 517, call the backend hook to prepare the metadata.
+ Under PEP 517 and PEP 660, call the backend hook to prepare the metadata.
Under legacy processing, call setup.py egg-info.
"""
assert self.source_dir
-
- with indent_log():
- self.metadata_directory = self._generate_metadata()
+ details = self.name or f"from {self.link}"
+
+ if self.use_pep517:
+ assert self.pep517_backend is not None
+ if (
+ self.editable
+ and self.permit_editable_wheels
+ and self.supports_pyproject_editable()
+ ):
+ self.metadata_directory = generate_editable_metadata(
+ build_env=self.build_env,
+ backend=self.pep517_backend,
+ details=details,
+ )
+ else:
+ self.metadata_directory = generate_metadata(
+ build_env=self.build_env,
+ backend=self.pep517_backend,
+ details=details,
+ )
+ else:
+ self.metadata_directory = generate_metadata_legacy(
+ build_env=self.build_env,
+ setup_py_path=self.setup_py_path,
+ source_dir=self.unpacked_source_directory,
+ isolated=self.isolated,
+ details=details,
+ )
# Act on the newly generated metadata, based on the name and version.
if not self.name:
@@ -533,12 +555,21 @@ class InstallRequirement:
@property
def metadata(self) -> Any:
if not hasattr(self, "_metadata"):
- self._metadata = get_metadata(self.get_dist())
+ self._metadata = self.get_dist().metadata
return self._metadata
- def get_dist(self) -> Distribution:
- return _get_dist(self.metadata_directory)
+ def get_dist(self) -> BaseDistribution:
+ if self.metadata_directory:
+ return get_directory_distribution(self.metadata_directory)
+ elif self.local_file_path and self.is_wheel:
+ return get_wheel_distribution(
+ FilesystemWheel(self.local_file_path), canonicalize_name(self.name)
+ )
+ raise AssertionError(
+ f"InstallRequirement {self} has no metadata directory and no wheel: "
+ f"can't make a distribution."
+ )
def assert_source_matches_version(self) -> None:
assert self.source_dir
@@ -598,7 +629,7 @@ class InstallRequirement:
# So here, if it's neither a path nor a valid VCS URL, it's a bug.
assert vcs_backend, f"Unsupported VCS URL {self.link.url}"
hidden_url = hide_url(self.link.url)
- vcs_backend.obtain(self.source_dir, url=hidden_url)
+ vcs_backend.obtain(self.source_dir, url=hidden_url, verbosity=0)
# Top-level Actions
def uninstall(
@@ -617,7 +648,7 @@ class InstallRequirement:
"""
assert self.req
- dist = get_distribution(self.req.name)
+ dist = get_default_environment().get_distribution(self.req.name)
if not dist:
logger.warning("Skipping %s as it is not installed.", self.name)
return None
@@ -728,7 +759,7 @@ class InstallRequirement:
)
global_options = global_options if global_options is not None else []
- if self.editable:
+ if self.editable and not self.is_wheel:
install_editable_legacy(
install_options,
global_options,
@@ -747,7 +778,10 @@ class InstallRequirement:
if self.is_wheel:
assert self.local_file_path
direct_url = None
- if self.original_link:
+ # TODO this can be refactored to direct_url = self.download_info
+ if self.editable:
+ direct_url = direct_url_for_editable(self.unpacked_source_directory)
+ elif self.original_link:
direct_url = direct_url_from_link(
self.original_link,
self.source_dir,
@@ -777,6 +811,11 @@ class InstallRequirement:
install_options = list(install_options) + self.install_options
try:
+ if (
+ self.legacy_install_reason is not None
+ and self.legacy_install_reason.emit_before_install
+ ):
+ self.legacy_install_reason.emit_deprecation(self.name)
success = install_legacy(
install_options=install_options,
global_options=global_options,
@@ -795,25 +834,19 @@ class InstallRequirement:
)
except LegacyInstallFailure as exc:
self.install_succeeded = False
- six.reraise(*exc.parent)
+ raise exc
except Exception:
self.install_succeeded = True
raise
self.install_succeeded = success
- if success and self.legacy_install_reason == 8368:
- deprecated(
- reason=(
- "{} was installed using the legacy 'setup.py install' "
- "method, because a wheel could not be built for it.".format(
- self.name
- )
- ),
- replacement="to fix the wheel build issue reported above",
- gone_in=None,
- issue=8368,
- )
+ if (
+ success
+ and self.legacy_install_reason is not None
+ and self.legacy_install_reason.emit_after_success
+ ):
+ self.legacy_install_reason.emit_deprecation(self.name)
def check_invalid_constraint_type(req: InstallRequirement) -> str:
@@ -836,7 +869,7 @@ def check_invalid_constraint_type(req: InstallRequirement) -> str:
"undocumented. The new implementation of the resolver no "
"longer supports these forms."
),
- replacement="replacing the constraint with a requirement.",
+ replacement="replacing the constraint with a requirement",
# No plan yet for when the new resolver becomes default
gone_in=None,
issue=8210,
diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py
index 6626c37e2..ec7a6e07a 100644
--- a/src/pip/_internal/req/req_set.py
+++ b/src/pip/_internal/req/req_set.py
@@ -1,13 +1,10 @@
import logging
from collections import OrderedDict
-from typing import Dict, Iterable, List, Optional, Tuple
+from typing import Dict, List
from pip._vendor.packaging.utils import canonicalize_name
-from pip._internal.exceptions import InstallationError
-from pip._internal.models.wheel import Wheel
from pip._internal.req.req_install import InstallRequirement
-from pip._internal.utils import compatibility_tags
logger = logging.getLogger(__name__)
@@ -51,123 +48,6 @@ class RequirementSet:
project_name = canonicalize_name(install_req.name)
self.requirements[project_name] = install_req
- def add_requirement(
- self,
- install_req: InstallRequirement,
- parent_req_name: Optional[str] = None,
- extras_requested: Optional[Iterable[str]] = None,
- ) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]:
- """Add install_req as a requirement to install.
-
- :param parent_req_name: The name of the requirement that needed this
- added. The name is used because when multiple unnamed requirements
- resolve to the same name, we could otherwise end up with dependency
- links that point outside the Requirements set. parent_req must
- already be added. Note that None implies that this is a user
- supplied requirement, vs an inferred one.
- :param extras_requested: an iterable of extras used to evaluate the
- environment markers.
- :return: Additional requirements to scan. That is either [] if
- the requirement is not applicable, or [install_req] if the
- requirement is applicable and has just been added.
- """
- # If the markers do not match, ignore this requirement.
- if not install_req.match_markers(extras_requested):
- logger.info(
- "Ignoring %s: markers '%s' don't match your environment",
- install_req.name,
- install_req.markers,
- )
- return [], None
-
- # If the wheel is not supported, raise an error.
- # Should check this after filtering out based on environment markers to
- # allow specifying different wheels based on the environment/OS, in a
- # single requirements file.
- if install_req.link and install_req.link.is_wheel:
- wheel = Wheel(install_req.link.filename)
- tags = compatibility_tags.get_supported()
- if self.check_supported_wheels and not wheel.supported(tags):
- raise InstallationError(
- "{} is not a supported wheel on this platform.".format(
- wheel.filename
- )
- )
-
- # This next bit is really a sanity check.
- assert (
- not install_req.user_supplied or parent_req_name is None
- ), "a user supplied req shouldn't have a parent"
-
- # Unnamed requirements are scanned again and the requirement won't be
- # added as a dependency until after scanning.
- if not install_req.name:
- self.add_unnamed_requirement(install_req)
- return [install_req], None
-
- try:
- existing_req: Optional[InstallRequirement] = self.get_requirement(
- install_req.name
- )
- except KeyError:
- existing_req = None
-
- has_conflicting_requirement = (
- parent_req_name is None
- and existing_req
- and not existing_req.constraint
- and existing_req.extras == install_req.extras
- and existing_req.req
- and install_req.req
- and existing_req.req.specifier != install_req.req.specifier
- )
- if has_conflicting_requirement:
- raise InstallationError(
- "Double requirement given: {} (already in {}, name={!r})".format(
- install_req, existing_req, install_req.name
- )
- )
-
- # When no existing requirement exists, add the requirement as a
- # dependency and it will be scanned again after.
- if not existing_req:
- self.add_named_requirement(install_req)
- # We'd want to rescan this requirement later
- return [install_req], install_req
-
- # Assume there's no need to scan, and that we've already
- # encountered this for scanning.
- if install_req.constraint or not existing_req.constraint:
- return [], existing_req
-
- does_not_satisfy_constraint = install_req.link and not (
- existing_req.link and install_req.link.path == existing_req.link.path
- )
- if does_not_satisfy_constraint:
- raise InstallationError(
- "Could not satisfy constraints for '{}': "
- "installation from path or url cannot be "
- "constrained to a version".format(install_req.name)
- )
- # If we're now installing a constraint, mark the existing
- # object for real installation.
- existing_req.constraint = False
- # If we're now installing a user supplied requirement,
- # mark the existing object as such.
- if install_req.user_supplied:
- existing_req.user_supplied = True
- existing_req.extras = tuple(
- sorted(set(existing_req.extras) | set(install_req.extras))
- )
- logger.debug(
- "Setting %s extras to: %s",
- existing_req,
- existing_req.extras,
- )
- # Return the existing requirement for addition to the parent and
- # scanning again.
- return [existing_req], existing_req
-
def has_requirement(self, name: str) -> bool:
project_name = canonicalize_name(name)
@@ -187,3 +67,16 @@ class RequirementSet:
@property
def all_requirements(self) -> List[InstallRequirement]:
return self.unnamed_requirements + list(self.requirements.values())
+
+ @property
+ def requirements_to_install(self) -> List[InstallRequirement]:
+ """Return the list of requirements that need to be installed.
+
+ TODO remove this property together with the legacy resolver, since the new
+ resolver only returns requirements that need to be installed.
+ """
+ return [
+ install_req
+ for install_req in self.all_requirements
+ if not install_req.constraint and not install_req.satisfied_by
+ ]
diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py
index ef7352f7b..15b67385c 100644
--- a/src/pip/_internal/req/req_uninstall.py
+++ b/src/pip/_internal/req/req_uninstall.py
@@ -1,57 +1,46 @@
-import csv
import functools
import os
import sys
import sysconfig
from importlib.util import cache_from_source
-from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple
-
-from pip._vendor import pkg_resources
-from pip._vendor.pkg_resources import Distribution
+from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Set, Tuple
from pip._internal.exceptions import UninstallationError
from pip._internal.locations import get_bin_prefix, get_bin_user
+from pip._internal.metadata import BaseDistribution
from pip._internal.utils.compat import WINDOWS
+from pip._internal.utils.egg_link import egg_link_path_from_location
from pip._internal.utils.logging import getLogger, indent_log
-from pip._internal.utils.misc import (
- ask,
- dist_in_usersite,
- dist_is_local,
- egg_link_path,
- is_local,
- normalize_path,
- renames,
- rmtree,
-)
+from pip._internal.utils.misc import ask, is_local, normalize_path, renames, rmtree
from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
logger = getLogger(__name__)
-def _script_names(dist: Distribution, script_name: str, is_gui: bool) -> List[str]:
+def _script_names(
+ bin_dir: str, script_name: str, is_gui: bool
+) -> Generator[str, None, None]:
"""Create the fully qualified name of the files created by
{console,gui}_scripts for the given ``dist``.
Returns the list of file names
"""
- if dist_in_usersite(dist):
- bin_dir = get_bin_user()
- else:
- bin_dir = get_bin_prefix()
exe_name = os.path.join(bin_dir, script_name)
- paths_to_remove = [exe_name]
- if WINDOWS:
- paths_to_remove.append(exe_name + ".exe")
- paths_to_remove.append(exe_name + ".exe.manifest")
- if is_gui:
- paths_to_remove.append(exe_name + "-script.pyw")
- else:
- paths_to_remove.append(exe_name + "-script.py")
- return paths_to_remove
+ yield exe_name
+ if not WINDOWS:
+ return
+ yield f"{exe_name}.exe"
+ yield f"{exe_name}.exe.manifest"
+ if is_gui:
+ yield f"{exe_name}-script.pyw"
+ else:
+ yield f"{exe_name}-script.py"
-def _unique(fn: Callable[..., Iterator[Any]]) -> Callable[..., Iterator[Any]]:
+def _unique(
+ fn: Callable[..., Generator[Any, None, None]]
+) -> Callable[..., Generator[Any, None, None]]:
@functools.wraps(fn)
- def unique(*args: Any, **kw: Any) -> Iterator[Any]:
+ def unique(*args: Any, **kw: Any) -> Generator[Any, None, None]:
seen: Set[Any] = set()
for item in fn(*args, **kw):
if item not in seen:
@@ -62,7 +51,7 @@ def _unique(fn: Callable[..., Iterator[Any]]) -> Callable[..., Iterator[Any]]:
@_unique
-def uninstallation_paths(dist: Distribution) -> Iterator[str]:
+def uninstallation_paths(dist: BaseDistribution) -> Generator[str, None, None]:
"""
Yield all the uninstallation paths for dist based on RECORD-without-.py[co]
@@ -76,25 +65,25 @@ def uninstallation_paths(dist: Distribution) -> Iterator[str]:
https://packaging.python.org/specifications/recording-installed-packages/
"""
- try:
- r = csv.reader(dist.get_metadata_lines("RECORD"))
- except FileNotFoundError as missing_record_exception:
+ location = dist.location
+ assert location is not None, "not installed"
+
+ entries = dist.iter_declared_entries()
+ if entries is None:
msg = "Cannot uninstall {dist}, RECORD file not found.".format(dist=dist)
- try:
- installer = next(dist.get_metadata_lines("INSTALLER"))
- if not installer or installer == "pip":
- raise ValueError()
- except (OSError, StopIteration, ValueError):
- dep = "{}=={}".format(dist.project_name, dist.version)
+ installer = dist.installer
+ if not installer or installer == "pip":
+ dep = "{}=={}".format(dist.raw_name, dist.version)
msg += (
" You might be able to recover from this via: "
"'pip install --force-reinstall --no-deps {}'.".format(dep)
)
else:
msg += " Hint: The package was installed by {}.".format(installer)
- raise UninstallationError(msg) from missing_record_exception
- for row in r:
- path = os.path.join(dist.location, row[0])
+ raise UninstallationError(msg)
+
+ for entry in entries:
+ path = os.path.join(location, entry)
yield path
if path.endswith(".py"):
dn, fn = os.path.split(path)
@@ -317,11 +306,11 @@ class UninstallPathSet:
"""A set of file paths to be removed in the uninstallation of a
requirement."""
- def __init__(self, dist: Distribution) -> None:
- self.paths: Set[str] = set()
+ def __init__(self, dist: BaseDistribution) -> None:
+ self._paths: Set[str] = set()
self._refuse: Set[str] = set()
- self.pth: Dict[str, UninstallPthEntries] = {}
- self.dist = dist
+ self._pth: Dict[str, UninstallPthEntries] = {}
+ self._dist = dist
self._moved_paths = StashedUninstallPathSet()
def _permitted(self, path: str) -> bool:
@@ -342,7 +331,7 @@ class UninstallPathSet:
if not os.path.exists(path):
return
if self._permitted(path):
- self.paths.add(path)
+ self._paths.add(path)
else:
self._refuse.add(path)
@@ -354,37 +343,37 @@ class UninstallPathSet:
def add_pth(self, pth_file: str, entry: str) -> None:
pth_file = normalize_path(pth_file)
if self._permitted(pth_file):
- if pth_file not in self.pth:
- self.pth[pth_file] = UninstallPthEntries(pth_file)
- self.pth[pth_file].add(entry)
+ if pth_file not in self._pth:
+ self._pth[pth_file] = UninstallPthEntries(pth_file)
+ self._pth[pth_file].add(entry)
else:
self._refuse.add(pth_file)
def remove(self, auto_confirm: bool = False, verbose: bool = False) -> None:
- """Remove paths in ``self.paths`` with confirmation (unless
+ """Remove paths in ``self._paths`` with confirmation (unless
``auto_confirm`` is True)."""
- if not self.paths:
+ if not self._paths:
logger.info(
"Can't uninstall '%s'. No files were found to uninstall.",
- self.dist.project_name,
+ self._dist.raw_name,
)
return
- dist_name_version = self.dist.project_name + "-" + self.dist.version
+ dist_name_version = f"{self._dist.raw_name}-{self._dist.version}"
logger.info("Uninstalling %s:", dist_name_version)
with indent_log():
if auto_confirm or self._allowed_to_proceed(verbose):
moved = self._moved_paths
- for_rename = compress_for_rename(self.paths)
+ for_rename = compress_for_rename(self._paths)
for path in sorted(compact(for_rename)):
moved.stash(path)
logger.verbose("Removing file or directory %s", path)
- for pth in self.pth.values():
+ for pth in self._pth.values():
pth.remove()
logger.info("Successfully uninstalled %s", dist_name_version)
@@ -402,18 +391,18 @@ class UninstallPathSet:
logger.info(path)
if not verbose:
- will_remove, will_skip = compress_for_output_listing(self.paths)
+ will_remove, will_skip = compress_for_output_listing(self._paths)
else:
# In verbose mode, display all the files that are going to be
# deleted.
- will_remove = set(self.paths)
+ will_remove = set(self._paths)
will_skip = set()
_display("Would remove:", will_remove)
_display("Would not remove (might be manually added):", will_skip)
_display("Would not remove (outside of prefix):", self._refuse)
if verbose:
- _display("Will actually move:", compress_for_rename(self.paths))
+ _display("Will actually move:", compress_for_rename(self._paths))
return ask("Proceed (Y/n)? ", ("y", "n", "")) != "n"
@@ -422,12 +411,12 @@ class UninstallPathSet:
if not self._moved_paths.can_rollback:
logger.error(
"Can't roll back %s; was not uninstalled",
- self.dist.project_name,
+ self._dist.raw_name,
)
return
- logger.info("Rolling back uninstall of %s", self.dist.project_name)
+ logger.info("Rolling back uninstall of %s", self._dist.raw_name)
self._moved_paths.rollback()
- for pth in self.pth.values():
+ for pth in self._pth.values():
pth.rollback()
def commit(self) -> None:
@@ -435,141 +424,159 @@ class UninstallPathSet:
self._moved_paths.commit()
@classmethod
- def from_dist(cls, dist: Distribution) -> "UninstallPathSet":
- dist_path = normalize_path(dist.location)
- if not dist_is_local(dist):
+ def from_dist(cls, dist: BaseDistribution) -> "UninstallPathSet":
+ dist_location = dist.location
+ info_location = dist.info_location
+ if dist_location is None:
+ logger.info(
+ "Not uninstalling %s since it is not installed",
+ dist.canonical_name,
+ )
+ return cls(dist)
+
+ normalized_dist_location = normalize_path(dist_location)
+ if not dist.local:
logger.info(
"Not uninstalling %s at %s, outside environment %s",
- dist.key,
- dist_path,
+ dist.canonical_name,
+ normalized_dist_location,
sys.prefix,
)
return cls(dist)
- if dist_path in {
+ if normalized_dist_location in {
p
for p in {sysconfig.get_path("stdlib"), sysconfig.get_path("platstdlib")}
if p
}:
logger.info(
"Not uninstalling %s at %s, as it is in the standard library.",
- dist.key,
- dist_path,
+ dist.canonical_name,
+ normalized_dist_location,
)
return cls(dist)
paths_to_remove = cls(dist)
- develop_egg_link = egg_link_path(dist)
- develop_egg_link_egg_info = "{}.egg-info".format(
- pkg_resources.to_filename(dist.project_name)
+ develop_egg_link = egg_link_path_from_location(dist.raw_name)
+
+ # Distribution is installed with metadata in a "flat" .egg-info
+ # directory. This means it is not a modern .dist-info installation, an
+ # egg, or legacy editable.
+ setuptools_flat_installation = (
+ dist.installed_with_setuptools_egg_info
+ and info_location is not None
+ and os.path.exists(info_location)
+ # If dist is editable and the location points to a ``.egg-info``,
+ # we are in fact in the legacy editable case.
+ and not info_location.endswith(f"{dist.setuptools_filename}.egg-info")
)
- egg_info_exists = dist.egg_info and os.path.exists(dist.egg_info)
- # Special case for distutils installed package
- distutils_egg_info = getattr(dist._provider, "path", None)
# Uninstall cases order do matter as in the case of 2 installs of the
# same package, pip needs to uninstall the currently detected version
- if (
- egg_info_exists
- and dist.egg_info.endswith(".egg-info")
- and not dist.egg_info.endswith(develop_egg_link_egg_info)
- ):
- # if dist.egg_info.endswith(develop_egg_link_egg_info), we
- # are in fact in the develop_egg_link case
- paths_to_remove.add(dist.egg_info)
- if dist.has_metadata("installed-files.txt"):
- for installed_file in dist.get_metadata(
- "installed-files.txt"
- ).splitlines():
- path = os.path.normpath(os.path.join(dist.egg_info, installed_file))
- paths_to_remove.add(path)
+ if setuptools_flat_installation:
+ if info_location is not None:
+ paths_to_remove.add(info_location)
+ installed_files = dist.iter_declared_entries()
+ if installed_files is not None:
+ for installed_file in installed_files:
+ paths_to_remove.add(os.path.join(dist_location, installed_file))
# FIXME: need a test for this elif block
# occurs with --single-version-externally-managed/--record outside
# of pip
- elif dist.has_metadata("top_level.txt"):
- if dist.has_metadata("namespace_packages.txt"):
- namespaces = dist.get_metadata("namespace_packages.txt")
- else:
+ elif dist.is_file("top_level.txt"):
+ try:
+ namespace_packages = dist.read_text("namespace_packages.txt")
+ except FileNotFoundError:
namespaces = []
+ else:
+ namespaces = namespace_packages.splitlines(keepends=False)
for top_level_pkg in [
p
- for p in dist.get_metadata("top_level.txt").splitlines()
+ for p in dist.read_text("top_level.txt").splitlines()
if p and p not in namespaces
]:
- path = os.path.join(dist.location, top_level_pkg)
+ path = os.path.join(dist_location, top_level_pkg)
paths_to_remove.add(path)
- paths_to_remove.add(path + ".py")
- paths_to_remove.add(path + ".pyc")
- paths_to_remove.add(path + ".pyo")
+ paths_to_remove.add(f"{path}.py")
+ paths_to_remove.add(f"{path}.pyc")
+ paths_to_remove.add(f"{path}.pyo")
- elif distutils_egg_info:
+ elif dist.installed_by_distutils:
raise UninstallationError(
"Cannot uninstall {!r}. It is a distutils installed project "
"and thus we cannot accurately determine which files belong "
"to it which would lead to only a partial uninstall.".format(
- dist.project_name,
+ dist.raw_name,
)
)
- elif dist.location.endswith(".egg"):
+ elif dist.installed_as_egg:
# package installed by easy_install
# We cannot match on dist.egg_name because it can slightly vary
# i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg
- paths_to_remove.add(dist.location)
- easy_install_egg = os.path.split(dist.location)[1]
+ paths_to_remove.add(dist_location)
+ easy_install_egg = os.path.split(dist_location)[1]
easy_install_pth = os.path.join(
- os.path.dirname(dist.location), "easy-install.pth"
+ os.path.dirname(dist_location),
+ "easy-install.pth",
)
paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg)
- elif egg_info_exists and dist.egg_info.endswith(".dist-info"):
+ elif dist.installed_with_dist_info:
for path in uninstallation_paths(dist):
paths_to_remove.add(path)
elif develop_egg_link:
- # develop egg
+ # PEP 660 modern editable is handled in the ``.dist-info`` case
+ # above, so this only covers the setuptools-style editable.
with open(develop_egg_link) as fh:
link_pointer = os.path.normcase(fh.readline().strip())
- assert (
- link_pointer == dist.location
- ), "Egg-link {} does not match installed location of {} (at {})".format(
- link_pointer, dist.project_name, dist.location
+ normalized_link_pointer = normalize_path(link_pointer)
+ assert os.path.samefile(
+ normalized_link_pointer, normalized_dist_location
+ ), (
+ f"Egg-link {link_pointer} does not match installed location of "
+ f"{dist.raw_name} (at {dist_location})"
)
paths_to_remove.add(develop_egg_link)
easy_install_pth = os.path.join(
os.path.dirname(develop_egg_link), "easy-install.pth"
)
- paths_to_remove.add_pth(easy_install_pth, dist.location)
+ paths_to_remove.add_pth(easy_install_pth, dist_location)
else:
logger.debug(
"Not sure how to uninstall: %s - Check: %s",
dist,
- dist.location,
+ dist_location,
)
+ if dist.in_usersite:
+ bin_dir = get_bin_user()
+ else:
+ bin_dir = get_bin_prefix()
+
# find distutils scripts= scripts
- if dist.has_metadata("scripts") and dist.metadata_isdir("scripts"):
- for script in dist.metadata_listdir("scripts"):
- if dist_in_usersite(dist):
- bin_dir = get_bin_user()
- else:
- bin_dir = get_bin_prefix()
+ try:
+ for script in dist.iter_distutils_script_names():
paths_to_remove.add(os.path.join(bin_dir, script))
if WINDOWS:
- paths_to_remove.add(os.path.join(bin_dir, script) + ".bat")
-
- # find console_scripts
- _scripts_to_remove = []
- console_scripts = dist.get_entry_map(group="console_scripts")
- for name in console_scripts.keys():
- _scripts_to_remove.extend(_script_names(dist, name, False))
- # find gui_scripts
- gui_scripts = dist.get_entry_map(group="gui_scripts")
- for name in gui_scripts.keys():
- _scripts_to_remove.extend(_script_names(dist, name, True))
-
- for s in _scripts_to_remove:
+ paths_to_remove.add(os.path.join(bin_dir, f"{script}.bat"))
+ except (FileNotFoundError, NotADirectoryError):
+ pass
+
+ # find console_scripts and gui_scripts
+ def iter_scripts_to_remove(
+ dist: BaseDistribution,
+ bin_dir: str,
+ ) -> Generator[str, None, None]:
+ for entry_point in dist.iter_entry_points():
+ if entry_point.group == "console_scripts":
+ yield from _script_names(bin_dir, entry_point.name, False)
+ elif entry_point.group == "gui_scripts":
+ yield from _script_names(bin_dir, entry_point.name, True)
+
+ for s in iter_scripts_to_remove(dist, bin_dir):
paths_to_remove.add(s)
return paths_to_remove
diff --git a/src/pip/_internal/resolution/base.py b/src/pip/_internal/resolution/base.py
index 3f83ef0f5..42dade18c 100644
--- a/src/pip/_internal/resolution/base.py
+++ b/src/pip/_internal/resolution/base.py
@@ -1,9 +1,11 @@
-from typing import Callable, List
+from typing import Callable, List, Optional
from pip._internal.req.req_install import InstallRequirement
from pip._internal.req.req_set import RequirementSet
-InstallRequirementProvider = Callable[[str, InstallRequirement], InstallRequirement]
+InstallRequirementProvider = Callable[
+ [str, Optional[InstallRequirement]], InstallRequirement
+]
class BaseResolver:
diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py
index 4df8f7ef1..fb49d4169 100644
--- a/src/pip/_internal/resolution/legacy/resolver.py
+++ b/src/pip/_internal/resolution/legacy/resolver.py
@@ -20,7 +20,7 @@ from itertools import chain
from typing import DefaultDict, Iterable, List, Optional, Set, Tuple
from pip._vendor.packaging import specifiers
-from pip._vendor.pkg_resources import Distribution
+from pip._vendor.packaging.requirements import Requirement
from pip._internal.cache import WheelCache
from pip._internal.exceptions import (
@@ -28,10 +28,14 @@ from pip._internal.exceptions import (
DistributionNotFound,
HashError,
HashErrors,
+ InstallationError,
+ NoneMetadataError,
UnsupportedPythonVersion,
)
from pip._internal.index.package_finder import PackageFinder
+from pip._internal.metadata import BaseDistribution
from pip._internal.models.link import Link
+from pip._internal.models.wheel import Wheel
from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req.req_install import (
InstallRequirement,
@@ -39,10 +43,12 @@ from pip._internal.req.req_install import (
)
from pip._internal.req.req_set import RequirementSet
from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
+from pip._internal.utils import compatibility_tags
from pip._internal.utils.compatibility_tags import get_supported
+from pip._internal.utils.direct_url_helpers import direct_url_from_link
from pip._internal.utils.logging import indent_log
-from pip._internal.utils.misc import dist_in_usersite, normalize_version_info
-from pip._internal.utils.packaging import check_requires_python, get_requires_python
+from pip._internal.utils.misc import normalize_version_info
+from pip._internal.utils.packaging import check_requires_python
logger = logging.getLogger(__name__)
@@ -50,7 +56,7 @@ DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]]
def _check_dist_requires_python(
- dist: Distribution,
+ dist: BaseDistribution,
version_info: Tuple[int, int, int],
ignore_requires_python: bool = False,
) -> None:
@@ -66,14 +72,21 @@ def _check_dist_requires_python(
:raises UnsupportedPythonVersion: When the given Python version isn't
compatible.
"""
- requires_python = get_requires_python(dist)
+ # This idiosyncratically converts the SpecifierSet to str and let
+ # check_requires_python then parse it again into SpecifierSet. But this
+ # is the legacy resolver so I'm just not going to bother refactoring.
+ try:
+ requires_python = str(dist.requires_python)
+ except FileNotFoundError as e:
+ raise NoneMetadataError(dist, str(e))
try:
is_compatible = check_requires_python(
- requires_python, version_info=version_info
+ requires_python,
+ version_info=version_info,
)
except specifiers.InvalidSpecifier as exc:
logger.warning(
- "Package %r has an invalid Requires-Python: %s", dist.project_name, exc
+ "Package %r has an invalid Requires-Python: %s", dist.raw_name, exc
)
return
@@ -84,7 +97,7 @@ def _check_dist_requires_python(
if ignore_requires_python:
logger.debug(
"Ignoring failed Requires-Python check for package %r: %s not in %r",
- dist.project_name,
+ dist.raw_name,
version,
requires_python,
)
@@ -92,7 +105,7 @@ def _check_dist_requires_python(
raise UnsupportedPythonVersion(
"Package {!r} requires a different Python: {} not in {!r}".format(
- dist.project_name, version, requires_python
+ dist.raw_name, version, requires_python
)
)
@@ -159,7 +172,7 @@ class Resolver(BaseResolver):
for req in root_reqs:
if req.constraint:
check_invalid_constraint_type(req)
- requirement_set.add_requirement(req)
+ self._add_requirement_to_set(requirement_set, req)
# Actually prepare the files, and collect any exceptions. Most hash
# exceptions cannot be checked ahead of time, because
@@ -179,6 +192,124 @@ class Resolver(BaseResolver):
return requirement_set
+ def _add_requirement_to_set(
+ self,
+ requirement_set: RequirementSet,
+ install_req: InstallRequirement,
+ parent_req_name: Optional[str] = None,
+ extras_requested: Optional[Iterable[str]] = None,
+ ) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]:
+ """Add install_req as a requirement to install.
+
+ :param parent_req_name: The name of the requirement that needed this
+ added. The name is used because when multiple unnamed requirements
+ resolve to the same name, we could otherwise end up with dependency
+ links that point outside the Requirements set. parent_req must
+ already be added. Note that None implies that this is a user
+ supplied requirement, vs an inferred one.
+ :param extras_requested: an iterable of extras used to evaluate the
+ environment markers.
+ :return: Additional requirements to scan. That is either [] if
+ the requirement is not applicable, or [install_req] if the
+ requirement is applicable and has just been added.
+ """
+ # If the markers do not match, ignore this requirement.
+ if not install_req.match_markers(extras_requested):
+ logger.info(
+ "Ignoring %s: markers '%s' don't match your environment",
+ install_req.name,
+ install_req.markers,
+ )
+ return [], None
+
+ # If the wheel is not supported, raise an error.
+ # Should check this after filtering out based on environment markers to
+ # allow specifying different wheels based on the environment/OS, in a
+ # single requirements file.
+ if install_req.link and install_req.link.is_wheel:
+ wheel = Wheel(install_req.link.filename)
+ tags = compatibility_tags.get_supported()
+ if requirement_set.check_supported_wheels and not wheel.supported(tags):
+ raise InstallationError(
+ "{} is not a supported wheel on this platform.".format(
+ wheel.filename
+ )
+ )
+
+ # This next bit is really a sanity check.
+ assert (
+ not install_req.user_supplied or parent_req_name is None
+ ), "a user supplied req shouldn't have a parent"
+
+ # Unnamed requirements are scanned again and the requirement won't be
+ # added as a dependency until after scanning.
+ if not install_req.name:
+ requirement_set.add_unnamed_requirement(install_req)
+ return [install_req], None
+
+ try:
+ existing_req: Optional[
+ InstallRequirement
+ ] = requirement_set.get_requirement(install_req.name)
+ except KeyError:
+ existing_req = None
+
+ has_conflicting_requirement = (
+ parent_req_name is None
+ and existing_req
+ and not existing_req.constraint
+ and existing_req.extras == install_req.extras
+ and existing_req.req
+ and install_req.req
+ and existing_req.req.specifier != install_req.req.specifier
+ )
+ if has_conflicting_requirement:
+ raise InstallationError(
+ "Double requirement given: {} (already in {}, name={!r})".format(
+ install_req, existing_req, install_req.name
+ )
+ )
+
+ # When no existing requirement exists, add the requirement as a
+ # dependency and it will be scanned again after.
+ if not existing_req:
+ requirement_set.add_named_requirement(install_req)
+ # We'd want to rescan this requirement later
+ return [install_req], install_req
+
+ # Assume there's no need to scan, and that we've already
+ # encountered this for scanning.
+ if install_req.constraint or not existing_req.constraint:
+ return [], existing_req
+
+ does_not_satisfy_constraint = install_req.link and not (
+ existing_req.link and install_req.link.path == existing_req.link.path
+ )
+ if does_not_satisfy_constraint:
+ raise InstallationError(
+ "Could not satisfy constraints for '{}': "
+ "installation from path or url cannot be "
+ "constrained to a version".format(install_req.name)
+ )
+ # If we're now installing a constraint, mark the existing
+ # object for real installation.
+ existing_req.constraint = False
+ # If we're now installing a user supplied requirement,
+ # mark the existing object as such.
+ if install_req.user_supplied:
+ existing_req.user_supplied = True
+ existing_req.extras = tuple(
+ sorted(set(existing_req.extras) | set(install_req.extras))
+ )
+ logger.debug(
+ "Setting %s extras to: %s",
+ existing_req,
+ existing_req.extras,
+ )
+ # Return the existing requirement for addition to the parent and
+ # scanning again.
+ return [existing_req], existing_req
+
def _is_upgrade_allowed(self, req: InstallRequirement) -> bool:
if self.upgrade_strategy == "to-satisfy-only":
return False
@@ -194,7 +325,7 @@ class Resolver(BaseResolver):
"""
# Don't uninstall the conflict if doing a user install and the
# conflict is not a user install.
- if not self.use_user_site or dist_in_usersite(req.satisfied_by):
+ if not self.use_user_site or req.satisfied_by.in_usersite:
req.should_reinstall = True
req.satisfied_by = None
@@ -301,9 +432,17 @@ class Resolver(BaseResolver):
logger.debug("Using cached wheel link: %s", cache_entry.link)
if req.link is req.original_link and cache_entry.persistent:
req.original_link_is_in_wheel_cache = True
+ if cache_entry.origin is not None:
+ req.download_info = cache_entry.origin
+ else:
+ # Legacy cache entry that does not have origin.json.
+ # download_info may miss the archive_info.hash field.
+ req.download_info = direct_url_from_link(
+ req.link, link_is_in_wheel_cache=cache_entry.persistent
+ )
req.link = cache_entry.link
- def _get_dist_for(self, req: InstallRequirement) -> Distribution:
+ def _get_dist_for(self, req: InstallRequirement) -> BaseDistribution:
"""Takes a InstallRequirement and returns a single AbstractDist \
representing a prepared variant of the same.
"""
@@ -378,13 +517,14 @@ class Resolver(BaseResolver):
more_reqs: List[InstallRequirement] = []
- def add_req(subreq: Distribution, extras_requested: Iterable[str]) -> None:
- sub_install_req = self._make_install_req(
- str(subreq),
- req_to_install,
- )
+ def add_req(subreq: Requirement, extras_requested: Iterable[str]) -> None:
+ # This idiosyncratically converts the Requirement to str and let
+ # make_install_req then parse it again into Requirement. But this is
+ # the legacy resolver so I'm just not going to bother refactoring.
+ sub_install_req = self._make_install_req(str(subreq), req_to_install)
parent_req_name = req_to_install.name
- to_scan_again, add_to_parent = requirement_set.add_requirement(
+ to_scan_again, add_to_parent = self._add_requirement_to_set(
+ requirement_set,
sub_install_req,
parent_req_name=parent_req_name,
extras_requested=extras_requested,
@@ -401,7 +541,9 @@ class Resolver(BaseResolver):
# 'unnamed' requirements can only come from being directly
# provided by the user.
assert req_to_install.user_supplied
- requirement_set.add_requirement(req_to_install, parent_req_name=None)
+ self._add_requirement_to_set(
+ requirement_set, req_to_install, parent_req_name=None
+ )
if not self.ignore_dependencies:
if req_to_install.extras:
@@ -410,15 +552,20 @@ class Resolver(BaseResolver):
",".join(req_to_install.extras),
)
missing_requested = sorted(
- set(req_to_install.extras) - set(dist.extras)
+ set(req_to_install.extras) - set(dist.iter_provided_extras())
)
for missing in missing_requested:
- logger.warning("%s does not provide the extra '%s'", dist, missing)
+ logger.warning(
+ "%s %s does not provide the extra '%s'",
+ dist.raw_name,
+ dist.version,
+ missing,
+ )
available_requested = sorted(
- set(dist.extras) & set(req_to_install.extras)
+ set(dist.iter_provided_extras()) & set(req_to_install.extras)
)
- for subreq in dist.requires(available_requested):
+ for subreq in dist.iter_dependencies(available_requested):
add_req(subreq, extras_requested=available_requested)
return more_reqs
diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py
index 7f258c574..b206692a0 100644
--- a/src/pip/_internal/resolution/resolvelib/base.py
+++ b/src/pip/_internal/resolution/resolvelib/base.py
@@ -36,11 +36,8 @@ class Constraint:
links = frozenset([ireq.link]) if ireq.link else frozenset()
return Constraint(ireq.specifier, ireq.hashes(trust_internet=False), links)
- def __nonzero__(self) -> bool:
- return bool(self.specifier) or bool(self.hashes) or bool(self.links)
-
def __bool__(self) -> bool:
- return self.__nonzero__()
+ return bool(self.specifier) or bool(self.hashes) or bool(self.links)
def __and__(self, other: InstallRequirement) -> "Constraint":
if not isinstance(other, InstallRequirement):
diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py
index 5d510db86..f5bc343b9 100644
--- a/src/pip/_internal/resolution/resolvelib/candidates.py
+++ b/src/pip/_internal/resolution/resolvelib/candidates.py
@@ -2,13 +2,15 @@ import logging
import sys
from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union, cast
-from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import Version
-from pip._vendor.packaging.version import parse as parse_version
-from pip._vendor.pkg_resources import Distribution
-from pip._internal.exceptions import HashError, MetadataInconsistent
+from pip._internal.exceptions import (
+ HashError,
+ InstallationSubprocessError,
+ MetadataInconsistent,
+)
+from pip._internal.metadata import BaseDistribution
from pip._internal.models.link import Link, links_equivalent
from pip._internal.models.wheel import Wheel
from pip._internal.req.constructors import (
@@ -16,8 +18,8 @@ from pip._internal.req.constructors import (
install_req_from_line,
)
from pip._internal.req.req_install import InstallRequirement
-from pip._internal.utils.misc import dist_is_editable, normalize_version_info
-from pip._internal.utils.packaging import get_requires_python
+from pip._internal.utils.direct_url_helpers import direct_url_from_link
+from pip._internal.utils.misc import normalize_version_info
from .base import Candidate, CandidateVersion, Requirement, format_name
@@ -68,6 +70,7 @@ def make_install_req_from_link(
global_options=template.global_options,
hashes=template.hash_options,
),
+ config_settings=template.config_settings,
)
ireq.original_link = template.original_link
ireq.link = link
@@ -85,24 +88,25 @@ def make_install_req_from_editable(
use_pep517=template.use_pep517,
isolated=template.isolated,
constraint=template.constraint,
+ permit_editable_wheels=template.permit_editable_wheels,
options=dict(
install_options=template.install_options,
global_options=template.global_options,
hashes=template.hash_options,
),
+ config_settings=template.config_settings,
)
-def make_install_req_from_dist(
- dist: Distribution, template: InstallRequirement
+def _make_install_req_from_dist(
+ dist: BaseDistribution, template: InstallRequirement
) -> InstallRequirement:
- project_name = canonicalize_name(dist.project_name)
if template.req:
line = str(template.req)
elif template.link:
- line = f"{project_name} @ {template.link.url}"
+ line = f"{dist.canonical_name} @ {template.link.url}"
else:
- line = f"{project_name}=={dist.parsed_version}"
+ line = f"{dist.canonical_name}=={dist.version}"
ireq = install_req_from_line(
line,
user_supplied=template.user_supplied,
@@ -115,6 +119,7 @@ def make_install_req_from_dist(
global_options=template.global_options,
hashes=template.hash_options,
),
+ config_settings=template.config_settings,
)
ireq.satisfied_by = dist
return ireq
@@ -136,6 +141,7 @@ class _InstallRequirementBackedCandidate(Candidate):
found remote link (e.g. from pypi.org).
"""
+ dist: BaseDistribution
is_installed = False
def __init__(
@@ -180,7 +186,7 @@ class _InstallRequirementBackedCandidate(Candidate):
def project_name(self) -> NormalizedName:
"""The normalised name of the project the candidate refers to"""
if self._name is None:
- self._name = canonicalize_name(self.dist.project_name)
+ self._name = self.dist.canonical_name
return self._name
@property
@@ -190,7 +196,7 @@ class _InstallRequirementBackedCandidate(Candidate):
@property
def version(self) -> CandidateVersion:
if self._version is None:
- self._version = parse_version(self.dist.version)
+ self._version = self.dist.version
return self._version
def format_for_error(self) -> str:
@@ -200,29 +206,27 @@ class _InstallRequirementBackedCandidate(Candidate):
self._link.file_path if self._link.is_file else self._link,
)
- def _prepare_distribution(self) -> Distribution:
+ def _prepare_distribution(self) -> BaseDistribution:
raise NotImplementedError("Override in subclass")
- def _check_metadata_consistency(self, dist: Distribution) -> None:
+ def _check_metadata_consistency(self, dist: BaseDistribution) -> None:
"""Check for consistency of project name and version of dist."""
- canonical_name = canonicalize_name(dist.project_name)
- if self._name is not None and self._name != canonical_name:
+ if self._name is not None and self._name != dist.canonical_name:
raise MetadataInconsistent(
self._ireq,
"name",
self._name,
- dist.project_name,
+ dist.canonical_name,
)
- parsed_version = parse_version(dist.version)
- if self._version is not None and self._version != parsed_version:
+ if self._version is not None and self._version != dist.version:
raise MetadataInconsistent(
self._ireq,
"version",
str(self._version),
- dist.version,
+ str(dist.version),
)
- def _prepare(self) -> Distribution:
+ def _prepare(self) -> BaseDistribution:
try:
dist = self._prepare_distribution()
except HashError as e:
@@ -231,26 +235,19 @@ class _InstallRequirementBackedCandidate(Candidate):
# offending line to the user.
e.req = self._ireq
raise
+ except InstallationSubprocessError as exc:
+ # The output has been presented already, so don't duplicate it.
+ exc.context = "See above for output."
+ raise
+
self._check_metadata_consistency(dist)
return dist
- def _get_requires_python_dependency(self) -> Optional[Requirement]:
- requires_python = get_requires_python(self.dist)
- if requires_python is None:
- return None
- try:
- spec = SpecifierSet(requires_python)
- except InvalidSpecifier as e:
- message = "Package %r has an invalid Requires-Python: %s"
- logger.warning(message, self.name, e)
- return None
- return self._factory.make_requires_python_requirement(spec)
-
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
- requires = self.dist.requires() if with_requires else ()
+ requires = self.dist.iter_dependencies() if with_requires else ()
for r in requires:
yield self._factory.make_requirement_from_spec(str(r), self._ireq)
- yield self._get_requires_python_dependency()
+ yield self._factory.make_requires_python_requirement(self.dist.requires_python)
def get_install_requirement(self) -> Optional[InstallRequirement]:
return self._ireq
@@ -285,12 +282,17 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
version, wheel_version, name
)
- if (
- cache_entry is not None
- and cache_entry.persistent
- and template.link is template.original_link
- ):
- ireq.original_link_is_in_wheel_cache = True
+ if cache_entry is not None:
+ if cache_entry.persistent and template.link is template.original_link:
+ ireq.original_link_is_in_wheel_cache = True
+ if cache_entry.origin is not None:
+ ireq.download_info = cache_entry.origin
+ else:
+ # Legacy cache entry that does not have origin.json.
+ # download_info may miss the archive_info.hash field.
+ ireq.download_info = direct_url_from_link(
+ source_link, link_is_in_wheel_cache=cache_entry.persistent
+ )
super().__init__(
link=link,
@@ -301,10 +303,9 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
version=version,
)
- def _prepare_distribution(self) -> Distribution:
- return self._factory.preparer.prepare_linked_requirement(
- self._ireq, parallel_builds=True
- )
+ def _prepare_distribution(self) -> BaseDistribution:
+ preparer = self._factory.preparer
+ return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
class EditableCandidate(_InstallRequirementBackedCandidate):
@@ -327,7 +328,7 @@ class EditableCandidate(_InstallRequirementBackedCandidate):
version=version,
)
- def _prepare_distribution(self) -> Distribution:
+ def _prepare_distribution(self) -> BaseDistribution:
return self._factory.preparer.prepare_editable_requirement(self._ireq)
@@ -337,17 +338,17 @@ class AlreadyInstalledCandidate(Candidate):
def __init__(
self,
- dist: Distribution,
+ dist: BaseDistribution,
template: InstallRequirement,
factory: "Factory",
) -> None:
self.dist = dist
- self._ireq = make_install_req_from_dist(dist, template)
+ self._ireq = _make_install_req_from_dist(dist, template)
self._factory = factory
# This is just logging some messages, so we can do it eagerly.
# The returned dist would be exactly the same as self.dist because we
- # set satisfied_by in make_install_req_from_dist.
+ # set satisfied_by in _make_install_req_from_dist.
# TODO: Supply reason based on force_reinstall and upgrade_strategy.
skip_reason = "already satisfied"
factory.preparer.prepare_installed_requirement(self._ireq, skip_reason)
@@ -371,7 +372,7 @@ class AlreadyInstalledCandidate(Candidate):
@property
def project_name(self) -> NormalizedName:
- return canonicalize_name(self.dist.project_name)
+ return self.dist.canonical_name
@property
def name(self) -> str:
@@ -379,11 +380,11 @@ class AlreadyInstalledCandidate(Candidate):
@property
def version(self) -> CandidateVersion:
- return parse_version(self.dist.version)
+ return self.dist.version
@property
def is_editable(self) -> bool:
- return dist_is_editable(self.dist)
+ return self.dist.editable
def format_for_error(self) -> str:
return f"{self.name} {self.version} (Installed)"
@@ -391,7 +392,7 @@ class AlreadyInstalledCandidate(Candidate):
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
if not with_requires:
return
- for r in self.dist.requires():
+ for r in self.dist.iter_dependencies():
yield self._factory.make_requirement_from_spec(str(r), self._ireq)
def get_install_requirement(self) -> Optional[InstallRequirement]:
@@ -491,8 +492,8 @@ class ExtrasCandidate(Candidate):
# The user may have specified extras that the candidate doesn't
# support. We ignore any unsupported extras here.
- valid_extras = self.extras.intersection(self.base.dist.extras)
- invalid_extras = self.extras.difference(self.base.dist.extras)
+ valid_extras = self.extras.intersection(self.base.dist.iter_provided_extras())
+ invalid_extras = self.extras.difference(self.base.dist.iter_provided_extras())
for extra in sorted(invalid_extras):
logger.warning(
"%s %s does not provide the extra '%s'",
@@ -501,7 +502,7 @@ class ExtrasCandidate(Candidate):
extra,
)
- for r in self.base.dist.requires(valid_extras):
+ for r in self.base.dist.iter_dependencies(valid_extras):
requirement = factory.make_requirement_from_spec(
str(r), self.base._ireq, valid_extras
)
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
index e7fd344aa..a4c24b52a 100644
--- a/src/pip/_internal/resolution/resolvelib/factory.py
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
@@ -19,7 +19,6 @@ from typing import (
)
from pip._vendor.packaging.requirements import InvalidRequirement
-from pip._vendor.packaging.requirements import Requirement as PackagingRequirement
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.resolvelib import ResolutionImpossible
@@ -28,7 +27,6 @@ from pip._internal.cache import CacheEntry, WheelCache
from pip._internal.exceptions import (
DistributionNotFound,
InstallationError,
- InstallationSubprocessError,
MetadataInconsistent,
UnsupportedPythonVersion,
UnsupportedWheel,
@@ -46,6 +44,7 @@ from pip._internal.req.req_install import (
from pip._internal.resolution.base import InstallRequirementProvider
from pip._internal.utils.compatibility_tags import get_supported
from pip._internal.utils.hashes import Hashes
+from pip._internal.utils.packaging import get_requirement
from pip._internal.utils.virtualenv import running_under_virtualenv
from .base import Candidate, CandidateVersion, Constraint, Requirement
@@ -158,10 +157,7 @@ class Factory:
try:
base = self._installed_candidate_cache[dist.canonical_name]
except KeyError:
- from pip._internal.metadata.pkg_resources import Distribution as _Dist
-
- compat_dist = cast(_Dist, dist)._dist
- base = AlreadyInstalledCandidate(compat_dist, template, factory=self)
+ base = AlreadyInstalledCandidate(dist, template, factory=self)
self._installed_candidate_cache[dist.canonical_name] = base
if not extras:
return base
@@ -193,10 +189,16 @@ class Factory:
name=name,
version=version,
)
- except (InstallationSubprocessError, MetadataInconsistent) as e:
- logger.warning("Discarding %s. %s", link, e)
+ except MetadataInconsistent as e:
+ logger.info(
+ "Discarding [blue underline]%s[/]: [yellow]%s[reset]",
+ link,
+ e,
+ extra={"markup": True},
+ )
self._build_failures[link] = e
return None
+
base: BaseCandidate = self._editable_candidate_cache[link]
else:
if link not in self._link_candidate_cache:
@@ -208,8 +210,13 @@ class Factory:
name=name,
version=version,
)
- except (InstallationSubprocessError, MetadataInconsistent) as e:
- logger.warning("Discarding %s. %s", link, e)
+ except MetadataInconsistent as e:
+ logger.info(
+ "Discarding [blue underline]%s[/]: [yellow]%s[reset]",
+ link,
+ e,
+ extra={"markup": True},
+ )
self._build_failures[link] = e
return None
base = self._link_candidate_cache[link]
@@ -263,7 +270,7 @@ class Factory:
extras=extras,
template=template,
)
- # The candidate is a known incompatiblity. Don't use it.
+ # The candidate is a known incompatibility. Don't use it.
if id(candidate) in incompatible_ids:
return None
return candidate
@@ -276,14 +283,27 @@ class Factory:
)
icans = list(result.iter_applicable())
- # PEP 592: Yanked releases must be ignored unless only yanked
- # releases can satisfy the version range. So if this is false,
- # all yanked icans need to be skipped.
+ # PEP 592: Yanked releases are ignored unless the specifier
+ # explicitly pins a version (via '==' or '===') that can be
+ # solely satisfied by a yanked release.
all_yanked = all(ican.link.is_yanked for ican in icans)
+ def is_pinned(specifier: SpecifierSet) -> bool:
+ for sp in specifier:
+ if sp.operator == "===":
+ return True
+ if sp.operator != "==":
+ continue
+ if sp.version.endswith(".*"):
+ continue
+ return True
+ return False
+
+ pinned = is_pinned(specifier)
+
# PackageFinder returns earlier versions first, so we reverse.
for ican in reversed(icans):
- if not all_yanked and ican.link.is_yanked:
+ if not (all_yanked and pinned) and ican.link.is_yanked:
continue
func = functools.partial(
self._make_candidate_from_link,
@@ -350,7 +370,7 @@ class Factory:
def find_candidates(
self,
identifier: str,
- requirements: Mapping[str, Iterator[Requirement]],
+ requirements: Mapping[str, Iterable[Requirement]],
incompatibilities: Mapping[str, Iterator[Candidate]],
constraint: Constraint,
prefers_installed: bool,
@@ -368,7 +388,7 @@ class Factory:
# If the current identifier contains extras, add explicit candidates
# from entries from extra-less identifier.
with contextlib.suppress(InvalidRequirement):
- parsed_requirement = PackagingRequirement(identifier)
+ parsed_requirement = get_requirement(identifier)
explicit_candidates.update(
self._iter_explicit_candidates_from_base(
requirements.get(parsed_requirement.name, ()),
@@ -377,7 +397,7 @@ class Factory:
)
# Add explicit candidates from constraints. We only do this if there are
- # kown ireqs, which represent requirements not already explicit. If
+ # known ireqs, which represent requirements not already explicit. If
# there are no ireqs, we're constraining already-explicit requirements,
# which is handled later when we return the explicit candidates.
if ireqs:
@@ -487,16 +507,20 @@ class Factory:
def make_requirement_from_spec(
self,
specifier: str,
- comes_from: InstallRequirement,
+ comes_from: Optional[InstallRequirement],
requested_extras: Iterable[str] = (),
) -> Optional[Requirement]:
ireq = self._make_install_req_from_spec(specifier, comes_from)
return self._make_requirement_from_install_req(ireq, requested_extras)
def make_requires_python_requirement(
- self, specifier: Optional[SpecifierSet]
+ self,
+ specifier: SpecifierSet,
) -> Optional[Requirement]:
- if self._ignore_requires_python or specifier is None:
+ if self._ignore_requires_python:
+ return None
+ # Don't bother creating a dependency for an empty Requires-Python.
+ if not str(specifier):
return None
return RequiresPythonRequirement(specifier, self._python_candidate)
@@ -578,8 +602,15 @@ class Factory:
req_disp = f"{req} (from {parent.name})"
cands = self._finder.find_all_candidates(req.project_name)
+ skipped_by_requires_python = self._finder.requires_python_skipped_reasons()
versions = [str(v) for v in sorted({c.version for c in cands})]
+ if skipped_by_requires_python:
+ logger.critical(
+ "Ignored the following versions that require a different python "
+ "version: %s",
+ "; ".join(skipped_by_requires_python) or "none",
+ )
logger.critical(
"Could not find a version that satisfies the requirement %s "
"(from versions: %s)",
@@ -614,7 +645,7 @@ class Factory:
]
if requires_python_causes:
# The comprehension above makes sure all Requirement instances are
- # RequiresPythonRequirement, so let's cast for convinience.
+ # RequiresPythonRequirement, so let's cast for convenience.
return self._report_requires_python_error(
cast("Sequence[ConflictCause]", requires_python_causes),
)
@@ -695,6 +726,6 @@ class Factory:
return DistributionNotFound(
"ResolutionImpossible: for help visit "
- "https://pip.pypa.io/en/latest/user_guide/"
- "#fixing-conflicting-dependencies"
+ "https://pip.pypa.io/en/latest/topics/dependency-resolution/"
+ "#dealing-with-dependency-conflicts"
)
diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py
index d2fa5ef55..8663097b4 100644
--- a/src/pip/_internal/resolution/resolvelib/found_candidates.py
+++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py
@@ -9,15 +9,30 @@ something.
"""
import functools
-from typing import Callable, Iterator, Optional, Set, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, Set, Tuple
from pip._vendor.packaging.version import _BaseVersion
-from pip._vendor.six.moves import collections_abc # type: ignore
from .base import Candidate
IndexCandidateInfo = Tuple[_BaseVersion, Callable[[], Optional[Candidate]]]
+if TYPE_CHECKING:
+ SequenceCandidate = Sequence[Candidate]
+else:
+ # For compatibility: Python before 3.9 does not support using [] on the
+ # Sequence class.
+ #
+ # >>> from collections.abc import Sequence
+ # >>> Sequence[str]
+ # Traceback (most recent call last):
+ # File "<stdin>", line 1, in <module>
+ # TypeError: 'ABCMeta' object is not subscriptable
+ #
+ # TODO: Remove this block after dropping Python 3.8 support.
+ SequenceCandidate = Sequence
+
def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
"""Iterator for ``FoundCandidates``.
@@ -90,7 +105,7 @@ def _iter_built_with_inserted(
yield installed
-class FoundCandidates(collections_abc.Sequence):
+class FoundCandidates(SequenceCandidate):
"""A lazy sequence to provide candidates to the resolver.
The intended usage is to return this from `find_matches()` so the resolver
@@ -111,7 +126,7 @@ class FoundCandidates(collections_abc.Sequence):
self._prefers_installed = prefers_installed
self._incompatible_ids = incompatible_ids
- def __getitem__(self, index: int) -> Candidate:
+ def __getitem__(self, index: Any) -> Any:
# Implemented to satisfy the ABC check. This is not needed by the
# resolver, and should not be used by the provider either (for
# performance reasons).
@@ -138,5 +153,3 @@ class FoundCandidates(collections_abc.Sequence):
if self._prefers_installed and self._installed:
return True
return any(self)
-
- __nonzero__ = __bool__ # XXX: Python 2.
diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py
index 632854d3b..6300dfc57 100644
--- a/src/pip/_internal/resolution/resolvelib/provider.py
+++ b/src/pip/_internal/resolution/resolvelib/provider.py
@@ -1,6 +1,15 @@
import collections
import math
-from typing import TYPE_CHECKING, Dict, Iterable, Iterator, Mapping, Sequence, Union
+from typing import (
+ TYPE_CHECKING,
+ Dict,
+ Iterable,
+ Iterator,
+ Mapping,
+ Sequence,
+ TypeVar,
+ Union,
+)
from pip._vendor.resolvelib.providers import AbstractProvider
@@ -37,6 +46,35 @@ else:
# services to those objects (access to pip's finder and preparer).
+D = TypeVar("D")
+V = TypeVar("V")
+
+
+def _get_with_identifier(
+ mapping: Mapping[str, V],
+ identifier: str,
+ default: D,
+) -> Union[D, V]:
+ """Get item from a package name lookup mapping with a resolver identifier.
+
+ This extra logic is needed when the target mapping is keyed by package
+ name, which cannot be directly looked up with an identifier (which may
+ contain requested extras). Additional logic is added to also look up a value
+ by "cleaning up" the extras from the identifier.
+ """
+ if identifier in mapping:
+ return mapping[identifier]
+ # HACK: Theoretically we should check whether this identifier is a valid
+ # "NAME[EXTRAS]" format, and parse out the name part with packaging or
+ # some regular expression. But since pip's resolver only spits out three
+ # kinds of identifiers: normalized PEP 503 names, normalized names plus
+ # extras, and Requires-Python, we can cheat a bit here.
+ name, open_bracket, _ = identifier.partition("[")
+ if open_bracket and name in mapping:
+ return mapping[name]
+ return default
+
+
class PipProvider(_ProviderBase):
"""Pip's provider implementation for resolvelib.
@@ -66,19 +104,20 @@ class PipProvider(_ProviderBase):
def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str:
return requirement_or_candidate.name
- def get_preference(
+ def get_preference( # type: ignore
self,
identifier: str,
resolutions: Mapping[str, Candidate],
candidates: Mapping[str, Iterator[Candidate]],
- information: Mapping[str, Iterator["PreferenceInformation"]],
+ information: Mapping[str, Iterable["PreferenceInformation"]],
+ backtrack_causes: Sequence["PreferenceInformation"],
) -> "Preference":
"""Produce a sort key for given requirement based on preference.
The lower the return value is, the more preferred this group of
arguments is.
- Currently pip considers the followings in order:
+ Currently pip considers the following in order:
* Prefer if any of the known requirements is "direct", e.g. points to an
explicit URL.
@@ -112,9 +151,9 @@ class PipProvider(_ProviderBase):
for _, parent in information[identifier]
)
inferred_depth = min(d for d in parent_depths) + 1.0
- self._known_depths[identifier] = inferred_depth
else:
inferred_depth = 1.0
+ self._known_depths[identifier] = inferred_depth
requested_order = self._user_requested.get(identifier, math.inf)
@@ -128,43 +167,34 @@ class PipProvider(_ProviderBase):
# (Most projects specify it only to request for an installer feature,
# which does not work, but that's another topic.) Intentionally
# delaying Setuptools helps reduce branches the resolver has to check.
- # This serves as a temporary fix for issues like "apache-airlfow[all]"
+ # This serves as a temporary fix for issues like "apache-airflow[all]"
# while we work on "proper" branch pruning techniques.
delay_this = identifier == "setuptools"
+ # Prefer the causes of backtracking on the assumption that the problem
+ # resolving the dependency tree is related to the failures that caused
+ # the backtracking
+ backtrack_cause = self.is_backtrack_cause(identifier, backtrack_causes)
+
return (
not requires_python,
delay_this,
not direct,
not pinned,
+ not backtrack_cause,
inferred_depth,
requested_order,
not unfree,
identifier,
)
- def _get_constraint(self, identifier: str) -> Constraint:
- if identifier in self._constraints:
- return self._constraints[identifier]
-
- # HACK: Theoratically we should check whether this identifier is a valid
- # "NAME[EXTRAS]" format, and parse out the name part with packaging or
- # some regular expression. But since pip's resolver only spits out
- # three kinds of identifiers: normalized PEP 503 names, normalized names
- # plus extras, and Requires-Python, we can cheat a bit here.
- name, open_bracket, _ = identifier.partition("[")
- if open_bracket and name in self._constraints:
- return self._constraints[name]
-
- return Constraint.empty()
-
def find_matches(
self,
identifier: str,
requirements: Mapping[str, Iterator[Requirement]],
incompatibilities: Mapping[str, Iterator[Candidate]],
) -> Iterable[Candidate]:
- def _eligible_for_upgrade(name: str) -> bool:
+ def _eligible_for_upgrade(identifier: str) -> bool:
"""Are upgrades allowed for this project?
This checks the upgrade strategy, and whether the project was one
@@ -178,13 +208,23 @@ class PipProvider(_ProviderBase):
if self._upgrade_strategy == "eager":
return True
elif self._upgrade_strategy == "only-if-needed":
- return name in self._user_requested
+ user_order = _get_with_identifier(
+ self._user_requested,
+ identifier,
+ default=None,
+ )
+ return user_order is not None
return False
+ constraint = _get_with_identifier(
+ self._constraints,
+ identifier,
+ default=Constraint.empty(),
+ )
return self._factory.find_candidates(
identifier=identifier,
requirements=requirements,
- constraint=self._get_constraint(identifier),
+ constraint=constraint,
prefers_installed=(not _eligible_for_upgrade(identifier)),
incompatibilities=incompatibilities,
)
@@ -195,3 +235,14 @@ class PipProvider(_ProviderBase):
def get_dependencies(self, candidate: Candidate) -> Sequence[Requirement]:
with_requires = not self._ignore_dependencies
return [r for r in candidate.iter_dependencies(with_requires) if r is not None]
+
+ @staticmethod
+ def is_backtrack_cause(
+ identifier: str, backtrack_causes: Sequence["PreferenceInformation"]
+ ) -> bool:
+ for backtrack_cause in backtrack_causes:
+ if identifier == backtrack_cause.requirement.name:
+ return True
+ if backtrack_cause.parent and identifier == backtrack_cause.parent.name:
+ return True
+ return False
diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py
index 7cf88ba11..6ced5329b 100644
--- a/src/pip/_internal/resolution/resolvelib/reporter.py
+++ b/src/pip/_internal/resolution/resolvelib/reporter.py
@@ -27,9 +27,8 @@ class PipReporter(BaseReporter):
13: (
"This is taking longer than usual. You might need to provide "
"the dependency resolver with stricter constraints to reduce "
- "runtime. If you want to abort this run, you can press "
- "Ctrl + C to do so. To improve how pip performs, tell us what "
- "happened here: https://pip.pypa.io/surveys/backtracking"
+ "runtime. See https://pip.pypa.io/warnings/backtracking for "
+ "guidance. If you want to abort this run, press Ctrl + C."
),
}
diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py
index c19f83c17..f561f1f1e 100644
--- a/src/pip/_internal/resolution/resolvelib/requirements.py
+++ b/src/pip/_internal/resolution/resolvelib/requirements.py
@@ -21,12 +21,12 @@ class ExplicitRequirement(Requirement):
@property
def project_name(self) -> NormalizedName:
- # No need to canonicalise - the candidate did this
+ # No need to canonicalize - the candidate did this
return self.candidate.project_name
@property
def name(self) -> str:
- # No need to canonicalise - the candidate did this
+ # No need to canonicalize - the candidate did this
return self.candidate.name
def format_for_error(self) -> str:
diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py
index f89afaf43..a605d6c25 100644
--- a/src/pip/_internal/resolution/resolvelib/resolver.py
+++ b/src/pip/_internal/resolution/resolvelib/resolver.py
@@ -19,8 +19,6 @@ from pip._internal.resolution.resolvelib.reporter import (
PipDebuggingReporter,
PipReporter,
)
-from pip._internal.utils.deprecation import deprecated
-from pip._internal.utils.filetypes import is_archive_file
from .base import Candidate, Requirement
from .factory import Factory
@@ -136,25 +134,6 @@ class Resolver(BaseResolver):
)
continue
- looks_like_sdist = (
- is_archive_file(candidate.source_link.file_path)
- and candidate.source_link.ext != ".zip"
- )
- if looks_like_sdist:
- # is a local sdist -- show a deprecation warning!
- reason = (
- "Source distribution is being reinstalled despite an "
- "installed package having the same name and version as "
- "the installed package."
- )
- replacement = "use --force-reinstall"
- deprecated(
- reason=reason,
- replacement=replacement,
- gone_in="21.3",
- issue=8711,
- )
-
# is a local sdist or path -- reinstall
ireq.should_reinstall = True
else:
@@ -192,17 +171,19 @@ class Resolver(BaseResolver):
get installed one-by-one.
The current implementation creates a topological ordering of the
- dependency graph, while breaking any cycles in the graph at arbitrary
- points. We make no guarantees about where the cycle would be broken,
- other than they would be broken.
+ dependency graph, giving more weight to packages with less
+ or no dependencies, while breaking any cycles in the graph at
+ arbitrary points. We make no guarantees about where the cycle
+ would be broken, other than it *would* be broken.
"""
assert self._result is not None, "must call resolve() first"
+ if not req_set.requirements:
+ # Nothing is left to install, so we do not need an order.
+ return []
+
graph = self._result.graph
- weights = get_topological_weights(
- graph,
- expected_node_count=len(self._result.mapping) + 1,
- )
+ weights = get_topological_weights(graph, set(req_set.requirements.keys()))
sorted_items = sorted(
req_set.requirements.items(),
@@ -213,23 +194,32 @@ class Resolver(BaseResolver):
def get_topological_weights(
- graph: "DirectedGraph[Optional[str]]", expected_node_count: int
+ graph: "DirectedGraph[Optional[str]]", requirement_keys: Set[str]
) -> Dict[Optional[str], int]:
"""Assign weights to each node based on how "deep" they are.
This implementation may change at any point in the future without prior
notice.
- We take the length for the longest path to any node from root, ignoring any
- paths that contain a single node twice (i.e. cycles). This is done through
- a depth-first search through the graph, while keeping track of the path to
- the node.
+ We first simplify the dependency graph by pruning any leaves and giving them
+ the highest weight: a package without any dependencies should be installed
+ first. This is done again and again in the same way, giving ever less weight
+ to the newly found leaves. The loop stops when no leaves are left: all
+ remaining packages have at least one dependency left in the graph.
+
+ Then we continue with the remaining graph, by taking the length for the
+ longest path to any node from root, ignoring any paths that contain a single
+ node twice (i.e. cycles). This is done through a depth-first search through
+ the graph, while keeping track of the path to the node.
Cycles in the graph result would result in node being revisited while also
- being it's own path. In this case, take no action. This helps ensure we
+ being on its own path. In this case, take no action. This helps ensure we
don't get stuck in a cycle.
When assigning weight, the longer path (i.e. larger length) is preferred.
+
+ We are only interested in the weights of packages that are in the
+ requirement_keys.
"""
path: Set[Optional[str]] = set()
weights: Dict[Optional[str], int] = {}
@@ -245,15 +235,49 @@ def get_topological_weights(
visit(child)
path.remove(node)
+ if node not in requirement_keys:
+ return
+
last_known_parent_count = weights.get(node, 0)
weights[node] = max(last_known_parent_count, len(path))
+ # Simplify the graph, pruning leaves that have no dependencies.
+ # This is needed for large graphs (say over 200 packages) because the
+ # `visit` function is exponentially slower then, taking minutes.
+ # See https://github.com/pypa/pip/issues/10557
+ # We will loop until we explicitly break the loop.
+ while True:
+ leaves = set()
+ for key in graph:
+ if key is None:
+ continue
+ for _child in graph.iter_children(key):
+ # This means we have at least one child
+ break
+ else:
+ # No child.
+ leaves.add(key)
+ if not leaves:
+ # We are done simplifying.
+ break
+ # Calculate the weight for the leaves.
+ weight = len(graph) - 1
+ for leaf in leaves:
+ if leaf not in requirement_keys:
+ continue
+ weights[leaf] = weight
+ # Remove the leaves from the graph, making it simpler.
+ for leaf in leaves:
+ graph.remove(leaf)
+
+ # Visit the remaining graph.
# `None` is guaranteed to be the root node by resolvelib.
visit(None)
- # Sanity checks
- assert weights[None] == 0
- assert len(weights) == expected_node_count
+ # Sanity check: all requirement keys should be in the weights,
+ # and no other keys should be in the weights.
+ difference = set(weights.keys()).difference(requirement_keys)
+ assert not difference, difference
return weights
diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py
index 6b24965b8..9e2149c52 100644
--- a/src/pip/_internal/self_outdated_check.py
+++ b/src/pip/_internal/self_outdated_check.py
@@ -1,97 +1,149 @@
import datetime
+import functools
import hashlib
import json
import logging
import optparse
import os.path
import sys
-from typing import Any, Dict
+from dataclasses import dataclass
+from typing import Any, Callable, Dict, Optional
from pip._vendor.packaging.version import parse as parse_version
+from pip._vendor.rich.console import Group
+from pip._vendor.rich.markup import escape
+from pip._vendor.rich.text import Text
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import get_default_environment
+from pip._internal.metadata.base import DistributionVersion
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.network.session import PipSession
+from pip._internal.utils.compat import WINDOWS
+from pip._internal.utils.entrypoints import (
+ get_best_invocation_for_this_pip,
+ get_best_invocation_for_this_python,
+)
from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace
from pip._internal.utils.misc import ensure_dir
-SELFCHECK_DATE_FMT = "%Y-%m-%dT%H:%M:%SZ"
+_DATE_FMT = "%Y-%m-%dT%H:%M:%SZ"
logger = logging.getLogger(__name__)
-def _get_statefile_name(key):
- # type: (str) -> str
+def _get_statefile_name(key: str) -> str:
key_bytes = key.encode()
name = hashlib.sha224(key_bytes).hexdigest()
return name
class SelfCheckState:
- def __init__(self, cache_dir):
- # type: (str) -> None
- self.state = {} # type: Dict[str, Any]
- self.statefile_path = None
+ def __init__(self, cache_dir: str) -> None:
+ self._state: Dict[str, Any] = {}
+ self._statefile_path = None
# Try to load the existing state
if cache_dir:
- self.statefile_path = os.path.join(
+ self._statefile_path = os.path.join(
cache_dir, "selfcheck", _get_statefile_name(self.key)
)
try:
- with open(self.statefile_path, encoding="utf-8") as statefile:
- self.state = json.load(statefile)
+ with open(self._statefile_path, encoding="utf-8") as statefile:
+ self._state = json.load(statefile)
except (OSError, ValueError, KeyError):
# Explicitly suppressing exceptions, since we don't want to
# error out if the cache file is invalid.
pass
@property
- def key(self):
- # type: () -> str
+ def key(self) -> str:
return sys.prefix
- def save(self, pypi_version, current_time):
- # type: (str, datetime.datetime) -> None
+ def get(self, current_time: datetime.datetime) -> Optional[str]:
+ """Check if we have a not-outdated version loaded already."""
+ if not self._state:
+ return None
+
+ if "last_check" not in self._state:
+ return None
+
+ if "pypi_version" not in self._state:
+ return None
+
+ seven_days_in_seconds = 7 * 24 * 60 * 60
+
+ # Determine if we need to refresh the state
+ last_check = datetime.datetime.strptime(self._state["last_check"], _DATE_FMT)
+ seconds_since_last_check = (current_time - last_check).total_seconds()
+ if seconds_since_last_check > seven_days_in_seconds:
+ return None
+
+ return self._state["pypi_version"]
+
+ def set(self, pypi_version: str, current_time: datetime.datetime) -> None:
# If we do not have a path to cache in, don't bother saving.
- if not self.statefile_path:
+ if not self._statefile_path:
return
# Check to make sure that we own the directory
- if not check_path_owner(os.path.dirname(self.statefile_path)):
+ if not check_path_owner(os.path.dirname(self._statefile_path)):
return
# Now that we've ensured the directory is owned by this user, we'll go
# ahead and make sure that all our directories are created.
- ensure_dir(os.path.dirname(self.statefile_path))
+ ensure_dir(os.path.dirname(self._statefile_path))
state = {
# Include the key so it's easy to tell which pip wrote the
# file.
"key": self.key,
- "last_check": current_time.strftime(SELFCHECK_DATE_FMT),
+ "last_check": current_time.strftime(_DATE_FMT),
"pypi_version": pypi_version,
}
text = json.dumps(state, sort_keys=True, separators=(",", ":"))
- with adjacent_tmp_file(self.statefile_path) as f:
+ with adjacent_tmp_file(self._statefile_path) as f:
f.write(text.encode())
try:
# Since we have a prefix-specific state file, we can just
# overwrite whatever is there, no need to check.
- replace(f.name, self.statefile_path)
+ replace(f.name, self._statefile_path)
except OSError:
# Best effort.
pass
-def was_installed_by_pip(pkg):
- # type: (str) -> bool
+@dataclass
+class UpgradePrompt:
+ old: str
+ new: str
+
+ def __rich__(self) -> Group:
+ if WINDOWS:
+ pip_cmd = f"{get_best_invocation_for_this_python()} -m pip"
+ else:
+ pip_cmd = get_best_invocation_for_this_pip()
+
+ notice = "[bold][[reset][blue]notice[reset][bold]][reset]"
+ return Group(
+ Text(),
+ Text.from_markup(
+ f"{notice} A new release of pip available: "
+ f"[red]{self.old}[reset] -> [green]{self.new}[reset]"
+ ),
+ Text.from_markup(
+ f"{notice} To update, run: "
+ f"[green]{escape(pip_cmd)} install --upgrade pip"
+ ),
+ )
+
+
+def was_installed_by_pip(pkg: str) -> bool:
"""Checks whether pkg was installed by pip
This is used not to display the upgrade message when pip is in fact
@@ -101,87 +153,87 @@ def was_installed_by_pip(pkg):
return dist is not None and "pip" == dist.installer
-def pip_self_version_check(session, options):
- # type: (PipSession, optparse.Values) -> None
- """Check for an update for pip.
-
- Limit the frequency of checks to once per week. State is stored either in
- the active virtualenv or in the user's USER_CACHE_DIR keyed off the prefix
- of the pip script path.
- """
- installed_dist = get_default_environment().get_distribution("pip")
- if not installed_dist:
+def _get_current_remote_pip_version(
+ session: PipSession, options: optparse.Values
+) -> str:
+ # Lets use PackageFinder to see what the latest pip version is
+ link_collector = LinkCollector.create(
+ session,
+ options=options,
+ suppress_no_index=True,
+ )
+
+ # Pass allow_yanked=False so we don't suggest upgrading to a
+ # yanked version.
+ selection_prefs = SelectionPreferences(
+ allow_yanked=False,
+ allow_all_prereleases=False, # Explicitly set to False
+ )
+
+ finder = PackageFinder.create(
+ link_collector=link_collector,
+ selection_prefs=selection_prefs,
+ )
+ best_candidate = finder.find_best_candidate("pip").best_candidate
+ if best_candidate is None:
return
- pip_version = installed_dist.version
- pypi_version = None
+ return str(best_candidate.version)
- try:
- state = SelfCheckState(cache_dir=options.cache_dir)
- current_time = datetime.datetime.utcnow()
- # Determine if we need to refresh the state
- if "last_check" in state.state and "pypi_version" in state.state:
- last_check = datetime.datetime.strptime(
- state.state["last_check"],
- SELFCHECK_DATE_FMT
- )
- if (current_time - last_check).total_seconds() < 7 * 24 * 60 * 60:
- pypi_version = state.state["pypi_version"]
-
- # Refresh the version if we need to or just see if we need to warn
- if pypi_version is None:
- # Lets use PackageFinder to see what the latest pip version is
- link_collector = LinkCollector.create(
- session,
- options=options,
- suppress_no_index=True,
- )
+def _self_version_check_logic(
+ *,
+ state: SelfCheckState,
+ current_time: datetime.datetime,
+ local_version: DistributionVersion,
+ get_remote_version: Callable[[], str],
+) -> Optional[UpgradePrompt]:
+ remote_version_str = state.get(current_time)
+ if remote_version_str is None:
+ remote_version_str = get_remote_version()
+ state.set(remote_version_str, current_time)
- # Pass allow_yanked=False so we don't suggest upgrading to a
- # yanked version.
- selection_prefs = SelectionPreferences(
- allow_yanked=False,
- allow_all_prereleases=False, # Explicitly set to False
- )
+ remote_version = parse_version(remote_version_str)
+ logger.debug("Remote version of pip: %s", remote_version)
+ logger.debug("Local version of pip: %s", local_version)
- finder = PackageFinder.create(
- link_collector=link_collector,
- selection_prefs=selection_prefs,
- )
- best_candidate = finder.find_best_candidate("pip").best_candidate
- if best_candidate is None:
- return
- pypi_version = str(best_candidate.version)
+ pip_installed_by_pip = was_installed_by_pip("pip")
+ logger.debug("Was pip installed by pip? %s", pip_installed_by_pip)
+ if not pip_installed_by_pip:
+ return None # Only suggest upgrade if pip is installed by pip.
- # save that we've performed a check
- state.save(pypi_version, current_time)
+ local_version_is_older = (
+ local_version < remote_version
+ and local_version.base_version != remote_version.base_version
+ )
+ if local_version_is_older:
+ return UpgradePrompt(old=str(local_version), new=remote_version_str)
- remote_version = parse_version(pypi_version)
+ return None
- local_version_is_older = (
- pip_version < remote_version and
- pip_version.base_version != remote_version.base_version and
- was_installed_by_pip('pip')
- )
- # Determine if our pypi_version is older
- if not local_version_is_older:
- return
+def pip_self_version_check(session: PipSession, options: optparse.Values) -> None:
+ """Check for an update for pip.
+
+ Limit the frequency of checks to once per week. State is stored either in
+ the active virtualenv or in the user's USER_CACHE_DIR keyed off the prefix
+ of the pip script path.
+ """
+ installed_dist = get_default_environment().get_distribution("pip")
+ if not installed_dist:
+ return
- # We cannot tell how the current pip is available in the current
- # command context, so be pragmatic here and suggest the command
- # that's always available. This does not accommodate spaces in
- # `sys.executable`.
- pip_cmd = f"{sys.executable} -m pip"
- logger.warning(
- "You are using pip version %s; however, version %s is "
- "available.\nYou should consider upgrading via the "
- "'%s install --upgrade pip' command.",
- pip_version, pypi_version, pip_cmd
+ try:
+ upgrade_prompt = _self_version_check_logic(
+ state=SelfCheckState(cache_dir=options.cache_dir),
+ current_time=datetime.datetime.utcnow(),
+ local_version=installed_dist.version,
+ get_remote_version=functools.partial(
+ _get_current_remote_pip_version, session, options
+ ),
)
+ if upgrade_prompt is not None:
+ logger.warning("[present-rich] %s", upgrade_prompt)
except Exception:
- logger.debug(
- "There was an error checking the latest version of pip",
- exc_info=True,
- )
+ logger.warning("There was an error checking the latest version of pip.")
+ logger.debug("See below for error", exc_info=True)
diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py
index a8403b7de..16933bf8a 100644
--- a/src/pip/_internal/utils/appdirs.py
+++ b/src/pip/_internal/utils/appdirs.py
@@ -7,29 +7,46 @@ and eventually drop this after all usages are changed.
"""
import os
+import sys
from typing import List
-from pip._vendor import appdirs as _appdirs
+from pip._vendor import platformdirs as _appdirs
def user_cache_dir(appname: str) -> str:
return _appdirs.user_cache_dir(appname, appauthor=False)
+def _macos_user_config_dir(appname: str, roaming: bool = True) -> str:
+ # Use ~/Application Support/pip, if the directory exists.
+ path = _appdirs.user_data_dir(appname, appauthor=False, roaming=roaming)
+ if os.path.isdir(path):
+ return path
+
+ # Use a Linux-like ~/.config/pip, by default.
+ linux_like_path = "~/.config/"
+ if appname:
+ linux_like_path = os.path.join(linux_like_path, appname)
+
+ return os.path.expanduser(linux_like_path)
+
+
def user_config_dir(appname: str, roaming: bool = True) -> str:
- path = _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming)
- if _appdirs.system == "darwin" and not os.path.isdir(path):
- path = os.path.expanduser("~/.config/")
- if appname:
- path = os.path.join(path, appname)
- return path
+ if sys.platform == "darwin":
+ return _macos_user_config_dir(appname, roaming)
+
+ return _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming)
# for the discussion regarding site_config_dir locations
# see <https://github.com/pypa/pip/issues/1733>
def site_config_dirs(appname: str) -> List[str]:
+ if sys.platform == "darwin":
+ return [_appdirs.site_data_dir(appname, appauthor=False, multipath=True)]
+
dirval = _appdirs.site_config_dir(appname, appauthor=False, multipath=True)
- if _appdirs.system not in ["win32", "darwin"]:
- # always look in /etc directly as well
- return dirval.split(os.pathsep) + ["/etc"]
- return [dirval]
+ if sys.platform == "win32":
+ return [dirval]
+
+ # Unix-y system. Look in /etc as well.
+ return dirval.split(os.pathsep) + ["/etc"]
diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py
index f1c0f0633..b6ed9a78e 100644
--- a/src/pip/_internal/utils/compatibility_tags.py
+++ b/src/pip/_internal/utils/compatibility_tags.py
@@ -2,9 +2,10 @@
"""
import re
-from typing import TYPE_CHECKING, List, Optional, Tuple
+from typing import List, Optional, Tuple
from pip._vendor.packaging.tags import (
+ PythonVersion,
Tag,
compatible_tags,
cpython_tags,
@@ -14,10 +15,6 @@ from pip._vendor.packaging.tags import (
mac_platforms,
)
-if TYPE_CHECKING:
- from pip._vendor.packaging.tags import PythonVersion
-
-
_osx_arch_pat = re.compile(r"(.+)_(\d+)_(\d+)_(.+)")
@@ -95,7 +92,7 @@ def _expand_allowed_platforms(platforms: Optional[List[str]]) -> Optional[List[s
return result
-def _get_python_version(version: str) -> "PythonVersion":
+def _get_python_version(version: str) -> PythonVersion:
if len(version) > 1:
return int(version[0]), int(version[1:])
else:
@@ -132,7 +129,7 @@ def get_supported(
"""
supported: List[Tag] = []
- python_version: Optional["PythonVersion"] = None
+ python_version: Optional[PythonVersion] = None
if version is not None:
python_version = _get_python_version(version)
diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py
index a163395c4..7c7ace6ff 100644
--- a/src/pip/_internal/utils/deprecation.py
+++ b/src/pip/_internal/utils/deprecation.py
@@ -8,18 +8,9 @@ from typing import Any, Optional, TextIO, Type, Union
from pip._vendor.packaging.version import parse
-from pip import __version__ as current_version
+from pip import __version__ as current_version # NOTE: tests patch this name.
DEPRECATION_MSG_PREFIX = "DEPRECATION: "
-DEPRECATION_MESSAGE = DEPRECATION_MSG_PREFIX + "{reason}"
-GONE_IN_MESSAGE_FUTURE = "pip {gone_in} will enforce this behavior change."
-GONE_IN_MESSAGE_PAST = "This behavior change has been enforced since pip {gone_in}."
-REPLACEMENT_MESSAGE = "A possible replacement is {replacement}."
-FEATURE_FLAG_MESSAGE = (
- "You can temporarily use the flag --use-feature={feature_flag} "
- "to test the upcoming behavior."
-)
-ISSUE_MESSAGE = "Discussion can be found at https://github.com/pypa/pip/issues/{issue}."
class PipDeprecationWarning(Warning):
@@ -62,6 +53,7 @@ def install_warning_logger() -> None:
def deprecated(
+ *,
reason: str,
replacement: Optional[str],
gone_in: Optional[str],
@@ -86,42 +78,98 @@ def deprecated(
issue:
Issue number on the tracker that would serve as a useful place for
users to find related discussion and provide feedback.
-
- Always pass replacement, gone_in and issue as keyword arguments for clarity
- at the call site.
"""
+
# Determine whether or not the feature is already gone in this version.
is_gone = gone_in is not None and parse(current_version) >= parse(gone_in)
- # Allow variable substitutions within the "reason" variable.
- formatted_reason = reason.format(gone_in=gone_in)
- # Construct a nice message.
- # This is eagerly formatted as we want it to get logged as if someone
- # typed this entire message out.
- formatted_deprecation_message = DEPRECATION_MESSAGE.format(reason=formatted_reason)
- gone_in_message = GONE_IN_MESSAGE_PAST if is_gone else GONE_IN_MESSAGE_FUTURE
- formatted_gone_in_message = (
- gone_in_message.format(gone_in=gone_in) if gone_in else None
- )
- formatted_replacement_message = (
- REPLACEMENT_MESSAGE.format(replacement=replacement) if replacement else None
- )
- formatted_feature_flag_message = (
- None
- if is_gone or not feature_flag
- else FEATURE_FLAG_MESSAGE.format(feature_flag=feature_flag)
- )
- formatted_issue_message = ISSUE_MESSAGE.format(issue=issue) if issue else None
- sentences = [
- formatted_deprecation_message,
- formatted_gone_in_message,
- formatted_replacement_message,
- formatted_feature_flag_message,
- formatted_issue_message,
+
+ message_parts = [
+ (reason, f"{DEPRECATION_MSG_PREFIX}{{}}"),
+ (
+ gone_in,
+ "pip {} will enforce this behaviour change."
+ if not is_gone
+ else "Since pip {}, this is no longer supported.",
+ ),
+ (
+ replacement,
+ "A possible replacement is {}.",
+ ),
+ (
+ feature_flag,
+ "You can use the flag --use-feature={} to test the upcoming behaviour."
+ if not is_gone
+ else None,
+ ),
+ (
+ issue,
+ "Discussion can be found at https://github.com/pypa/pip/issues/{}",
+ ),
]
- message = " ".join(sentence for sentence in sentences if sentence)
- # Raise as an error if the functionality is gone.
+ message = " ".join(
+ format_str.format(value)
+ for value, format_str in message_parts
+ if format_str is not None and value is not None
+ )
+
+ # Raise as an error if this behaviour is deprecated.
if is_gone:
raise PipDeprecationWarning(message)
- else:
- warnings.warn(message, category=PipDeprecationWarning, stacklevel=2)
+
+ warnings.warn(message, category=PipDeprecationWarning, stacklevel=2)
+
+
+class LegacyInstallReason:
+ def __init__(
+ self,
+ reason: str,
+ replacement: Optional[str],
+ gone_in: Optional[str],
+ feature_flag: Optional[str] = None,
+ issue: Optional[int] = None,
+ emit_after_success: bool = False,
+ emit_before_install: bool = False,
+ ):
+ self._reason = reason
+ self._replacement = replacement
+ self._gone_in = gone_in
+ self._feature_flag = feature_flag
+ self._issue = issue
+ self.emit_after_success = emit_after_success
+ self.emit_before_install = emit_before_install
+
+ def emit_deprecation(self, name: str) -> None:
+ deprecated(
+ reason=self._reason.format(name=name),
+ replacement=self._replacement,
+ gone_in=self._gone_in,
+ feature_flag=self._feature_flag,
+ issue=self._issue,
+ )
+
+
+LegacyInstallReasonFailedBdistWheel = LegacyInstallReason(
+ reason=(
+ "{name} was installed using the legacy 'setup.py install' "
+ "method, because a wheel could not be built for it."
+ ),
+ replacement="to fix the wheel build issue reported above",
+ gone_in=None,
+ issue=8368,
+ emit_after_success=True,
+)
+
+
+LegacyInstallReasonMissingWheelPackage = LegacyInstallReason(
+ reason=(
+ "{name} is being installed using the legacy "
+ "'setup.py install' method, because it does not have a "
+ "'pyproject.toml' and the 'wheel' package "
+ "is not installed."
+ ),
+ replacement="to enable the '--use-pep517' option",
+ gone_in=None,
+ issue=8559,
+ emit_before_install=True,
+)
diff --git a/src/pip/_internal/utils/direct_url_helpers.py b/src/pip/_internal/utils/direct_url_helpers.py
index 088e977b5..0e8e5e160 100644
--- a/src/pip/_internal/utils/direct_url_helpers.py
+++ b/src/pip/_internal/utils/direct_url_helpers.py
@@ -2,6 +2,7 @@ from typing import Optional
from pip._internal.models.direct_url import ArchiveInfo, DirectUrl, DirInfo, VcsInfo
from pip._internal.models.link import Link
+from pip._internal.utils.urls import path_to_url
from pip._internal.vcs import vcs
@@ -28,6 +29,13 @@ def direct_url_as_pep440_direct_reference(direct_url: DirectUrl, name: str) -> s
return requirement
+def direct_url_for_editable(source_dir: str) -> DirectUrl:
+ return DirectUrl(
+ url=path_to_url(source_dir),
+ info=DirInfo(editable=True),
+ )
+
+
def direct_url_from_link(
link: Link, source_dir: Optional[str] = None, link_is_in_wheel_cache: bool = False
) -> DirectUrl:
diff --git a/src/pip/_internal/utils/distutils_args.py b/src/pip/_internal/utils/distutils_args.py
index e4aa5b827..2fd186207 100644
--- a/src/pip/_internal/utils/distutils_args.py
+++ b/src/pip/_internal/utils/distutils_args.py
@@ -1,42 +1,43 @@
-from distutils.errors import DistutilsArgError
-from distutils.fancy_getopt import FancyGetopt
+from getopt import GetoptError, getopt
from typing import Dict, List
_options = [
- ("exec-prefix=", None, ""),
- ("home=", None, ""),
- ("install-base=", None, ""),
- ("install-data=", None, ""),
- ("install-headers=", None, ""),
- ("install-lib=", None, ""),
- ("install-platlib=", None, ""),
- ("install-purelib=", None, ""),
- ("install-scripts=", None, ""),
- ("prefix=", None, ""),
- ("root=", None, ""),
- ("user", None, ""),
+ "exec-prefix=",
+ "home=",
+ "install-base=",
+ "install-data=",
+ "install-headers=",
+ "install-lib=",
+ "install-platlib=",
+ "install-purelib=",
+ "install-scripts=",
+ "prefix=",
+ "root=",
+ "user",
]
-# typeshed doesn't permit Tuple[str, None, str], see python/typeshed#3469.
-_distutils_getopt = FancyGetopt(_options) # type: ignore
-
-
def parse_distutils_args(args: List[str]) -> Dict[str, str]:
- """Parse provided arguments, returning an object that has the
- matched arguments.
+ """Parse provided arguments, returning an object that has the matched arguments.
Any unknown arguments are ignored.
"""
result = {}
for arg in args:
try:
- _, match = _distutils_getopt.getopt(args=[arg])
- except DistutilsArgError:
+ parsed_opt, _ = getopt(args=[arg], shortopts="", longopts=_options)
+ except GetoptError:
# We don't care about any other options, which here may be
# considered unrecognized since our option list is not
# exhaustive.
- pass
- else:
- result.update(match.__dict__)
+ continue
+
+ if not parsed_opt:
+ continue
+
+ option = parsed_opt[0]
+ name_from_parsed = option[0][2:].replace("-", "_")
+ value_from_parsed = option[1] or "true"
+ result[name_from_parsed] = value_from_parsed
+
return result
diff --git a/src/pip/_internal/utils/egg_link.py b/src/pip/_internal/utils/egg_link.py
new file mode 100644
index 000000000..9e0da8d2d
--- /dev/null
+++ b/src/pip/_internal/utils/egg_link.py
@@ -0,0 +1,75 @@
+# The following comment should be removed at some point in the future.
+# mypy: strict-optional=False
+
+import os
+import re
+import sys
+from typing import Optional
+
+from pip._internal.locations import site_packages, user_site
+from pip._internal.utils.virtualenv import (
+ running_under_virtualenv,
+ virtualenv_no_global,
+)
+
+__all__ = [
+ "egg_link_path_from_sys_path",
+ "egg_link_path_from_location",
+]
+
+
+def _egg_link_name(raw_name: str) -> str:
+ """
+ Convert a Name metadata value to a .egg-link name, by applying
+ the same substitution as pkg_resources's safe_name function.
+ Note: we cannot use canonicalize_name because it has a different logic.
+ """
+ return re.sub("[^A-Za-z0-9.]+", "-", raw_name) + ".egg-link"
+
+
+def egg_link_path_from_sys_path(raw_name: str) -> Optional[str]:
+ """
+ Look for a .egg-link file for project name, by walking sys.path.
+ """
+ egg_link_name = _egg_link_name(raw_name)
+ for path_item in sys.path:
+ egg_link = os.path.join(path_item, egg_link_name)
+ if os.path.isfile(egg_link):
+ return egg_link
+ return None
+
+
+def egg_link_path_from_location(raw_name: str) -> Optional[str]:
+ """
+ Return the path for the .egg-link file if it exists, otherwise, None.
+
+ There's 3 scenarios:
+ 1) not in a virtualenv
+ try to find in site.USER_SITE, then site_packages
+ 2) in a no-global virtualenv
+ try to find in site_packages
+ 3) in a yes-global virtualenv
+ try to find in site_packages, then site.USER_SITE
+ (don't look in global location)
+
+ For #1 and #3, there could be odd cases, where there's an egg-link in 2
+ locations.
+
+ This method will just return the first one found.
+ """
+ sites = []
+ if running_under_virtualenv():
+ sites.append(site_packages)
+ if not virtualenv_no_global() and user_site:
+ sites.append(user_site)
+ else:
+ if user_site:
+ sites.append(user_site)
+ sites.append(site_packages)
+
+ egg_link_name = _egg_link_name(raw_name)
+ for site in sites:
+ egglink = os.path.join(site, egg_link_name)
+ if os.path.isfile(egglink):
+ return egglink
+ return None
diff --git a/src/pip/_internal/utils/encoding.py b/src/pip/_internal/utils/encoding.py
index 1c73f6c9a..008f06a79 100644
--- a/src/pip/_internal/utils/encoding.py
+++ b/src/pip/_internal/utils/encoding.py
@@ -14,7 +14,7 @@ BOMS: List[Tuple[bytes, str]] = [
(codecs.BOM_UTF32_LE, "utf-32-le"),
]
-ENCODING_RE = re.compile(br"coding[:=]\s*([-\w.]+)")
+ENCODING_RE = re.compile(rb"coding[:=]\s*([-\w.]+)")
def auto_decode(data: bytes) -> str:
diff --git a/src/pip/_internal/utils/entrypoints.py b/src/pip/_internal/utils/entrypoints.py
index 1504a1291..150136938 100644
--- a/src/pip/_internal/utils/entrypoints.py
+++ b/src/pip/_internal/utils/entrypoints.py
@@ -1,7 +1,23 @@
+import itertools
+import os
+import shutil
import sys
from typing import List, Optional
from pip._internal.cli.main import main
+from pip._internal.utils.compat import WINDOWS
+
+_EXECUTABLE_NAMES = [
+ "pip",
+ f"pip{sys.version_info.major}",
+ f"pip{sys.version_info.major}.{sys.version_info.minor}",
+]
+if WINDOWS:
+ _allowed_extensions = {"", ".exe"}
+ _EXECUTABLE_NAMES = [
+ "".join(parts)
+ for parts in itertools.product(_EXECUTABLE_NAMES, _allowed_extensions)
+ ]
def _wrapper(args: Optional[List[str]] = None) -> int:
@@ -25,3 +41,44 @@ def _wrapper(args: Optional[List[str]] = None) -> int:
"running pip directly.\n"
)
return main(args)
+
+
+def get_best_invocation_for_this_pip() -> str:
+ """Try to figure out the best way to invoke pip in the current environment."""
+ binary_directory = "Scripts" if WINDOWS else "bin"
+ binary_prefix = os.path.join(sys.prefix, binary_directory)
+
+ # Try to use pip[X[.Y]] names, if those executables for this environment are
+ # the first on PATH with that name.
+ path_parts = os.path.normcase(os.environ.get("PATH", "")).split(os.pathsep)
+ exe_are_in_PATH = os.path.normcase(binary_prefix) in path_parts
+ if exe_are_in_PATH:
+ for exe_name in _EXECUTABLE_NAMES:
+ found_executable = shutil.which(exe_name)
+ binary_executable = os.path.join(binary_prefix, exe_name)
+ if (
+ found_executable
+ and os.path.exists(binary_executable)
+ and os.path.samefile(
+ found_executable,
+ binary_executable,
+ )
+ ):
+ return exe_name
+
+ # Use the `-m` invocation, if there's no "nice" invocation.
+ return f"{get_best_invocation_for_this_python()} -m pip"
+
+
+def get_best_invocation_for_this_python() -> str:
+ """Try to figure out the best way to invoke the current Python."""
+ exe = sys.executable
+ exe_name = os.path.basename(exe)
+
+ # Try to use the basename, if it's the first executable.
+ found_executable = shutil.which(exe_name)
+ if found_executable and os.path.samefile(found_executable, exe):
+ return exe_name
+
+ # Use the full executable name, because we couldn't find something simpler.
+ return exe
diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py
index b7e6191ab..83c2df75b 100644
--- a/src/pip/_internal/utils/filesystem.py
+++ b/src/pip/_internal/utils/filesystem.py
@@ -2,12 +2,10 @@ import fnmatch
import os
import os.path
import random
-import shutil
-import stat
import sys
from contextlib import contextmanager
from tempfile import NamedTemporaryFile
-from typing import Any, BinaryIO, Iterator, List, Union, cast
+from typing import Any, BinaryIO, Generator, List, Union, cast
from pip._vendor.tenacity import retry, stop_after_delay, wait_fixed
@@ -42,35 +40,8 @@ def check_path_owner(path: str) -> bool:
return False # assume we don't own the path
-def copy2_fixed(src: str, dest: str) -> None:
- """Wrap shutil.copy2() but map errors copying socket files to
- SpecialFileError as expected.
-
- See also https://bugs.python.org/issue37700.
- """
- try:
- shutil.copy2(src, dest)
- except OSError:
- for f in [src, dest]:
- try:
- is_socket_file = is_socket(f)
- except OSError:
- # An error has already occurred. Another error here is not
- # a problem and we can ignore it.
- pass
- else:
- if is_socket_file:
- raise shutil.SpecialFileError(f"`{f}` is a socket")
-
- raise
-
-
-def is_socket(path: str) -> bool:
- return stat.S_ISSOCK(os.lstat(path).st_mode)
-
-
@contextmanager
-def adjacent_tmp_file(path: str, **kwargs: Any) -> Iterator[BinaryIO]:
+def adjacent_tmp_file(path: str, **kwargs: Any) -> Generator[BinaryIO, None, None]:
"""Return a file-like object pointing to a tmp file next to path.
The file is created securely and is ensured to be written to disk
diff --git a/src/pip/_internal/utils/filetypes.py b/src/pip/_internal/utils/filetypes.py
index da935846f..594857017 100644
--- a/src/pip/_internal/utils/filetypes.py
+++ b/src/pip/_internal/utils/filetypes.py
@@ -6,21 +6,20 @@ from typing import Tuple
from pip._internal.utils.misc import splitext
WHEEL_EXTENSION = ".whl"
-BZ2_EXTENSIONS = (".tar.bz2", ".tbz") # type: Tuple[str, ...]
-XZ_EXTENSIONS = (
+BZ2_EXTENSIONS: Tuple[str, ...] = (".tar.bz2", ".tbz")
+XZ_EXTENSIONS: Tuple[str, ...] = (
".tar.xz",
".txz",
".tlz",
".tar.lz",
".tar.lzma",
-) # type: Tuple[str, ...]
-ZIP_EXTENSIONS = (".zip", WHEEL_EXTENSION) # type: Tuple[str, ...]
-TAR_EXTENSIONS = (".tar.gz", ".tgz", ".tar") # type: Tuple[str, ...]
+)
+ZIP_EXTENSIONS: Tuple[str, ...] = (".zip", WHEEL_EXTENSION)
+TAR_EXTENSIONS: Tuple[str, ...] = (".tar.gz", ".tgz", ".tar")
ARCHIVE_EXTENSIONS = ZIP_EXTENSIONS + BZ2_EXTENSIONS + TAR_EXTENSIONS + XZ_EXTENSIONS
-def is_archive_file(name):
- # type: (str) -> bool
+def is_archive_file(name: str) -> bool:
"""Return True if `name` is a considered as an archive file."""
ext = splitext(name)[1].lower()
if ext in ARCHIVE_EXTENSIONS:
diff --git a/src/pip/_internal/utils/glibc.py b/src/pip/_internal/utils/glibc.py
index 1c9ff3544..7bd3c2068 100644
--- a/src/pip/_internal/utils/glibc.py
+++ b/src/pip/_internal/utils/glibc.py
@@ -6,14 +6,12 @@ import sys
from typing import Optional, Tuple
-def glibc_version_string():
- # type: () -> Optional[str]
+def glibc_version_string() -> Optional[str]:
"Returns glibc version string, or None if not using glibc."
return glibc_version_string_confstr() or glibc_version_string_ctypes()
-def glibc_version_string_confstr():
- # type: () -> Optional[str]
+def glibc_version_string_confstr() -> Optional[str]:
"Primary implementation of glibc_version_string using os.confstr."
# os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
# to be broken or missing. This strategy is used in the standard library
@@ -30,8 +28,7 @@ def glibc_version_string_confstr():
return version
-def glibc_version_string_ctypes():
- # type: () -> Optional[str]
+def glibc_version_string_ctypes() -> Optional[str]:
"Fallback implementation of glibc_version_string using ctypes."
try:
@@ -78,8 +75,7 @@ def glibc_version_string_ctypes():
# versions that was generated by pip 8.1.2 and earlier is useless and
# misleading. Solution: instead of using platform, use our code that actually
# works.
-def libc_ver():
- # type: () -> Tuple[str, str]
+def libc_ver() -> Tuple[str, str]:
"""Try to determine the glibc version
Returns a tuple of strings (lib, version) which default to empty strings
diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py
index 3d20b8d02..76727306a 100644
--- a/src/pip/_internal/utils/hashes.py
+++ b/src/pip/_internal/utils/hashes.py
@@ -1,5 +1,5 @@
import hashlib
-from typing import TYPE_CHECKING, BinaryIO, Dict, Iterator, List
+from typing import TYPE_CHECKING, BinaryIO, Dict, Iterable, List, Optional
from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError
from pip._internal.utils.misc import read_chunks
@@ -28,8 +28,7 @@ class Hashes:
"""
- def __init__(self, hashes=None):
- # type: (Dict[str, List[str]]) -> None
+ def __init__(self, hashes: Optional[Dict[str, List[str]]] = None) -> None:
"""
:param hashes: A dict of algorithm names pointing to lists of allowed
hex digests
@@ -41,8 +40,7 @@ class Hashes:
allowed[alg] = sorted(keys)
self._allowed = allowed
- def __and__(self, other):
- # type: (Hashes) -> Hashes
+ def __and__(self, other: "Hashes") -> "Hashes":
if not isinstance(other, Hashes):
return NotImplemented
@@ -62,21 +60,14 @@ class Hashes:
return Hashes(new)
@property
- def digest_count(self):
- # type: () -> int
+ def digest_count(self) -> int:
return sum(len(digests) for digests in self._allowed.values())
- def is_hash_allowed(
- self,
- hash_name, # type: str
- hex_digest, # type: str
- ):
- # type: (...) -> bool
+ def is_hash_allowed(self, hash_name: str, hex_digest: str) -> bool:
"""Return whether the given hex digest is allowed."""
return hex_digest in self._allowed.get(hash_name, [])
- def check_against_chunks(self, chunks):
- # type: (Iterator[bytes]) -> None
+ def check_against_chunks(self, chunks: Iterable[bytes]) -> None:
"""Check good hashes against ones built from iterable of chunks of
data.
@@ -99,12 +90,10 @@ class Hashes:
return
self._raise(gots)
- def _raise(self, gots):
- # type: (Dict[str, _Hash]) -> NoReturn
+ def _raise(self, gots: Dict[str, "_Hash"]) -> "NoReturn":
raise HashMismatch(self._allowed, gots)
- def check_against_file(self, file):
- # type: (BinaryIO) -> None
+ def check_against_file(self, file: BinaryIO) -> None:
"""Check good hashes against a file-like object
Raise HashMismatch if none match.
@@ -112,28 +101,20 @@ class Hashes:
"""
return self.check_against_chunks(read_chunks(file))
- def check_against_path(self, path):
- # type: (str) -> None
+ def check_against_path(self, path: str) -> None:
with open(path, "rb") as file:
return self.check_against_file(file)
- def __nonzero__(self):
- # type: () -> bool
+ def __bool__(self) -> bool:
"""Return whether I know any known-good hashes."""
return bool(self._allowed)
- def __bool__(self):
- # type: () -> bool
- return self.__nonzero__()
-
- def __eq__(self, other):
- # type: (object) -> bool
+ def __eq__(self, other: object) -> bool:
if not isinstance(other, Hashes):
return NotImplemented
return self._allowed == other._allowed
- def __hash__(self):
- # type: () -> int
+ def __hash__(self) -> int:
return hash(
",".join(
sorted(
@@ -153,13 +134,11 @@ class MissingHashes(Hashes):
"""
- def __init__(self):
- # type: () -> None
+ def __init__(self) -> None:
"""Don't offer the ``hashes`` kwarg."""
# Pass our favorite hash in to generate a "gotten hash". With the
# empty list, it will never match, so an error will always raise.
super().__init__(hashes={FAVORITE_HASH: []})
- def _raise(self, gots):
- # type: (Dict[str, _Hash]) -> NoReturn
+ def _raise(self, gots: Dict[str, "_Hash"]) -> "NoReturn":
raise HashMissing(gots[FAVORITE_HASH].hexdigest())
diff --git a/src/pip/_internal/utils/inject_securetransport.py b/src/pip/_internal/utils/inject_securetransport.py
index b6863d934..276aa79bb 100644
--- a/src/pip/_internal/utils/inject_securetransport.py
+++ b/src/pip/_internal/utils/inject_securetransport.py
@@ -10,8 +10,7 @@ old to handle TLSv1.2.
import sys
-def inject_securetransport():
- # type: () -> None
+def inject_securetransport() -> None:
# Only relevant on macOS
if sys.platform != "darwin":
return
diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py
index 39a18fd6c..c10e1f4ce 100644
--- a/src/pip/_internal/utils/logging.py
+++ b/src/pip/_internal/utils/logging.py
@@ -4,28 +4,30 @@ import logging
import logging.handlers
import os
import sys
+import threading
+from dataclasses import dataclass
+from io import TextIOWrapper
from logging import Filter
-from typing import IO, Any, Callable, Iterator, Optional, TextIO, Type, cast
+from typing import Any, ClassVar, Generator, List, Optional, TextIO, Type
+
+from pip._vendor.rich.console import (
+ Console,
+ ConsoleOptions,
+ ConsoleRenderable,
+ RenderableType,
+ RenderResult,
+ RichCast,
+)
+from pip._vendor.rich.highlighter import NullHighlighter
+from pip._vendor.rich.logging import RichHandler
+from pip._vendor.rich.segment import Segment
+from pip._vendor.rich.style import Style
from pip._internal.utils._log import VERBOSE, getLogger
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX
from pip._internal.utils.misc import ensure_dir
-try:
- import threading
-except ImportError:
- import dummy_threading as threading # type: ignore
-
-
-try:
- from pip._vendor import colorama
-# Lots of different errors can come from this, including SystemError and
-# ImportError.
-except Exception:
- colorama = None
-
-
_log_state = threading.local()
subprocess_logger = getLogger("pip.subprocessor")
@@ -35,39 +37,22 @@ class BrokenStdoutLoggingError(Exception):
Raised if BrokenPipeError occurs for the stdout stream while logging.
"""
- pass
+def _is_broken_pipe_error(exc_class: Type[BaseException], exc: BaseException) -> bool:
+ if exc_class is BrokenPipeError:
+ return True
-# BrokenPipeError manifests differently in Windows and non-Windows.
-if WINDOWS:
- # In Windows, a broken pipe can show up as EINVAL rather than EPIPE:
+ # On Windows, a broken pipe can show up as EINVAL rather than EPIPE:
# https://bugs.python.org/issue19612
# https://bugs.python.org/issue30418
- def _is_broken_pipe_error(exc_class, exc):
- # type: (Type[BaseException], BaseException) -> bool
- """See the docstring for non-Windows below."""
- return (exc_class is BrokenPipeError) or (
- isinstance(exc, OSError) and exc.errno in (errno.EINVAL, errno.EPIPE)
- )
-
-
-else:
- # Then we are in the non-Windows case.
- def _is_broken_pipe_error(exc_class, exc):
- # type: (Type[BaseException], BaseException) -> bool
- """
- Return whether an exception is a broken pipe error.
+ if not WINDOWS:
+ return False
- Args:
- exc_class: an exception class.
- exc: an exception instance.
- """
- return exc_class is BrokenPipeError
+ return isinstance(exc, OSError) and exc.errno in (errno.EINVAL, errno.EPIPE)
@contextlib.contextmanager
-def indent_log(num=2):
- # type: (int) -> Iterator[None]
+def indent_log(num: int = 2) -> Generator[None, None, None]:
"""
A context manager which will cause the log output to be indented for any
log messages emitted inside it.
@@ -81,8 +66,7 @@ def indent_log(num=2):
_log_state.indentation -= num
-def get_indentation():
- # type: () -> int
+def get_indentation() -> int:
return getattr(_log_state, "indentation", 0)
@@ -91,11 +75,10 @@ class IndentingFormatter(logging.Formatter):
def __init__(
self,
- *args, # type: Any
- add_timestamp=False, # type: bool
- **kwargs, # type: Any
- ):
- # type: (...) -> None
+ *args: Any,
+ add_timestamp: bool = False,
+ **kwargs: Any,
+ ) -> None:
"""
A logging.Formatter that obeys the indent_log() context manager.
@@ -105,8 +88,7 @@ class IndentingFormatter(logging.Formatter):
self.add_timestamp = add_timestamp
super().__init__(*args, **kwargs)
- def get_message_start(self, formatted, levelno):
- # type: (str, int) -> str
+ def get_message_start(self, formatted: str, levelno: int) -> str:
"""
Return the start of the formatted log message (not counting the
prefix to add to each line).
@@ -122,8 +104,7 @@ class IndentingFormatter(logging.Formatter):
return "ERROR: "
- def format(self, record):
- # type: (logging.LogRecord) -> str
+ def format(self, record: logging.LogRecord) -> str:
"""
Calls the standard formatter, but will indent all of the log message
lines by our current indentation level.
@@ -140,85 +121,66 @@ class IndentingFormatter(logging.Formatter):
return formatted
-def _color_wrap(*colors):
- # type: (*str) -> Callable[[str], str]
- def wrapped(inp):
- # type: (str) -> str
- return "".join(list(colors) + [inp, colorama.Style.RESET_ALL])
-
- return wrapped
-
-
-class ColorizedStreamHandler(logging.StreamHandler):
-
- # Don't build up a list of colors if we don't have colorama
- if colorama:
- COLORS = [
- # This needs to be in order from highest logging level to lowest.
- (logging.ERROR, _color_wrap(colorama.Fore.RED)),
- (logging.WARNING, _color_wrap(colorama.Fore.YELLOW)),
- ]
- else:
- COLORS = []
-
- def __init__(self, stream=None, no_color=None):
- # type: (Optional[TextIO], bool) -> None
- super().__init__(stream)
- self._no_color = no_color
-
- if WINDOWS and colorama:
- self.stream = colorama.AnsiToWin32(self.stream)
-
- def _using_stdout(self):
- # type: () -> bool
- """
- Return whether the handler is using sys.stdout.
- """
- if WINDOWS and colorama:
- # Then self.stream is an AnsiToWin32 object.
- stream = cast(colorama.AnsiToWin32, self.stream)
- return stream.wrapped is sys.stdout
-
- return self.stream is sys.stdout
-
- def should_color(self):
- # type: () -> bool
- # Don't colorize things if we do not have colorama or if told not to
- if not colorama or self._no_color:
- return False
-
- real_stream = (
- self.stream
- if not isinstance(self.stream, colorama.AnsiToWin32)
- else self.stream.wrapped
+@dataclass
+class IndentedRenderable:
+ renderable: RenderableType
+ indent: int
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ segments = console.render(self.renderable, options)
+ lines = Segment.split_lines(segments)
+ for line in lines:
+ yield Segment(" " * self.indent)
+ yield from line
+ yield Segment("\n")
+
+
+class RichPipStreamHandler(RichHandler):
+ KEYWORDS: ClassVar[Optional[List[str]]] = []
+
+ def __init__(self, stream: Optional[TextIO], no_color: bool) -> None:
+ super().__init__(
+ console=Console(file=stream, no_color=no_color, soft_wrap=True),
+ show_time=False,
+ show_level=False,
+ show_path=False,
+ highlighter=NullHighlighter(),
)
- # If the stream is a tty we should color it
- if hasattr(real_stream, "isatty") and real_stream.isatty():
- return True
-
- # If we have an ANSI term we should color it
- if os.environ.get("TERM") == "ANSI":
- return True
+ # Our custom override on Rich's logger, to make things work as we need them to.
+ def emit(self, record: logging.LogRecord) -> None:
+ style: Optional[Style] = None
+
+ # If we are given a diagnostic error to present, present it with indentation.
+ assert isinstance(record.args, tuple)
+ if record.msg == "[present-rich] %s" and len(record.args) == 1:
+ rich_renderable = record.args[0]
+ assert isinstance(
+ rich_renderable, (ConsoleRenderable, RichCast, str)
+ ), f"{rich_renderable} is not rich-console-renderable"
+
+ renderable: RenderableType = IndentedRenderable(
+ rich_renderable, indent=get_indentation()
+ )
+ else:
+ message = self.format(record)
+ renderable = self.render_message(record, message)
+ if record.levelno is not None:
+ if record.levelno >= logging.ERROR:
+ style = Style(color="red")
+ elif record.levelno >= logging.WARNING:
+ style = Style(color="yellow")
+
+ try:
+ self.console.print(renderable, overflow="ignore", crop=False, style=style)
+ except Exception:
+ self.handleError(record)
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ """Called when logging is unable to log some output."""
- # If anything else we should not color it
- return False
-
- def format(self, record):
- # type: (logging.LogRecord) -> str
- msg = super().format(record)
-
- if self.should_color():
- for level, color in self.COLORS:
- if record.levelno >= level:
- msg = color(msg)
- break
-
- return msg
-
- # The logging module says handleError() can be customized.
- def handleError(self, record):
- # type: (logging.LogRecord) -> None
exc_class, exc = sys.exc_info()[:2]
# If a broken pipe occurred while calling write() or flush() on the
# stdout stream in logging's Handler.emit(), then raise our special
@@ -227,7 +189,7 @@ class ColorizedStreamHandler(logging.StreamHandler):
if (
exc_class
and exc
- and self._using_stdout()
+ and self.console.file is sys.stdout
and _is_broken_pipe_error(exc_class, exc)
):
raise BrokenStdoutLoggingError()
@@ -236,19 +198,16 @@ class ColorizedStreamHandler(logging.StreamHandler):
class BetterRotatingFileHandler(logging.handlers.RotatingFileHandler):
- def _open(self):
- # type: () -> IO[Any]
+ def _open(self) -> TextIOWrapper:
ensure_dir(os.path.dirname(self.baseFilename))
return super()._open()
class MaxLevelFilter(Filter):
- def __init__(self, level):
- # type: (int) -> None
+ def __init__(self, level: int) -> None:
self.level = level
- def filter(self, record):
- # type: (logging.LogRecord) -> bool
+ def filter(self, record: logging.LogRecord) -> bool:
return record.levelno < self.level
@@ -258,15 +217,13 @@ class ExcludeLoggerFilter(Filter):
A logging Filter that excludes records from a logger (or its children).
"""
- def filter(self, record):
- # type: (logging.LogRecord) -> bool
+ def filter(self, record: logging.LogRecord) -> bool:
# The base Filter class allows only records from a logger (or its
# children).
return not super().filter(record)
-def setup_logging(verbosity, no_color, user_log_file):
- # type: (int, bool, Optional[str]) -> int
+def setup_logging(verbosity: int, no_color: bool, user_log_file: Optional[str]) -> int:
"""Configures and sets up all of the logging
Returns the requested logging level, as its integer value.
@@ -308,7 +265,7 @@ def setup_logging(verbosity, no_color, user_log_file):
"stderr": "ext://sys.stderr",
}
handler_classes = {
- "stream": "pip._internal.utils.logging.ColorizedStreamHandler",
+ "stream": "pip._internal.utils.logging.RichPipStreamHandler",
"file": "pip._internal.utils.logging.BetterRotatingFileHandler",
}
handlers = ["console", "console_errors", "console_subprocess"] + (
@@ -366,8 +323,8 @@ def setup_logging(verbosity, no_color, user_log_file):
"console_subprocess": {
"level": level,
"class": handler_classes["stream"],
- "no_color": no_color,
"stream": log_streams["stderr"],
+ "no_color": no_color,
"filters": ["restrict_to_subprocess"],
"formatter": "indent",
},
diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py
index 99ebea30c..a8f4cb5cf 100644
--- a/src/pip/_internal/utils/misc.py
+++ b/src/pip/_internal/utils/misc.py
@@ -18,11 +18,11 @@ from itertools import filterfalse, tee, zip_longest
from types import TracebackType
from typing import (
Any,
- AnyStr,
BinaryIO,
Callable,
- Container,
ContextManager,
+ Dict,
+ Generator,
Iterable,
Iterator,
List,
@@ -34,17 +34,14 @@ from typing import (
cast,
)
-from pip._vendor.pkg_resources import Distribution
+from pip._vendor.pep517 import Pep517HookCaller
from pip._vendor.tenacity import retry, stop_after_delay, wait_fixed
from pip import __version__
from pip._internal.exceptions import CommandError
-from pip._internal.locations import get_major_minor_version, site_packages, user_site
-from pip._internal.utils.compat import WINDOWS, stdlib_pkgs
-from pip._internal.utils.virtualenv import (
- running_under_virtualenv,
- virtualenv_no_global,
-)
+from pip._internal.locations import get_major_minor_version
+from pip._internal.utils.compat import WINDOWS
+from pip._internal.utils.virtualenv import running_under_virtualenv
__all__ = [
"rmtree",
@@ -60,6 +57,7 @@ __all__ = [
"captured_stdout",
"ensure_dir",
"remove_auth_from_url",
+ "ConfiguredPep517HookCaller",
]
@@ -71,8 +69,7 @@ VersionInfo = Tuple[int, int, int]
NetlocTuple = Tuple[str, Tuple[Optional[str], Optional[str]]]
-def get_pip_version():
- # type: () -> str
+def get_pip_version() -> str:
pip_pkg_dir = os.path.join(os.path.dirname(__file__), "..", "..")
pip_pkg_dir = os.path.abspath(pip_pkg_dir)
@@ -83,8 +80,7 @@ def get_pip_version():
)
-def normalize_version_info(py_version_info):
- # type: (Tuple[int, ...]) -> Tuple[int, int, int]
+def normalize_version_info(py_version_info: Tuple[int, ...]) -> Tuple[int, int, int]:
"""
Convert a tuple of ints representing a Python version to one of length
three.
@@ -103,8 +99,7 @@ def normalize_version_info(py_version_info):
return cast("VersionInfo", py_version_info)
-def ensure_dir(path):
- # type: (AnyStr) -> None
+def ensure_dir(path: str) -> None:
"""os.path.makedirs without EEXIST."""
try:
os.makedirs(path)
@@ -114,8 +109,7 @@ def ensure_dir(path):
raise
-def get_prog():
- # type: () -> str
+def get_prog() -> str:
try:
prog = os.path.basename(sys.argv[0])
if prog in ("__main__.py", "-c"):
@@ -130,13 +124,11 @@ def get_prog():
# Retry every half second for up to 3 seconds
# Tenacity raises RetryError by default, explicitly raise the original exception
@retry(reraise=True, stop=stop_after_delay(3), wait=wait_fixed(0.5))
-def rmtree(dir, ignore_errors=False):
- # type: (AnyStr, bool) -> None
+def rmtree(dir: str, ignore_errors: bool = False) -> None:
shutil.rmtree(dir, ignore_errors=ignore_errors, onerror=rmtree_errorhandler)
-def rmtree_errorhandler(func, path, exc_info):
- # type: (Callable[..., Any], str, ExcInfo) -> None
+def rmtree_errorhandler(func: Callable[..., Any], path: str, exc_info: ExcInfo) -> None:
"""On Windows, the files in .svn are read-only, so when rmtree() tries to
remove them, an exception is thrown. We catch that here, remove the
read-only attribute, and hopefully continue without problems."""
@@ -156,8 +148,7 @@ def rmtree_errorhandler(func, path, exc_info):
raise
-def display_path(path):
- # type: (str) -> str
+def display_path(path: str) -> str:
"""Gives the display value for a given path, making it relative to cwd
if possible."""
path = os.path.normcase(os.path.abspath(path))
@@ -166,8 +157,7 @@ def display_path(path):
return path
-def backup_dir(dir, ext=".bak"):
- # type: (str, str) -> str
+def backup_dir(dir: str, ext: str = ".bak") -> str:
"""Figure out the name of a directory to back up the given dir to
(adding .bak, .bak2, etc)"""
n = 1
@@ -178,16 +168,14 @@ def backup_dir(dir, ext=".bak"):
return dir + extension
-def ask_path_exists(message, options):
- # type: (str, Iterable[str]) -> str
+def ask_path_exists(message: str, options: Iterable[str]) -> str:
for action in os.environ.get("PIP_EXISTS_ACTION", "").split():
if action in options:
return action
return ask(message, options)
-def _check_no_input(message):
- # type: (str) -> None
+def _check_no_input(message: str) -> None:
"""Raise an error if no input is allowed."""
if os.environ.get("PIP_NO_INPUT"):
raise Exception(
@@ -195,8 +183,7 @@ def _check_no_input(message):
)
-def ask(message, options):
- # type: (str, Iterable[str]) -> str
+def ask(message: str, options: Iterable[str]) -> str:
"""Ask the message interactively, with the given possible responses"""
while 1:
_check_no_input(message)
@@ -211,22 +198,19 @@ def ask(message, options):
return response
-def ask_input(message):
- # type: (str) -> str
+def ask_input(message: str) -> str:
"""Ask for input interactively."""
_check_no_input(message)
return input(message)
-def ask_password(message):
- # type: (str) -> str
+def ask_password(message: str) -> str:
"""Ask for a password interactively."""
_check_no_input(message)
return getpass.getpass(message)
-def strtobool(val):
- # type: (str) -> int
+def strtobool(val: str) -> int:
"""Convert a string representation of truth to true (1) or false (0).
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
@@ -242,8 +226,7 @@ def strtobool(val):
raise ValueError(f"invalid truth value {val!r}")
-def format_size(bytes):
- # type: (float) -> str
+def format_size(bytes: float) -> str:
if bytes > 1000 * 1000:
return "{:.1f} MB".format(bytes / 1000.0 / 1000)
elif bytes > 10 * 1000:
@@ -254,8 +237,7 @@ def format_size(bytes):
return "{} bytes".format(int(bytes))
-def tabulate(rows):
- # type: (Iterable[Iterable[Any]]) -> Tuple[List[str], List[int]]
+def tabulate(rows: Iterable[Iterable[Any]]) -> Tuple[List[str], List[int]]:
"""Return a list of formatted rows and a list of column sizes.
For example::
@@ -286,8 +268,9 @@ def is_installable_dir(path: str) -> bool:
return False
-def read_chunks(file, size=io.DEFAULT_BUFFER_SIZE):
- # type: (BinaryIO, int) -> Iterator[bytes]
+def read_chunks(
+ file: BinaryIO, size: int = io.DEFAULT_BUFFER_SIZE
+) -> Generator[bytes, None, None]:
"""Yield pieces of data from a file-like object until EOF."""
while True:
chunk = file.read(size)
@@ -296,8 +279,7 @@ def read_chunks(file, size=io.DEFAULT_BUFFER_SIZE):
yield chunk
-def normalize_path(path, resolve_symlinks=True):
- # type: (str, bool) -> str
+def normalize_path(path: str, resolve_symlinks: bool = True) -> str:
"""
Convert a path to its canonical, case-normalized, absolute version.
@@ -310,8 +292,7 @@ def normalize_path(path, resolve_symlinks=True):
return os.path.normcase(path)
-def splitext(path):
- # type: (str) -> Tuple[str, str]
+def splitext(path: str) -> Tuple[str, str]:
"""Like os.path.splitext, but take off .tar too"""
base, ext = posixpath.splitext(path)
if base.lower().endswith(".tar"):
@@ -320,8 +301,7 @@ def splitext(path):
return base, ext
-def renames(old, new):
- # type: (str, str) -> None
+def renames(old: str, new: str) -> None:
"""Like os.renames(), but handles renaming across devices."""
# Implementation borrowed from os.renames().
head, tail = os.path.split(new)
@@ -338,8 +318,7 @@ def renames(old, new):
pass
-def is_local(path):
- # type: (str) -> bool
+def is_local(path: str) -> bool:
"""
Return True if path is within sys.prefix, if we're running in a virtualenv.
@@ -353,158 +332,15 @@ def is_local(path):
return path.startswith(normalize_path(sys.prefix))
-def dist_is_local(dist):
- # type: (Distribution) -> bool
- """
- Return True if given Distribution object is installed locally
- (i.e. within current virtualenv).
-
- Always True if we're not in a virtualenv.
-
- """
- return is_local(dist_location(dist))
-
-
-def dist_in_usersite(dist):
- # type: (Distribution) -> bool
- """
- Return True if given Distribution is installed in user site.
- """
- return dist_location(dist).startswith(normalize_path(user_site))
-
-
-def dist_in_site_packages(dist):
- # type: (Distribution) -> bool
- """
- Return True if given Distribution is installed in
- sysconfig.get_python_lib().
- """
- return dist_location(dist).startswith(normalize_path(site_packages))
-
-
-def dist_is_editable(dist):
- # type: (Distribution) -> bool
- """
- Return True if given Distribution is an editable install.
- """
- for path_item in sys.path:
- egg_link = os.path.join(path_item, dist.project_name + ".egg-link")
- if os.path.isfile(egg_link):
- return True
- return False
-
-
-def get_installed_distributions(
- local_only=True, # type: bool
- skip=stdlib_pkgs, # type: Container[str]
- include_editables=True, # type: bool
- editables_only=False, # type: bool
- user_only=False, # type: bool
- paths=None, # type: Optional[List[str]]
-):
- # type: (...) -> List[Distribution]
- """Return a list of installed Distribution objects.
-
- Left for compatibility until direct pkg_resources uses are refactored out.
- """
- from pip._internal.metadata import get_default_environment, get_environment
- from pip._internal.metadata.pkg_resources import Distribution as _Dist
-
- if paths is None:
- env = get_default_environment()
- else:
- env = get_environment(paths)
- dists = env.iter_installed_distributions(
- local_only=local_only,
- skip=skip,
- include_editables=include_editables,
- editables_only=editables_only,
- user_only=user_only,
- )
- return [cast(_Dist, dist)._dist for dist in dists]
-
-
-def get_distribution(req_name):
- # type: (str) -> Optional[Distribution]
- """Given a requirement name, return the installed Distribution object.
-
- This searches from *all* distributions available in the environment, to
- match the behavior of ``pkg_resources.get_distribution()``.
-
- Left for compatibility until direct pkg_resources uses are refactored out.
- """
- from pip._internal.metadata import get_default_environment
- from pip._internal.metadata.pkg_resources import Distribution as _Dist
-
- dist = get_default_environment().get_distribution(req_name)
- if dist is None:
- return None
- return cast(_Dist, dist)._dist
-
-
-def egg_link_path(dist):
- # type: (Distribution) -> Optional[str]
- """
- Return the path for the .egg-link file if it exists, otherwise, None.
-
- There's 3 scenarios:
- 1) not in a virtualenv
- try to find in site.USER_SITE, then site_packages
- 2) in a no-global virtualenv
- try to find in site_packages
- 3) in a yes-global virtualenv
- try to find in site_packages, then site.USER_SITE
- (don't look in global location)
-
- For #1 and #3, there could be odd cases, where there's an egg-link in 2
- locations.
-
- This method will just return the first one found.
- """
- sites = []
- if running_under_virtualenv():
- sites.append(site_packages)
- if not virtualenv_no_global() and user_site:
- sites.append(user_site)
- else:
- if user_site:
- sites.append(user_site)
- sites.append(site_packages)
-
- for site in sites:
- egglink = os.path.join(site, dist.project_name) + ".egg-link"
- if os.path.isfile(egglink):
- return egglink
- return None
-
-
-def dist_location(dist):
- # type: (Distribution) -> str
- """
- Get the site-packages location of this distribution. Generally
- this is dist.location, except in the case of develop-installed
- packages, where dist.location is the source code location, and we
- want to know where the egg-link file is.
-
- The returned location is normalized (in particular, with symlinks removed).
- """
- egg_link = egg_link_path(dist)
- if egg_link:
- return normalize_path(egg_link)
- return normalize_path(dist.location)
-
-
-def write_output(msg, *args):
- # type: (Any, Any) -> None
+def write_output(msg: Any, *args: Any) -> None:
logger.info(msg, *args)
class StreamWrapper(StringIO):
- orig_stream = None # type: TextIO
+ orig_stream: TextIO = None
@classmethod
- def from_stream(cls, orig_stream):
- # type: (TextIO) -> StreamWrapper
+ def from_stream(cls, orig_stream: TextIO) -> "StreamWrapper":
cls.orig_stream = orig_stream
return cls()
@@ -516,8 +352,7 @@ class StreamWrapper(StringIO):
@contextlib.contextmanager
-def captured_output(stream_name):
- # type: (str) -> Iterator[StreamWrapper]
+def captured_output(stream_name: str) -> Generator[StreamWrapper, None, None]:
"""Return a context manager used by captured_stdout/stdin/stderr
that temporarily replaces the sys stream *stream_name* with a StringIO.
@@ -531,8 +366,7 @@ def captured_output(stream_name):
setattr(sys, stream_name, orig_stdout)
-def captured_stdout():
- # type: () -> ContextManager[StreamWrapper]
+def captured_stdout() -> ContextManager[StreamWrapper]:
"""Capture the output of sys.stdout:
with captured_stdout() as stdout:
@@ -544,8 +378,7 @@ def captured_stdout():
return captured_output("stdout")
-def captured_stderr():
- # type: () -> ContextManager[StreamWrapper]
+def captured_stderr() -> ContextManager[StreamWrapper]:
"""
See captured_stdout().
"""
@@ -553,16 +386,14 @@ def captured_stderr():
# Simulates an enum
-def enum(*sequential, **named):
- # type: (*Any, **Any) -> Type[Any]
+def enum(*sequential: Any, **named: Any) -> Type[Any]:
enums = dict(zip(sequential, range(len(sequential))), **named)
reverse = {value: key for key, value in enums.items()}
enums["reverse_mapping"] = reverse
return type("Enum", (), enums)
-def build_netloc(host, port):
- # type: (str, Optional[int]) -> str
+def build_netloc(host: str, port: Optional[int]) -> str:
"""
Build a netloc from a host-port pair
"""
@@ -574,8 +405,7 @@ def build_netloc(host, port):
return f"{host}:{port}"
-def build_url_from_netloc(netloc, scheme="https"):
- # type: (str, str) -> str
+def build_url_from_netloc(netloc: str, scheme: str = "https") -> str:
"""
Build a full URL from a netloc.
"""
@@ -585,8 +415,7 @@ def build_url_from_netloc(netloc, scheme="https"):
return f"{scheme}://{netloc}"
-def parse_netloc(netloc):
- # type: (str) -> Tuple[str, Optional[int]]
+def parse_netloc(netloc: str) -> Tuple[str, Optional[int]]:
"""
Return the host-port pair from a netloc.
"""
@@ -595,8 +424,7 @@ def parse_netloc(netloc):
return parsed.hostname, parsed.port
-def split_auth_from_netloc(netloc):
- # type: (str) -> NetlocTuple
+def split_auth_from_netloc(netloc: str) -> NetlocTuple:
"""
Parse out and remove the auth information from a netloc.
@@ -609,7 +437,7 @@ def split_auth_from_netloc(netloc):
# 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)
- pw = None # type: Optional[str]
+ pw: Optional[str] = None
if ":" in auth:
# Split from the left because that's how urllib.parse.urlsplit()
# behaves if more than one : is present (which again can be checked
@@ -625,8 +453,7 @@ def split_auth_from_netloc(netloc):
return netloc, (user, pw)
-def redact_netloc(netloc):
- # type: (str) -> str
+def redact_netloc(netloc: str) -> str:
"""
Replace the sensitive data in a netloc with "****", if it exists.
@@ -648,8 +475,9 @@ def redact_netloc(netloc):
)
-def _transform_url(url, transform_netloc):
- # type: (str, Callable[[str], Tuple[Any, ...]]) -> Tuple[str, NetlocTuple]
+def _transform_url(
+ url: str, transform_netloc: Callable[[str], Tuple[Any, ...]]
+) -> Tuple[str, NetlocTuple]:
"""Transform and replace netloc in a url.
transform_netloc is a function taking the netloc and returning a
@@ -667,18 +495,15 @@ def _transform_url(url, transform_netloc):
return surl, cast("NetlocTuple", netloc_tuple)
-def _get_netloc(netloc):
- # type: (str) -> NetlocTuple
+def _get_netloc(netloc: str) -> NetlocTuple:
return split_auth_from_netloc(netloc)
-def _redact_netloc(netloc):
- # type: (str) -> Tuple[str,]
+def _redact_netloc(netloc: str) -> Tuple[str]:
return (redact_netloc(netloc),)
-def split_auth_netloc_from_url(url):
- # type: (str) -> Tuple[str, str, Tuple[str, str]]
+def split_auth_netloc_from_url(url: str) -> Tuple[str, str, Tuple[str, str]]:
"""
Parse a url into separate netloc, auth, and url with no auth.
@@ -688,41 +513,31 @@ def split_auth_netloc_from_url(url):
return url_without_auth, netloc, auth
-def remove_auth_from_url(url):
- # type: (str) -> str
+def remove_auth_from_url(url: str) -> str:
"""Return a copy of url with 'username:password@' removed."""
# username/pass params are passed to subversion through flags
# and are not recognized in the url.
return _transform_url(url, _get_netloc)[0]
-def redact_auth_from_url(url):
- # type: (str) -> str
+def redact_auth_from_url(url: str) -> str:
"""Replace the password in a given url with ****."""
return _transform_url(url, _redact_netloc)[0]
class HiddenText:
- def __init__(
- self,
- secret, # type: str
- redacted, # type: str
- ):
- # type: (...) -> None
+ def __init__(self, secret: str, redacted: str) -> None:
self.secret = secret
self.redacted = redacted
- def __repr__(self):
- # type: (...) -> str
+ def __repr__(self) -> str:
return "<HiddenText {!r}>".format(str(self))
- def __str__(self):
- # type: (...) -> str
+ def __str__(self) -> str:
return self.redacted
# This is useful for testing.
- def __eq__(self, other):
- # type: (Any) -> bool
+ def __eq__(self, other: Any) -> bool:
if type(self) != type(other):
return False
@@ -731,28 +546,25 @@ class HiddenText:
return self.secret == other.secret
-def hide_value(value):
- # type: (str) -> HiddenText
+def hide_value(value: str) -> HiddenText:
return HiddenText(value, redacted="****")
-def hide_url(url):
- # type: (str) -> HiddenText
+def hide_url(url: str) -> HiddenText:
redacted = redact_auth_from_url(url)
return HiddenText(url, redacted=redacted)
-def protect_pip_from_modification_on_windows(modifying_pip):
- # type: (bool) -> None
+def protect_pip_from_modification_on_windows(modifying_pip: bool) -> None:
"""Protection of pip.exe from modification on Windows
On Windows, any operation modifying pip should be run as:
python -m pip ...
"""
pip_names = [
- "pip.exe",
- "pip{}.exe".format(sys.version_info[0]),
- "pip{}.{}.exe".format(*sys.version_info[:2]),
+ "pip",
+ f"pip{sys.version_info.major}",
+ f"pip{sys.version_info.major}.{sys.version_info.minor}",
]
# See https://github.com/pypa/pip/issues/1299 for more discussion
@@ -769,14 +581,12 @@ def protect_pip_from_modification_on_windows(modifying_pip):
)
-def is_console_interactive():
- # type: () -> bool
+def is_console_interactive() -> bool:
"""Is this console interactive?"""
return sys.stdin is not None and sys.stdin.isatty()
-def hash_file(path, blocksize=1 << 20):
- # type: (str, int) -> Tuple[Any, int]
+def hash_file(path: str, blocksize: int = 1 << 20) -> Tuple[Any, int]:
"""Return (hash, length) for path using hashlib.sha256()"""
h = hashlib.sha256()
@@ -788,8 +598,7 @@ def hash_file(path, blocksize=1 << 20):
return h, length
-def is_wheel_installed():
- # type: () -> bool
+def is_wheel_installed() -> bool:
"""
Return whether the wheel package is installed.
"""
@@ -801,8 +610,7 @@ def is_wheel_installed():
return True
-def pairwise(iterable):
- # type: (Iterable[Any]) -> Iterator[Tuple[Any, Any]]
+def pairwise(iterable: Iterable[Any]) -> Iterator[Tuple[Any, Any]]:
"""
Return paired elements.
@@ -814,10 +622,9 @@ def pairwise(iterable):
def partition(
- pred, # type: Callable[[T], bool]
- iterable, # type: Iterable[T]
-):
- # type: (...) -> Tuple[Iterable[T], Iterable[T]]
+ pred: Callable[[T], bool],
+ iterable: Iterable[T],
+) -> Tuple[Iterable[T], Iterable[T]]:
"""
Use a predicate to partition entries into false entries and true entries,
like
@@ -826,3 +633,91 @@ def partition(
"""
t1, t2 = tee(iterable)
return filterfalse(pred, t1), filter(pred, t2)
+
+
+class ConfiguredPep517HookCaller(Pep517HookCaller):
+ def __init__(
+ self,
+ config_holder: Any,
+ source_dir: str,
+ build_backend: str,
+ backend_path: Optional[str] = None,
+ runner: Optional[Callable[..., None]] = None,
+ python_executable: Optional[str] = None,
+ ):
+ super().__init__(
+ source_dir, build_backend, backend_path, runner, python_executable
+ )
+ self.config_holder = config_holder
+
+ def build_wheel(
+ self,
+ wheel_directory: str,
+ config_settings: Optional[Dict[str, str]] = None,
+ metadata_directory: Optional[str] = None,
+ ) -> str:
+ cs = self.config_holder.config_settings
+ return super().build_wheel(
+ wheel_directory, config_settings=cs, metadata_directory=metadata_directory
+ )
+
+ def build_sdist(
+ self, sdist_directory: str, config_settings: Optional[Dict[str, str]] = None
+ ) -> str:
+ cs = self.config_holder.config_settings
+ return super().build_sdist(sdist_directory, config_settings=cs)
+
+ def build_editable(
+ self,
+ wheel_directory: str,
+ config_settings: Optional[Dict[str, str]] = None,
+ metadata_directory: Optional[str] = None,
+ ) -> str:
+ cs = self.config_holder.config_settings
+ return super().build_editable(
+ wheel_directory, config_settings=cs, metadata_directory=metadata_directory
+ )
+
+ def get_requires_for_build_wheel(
+ self, config_settings: Optional[Dict[str, str]] = None
+ ) -> List[str]:
+ cs = self.config_holder.config_settings
+ return super().get_requires_for_build_wheel(config_settings=cs)
+
+ def get_requires_for_build_sdist(
+ self, config_settings: Optional[Dict[str, str]] = None
+ ) -> List[str]:
+ cs = self.config_holder.config_settings
+ return super().get_requires_for_build_sdist(config_settings=cs)
+
+ def get_requires_for_build_editable(
+ self, config_settings: Optional[Dict[str, str]] = None
+ ) -> List[str]:
+ cs = self.config_holder.config_settings
+ return super().get_requires_for_build_editable(config_settings=cs)
+
+ def prepare_metadata_for_build_wheel(
+ self,
+ metadata_directory: str,
+ config_settings: Optional[Dict[str, str]] = None,
+ _allow_fallback: bool = True,
+ ) -> str:
+ cs = self.config_holder.config_settings
+ return super().prepare_metadata_for_build_wheel(
+ metadata_directory=metadata_directory,
+ config_settings=cs,
+ _allow_fallback=_allow_fallback,
+ )
+
+ def prepare_metadata_for_build_editable(
+ self,
+ metadata_directory: str,
+ config_settings: Optional[Dict[str, str]] = None,
+ _allow_fallback: bool = True,
+ ) -> str:
+ cs = self.config_holder.config_settings
+ return super().prepare_metadata_for_build_editable(
+ metadata_directory=metadata_directory,
+ config_settings=cs,
+ _allow_fallback=_allow_fallback,
+ )
diff --git a/src/pip/_internal/utils/models.py b/src/pip/_internal/utils/models.py
index 0e02bc7a5..b6bb21a8b 100644
--- a/src/pip/_internal/utils/models.py
+++ b/src/pip/_internal/utils/models.py
@@ -10,37 +10,29 @@ class KeyBasedCompareMixin:
__slots__ = ["_compare_key", "_defining_class"]
- def __init__(self, key, defining_class):
- # type: (Any, Type[KeyBasedCompareMixin]) -> None
+ def __init__(self, key: Any, defining_class: Type["KeyBasedCompareMixin"]) -> None:
self._compare_key = key
self._defining_class = defining_class
- def __hash__(self):
- # type: () -> int
+ def __hash__(self) -> int:
return hash(self._compare_key)
- def __lt__(self, other):
- # type: (Any) -> bool
+ def __lt__(self, other: Any) -> bool:
return self._compare(other, operator.__lt__)
- def __le__(self, other):
- # type: (Any) -> bool
+ def __le__(self, other: Any) -> bool:
return self._compare(other, operator.__le__)
- def __gt__(self, other):
- # type: (Any) -> bool
+ def __gt__(self, other: Any) -> bool:
return self._compare(other, operator.__gt__)
- def __ge__(self, other):
- # type: (Any) -> bool
+ def __ge__(self, other: Any) -> bool:
return self._compare(other, operator.__ge__)
- def __eq__(self, other):
- # type: (Any) -> bool
+ def __eq__(self, other: Any) -> bool:
return self._compare(other, operator.__eq__)
- def _compare(self, other, method):
- # type: (Any, Callable[[Any, Any], bool]) -> bool
+ def _compare(self, other: Any, method: Callable[[Any, Any], bool]) -> bool:
if not isinstance(other, self._defining_class):
return NotImplemented
diff --git a/src/pip/_internal/utils/packaging.py b/src/pip/_internal/utils/packaging.py
index 3f9dbd3b7..b9f6af4d1 100644
--- a/src/pip/_internal/utils/packaging.py
+++ b/src/pip/_internal/utils/packaging.py
@@ -1,20 +1,19 @@
+import functools
import logging
-from email.message import Message
-from email.parser import FeedParser
-from typing import Optional, Tuple
+import re
+from typing import NewType, Optional, Tuple, cast
-from pip._vendor import pkg_resources
from pip._vendor.packaging import specifiers, version
-from pip._vendor.pkg_resources import Distribution
+from pip._vendor.packaging.requirements import Requirement
-from pip._internal.exceptions import NoneMetadataError
-from pip._internal.utils.misc import display_path
+NormalizedExtra = NewType("NormalizedExtra", str)
logger = logging.getLogger(__name__)
-def check_requires_python(requires_python, version_info):
- # type: (Optional[str], Tuple[int, ...]) -> bool
+def check_requires_python(
+ requires_python: Optional[str], version_info: Tuple[int, ...]
+) -> bool:
"""
Check if the given Python version matches a "Requires-Python" specifier.
@@ -35,55 +34,24 @@ def check_requires_python(requires_python, version_info):
return python_version in requires_python_specifier
-def get_metadata(dist):
- # type: (Distribution) -> Message
- """
- :raises NoneMetadataError: if the distribution reports `has_metadata()`
- True but `get_metadata()` returns None.
- """
- metadata_name = "METADATA"
- if isinstance(dist, pkg_resources.DistInfoDistribution) and dist.has_metadata(
- metadata_name
- ):
- metadata = dist.get_metadata(metadata_name)
- elif dist.has_metadata("PKG-INFO"):
- metadata_name = "PKG-INFO"
- metadata = dist.get_metadata(metadata_name)
- else:
- logger.warning("No metadata found in %s", display_path(dist.location))
- metadata = ""
+@functools.lru_cache(maxsize=512)
+def get_requirement(req_string: str) -> Requirement:
+ """Construct a packaging.Requirement object with caching"""
+ # Parsing requirement strings is expensive, and is also expected to happen
+ # with a low diversity of different arguments (at least relative the number
+ # constructed). This method adds a cache to requirement object creation to
+ # minimize repeated parsing of the same string to construct equivalent
+ # Requirement objects.
+ return Requirement(req_string)
- if metadata is None:
- raise NoneMetadataError(dist, metadata_name)
- feed_parser = FeedParser()
- # The following line errors out if with a "NoneType" TypeError if
- # passed metadata=None.
- feed_parser.feed(metadata)
- return feed_parser.close()
+def safe_extra(extra: str) -> NormalizedExtra:
+ """Convert an arbitrary string to a standard 'extra' name
+ Any runs of non-alphanumeric characters are replaced with a single '_',
+ and the result is always lowercased.
-def get_requires_python(dist):
- # type: (pkg_resources.Distribution) -> Optional[str]
+ This function is duplicated from ``pkg_resources``. Note that this is not
+ the same to either ``canonicalize_name`` or ``_egg_link_name``.
"""
- Return the "Requires-Python" metadata for a distribution, or None
- if not present.
- """
- pkg_info_dict = get_metadata(dist)
- requires_python = pkg_info_dict.get("Requires-Python")
-
- if requires_python is not None:
- # Convert to a str to satisfy the type checker, since requires_python
- # can be a Header object.
- requires_python = str(requires_python)
-
- return requires_python
-
-
-def get_installer(dist):
- # type: (Distribution) -> str
- if dist.has_metadata("INSTALLER"):
- for line in dist.get_metadata_lines("INSTALLER"):
- if line.strip():
- return line.strip()
- return ""
+ return cast(NormalizedExtra, re.sub("[^A-Za-z0-9.-]+", "_", extra).lower())
diff --git a/src/pip/_internal/utils/parallel.py b/src/pip/_internal/utils/parallel.py
deleted file mode 100644
index de91dc8ab..000000000
--- a/src/pip/_internal/utils/parallel.py
+++ /dev/null
@@ -1,101 +0,0 @@
-"""Convenient parallelization of higher order functions.
-
-This module provides two helper functions, with appropriate fallbacks on
-Python 2 and on systems lacking support for synchronization mechanisms:
-
-- map_multiprocess
-- map_multithread
-
-These helpers work like Python 3's map, with two differences:
-
-- They don't guarantee the order of processing of
- the elements of the iterable.
-- The underlying process/thread pools chop the iterable into
- a number of chunks, so that for very long iterables using
- a large value for chunksize can make the job complete much faster
- than using the default value of 1.
-"""
-
-__all__ = ["map_multiprocess", "map_multithread"]
-
-from contextlib import contextmanager
-from multiprocessing import Pool as ProcessPool
-from multiprocessing import pool
-from multiprocessing.dummy import Pool as ThreadPool
-from typing import Callable, Iterable, Iterator, TypeVar, Union
-
-from pip._vendor.requests.adapters import DEFAULT_POOLSIZE
-
-Pool = Union[pool.Pool, pool.ThreadPool]
-S = TypeVar("S")
-T = TypeVar("T")
-
-# On platforms without sem_open, multiprocessing[.dummy] Pool
-# cannot be created.
-try:
- import multiprocessing.synchronize # noqa
-except ImportError:
- LACK_SEM_OPEN = True
-else:
- LACK_SEM_OPEN = False
-
-# Incredibly large timeout to work around bpo-8296 on Python 2.
-TIMEOUT = 2000000
-
-
-@contextmanager
-def closing(pool):
- # type: (Pool) -> Iterator[Pool]
- """Return a context manager making sure the pool closes properly."""
- try:
- yield pool
- finally:
- # For Pool.imap*, close and join are needed
- # for the returned iterator to begin yielding.
- pool.close()
- pool.join()
- pool.terminate()
-
-
-def _map_fallback(func, iterable, chunksize=1):
- # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T]
- """Make an iterator applying func to each element in iterable.
-
- This function is the sequential fallback either on Python 2
- where Pool.imap* doesn't react to KeyboardInterrupt
- or when sem_open is unavailable.
- """
- return map(func, iterable)
-
-
-def _map_multiprocess(func, iterable, chunksize=1):
- # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T]
- """Chop iterable into chunks and submit them to a process pool.
-
- For very long iterables using a large value for chunksize can make
- the job complete much faster than using the default value of 1.
-
- Return an unordered iterator of the results.
- """
- with closing(ProcessPool()) as pool:
- return pool.imap_unordered(func, iterable, chunksize)
-
-
-def _map_multithread(func, iterable, chunksize=1):
- # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T]
- """Chop iterable into chunks and submit them to a thread pool.
-
- For very long iterables using a large value for chunksize can make
- the job complete much faster than using the default value of 1.
-
- Return an unordered iterator of the results.
- """
- with closing(ThreadPool(DEFAULT_POOLSIZE)) as pool:
- return pool.imap_unordered(func, iterable, chunksize)
-
-
-if LACK_SEM_OPEN:
- map_multiprocess = map_multithread = _map_fallback
-else:
- map_multiprocess = _map_multiprocess
- map_multithread = _map_multithread
diff --git a/src/pip/_internal/utils/pkg_resources.py b/src/pip/_internal/utils/pkg_resources.py
deleted file mode 100644
index ee1eca300..000000000
--- a/src/pip/_internal/utils/pkg_resources.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from typing import Dict, Iterable, List
-
-from pip._vendor.pkg_resources import yield_lines
-
-
-class DictMetadata:
- """IMetadataProvider that reads metadata files from a dictionary."""
-
- def __init__(self, metadata):
- # type: (Dict[str, bytes]) -> None
- self._metadata = metadata
-
- def has_metadata(self, name):
- # type: (str) -> bool
- return name in self._metadata
-
- def get_metadata(self, name):
- # type: (str) -> str
- try:
- return self._metadata[name].decode()
- except UnicodeDecodeError as e:
- # Mirrors handling done in pkg_resources.NullProvider.
- e.reason += f" in {name} file"
- raise
-
- def get_metadata_lines(self, name):
- # type: (str) -> Iterable[str]
- return yield_lines(self.get_metadata(name))
-
- def metadata_isdir(self, name):
- # type: (str) -> bool
- return False
-
- def metadata_listdir(self, name):
- # type: (str) -> List[str]
- return []
-
- def run_script(self, script_name, namespace):
- # type: (str, str) -> None
- pass
diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py
index 4b8e4b359..01ef4a4ca 100644
--- a/src/pip/_internal/utils/setuptools_build.py
+++ b/src/pip/_internal/utils/setuptools_build.py
@@ -1,30 +1,57 @@
import sys
+import textwrap
from typing import List, Optional, Sequence
# Shim to wrap setup.py invocation with setuptools
-#
-# We set sys.argv[0] to the path to the underlying setup.py file so
-# setuptools / distutils don't take the path to the setup.py to be "-c" when
-# invoking via the shim. This avoids e.g. the following manifest_maker
-# warning: "warning: manifest_maker: standard file '-c' not found".
-_SETUPTOOLS_SHIM = (
- "import io, os, sys, setuptools, tokenize; sys.argv[0] = {0!r}; __file__={0!r};"
- "f = getattr(tokenize, 'open', open)(__file__) "
- "if os.path.exists(__file__) "
- "else io.StringIO('from setuptools import setup; setup()');"
- "code = f.read().replace('\\r\\n', '\\n');"
- "f.close();"
- "exec(compile(code, __file__, 'exec'))"
-)
+# Note that __file__ is handled via two {!r} *and* %r, to ensure that paths on
+# Windows are correctly handled (it should be "C:\\Users" not "C:\Users").
+_SETUPTOOLS_SHIM = textwrap.dedent(
+ """
+ exec(compile('''
+ # This is <pip-setuptools-caller> -- a caller that pip uses to run setup.py
+ #
+ # - It imports setuptools before invoking setup.py, to enable projects that directly
+ # import from `distutils.core` to work with newer packaging standards.
+ # - It provides a clear error message when setuptools is not installed.
+ # - It sets `sys.argv[0]` to the underlying `setup.py`, when invoking `setup.py` so
+ # setuptools doesn't think the script is `-c`. This avoids the following warning:
+ # manifest_maker: standard file '-c' not found".
+ # - It generates a shim setup.py, for handling setup.cfg-only projects.
+ import os, sys, tokenize
+
+ try:
+ import setuptools
+ except ImportError as error:
+ print(
+ "ERROR: Can not execute `setup.py` since setuptools is not available in "
+ "the build environment.",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ __file__ = %r
+ sys.argv[0] = __file__
+
+ if os.path.exists(__file__):
+ filename = __file__
+ with tokenize.open(__file__) as f:
+ setup_py_code = f.read()
+ else:
+ filename = "<auto-generated setuptools caller>"
+ setup_py_code = "from setuptools import setup; setup()"
+
+ exec(compile(setup_py_code, filename, "exec"))
+ ''' % ({!r},), "<pip-setuptools-caller>", "exec"))
+ """
+).rstrip()
def make_setuptools_shim_args(
- setup_py_path, # type: str
- global_options=None, # type: Sequence[str]
- no_user_config=False, # type: bool
- unbuffered_output=False, # type: bool
-):
- # type: (...) -> List[str]
+ setup_py_path: str,
+ global_options: Optional[Sequence[str]] = None,
+ no_user_config: bool = False,
+ unbuffered_output: bool = False,
+) -> List[str]:
"""
Get setuptools command arguments with shim wrapped setup file invocation.
@@ -46,12 +73,11 @@ def make_setuptools_shim_args(
def make_setuptools_bdist_wheel_args(
- setup_py_path, # type: str
- global_options, # type: Sequence[str]
- build_options, # type: Sequence[str]
- destination_dir, # type: str
-):
- # type: (...) -> List[str]
+ setup_py_path: str,
+ global_options: Sequence[str],
+ build_options: Sequence[str],
+ destination_dir: str,
+) -> List[str]:
# NOTE: Eventually, we'd want to also -S to the flags here, when we're
# isolating. Currently, it breaks Python in virtualenvs, because it
# relies on site.py to find parts of the standard library outside the
@@ -65,10 +91,9 @@ def make_setuptools_bdist_wheel_args(
def make_setuptools_clean_args(
- setup_py_path, # type: str
- global_options, # type: Sequence[str]
-):
- # type: (...) -> List[str]
+ setup_py_path: str,
+ global_options: Sequence[str],
+) -> List[str]:
args = make_setuptools_shim_args(
setup_py_path, global_options=global_options, unbuffered_output=True
)
@@ -77,15 +102,14 @@ def make_setuptools_clean_args(
def make_setuptools_develop_args(
- setup_py_path, # type: str
- global_options, # type: Sequence[str]
- install_options, # type: Sequence[str]
- no_user_config, # type: bool
- prefix, # type: Optional[str]
- home, # type: Optional[str]
- use_user_site, # type: bool
-):
- # type: (...) -> List[str]
+ setup_py_path: str,
+ global_options: Sequence[str],
+ install_options: Sequence[str],
+ no_user_config: bool,
+ prefix: Optional[str],
+ home: Optional[str],
+ use_user_site: bool,
+) -> List[str]:
assert not (use_user_site and prefix)
args = make_setuptools_shim_args(
@@ -110,11 +134,10 @@ def make_setuptools_develop_args(
def make_setuptools_egg_info_args(
- setup_py_path, # type: str
- egg_info_dir, # type: Optional[str]
- no_user_config, # type: bool
-):
- # type: (...) -> List[str]
+ setup_py_path: str,
+ egg_info_dir: Optional[str],
+ no_user_config: bool,
+) -> List[str]:
args = make_setuptools_shim_args(setup_py_path, no_user_config=no_user_config)
args += ["egg_info"]
@@ -126,19 +149,18 @@ def make_setuptools_egg_info_args(
def make_setuptools_install_args(
- setup_py_path, # type: str
- global_options, # type: Sequence[str]
- install_options, # type: Sequence[str]
- record_filename, # type: str
- root, # type: Optional[str]
- prefix, # type: Optional[str]
- header_dir, # type: Optional[str]
- home, # type: Optional[str]
- use_user_site, # type: bool
- no_user_config, # type: bool
- pycompile, # type: bool
-):
- # type: (...) -> List[str]
+ setup_py_path: str,
+ global_options: Sequence[str],
+ install_options: Sequence[str],
+ record_filename: str,
+ root: Optional[str],
+ prefix: Optional[str],
+ header_dir: Optional[str],
+ home: Optional[str],
+ use_user_site: bool,
+ no_user_config: bool,
+ pycompile: bool,
+) -> List[str]:
assert not (use_user_site and prefix)
assert not (use_user_site and root)
diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py
index da052ee69..cf5bf6be1 100644
--- a/src/pip/_internal/utils/subprocess.py
+++ b/src/pip/_internal/utils/subprocess.py
@@ -2,25 +2,38 @@ import logging
import os
import shlex
import subprocess
-from typing import Any, Callable, Iterable, List, Mapping, Optional, Union
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Iterable,
+ List,
+ Mapping,
+ Optional,
+ Union,
+)
+
+from pip._vendor.rich.markup import escape
from pip._internal.cli.spinners import SpinnerInterface, open_spinner
from pip._internal.exceptions import InstallationSubprocessError
from pip._internal.utils.logging import VERBOSE, subprocess_logger
from pip._internal.utils.misc import HiddenText
-CommandArgs = List[Union[str, HiddenText]]
-
+if TYPE_CHECKING:
+ # Literal was introduced in Python 3.8.
+ #
+ # TODO: Remove `if TYPE_CHECKING` when dropping support for Python 3.7.
+ from typing import Literal
-LOG_DIVIDER = "----------------------------------------"
+CommandArgs = List[Union[str, HiddenText]]
-def make_command(*args):
- # type: (Union[str, HiddenText, CommandArgs]) -> CommandArgs
+def make_command(*args: Union[str, HiddenText, CommandArgs]) -> CommandArgs:
"""
Create a CommandArgs object.
"""
- command_args = [] # type: CommandArgs
+ command_args: CommandArgs = []
for arg in args:
# Check for list instead of CommandArgs since CommandArgs is
# only known during type-checking.
@@ -33,8 +46,7 @@ def make_command(*args):
return command_args
-def format_command_args(args):
- # type: (Union[List[str], CommandArgs]) -> str
+def format_command_args(args: Union[List[str], CommandArgs]) -> str:
"""
Format command arguments for display.
"""
@@ -49,64 +61,27 @@ def format_command_args(args):
)
-def reveal_command_args(args):
- # type: (Union[List[str], CommandArgs]) -> List[str]
+def reveal_command_args(args: Union[List[str], CommandArgs]) -> List[str]:
"""
Return the arguments in their raw, unredacted form.
"""
return [arg.secret if isinstance(arg, HiddenText) else arg for arg in args]
-def make_subprocess_output_error(
- cmd_args, # type: Union[List[str], CommandArgs]
- cwd, # type: Optional[str]
- lines, # type: List[str]
- exit_status, # type: int
-):
- # type: (...) -> str
- """
- Create and return the error message to use to log a subprocess error
- with command output.
-
- :param lines: A list of lines, each ending with a newline.
- """
- command = format_command_args(cmd_args)
-
- # We know the joined output value ends in a newline.
- output = "".join(lines)
- msg = (
- # Use a unicode string to avoid "UnicodeEncodeError: 'ascii'
- # codec can't encode character ..." in Python 2 when a format
- # argument (e.g. `output`) has a non-ascii character.
- "Command errored out with exit status {exit_status}:\n"
- " command: {command_display}\n"
- " cwd: {cwd_display}\n"
- "Complete output ({line_count} lines):\n{output}{divider}"
- ).format(
- exit_status=exit_status,
- command_display=command,
- cwd_display=cwd,
- line_count=len(lines),
- output=output,
- divider=LOG_DIVIDER,
- )
- return msg
-
-
def call_subprocess(
- cmd, # type: Union[List[str], CommandArgs]
- show_stdout=False, # type: bool
- cwd=None, # type: Optional[str]
- on_returncode="raise", # type: str
- extra_ok_returncodes=None, # type: Optional[Iterable[int]]
- command_desc=None, # type: Optional[str]
- extra_environ=None, # type: Optional[Mapping[str, Any]]
- unset_environ=None, # type: Optional[Iterable[str]]
- spinner=None, # type: Optional[SpinnerInterface]
- log_failed_cmd=True, # type: Optional[bool]
- stdout_only=False, # type: Optional[bool]
-):
- # type: (...) -> str
+ cmd: Union[List[str], CommandArgs],
+ show_stdout: bool = False,
+ cwd: Optional[str] = None,
+ on_returncode: 'Literal["raise", "warn", "ignore"]' = "raise",
+ extra_ok_returncodes: Optional[Iterable[int]] = None,
+ extra_environ: Optional[Mapping[str, Any]] = None,
+ unset_environ: Optional[Iterable[str]] = None,
+ spinner: Optional[SpinnerInterface] = None,
+ log_failed_cmd: Optional[bool] = True,
+ stdout_only: Optional[bool] = False,
+ *,
+ command_desc: str,
+) -> str:
"""
Args:
show_stdout: if true, use INFO to log the subprocess's stderr and
@@ -141,7 +116,7 @@ def call_subprocess(
# replaced by INFO.
if show_stdout:
# Then log the subprocess output at INFO level.
- log_subprocess = subprocess_logger.info
+ log_subprocess: Callable[..., None] = subprocess_logger.info
used_level = logging.INFO
else:
# Then log the subprocess output using VERBOSE. This also ensures
@@ -156,9 +131,6 @@ def call_subprocess(
# and we have a spinner.
use_spinner = not showing_subprocess and spinner is not None
- if command_desc is None:
- command_desc = format_command_args(cmd)
-
log_subprocess("Running command %s", command_desc)
env = os.environ.copy()
if extra_environ:
@@ -191,7 +163,7 @@ def call_subprocess(
proc.stdin.close()
# In this mode, stdout and stderr are in the same pipe.
while True:
- line = proc.stdout.readline() # type: str
+ line: str = proc.stdout.readline()
if not line:
break
line = line.rstrip()
@@ -231,17 +203,25 @@ def call_subprocess(
spinner.finish("done")
if proc_had_error:
if on_returncode == "raise":
- if not showing_subprocess and log_failed_cmd:
- # Then the subprocess streams haven't been logged to the
- # console yet.
- msg = make_subprocess_output_error(
- cmd_args=cmd,
- cwd=cwd,
- lines=all_output,
- exit_status=proc.returncode,
+ error = InstallationSubprocessError(
+ command_description=command_desc,
+ exit_code=proc.returncode,
+ output_lines=all_output if not showing_subprocess else None,
+ )
+ if log_failed_cmd:
+ subprocess_logger.error("[present-rich] %s", error)
+ subprocess_logger.verbose(
+ "[bold magenta]full command[/]: [blue]%s[/]",
+ escape(format_command_args(cmd)),
+ extra={"markup": True},
)
- subprocess_logger.error(msg)
- raise InstallationSubprocessError(proc.returncode, command_desc)
+ subprocess_logger.verbose(
+ "[bold magenta]cwd[/]: %s",
+ escape(cwd or "[inherit]"),
+ extra={"markup": True},
+ )
+
+ raise error
elif on_returncode == "warn":
subprocess_logger.warning(
'Command "%s" had error code %s in %s',
@@ -256,8 +236,7 @@ def call_subprocess(
return output
-def runner_with_spinner_message(message):
- # type: (str) -> Callable[..., None]
+def runner_with_spinner_message(message: str) -> Callable[..., None]:
"""Provide a subprocess_runner that shows a spinner message.
Intended for use with for pep517's Pep517HookCaller. Thus, the runner has
@@ -265,14 +244,14 @@ def runner_with_spinner_message(message):
"""
def runner(
- cmd, # type: List[str]
- cwd=None, # type: Optional[str]
- extra_environ=None, # type: Optional[Mapping[str, Any]]
- ):
- # type: (...) -> None
+ cmd: List[str],
+ cwd: Optional[str] = None,
+ extra_environ: Optional[Mapping[str, Any]] = None,
+ ) -> None:
with open_spinner(message) as spinner:
call_subprocess(
cmd,
+ command_desc=message,
cwd=cwd,
extra_environ=extra_environ,
spinner=spinner,
diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py
index 477cbe6b1..8ee8a1cb1 100644
--- a/src/pip/_internal/utils/temp_dir.py
+++ b/src/pip/_internal/utils/temp_dir.py
@@ -4,7 +4,7 @@ import logging
import os.path
import tempfile
from contextlib import ExitStack, contextmanager
-from typing import Any, Dict, Iterator, Optional, TypeVar, Union
+from typing import Any, Dict, Generator, Optional, TypeVar, Union
from pip._internal.utils.misc import enum, rmtree
@@ -22,12 +22,11 @@ tempdir_kinds = enum(
)
-_tempdir_manager = None # type: Optional[ExitStack]
+_tempdir_manager: Optional[ExitStack] = None
@contextmanager
-def global_tempdir_manager():
- # type: () -> Iterator[None]
+def global_tempdir_manager() -> Generator[None, None, None]:
global _tempdir_manager
with ExitStack() as stack:
old_tempdir_manager, _tempdir_manager = _tempdir_manager, stack
@@ -40,31 +39,27 @@ def global_tempdir_manager():
class TempDirectoryTypeRegistry:
"""Manages temp directory behavior"""
- def __init__(self):
- # type: () -> None
- self._should_delete = {} # type: Dict[str, bool]
+ def __init__(self) -> None:
+ self._should_delete: Dict[str, bool] = {}
- def set_delete(self, kind, value):
- # type: (str, bool) -> None
+ def set_delete(self, kind: str, value: bool) -> None:
"""Indicate whether a TempDirectory of the given kind should be
auto-deleted.
"""
self._should_delete[kind] = value
- def get_delete(self, kind):
- # type: (str) -> bool
+ def get_delete(self, kind: str) -> bool:
"""Get configured auto-delete flag for a given TempDirectory type,
default True.
"""
return self._should_delete.get(kind, True)
-_tempdir_registry = None # type: Optional[TempDirectoryTypeRegistry]
+_tempdir_registry: Optional[TempDirectoryTypeRegistry] = None
@contextmanager
-def tempdir_registry():
- # type: () -> Iterator[TempDirectoryTypeRegistry]
+def tempdir_registry() -> Generator[TempDirectoryTypeRegistry, None, None]:
"""Provides a scoped global tempdir registry that can be used to dictate
whether directories should be deleted.
"""
@@ -107,10 +102,10 @@ class TempDirectory:
def __init__(
self,
- path=None, # type: Optional[str]
- delete=_default, # type: Union[bool, None, _Default]
- kind="temp", # type: str
- globally_managed=False, # type: bool
+ path: Optional[str] = None,
+ delete: Union[bool, None, _Default] = _default,
+ kind: str = "temp",
+ globally_managed: bool = False,
):
super().__init__()
@@ -139,21 +134,17 @@ class TempDirectory:
_tempdir_manager.enter_context(self)
@property
- def path(self):
- # type: () -> str
+ def path(self) -> str:
assert not self._deleted, f"Attempted to access deleted path: {self._path}"
return self._path
- def __repr__(self):
- # type: () -> str
+ def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.path!r}>"
- def __enter__(self):
- # type: (_T) -> _T
+ def __enter__(self: _T) -> _T:
return self
- def __exit__(self, exc, value, tb):
- # type: (Any, Any, Any) -> None
+ def __exit__(self, exc: Any, value: Any, tb: Any) -> None:
if self.delete is not None:
delete = self.delete
elif _tempdir_registry:
@@ -164,8 +155,7 @@ class TempDirectory:
if delete:
self.cleanup()
- def _create(self, kind):
- # type: (str) -> str
+ def _create(self, kind: str) -> str:
"""Create a temporary directory and store its path in self.path"""
# We realpath here because some systems have their default tmpdir
# symlinked to another directory. This tends to confuse build
@@ -175,8 +165,7 @@ class TempDirectory:
logger.debug("Created temporary directory: %s", path)
return path
- def cleanup(self):
- # type: () -> None
+ def cleanup(self) -> None:
"""Remove the temporary directory created and reset state"""
self._deleted = True
if not os.path.exists(self._path):
@@ -206,14 +195,12 @@ class AdjacentTempDirectory(TempDirectory):
# with leading '-' and invalid metadata
LEADING_CHARS = "-~.=%0123456789"
- def __init__(self, original, delete=None):
- # type: (str, Optional[bool]) -> None
+ def __init__(self, original: str, delete: Optional[bool] = None) -> None:
self.original = original.rstrip("/\\")
super().__init__(delete=delete)
@classmethod
- def _generate_names(cls, name):
- # type: (str) -> Iterator[str]
+ def _generate_names(cls, name: str) -> Generator[str, None, None]:
"""Generates a series of temporary names.
The algorithm replaces the leading characters in the name
@@ -238,8 +225,7 @@ class AdjacentTempDirectory(TempDirectory):
if new_name != name:
yield new_name
- def _create(self, kind):
- # type: (str) -> str
+ def _create(self, kind: str) -> str:
root, name = os.path.split(self.original)
for candidate in self._generate_names(name):
path = os.path.join(root, candidate)
diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py
index bffb3cd65..78b5c13ce 100644
--- a/src/pip/_internal/utils/unpacking.py
+++ b/src/pip/_internal/utils/unpacking.py
@@ -40,16 +40,14 @@ except ImportError:
logger.debug("lzma module is not available")
-def current_umask():
- # type: () -> int
+def current_umask() -> int:
"""Get the current umask which involves having to set it temporarily."""
mask = os.umask(0)
os.umask(mask)
return mask
-def split_leading_dir(path):
- # type: (str) -> List[str]
+def split_leading_dir(path: str) -> List[str]:
path = path.lstrip("/").lstrip("\\")
if "/" in path and (
("\\" in path and path.find("/") < path.find("\\")) or "\\" not in path
@@ -61,8 +59,7 @@ def split_leading_dir(path):
return [path, ""]
-def has_leading_dir(paths):
- # type: (Iterable[str]) -> bool
+def has_leading_dir(paths: Iterable[str]) -> bool:
"""Returns true if all the paths have the same leading path name
(i.e., everything is in one subdirectory in an archive)"""
common_prefix = None
@@ -77,8 +74,7 @@ def has_leading_dir(paths):
return True
-def is_within_directory(directory, target):
- # type: (str, str) -> bool
+def is_within_directory(directory: str, target: str) -> bool:
"""
Return true if the absolute path of target is within the directory
"""
@@ -89,8 +85,7 @@ def is_within_directory(directory, target):
return prefix == abs_directory
-def set_extracted_file_to_default_mode_plus_executable(path):
- # type: (str) -> None
+def set_extracted_file_to_default_mode_plus_executable(path: str) -> None:
"""
Make file present at path have execute for user/group/world
(chmod +x) is no-op on windows per python docs
@@ -98,16 +93,14 @@ def set_extracted_file_to_default_mode_plus_executable(path):
os.chmod(path, (0o777 & ~current_umask() | 0o111))
-def zip_item_is_executable(info):
- # type: (ZipInfo) -> bool
+def zip_item_is_executable(info: ZipInfo) -> bool:
mode = info.external_attr >> 16
# if mode and regular file and any execute permissions for
# user/group/world?
return bool(mode and stat.S_ISREG(mode) and mode & 0o111)
-def unzip_file(filename, location, flatten=True):
- # type: (str, str, bool) -> None
+def unzip_file(filename: str, location: str, flatten: bool = True) -> None:
"""
Unzip the file (with path `filename`) to the destination `location`. All
files are written based on system defaults and umask (i.e. permissions are
@@ -153,8 +146,7 @@ def unzip_file(filename, location, flatten=True):
zipfp.close()
-def untar_file(filename, location):
- # type: (str, str) -> None
+def untar_file(filename: str, location: str) -> None:
"""
Untar the file (with path `filename`) to the destination `location`.
All files are written based on system defaults and umask (i.e. permissions
@@ -196,8 +188,7 @@ def untar_file(filename, location):
ensure_dir(path)
elif member.issym():
try:
- # https://github.com/python/typeshed/issues/2673
- tar._extract_member(member, path) # type: ignore
+ tar._extract_member(member, path)
except Exception as exc:
# Some corrupt tar files seem to produce this
# (specifically bad symlinks)
@@ -236,11 +227,10 @@ def untar_file(filename, location):
def unpack_file(
- filename, # type: str
- location, # type: str
- content_type=None, # type: Optional[str]
-):
- # type: (...) -> None
+ filename: str,
+ location: str,
+ content_type: Optional[str] = None,
+) -> None:
filename = os.path.realpath(filename)
if (
content_type == "application/zip"
diff --git a/src/pip/_internal/utils/urls.py b/src/pip/_internal/utils/urls.py
index 7b51052c9..6ba2e04f3 100644
--- a/src/pip/_internal/utils/urls.py
+++ b/src/pip/_internal/utils/urls.py
@@ -7,15 +7,13 @@ from typing import Optional
from .compat import WINDOWS
-def get_url_scheme(url):
- # type: (str) -> Optional[str]
+def get_url_scheme(url: str) -> Optional[str]:
if ":" not in url:
return None
return url.split(":", 1)[0].lower()
-def path_to_url(path):
- # type: (str) -> str
+def path_to_url(path: str) -> str:
"""
Convert a path to a file: URL. The path will be made absolute and have
quoted path parts.
@@ -25,8 +23,7 @@ def path_to_url(path):
return url
-def url_to_path(url):
- # type: (str) -> str
+def url_to_path(url: str) -> str:
"""
Convert a file: URL to a path.
"""
diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py
index 51cacb55c..c926db4c3 100644
--- a/src/pip/_internal/utils/virtualenv.py
+++ b/src/pip/_internal/utils/virtualenv.py
@@ -11,8 +11,7 @@ _INCLUDE_SYSTEM_SITE_PACKAGES_REGEX = re.compile(
)
-def _running_under_venv():
- # type: () -> bool
+def _running_under_venv() -> bool:
"""Checks if sys.base_prefix and sys.prefix match.
This handles PEP 405 compliant virtual environments.
@@ -20,8 +19,7 @@ def _running_under_venv():
return sys.prefix != getattr(sys, "base_prefix", sys.prefix)
-def _running_under_regular_virtualenv():
- # type: () -> bool
+def _running_under_regular_virtualenv() -> bool:
"""Checks if sys.real_prefix is set.
This handles virtual environments created with pypa's virtualenv.
@@ -30,14 +28,12 @@ def _running_under_regular_virtualenv():
return hasattr(sys, "real_prefix")
-def running_under_virtualenv():
- # type: () -> bool
+def running_under_virtualenv() -> bool:
"""Return True if we're running inside a virtualenv, False otherwise."""
return _running_under_venv() or _running_under_regular_virtualenv()
-def _get_pyvenv_cfg_lines():
- # type: () -> Optional[List[str]]
+def _get_pyvenv_cfg_lines() -> Optional[List[str]]:
"""Reads {sys.prefix}/pyvenv.cfg and returns its contents as list of lines
Returns None, if it could not read/access the file.
@@ -52,8 +48,7 @@ def _get_pyvenv_cfg_lines():
return None
-def _no_global_under_venv():
- # type: () -> bool
+def _no_global_under_venv() -> bool:
"""Check `{sys.prefix}/pyvenv.cfg` for system site-packages inclusion
PEP 405 specifies that when system site-packages are not supposed to be
@@ -82,8 +77,7 @@ def _no_global_under_venv():
return False
-def _no_global_under_regular_virtualenv():
- # type: () -> bool
+def _no_global_under_regular_virtualenv() -> bool:
"""Check if "no-global-site-packages.txt" exists beside site.py
This mirrors logic in pypa/virtualenv for determining whether system
@@ -97,8 +91,7 @@ def _no_global_under_regular_virtualenv():
return os.path.exists(no_global_site_packages_file)
-def virtualenv_no_global():
- # type: () -> bool
+def virtualenv_no_global() -> bool:
"""Returns a boolean, whether running in venv with no system site-packages."""
# PEP 405 compliance needs to be checked first since virtualenv >=20 would
# return True for both checks, but is only able to use the PEP 405 config.
diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py
index 42f080845..e5e3f34ed 100644
--- a/src/pip/_internal/utils/wheel.py
+++ b/src/pip/_internal/utils/wheel.py
@@ -4,14 +4,12 @@
import logging
from email.message import Message
from email.parser import Parser
-from typing import Dict, Tuple
+from typing import Tuple
from zipfile import BadZipFile, ZipFile
from pip._vendor.packaging.utils import canonicalize_name
-from pip._vendor.pkg_resources import DistInfoDistribution, Distribution
from pip._internal.exceptions import UnsupportedWheel
-from pip._internal.utils.pkg_resources import DictMetadata
VERSION_COMPATIBLE = (1, 0)
@@ -19,53 +17,7 @@ VERSION_COMPATIBLE = (1, 0)
logger = logging.getLogger(__name__)
-class WheelMetadata(DictMetadata):
- """Metadata provider that maps metadata decoding exceptions to our
- internal exception type.
- """
-
- def __init__(self, metadata, wheel_name):
- # type: (Dict[str, bytes], str) -> None
- super().__init__(metadata)
- self._wheel_name = wheel_name
-
- def get_metadata(self, name):
- # type: (str) -> str
- try:
- return super().get_metadata(name)
- except UnicodeDecodeError as e:
- # Augment the default error with the origin of the file.
- raise UnsupportedWheel(
- f"Error decoding metadata for {self._wheel_name}: {e}"
- )
-
-
-def pkg_resources_distribution_for_wheel(wheel_zip, name, location):
- # type: (ZipFile, str, str) -> Distribution
- """Get a pkg_resources distribution given a wheel.
-
- :raises UnsupportedWheel: on any errors
- """
- info_dir, _ = parse_wheel(wheel_zip, name)
-
- metadata_files = [p for p in wheel_zip.namelist() if p.startswith(f"{info_dir}/")]
-
- metadata_text = {} # type: Dict[str, bytes]
- for path in metadata_files:
- _, metadata_name = path.split("/", 1)
-
- try:
- metadata_text[metadata_name] = read_wheel_metadata_file(wheel_zip, path)
- except UnsupportedWheel as e:
- raise UnsupportedWheel("{} has an invalid wheel, {}".format(name, str(e)))
-
- metadata = WheelMetadata(metadata_text, location)
-
- return DistInfoDistribution(location=location, metadata=metadata, project_name=name)
-
-
-def parse_wheel(wheel_zip, name):
- # type: (ZipFile, str) -> Tuple[str, Message]
+def parse_wheel(wheel_zip: ZipFile, name: str) -> Tuple[str, Message]:
"""Extract information from the provided wheel, ensuring it meets basic
standards.
@@ -83,8 +35,7 @@ def parse_wheel(wheel_zip, name):
return info_dir, metadata
-def wheel_dist_info_dir(source, name):
- # type: (ZipFile, str) -> str
+def wheel_dist_info_dir(source: ZipFile, name: str) -> str:
"""Returns the name of the contained .dist-info directory.
Raises AssertionError or UnsupportedWheel if not found, >1 found, or
@@ -117,8 +68,7 @@ def wheel_dist_info_dir(source, name):
return info_dir
-def read_wheel_metadata_file(source, path):
- # type: (ZipFile, str) -> bytes
+def read_wheel_metadata_file(source: ZipFile, path: str) -> bytes:
try:
return source.read(path)
# BadZipFile for general corruption, KeyError for missing entry,
@@ -127,8 +77,7 @@ def read_wheel_metadata_file(source, path):
raise UnsupportedWheel(f"could not read {path!r} file: {e!r}")
-def wheel_metadata(source, dist_info_dir):
- # type: (ZipFile, str) -> Message
+def wheel_metadata(source: ZipFile, dist_info_dir: str) -> Message:
"""Return the WHEEL metadata of an extracted wheel, if possible.
Otherwise, raise UnsupportedWheel.
"""
@@ -147,8 +96,7 @@ def wheel_metadata(source, dist_info_dir):
return Parser().parsestr(wheel_text)
-def wheel_version(wheel_data):
- # type: (Message) -> Tuple[int, ...]
+def wheel_version(wheel_data: Message) -> Tuple[int, ...]:
"""Given WHEEL metadata, return the parsed Wheel-Version.
Otherwise, raise UnsupportedWheel.
"""
@@ -164,8 +112,7 @@ def wheel_version(wheel_data):
raise UnsupportedWheel(f"invalid Wheel-Version: {version!r}")
-def check_compatibility(version, name):
- # type: (Tuple[int, ...], str) -> None
+def check_compatibility(version: Tuple[int, ...], name: str) -> None:
"""Raises errors or warns if called with an incompatible Wheel-Version.
pip should refuse to install a Wheel-Version that's a major series
diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py
index 42b68773b..a7b16e2e0 100644
--- a/src/pip/_internal/vcs/bazaar.py
+++ b/src/pip/_internal/vcs/bazaar.py
@@ -16,61 +16,65 @@ logger = logging.getLogger(__name__)
class Bazaar(VersionControl):
- name = 'bzr'
- dirname = '.bzr'
- repo_name = 'branch'
+ name = "bzr"
+ dirname = ".bzr"
+ repo_name = "branch"
schemes = (
- 'bzr+http', 'bzr+https', 'bzr+ssh', 'bzr+sftp', 'bzr+ftp',
- 'bzr+lp', 'bzr+file'
+ "bzr+http",
+ "bzr+https",
+ "bzr+ssh",
+ "bzr+sftp",
+ "bzr+ftp",
+ "bzr+lp",
+ "bzr+file",
)
@staticmethod
- def get_base_rev_args(rev):
- # type: (str) -> List[str]
- return ['-r', rev]
+ def get_base_rev_args(rev: str) -> List[str]:
+ return ["-r", rev]
- def fetch_new(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
+ def fetch_new(
+ self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
+ ) -> None:
rev_display = rev_options.to_display()
logger.info(
- 'Checking out %s%s to %s',
+ "Checking out %s%s to %s",
url,
rev_display,
display_path(dest),
)
- cmd_args = (
- make_command('branch', '-q', rev_options.to_args(), url, dest)
- )
+ if verbosity <= 0:
+ flag = "--quiet"
+ elif verbosity == 1:
+ flag = ""
+ else:
+ flag = f"-{'v'*verbosity}"
+ cmd_args = make_command("branch", flag, rev_options.to_args(), url, dest)
self.run_command(cmd_args)
- def switch(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
- self.run_command(make_command('switch', url), cwd=dest)
+ def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
+ self.run_command(make_command("switch", url), cwd=dest)
- def update(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
- cmd_args = make_command('pull', '-q', rev_options.to_args())
+ def update(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
+ cmd_args = make_command("pull", "-q", rev_options.to_args())
self.run_command(cmd_args, cwd=dest)
@classmethod
- def get_url_rev_and_auth(cls, url):
- # type: (str) -> Tuple[str, Optional[str], AuthInfo]
+ def get_url_rev_and_auth(cls, url: str) -> Tuple[str, Optional[str], AuthInfo]:
# hotfix the URL scheme after removing bzr+ from bzr+ssh:// readd it
url, rev, user_pass = super().get_url_rev_and_auth(url)
- if url.startswith('ssh://'):
- url = 'bzr+' + url
+ if url.startswith("ssh://"):
+ url = "bzr+" + url
return url, rev, user_pass
@classmethod
- def get_remote_url(cls, location):
- # type: (str) -> str
+ def get_remote_url(cls, location: str) -> str:
urls = cls.run_command(
- ['info'], show_stdout=False, stdout_only=True, cwd=location
+ ["info"], show_stdout=False, stdout_only=True, cwd=location
)
for line in urls.splitlines():
line = line.strip()
- for x in ('checkout of branch: ',
- 'parent branch: '):
+ for x in ("checkout of branch: ", "parent branch: "):
if line.startswith(x):
repo = line.split(x)[1]
if cls._is_local_repository(repo):
@@ -79,16 +83,17 @@ class Bazaar(VersionControl):
raise RemoteNotFoundError
@classmethod
- def get_revision(cls, location):
- # type: (str) -> str
+ def get_revision(cls, location: str) -> str:
revision = cls.run_command(
- ['revno'], show_stdout=False, stdout_only=True, cwd=location,
+ ["revno"],
+ show_stdout=False,
+ stdout_only=True,
+ cwd=location,
)
return revision.splitlines()[-1]
@classmethod
- def is_commit_id_equal(cls, dest, name):
- # type: (str, Optional[str]) -> bool
+ def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:
"""Always assume the versions don't match"""
return False
diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py
index 8919aa538..8d1d49937 100644
--- a/src/pip/_internal/vcs/git.py
+++ b/src/pip/_internal/vcs/git.py
@@ -34,10 +34,11 @@ GIT_VERSION_REGEX = re.compile(
r".*$" # Suffix, including any pre- and post-release segments we don't care about.
)
-HASH_REGEX = re.compile('^[a-fA-F0-9]{40}$')
+HASH_REGEX = re.compile("^[a-fA-F0-9]{40}$")
# SCP (Secure copy protocol) shorthand. e.g. 'git@example.com:foo/bar.git'
-SCP_REGEX = re.compile(r"""^
+SCP_REGEX = re.compile(
+ r"""^
# Optional user, e.g. 'git@'
(\w+@)?
# Server, e.g. 'github.com'.
@@ -46,33 +47,36 @@ SCP_REGEX = re.compile(r"""^
# alphanumeric character so as not to be confusable with a Windows paths
# like 'C:/foo/bar' or 'C:\foo\bar'.
(\w[^:]*)
-$""", re.VERBOSE)
+ $""",
+ re.VERBOSE,
+)
-def looks_like_hash(sha):
- # type: (str) -> bool
+def looks_like_hash(sha: str) -> bool:
return bool(HASH_REGEX.match(sha))
class Git(VersionControl):
- name = 'git'
- dirname = '.git'
- repo_name = 'clone'
+ name = "git"
+ dirname = ".git"
+ repo_name = "clone"
schemes = (
- 'git+http', 'git+https', 'git+ssh', 'git+git', 'git+file',
+ "git+http",
+ "git+https",
+ "git+ssh",
+ "git+git",
+ "git+file",
)
# Prevent the user's environment variables from interfering with pip:
# https://github.com/pypa/pip/issues/1130
- unset_environ = ('GIT_DIR', 'GIT_WORK_TREE')
- default_arg_rev = 'HEAD'
+ unset_environ = ("GIT_DIR", "GIT_WORK_TREE")
+ default_arg_rev = "HEAD"
@staticmethod
- def get_base_rev_args(rev):
- # type: (str) -> List[str]
+ def get_base_rev_args(rev: str) -> List[str]:
return [rev]
- def is_immutable_rev_checkout(self, url, dest):
- # type: (str, str) -> bool
+ def is_immutable_rev_checkout(self, url: str, dest: str) -> bool:
_, rev_options = self.get_url_rev_options(hide_url(url))
if not rev_options.rev:
return False
@@ -83,23 +87,24 @@ class Git(VersionControl):
# return False in the rare case rev is both a commit hash
# and a tag or a branch; we don't want to cache in that case
# because that branch/tag could point to something else in the future
- is_tag_or_branch = bool(
- self.get_revision_sha(dest, rev_options.rev)[0]
- )
+ is_tag_or_branch = bool(self.get_revision_sha(dest, rev_options.rev)[0])
return not is_tag_or_branch
def get_git_version(self) -> Tuple[int, ...]:
version = self.run_command(
- ['version'], show_stdout=False, stdout_only=True
+ ["version"],
+ command_desc="git version",
+ show_stdout=False,
+ stdout_only=True,
)
match = GIT_VERSION_REGEX.match(version)
if not match:
+ logger.warning("Can't parse git version: %s", version)
return ()
return tuple(int(c) for c in match.groups())
@classmethod
- def get_current_branch(cls, location):
- # type: (str) -> Optional[str]
+ def get_current_branch(cls, location: str) -> Optional[str]:
"""
Return the current branch, or None if HEAD isn't at a branch
(e.g. detached HEAD).
@@ -108,24 +113,23 @@ class Git(VersionControl):
# HEAD rather than a symbolic ref. In addition, the -q causes the
# command to exit with status code 1 instead of 128 in this case
# and to suppress the message to stderr.
- args = ['symbolic-ref', '-q', 'HEAD']
+ args = ["symbolic-ref", "-q", "HEAD"]
output = cls.run_command(
args,
- extra_ok_returncodes=(1, ),
+ extra_ok_returncodes=(1,),
show_stdout=False,
stdout_only=True,
cwd=location,
)
ref = output.strip()
- if ref.startswith('refs/heads/'):
- return ref[len('refs/heads/'):]
+ if ref.startswith("refs/heads/"):
+ return ref[len("refs/heads/") :]
return None
@classmethod
- def get_revision_sha(cls, dest, rev):
- # type: (str, str) -> Tuple[Optional[str], bool]
+ def get_revision_sha(cls, dest: str, rev: str) -> Tuple[Optional[str], bool]:
"""
Return (sha_or_none, is_branch), where sha_or_none is a commit hash
if the revision names a remote branch or tag, otherwise None.
@@ -136,11 +140,11 @@ class Git(VersionControl):
"""
# Pass rev to pre-filter the list.
output = cls.run_command(
- ['show-ref', rev],
+ ["show-ref", rev],
cwd=dest,
show_stdout=False,
stdout_only=True,
- on_returncode='ignore',
+ on_returncode="ignore",
)
refs = {}
# NOTE: We do not use splitlines here since that would split on other
@@ -155,12 +159,12 @@ class Git(VersionControl):
except ValueError:
# Include the offending line to simplify troubleshooting if
# this error ever occurs.
- raise ValueError(f'unexpected show-ref line: {line!r}')
+ raise ValueError(f"unexpected show-ref line: {line!r}")
refs[ref_name] = ref_sha
- branch_ref = f'refs/remotes/origin/{rev}'
- tag_ref = f'refs/tags/{rev}'
+ branch_ref = f"refs/remotes/origin/{rev}"
+ tag_ref = f"refs/tags/{rev}"
sha = refs.get(branch_ref)
if sha is not None:
@@ -171,8 +175,7 @@ class Git(VersionControl):
return (sha, False)
@classmethod
- def _should_fetch(cls, dest, rev):
- # type: (str, str) -> bool
+ def _should_fetch(cls, dest: str, rev: str) -> bool:
"""
Return true if rev is a ref or is a commit that we don't have locally.
@@ -195,8 +198,9 @@ class Git(VersionControl):
return True
@classmethod
- def resolve_revision(cls, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> RevOptions
+ def resolve_revision(
+ cls, dest: str, url: HiddenText, rev_options: RevOptions
+ ) -> RevOptions:
"""
Resolve a revision to a new RevOptions object with the SHA1 of the
branch, tag, or ref if found.
@@ -230,18 +234,17 @@ class Git(VersionControl):
# fetch the requested revision
cls.run_command(
- make_command('fetch', '-q', url, rev_options.to_args()),
+ make_command("fetch", "-q", url, rev_options.to_args()),
cwd=dest,
)
# Change the revision to the SHA of the ref we fetched
- sha = cls.get_revision(dest, rev='FETCH_HEAD')
+ sha = cls.get_revision(dest, rev="FETCH_HEAD")
rev_options = rev_options.make_new(sha)
return rev_options
@classmethod
- def is_commit_id_equal(cls, dest, name):
- # type: (str, Optional[str]) -> bool
+ def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:
"""
Return whether the current commit hash equals the given name.
@@ -255,30 +258,58 @@ class Git(VersionControl):
return cls.get_revision(dest) == name
- def fetch_new(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
+ def fetch_new(
+ self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
+ ) -> None:
rev_display = rev_options.to_display()
- logger.info('Cloning %s%s to %s', url, rev_display, display_path(dest))
- self.run_command(make_command('clone', '-q', url, dest))
+ logger.info("Cloning %s%s to %s", url, rev_display, display_path(dest))
+ if verbosity <= 0:
+ flags: Tuple[str, ...] = ("--quiet",)
+ elif verbosity == 1:
+ flags = ()
+ else:
+ flags = ("--verbose", "--progress")
+ if self.get_git_version() >= (2, 17):
+ # Git added support for partial clone in 2.17
+ # https://git-scm.com/docs/partial-clone
+ # Speeds up cloning by functioning without a complete copy of repository
+ self.run_command(
+ make_command(
+ "clone",
+ "--filter=blob:none",
+ *flags,
+ url,
+ dest,
+ )
+ )
+ else:
+ self.run_command(make_command("clone", *flags, url, dest))
if rev_options.rev:
# Then a specific revision was requested.
rev_options = self.resolve_revision(dest, url, rev_options)
- branch_name = getattr(rev_options, 'branch_name', None)
+ branch_name = getattr(rev_options, "branch_name", None)
+ logger.debug("Rev options %s, branch_name %s", rev_options, branch_name)
if branch_name is None:
# Only do a checkout if the current commit id doesn't match
# the requested revision.
if not self.is_commit_id_equal(dest, rev_options.rev):
cmd_args = make_command(
- 'checkout', '-q', rev_options.to_args(),
+ "checkout",
+ "-q",
+ rev_options.to_args(),
)
self.run_command(cmd_args, cwd=dest)
elif self.get_current_branch(dest) != branch_name:
# Then a specific branch was requested, and that branch
# is not yet checked out.
- track_branch = f'origin/{branch_name}'
+ track_branch = f"origin/{branch_name}"
cmd_args = [
- 'checkout', '-b', branch_name, '--track', track_branch,
+ "checkout",
+ "-b",
+ branch_name,
+ "--track",
+ track_branch,
]
self.run_command(cmd_args, cwd=dest)
else:
@@ -290,35 +321,32 @@ class Git(VersionControl):
#: repo may contain submodules
self.update_submodules(dest)
- def switch(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
+ def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
self.run_command(
- make_command('config', 'remote.origin.url', url),
+ make_command("config", "remote.origin.url", url),
cwd=dest,
)
- cmd_args = make_command('checkout', '-q', rev_options.to_args())
+ cmd_args = make_command("checkout", "-q", rev_options.to_args())
self.run_command(cmd_args, cwd=dest)
self.update_submodules(dest)
- def update(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
+ def update(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
# First fetch changes from the default remote
if self.get_git_version() >= (1, 9):
# fetch tags in addition to everything else
- self.run_command(['fetch', '-q', '--tags'], cwd=dest)
+ self.run_command(["fetch", "-q", "--tags"], cwd=dest)
else:
- self.run_command(['fetch', '-q'], cwd=dest)
+ self.run_command(["fetch", "-q"], cwd=dest)
# Then reset to wanted revision (maybe even origin/master)
rev_options = self.resolve_revision(dest, url, rev_options)
- cmd_args = make_command('reset', '--hard', '-q', rev_options.to_args())
+ cmd_args = make_command("reset", "--hard", "-q", rev_options.to_args())
self.run_command(cmd_args, cwd=dest)
#: update submodules
self.update_submodules(dest)
@classmethod
- def get_remote_url(cls, location):
- # type: (str) -> str
+ def get_remote_url(cls, location: str) -> str:
"""
Return URL of the first remote encountered.
@@ -328,8 +356,8 @@ class Git(VersionControl):
# We need to pass 1 for extra_ok_returncodes since the command
# exits with return code 1 if there are no matching lines.
stdout = cls.run_command(
- ['config', '--get-regexp', r'remote\..*\.url'],
- extra_ok_returncodes=(1, ),
+ ["config", "--get-regexp", r"remote\..*\.url"],
+ extra_ok_returncodes=(1,),
show_stdout=False,
stdout_only=True,
cwd=location,
@@ -341,15 +369,14 @@ class Git(VersionControl):
raise RemoteNotFoundError
for remote in remotes:
- if remote.startswith('remote.origin.url '):
+ if remote.startswith("remote.origin.url "):
found_remote = remote
break
- url = found_remote.split(' ')[1]
+ url = found_remote.split(" ")[1]
return cls._git_remote_to_pip_url(url.strip())
@staticmethod
- def _git_remote_to_pip_url(url):
- # type: (str) -> str
+ def _git_remote_to_pip_url(url: str) -> str:
"""
Convert a remote url from what git uses to what pip accepts.
@@ -380,14 +407,13 @@ class Git(VersionControl):
raise RemoteNotValidError(url)
@classmethod
- def has_commit(cls, location, rev):
- # type: (str, str) -> bool
+ def has_commit(cls, location: str, rev: str) -> bool:
"""
Check if rev is a commit that is available in the local repository.
"""
try:
cls.run_command(
- ['rev-parse', '-q', '--verify', "sha^" + rev],
+ ["rev-parse", "-q", "--verify", "sha^" + rev],
cwd=location,
log_failed_cmd=False,
)
@@ -397,12 +423,11 @@ class Git(VersionControl):
return True
@classmethod
- def get_revision(cls, location, rev=None):
- # type: (str, Optional[str]) -> str
+ def get_revision(cls, location: str, rev: Optional[str] = None) -> str:
if rev is None:
- rev = 'HEAD'
+ rev = "HEAD"
current_rev = cls.run_command(
- ['rev-parse', rev],
+ ["rev-parse", rev],
show_stdout=False,
stdout_only=True,
cwd=location,
@@ -410,27 +435,25 @@ class Git(VersionControl):
return current_rev.strip()
@classmethod
- def get_subdirectory(cls, location):
- # type: (str) -> Optional[str]
+ def get_subdirectory(cls, location: str) -> Optional[str]:
"""
Return the path to Python project root, relative to the repo root.
Return None if the project root is in the repo root.
"""
# find the repo root
git_dir = cls.run_command(
- ['rev-parse', '--git-dir'],
+ ["rev-parse", "--git-dir"],
show_stdout=False,
stdout_only=True,
cwd=location,
).strip()
if not os.path.isabs(git_dir):
git_dir = os.path.join(location, git_dir)
- repo_root = os.path.abspath(os.path.join(git_dir, '..'))
+ repo_root = os.path.abspath(os.path.join(git_dir, ".."))
return find_path_to_project_root_from_repo_root(location, repo_root)
@classmethod
- def get_url_rev_and_auth(cls, url):
- # type: (str) -> Tuple[str, Optional[str], AuthInfo]
+ def get_url_rev_and_auth(cls, url: str) -> Tuple[str, Optional[str], AuthInfo]:
"""
Prefixes stub URLs like 'user@hostname:user/repo.git' with 'ssh://'.
That's required because although they use SSH they sometimes don't
@@ -440,66 +463,63 @@ class Git(VersionControl):
# Works around an apparent Git bug
# (see https://article.gmane.org/gmane.comp.version-control.git/146500)
scheme, netloc, path, query, fragment = urlsplit(url)
- if scheme.endswith('file'):
- initial_slashes = path[:-len(path.lstrip('/'))]
- newpath = (
- initial_slashes +
- urllib.request.url2pathname(path)
- .replace('\\', '/').lstrip('/')
- )
- after_plus = scheme.find('+') + 1
+ if scheme.endswith("file"):
+ initial_slashes = path[: -len(path.lstrip("/"))]
+ newpath = initial_slashes + urllib.request.url2pathname(path).replace(
+ "\\", "/"
+ ).lstrip("/")
+ after_plus = scheme.find("+") + 1
url = scheme[:after_plus] + urlunsplit(
(scheme[after_plus:], netloc, newpath, query, fragment),
)
- if '://' not in url:
- assert 'file:' not in url
- url = url.replace('git+', 'git+ssh://')
+ if "://" not in url:
+ assert "file:" not in url
+ url = url.replace("git+", "git+ssh://")
url, rev, user_pass = super().get_url_rev_and_auth(url)
- url = url.replace('ssh://', '')
+ url = url.replace("ssh://", "")
else:
url, rev, user_pass = super().get_url_rev_and_auth(url)
return url, rev, user_pass
@classmethod
- def update_submodules(cls, location):
- # type: (str) -> None
- if not os.path.exists(os.path.join(location, '.gitmodules')):
+ def update_submodules(cls, location: str) -> None:
+ if not os.path.exists(os.path.join(location, ".gitmodules")):
return
cls.run_command(
- ['submodule', 'update', '--init', '--recursive', '-q'],
+ ["submodule", "update", "--init", "--recursive", "-q"],
cwd=location,
)
@classmethod
- def get_repository_root(cls, location):
- # type: (str) -> Optional[str]
+ def get_repository_root(cls, location: str) -> Optional[str]:
loc = super().get_repository_root(location)
if loc:
return loc
try:
r = cls.run_command(
- ['rev-parse', '--show-toplevel'],
+ ["rev-parse", "--show-toplevel"],
cwd=location,
show_stdout=False,
stdout_only=True,
- on_returncode='raise',
+ on_returncode="raise",
log_failed_cmd=False,
)
except BadCommand:
- logger.debug("could not determine if %s is under git control "
- "because git is not available", location)
+ logger.debug(
+ "could not determine if %s is under git control "
+ "because git is not available",
+ location,
+ )
return None
except InstallationError:
return None
- return os.path.normpath(r.rstrip('\r\n'))
+ return os.path.normpath(r.rstrip("\r\n"))
@staticmethod
- def should_add_vcs_url_prefix(repo_url):
- # type: (str) -> bool
- """In either https or ssh form, requirements must be prefixed with git+.
- """
+ def should_add_vcs_url_prefix(repo_url: str) -> bool:
+ """In either https or ssh form, requirements must be prefixed with git+."""
return True
diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py
index 8f8b09bd2..2a005e0af 100644
--- a/src/pip/_internal/vcs/mercurial.py
+++ b/src/pip/_internal/vcs/mercurial.py
@@ -1,7 +1,7 @@
import configparser
import logging
import os
-from typing import List, Optional
+from typing import List, Optional, Tuple
from pip._internal.exceptions import BadCommand, InstallationError
from pip._internal.utils.misc import HiddenText, display_path
@@ -18,61 +18,68 @@ logger = logging.getLogger(__name__)
class Mercurial(VersionControl):
- name = 'hg'
- dirname = '.hg'
- repo_name = 'clone'
+ name = "hg"
+ dirname = ".hg"
+ repo_name = "clone"
schemes = (
- 'hg+file', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http',
+ "hg+file",
+ "hg+http",
+ "hg+https",
+ "hg+ssh",
+ "hg+static-http",
)
@staticmethod
- def get_base_rev_args(rev):
- # type: (str) -> List[str]
+ def get_base_rev_args(rev: str) -> List[str]:
return [rev]
- def fetch_new(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
+ def fetch_new(
+ self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
+ ) -> None:
rev_display = rev_options.to_display()
logger.info(
- 'Cloning hg %s%s to %s',
+ "Cloning hg %s%s to %s",
url,
rev_display,
display_path(dest),
)
- self.run_command(make_command('clone', '--noupdate', '-q', url, dest))
+ if verbosity <= 0:
+ flags: Tuple[str, ...] = ("--quiet",)
+ elif verbosity == 1:
+ flags = ()
+ elif verbosity == 2:
+ flags = ("--verbose",)
+ else:
+ flags = ("--verbose", "--debug")
+ self.run_command(make_command("clone", "--noupdate", *flags, url, dest))
self.run_command(
- make_command('update', '-q', rev_options.to_args()),
+ make_command("update", *flags, rev_options.to_args()),
cwd=dest,
)
- def switch(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
- repo_config = os.path.join(dest, self.dirname, 'hgrc')
+ def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
+ repo_config = os.path.join(dest, self.dirname, "hgrc")
config = configparser.RawConfigParser()
try:
config.read(repo_config)
- config.set('paths', 'default', url.secret)
- with open(repo_config, 'w') as config_file:
+ config.set("paths", "default", url.secret)
+ with open(repo_config, "w") as config_file:
config.write(config_file)
except (OSError, configparser.NoSectionError) as exc:
- logger.warning(
- 'Could not switch Mercurial repository to %s: %s', url, exc,
- )
+ logger.warning("Could not switch Mercurial repository to %s: %s", url, exc)
else:
- cmd_args = make_command('update', '-q', rev_options.to_args())
+ cmd_args = make_command("update", "-q", rev_options.to_args())
self.run_command(cmd_args, cwd=dest)
- def update(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
- self.run_command(['pull', '-q'], cwd=dest)
- cmd_args = make_command('update', '-q', rev_options.to_args())
+ def update(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
+ self.run_command(["pull", "-q"], cwd=dest)
+ cmd_args = make_command("update", "-q", rev_options.to_args())
self.run_command(cmd_args, cwd=dest)
@classmethod
- def get_remote_url(cls, location):
- # type: (str) -> str
+ def get_remote_url(cls, location: str) -> str:
url = cls.run_command(
- ['showconfig', 'paths.default'],
+ ["showconfig", "paths.default"],
show_stdout=False,
stdout_only=True,
cwd=location,
@@ -82,13 +89,12 @@ class Mercurial(VersionControl):
return url.strip()
@classmethod
- def get_revision(cls, location):
- # type: (str) -> str
+ def get_revision(cls, location: str) -> str:
"""
Return the repository-local changeset revision number, as an integer.
"""
current_revision = cls.run_command(
- ['parents', '--template={rev}'],
+ ["parents", "--template={rev}"],
show_stdout=False,
stdout_only=True,
cwd=location,
@@ -96,14 +102,13 @@ class Mercurial(VersionControl):
return current_revision
@classmethod
- def get_requirement_revision(cls, location):
- # type: (str) -> str
+ def get_requirement_revision(cls, location: str) -> str:
"""
Return the changeset identification hash, as a 40-character
hexadecimal string
"""
current_rev_hash = cls.run_command(
- ['parents', '--template={node}'],
+ ["parents", "--template={node}"],
show_stdout=False,
stdout_only=True,
cwd=location,
@@ -111,48 +116,48 @@ class Mercurial(VersionControl):
return current_rev_hash
@classmethod
- def is_commit_id_equal(cls, dest, name):
- # type: (str, Optional[str]) -> bool
+ def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:
"""Always assume the versions don't match"""
return False
@classmethod
- def get_subdirectory(cls, location):
- # type: (str) -> Optional[str]
+ def get_subdirectory(cls, location: str) -> Optional[str]:
"""
Return the path to Python project root, relative to the repo root.
Return None if the project root is in the repo root.
"""
# find the repo root
repo_root = cls.run_command(
- ['root'], show_stdout=False, stdout_only=True, cwd=location
+ ["root"], show_stdout=False, stdout_only=True, cwd=location
).strip()
if not os.path.isabs(repo_root):
repo_root = os.path.abspath(os.path.join(location, repo_root))
return find_path_to_project_root_from_repo_root(location, repo_root)
@classmethod
- def get_repository_root(cls, location):
- # type: (str) -> Optional[str]
+ def get_repository_root(cls, location: str) -> Optional[str]:
loc = super().get_repository_root(location)
if loc:
return loc
try:
r = cls.run_command(
- ['root'],
+ ["root"],
cwd=location,
show_stdout=False,
stdout_only=True,
- on_returncode='raise',
+ on_returncode="raise",
log_failed_cmd=False,
)
except BadCommand:
- logger.debug("could not determine if %s is under hg control "
- "because hg is not available", location)
+ logger.debug(
+ "could not determine if %s is under hg control "
+ "because hg is not available",
+ location,
+ )
return None
except InstallationError:
return None
- return os.path.normpath(r.rstrip('\r\n'))
+ return os.path.normpath(r.rstrip("\r\n"))
vcs.register(Mercurial)
diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py
index 965e0b425..2cd6f0ae9 100644
--- a/src/pip/_internal/vcs/subversion.py
+++ b/src/pip/_internal/vcs/subversion.py
@@ -24,30 +24,25 @@ logger = logging.getLogger(__name__)
_svn_xml_url_re = re.compile('url="([^"]+)"')
_svn_rev_re = re.compile(r'committed-rev="(\d+)"')
_svn_info_xml_rev_re = re.compile(r'\s*revision="(\d+)"')
-_svn_info_xml_url_re = re.compile(r'<url>(.*)</url>')
+_svn_info_xml_url_re = re.compile(r"<url>(.*)</url>")
class Subversion(VersionControl):
- name = 'svn'
- dirname = '.svn'
- repo_name = 'checkout'
- schemes = (
- 'svn+ssh', 'svn+http', 'svn+https', 'svn+svn', 'svn+file'
- )
+ name = "svn"
+ dirname = ".svn"
+ repo_name = "checkout"
+ schemes = ("svn+ssh", "svn+http", "svn+https", "svn+svn", "svn+file")
@classmethod
- def should_add_vcs_url_prefix(cls, remote_url):
- # type: (str) -> bool
+ def should_add_vcs_url_prefix(cls, remote_url: str) -> bool:
return True
@staticmethod
- def get_base_rev_args(rev):
- # type: (str) -> List[str]
- return ['-r', rev]
+ def get_base_rev_args(rev: str) -> List[str]:
+ return ["-r", rev]
@classmethod
- def get_revision(cls, location):
- # type: (str) -> str
+ def get_revision(cls, location: str) -> str:
"""
Return the maximum revision for all files under a given location
"""
@@ -57,9 +52,9 @@ class Subversion(VersionControl):
for base, dirs, _ in os.walk(location):
if cls.dirname not in dirs:
dirs[:] = []
- continue # no sense walking uncontrolled subdirs
+ continue # no sense walking uncontrolled subdirs
dirs.remove(cls.dirname)
- entries_fn = os.path.join(base, cls.dirname, 'entries')
+ entries_fn = os.path.join(base, cls.dirname, "entries")
if not os.path.exists(entries_fn):
# FIXME: should we warn?
continue
@@ -68,21 +63,22 @@ class Subversion(VersionControl):
if base == location:
assert dirurl is not None
- base = dirurl + '/' # save the root url
+ base = dirurl + "/" # save the root url
elif not dirurl or not dirurl.startswith(base):
dirs[:] = []
- continue # not part of the same svn tree, skip it
+ continue # not part of the same svn tree, skip it
revision = max(revision, localrev)
return str(revision)
@classmethod
- def get_netloc_and_auth(cls, netloc, scheme):
- # type: (str, str) -> Tuple[str, Tuple[Optional[str], Optional[str]]]
+ def get_netloc_and_auth(
+ cls, netloc: str, scheme: str
+ ) -> Tuple[str, Tuple[Optional[str], Optional[str]]]:
"""
This override allows the auth information to be passed to svn via the
--username and --password options instead of via the URL.
"""
- if scheme == 'ssh':
+ if scheme == "ssh":
# The --username and --password options can't be used for
# svn+ssh URLs, so keep the auth information in the URL.
return super().get_netloc_and_auth(netloc, scheme)
@@ -90,28 +86,27 @@ class Subversion(VersionControl):
return split_auth_from_netloc(netloc)
@classmethod
- def get_url_rev_and_auth(cls, url):
- # type: (str) -> Tuple[str, Optional[str], AuthInfo]
+ def get_url_rev_and_auth(cls, url: str) -> Tuple[str, Optional[str], AuthInfo]:
# hotfix the URL scheme after removing svn+ from svn+ssh:// readd it
url, rev, user_pass = super().get_url_rev_and_auth(url)
- if url.startswith('ssh://'):
- url = 'svn+' + url
+ if url.startswith("ssh://"):
+ url = "svn+" + url
return url, rev, user_pass
@staticmethod
- def make_rev_args(username, password):
- # type: (Optional[str], Optional[HiddenText]) -> CommandArgs
- extra_args = [] # type: CommandArgs
+ def make_rev_args(
+ username: Optional[str], password: Optional[HiddenText]
+ ) -> CommandArgs:
+ extra_args: CommandArgs = []
if username:
- extra_args += ['--username', username]
+ extra_args += ["--username", username]
if password:
- extra_args += ['--password', password]
+ extra_args += ["--password", password]
return extra_args
@classmethod
- def get_remote_url(cls, location):
- # type: (str) -> str
+ def get_remote_url(cls, location: str) -> str:
# In cases where the source is in a subdirectory, we have to look up in
# the location until we find a valid project root.
orig_location = location
@@ -135,30 +130,27 @@ class Subversion(VersionControl):
return url
@classmethod
- def _get_svn_url_rev(cls, location):
- # type: (str) -> Tuple[Optional[str], int]
+ def _get_svn_url_rev(cls, location: str) -> Tuple[Optional[str], int]:
from pip._internal.exceptions import InstallationError
- entries_path = os.path.join(location, cls.dirname, 'entries')
+ entries_path = os.path.join(location, cls.dirname, "entries")
if os.path.exists(entries_path):
with open(entries_path) as f:
data = f.read()
else: # subversion >= 1.7 does not have the 'entries' file
- data = ''
+ data = ""
url = None
- if (data.startswith('8') or
- data.startswith('9') or
- data.startswith('10')):
- entries = list(map(str.splitlines, data.split('\n\x0c\n')))
+ if data.startswith("8") or data.startswith("9") or data.startswith("10"):
+ entries = list(map(str.splitlines, data.split("\n\x0c\n")))
del entries[0][0] # get rid of the '8'
url = entries[0][3]
revs = [int(d[9]) for d in entries if len(d) > 9 and d[9]] + [0]
- elif data.startswith('<?xml'):
+ elif data.startswith("<?xml"):
match = _svn_xml_url_re.search(data)
if not match:
- raise ValueError(f'Badly formatted data: {data!r}')
- url = match.group(1) # get repository URL
+ raise ValueError(f"Badly formatted data: {data!r}")
+ url = match.group(1) # get repository URL
revs = [int(m.group(1)) for m in _svn_rev_re.finditer(data)] + [0]
else:
try:
@@ -169,16 +161,14 @@ class Subversion(VersionControl):
# is being used to prompt for passwords, because passwords
# are only potentially needed for remote server requests.
xml = cls.run_command(
- ['info', '--xml', location],
+ ["info", "--xml", location],
show_stdout=False,
stdout_only=True,
)
match = _svn_info_xml_url_re.search(xml)
assert match is not None
url = match.group(1)
- revs = [
- int(m.group(1)) for m in _svn_info_xml_rev_re.finditer(xml)
- ]
+ revs = [int(m.group(1)) for m in _svn_info_xml_rev_re.finditer(xml)]
except InstallationError:
url, revs = None, []
@@ -190,13 +180,11 @@ class Subversion(VersionControl):
return url, rev
@classmethod
- def is_commit_id_equal(cls, dest, name):
- # type: (str, Optional[str]) -> bool
+ def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:
"""Always assume the versions don't match"""
return False
- def __init__(self, use_interactive=None):
- # type: (bool) -> None
+ def __init__(self, use_interactive: Optional[bool] = None) -> None:
if use_interactive is None:
use_interactive = is_console_interactive()
self.use_interactive = use_interactive
@@ -206,12 +194,11 @@ class Subversion(VersionControl):
# Special value definitions:
# None: Not evaluated yet.
# Empty tuple: Could not parse version.
- self._vcs_version = None # type: Optional[Tuple[int, ...]]
+ self._vcs_version: Optional[Tuple[int, ...]] = None
super().__init__()
- def call_vcs_version(self):
- # type: () -> Tuple[int, ...]
+ def call_vcs_version(self) -> Tuple[int, ...]:
"""Query the version of the currently installed Subversion client.
:return: A tuple containing the parts of the version information or
@@ -225,15 +212,13 @@ class Subversion(VersionControl):
# compiled Mar 28 2018, 08:49:13 on x86_64-pc-linux-gnu
# svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0)
# compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2
- version_prefix = 'svn, version '
- version = self.run_command(
- ['--version'], show_stdout=False, stdout_only=True
- )
+ version_prefix = "svn, version "
+ version = self.run_command(["--version"], show_stdout=False, stdout_only=True)
if not version.startswith(version_prefix):
return ()
- version = version[len(version_prefix):].split()[0]
- version_list = version.partition('-')[0].split('.')
+ version = version[len(version_prefix) :].split()[0]
+ version_list = version.partition("-")[0].split(".")
try:
parsed_version = tuple(map(int, version_list))
except ValueError:
@@ -241,8 +226,7 @@ class Subversion(VersionControl):
return parsed_version
- def get_vcs_version(self):
- # type: () -> Tuple[int, ...]
+ def get_vcs_version(self) -> Tuple[int, ...]:
"""Return the version of the currently installed Subversion client.
If the version of the Subversion client has already been queried,
@@ -262,8 +246,7 @@ class Subversion(VersionControl):
self._vcs_version = vcs_version
return vcs_version
- def get_remote_call_options(self):
- # type: () -> CommandArgs
+ def get_remote_call_options(self) -> CommandArgs:
"""Return options to be used on calls to Subversion that contact the server.
These options are applicable for the following ``svn`` subcommands used
@@ -278,7 +261,7 @@ class Subversion(VersionControl):
if not self.use_interactive:
# --non-interactive switch is available since Subversion 0.14.4.
# Subversion < 1.8 runs in interactive mode by default.
- return ['--non-interactive']
+ return ["--non-interactive"]
svn_version = self.get_vcs_version()
# By default, Subversion >= 1.8 runs in non-interactive mode if
@@ -290,37 +273,49 @@ class Subversion(VersionControl):
# SVN 1.7, pip should continue to support SVN 1.7. Therefore, pip
# can't safely add the option if the SVN version is < 1.8 (or unknown).
if svn_version >= (1, 8):
- return ['--force-interactive']
+ return ["--force-interactive"]
return []
- def fetch_new(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
+ def fetch_new(
+ self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
+ ) -> None:
rev_display = rev_options.to_display()
logger.info(
- 'Checking out %s%s to %s',
+ "Checking out %s%s to %s",
url,
rev_display,
display_path(dest),
)
+ if verbosity <= 0:
+ flag = "--quiet"
+ else:
+ flag = ""
cmd_args = make_command(
- 'checkout', '-q', self.get_remote_call_options(),
- rev_options.to_args(), url, dest,
+ "checkout",
+ flag,
+ self.get_remote_call_options(),
+ rev_options.to_args(),
+ url,
+ dest,
)
self.run_command(cmd_args)
- def switch(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
+ def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
cmd_args = make_command(
- 'switch', self.get_remote_call_options(), rev_options.to_args(),
- url, dest,
+ "switch",
+ self.get_remote_call_options(),
+ rev_options.to_args(),
+ url,
+ dest,
)
self.run_command(cmd_args)
- def update(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
+ def update(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
cmd_args = make_command(
- 'update', self.get_remote_call_options(), rev_options.to_args(),
+ "update",
+ self.get_remote_call_options(),
+ rev_options.to_args(),
dest,
)
self.run_command(cmd_args)
diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py
index cddd78c5e..02bbf68e7 100644
--- a/src/pip/_internal/vcs/versioncontrol.py
+++ b/src/pip/_internal/vcs/versioncontrol.py
@@ -6,6 +6,7 @@ import shutil
import sys
import urllib.parse
from typing import (
+ TYPE_CHECKING,
Any,
Dict,
Iterable,
@@ -30,10 +31,22 @@ from pip._internal.utils.misc import (
is_installable_dir,
rmtree,
)
-from pip._internal.utils.subprocess import CommandArgs, call_subprocess, make_command
+from pip._internal.utils.subprocess import (
+ CommandArgs,
+ call_subprocess,
+ format_command_args,
+ make_command,
+)
from pip._internal.utils.urls import get_url_scheme
-__all__ = ['vcs']
+if TYPE_CHECKING:
+ # Literal was introduced in Python 3.8.
+ #
+ # TODO: Remove `if TYPE_CHECKING` when dropping support for Python 3.7.
+ from typing import Literal
+
+
+__all__ = ["vcs"]
logger = logging.getLogger(__name__)
@@ -41,19 +54,19 @@ logger = logging.getLogger(__name__)
AuthInfo = Tuple[Optional[str], Optional[str]]
-def is_url(name):
- # type: (str) -> bool
+def is_url(name: str) -> bool:
"""
Return true if the name looks like a URL.
"""
scheme = get_url_scheme(name)
if scheme is None:
return False
- return scheme in ['http', 'https', 'file', 'ftp'] + vcs.all_schemes
+ return scheme in ["http", "https", "file", "ftp"] + vcs.all_schemes
-def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None):
- # type: (str, str, str, Optional[str]) -> str
+def make_vcs_requirement_url(
+ repo_url: str, rev: str, project_name: str, subdir: Optional[str] = None
+) -> str:
"""
Return the URL for a VCS requirement.
@@ -62,15 +75,16 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None):
project_name: the (unescaped) project name.
"""
egg_project_name = project_name.replace("-", "_")
- req = f'{repo_url}@{rev}#egg={egg_project_name}'
+ req = f"{repo_url}@{rev}#egg={egg_project_name}"
if subdir:
- req += f'&subdirectory={subdir}'
+ req += f"&subdirectory={subdir}"
return req
-def find_path_to_project_root_from_repo_root(location, repo_root):
- # type: (str, str) -> Optional[str]
+def find_path_to_project_root_from_repo_root(
+ location: str, repo_root: str
+) -> Optional[str]:
"""
Find the the Python project's root by searching up the filesystem from
`location`. Return the path to project root relative to `repo_root`.
@@ -118,11 +132,10 @@ class RevOptions:
def __init__(
self,
- vc_class, # type: Type[VersionControl]
- rev=None, # type: Optional[str]
- extra_args=None, # type: Optional[CommandArgs]
- ):
- # type: (...) -> None
+ vc_class: Type["VersionControl"],
+ rev: Optional[str] = None,
+ extra_args: Optional[CommandArgs] = None,
+ ) -> None:
"""
Args:
vc_class: a VersionControl subclass.
@@ -135,26 +148,23 @@ class RevOptions:
self.extra_args = extra_args
self.rev = rev
self.vc_class = vc_class
- self.branch_name = None # type: Optional[str]
+ self.branch_name: Optional[str] = None
- def __repr__(self):
- # type: () -> str
- return f'<RevOptions {self.vc_class.name}: rev={self.rev!r}>'
+ def __repr__(self) -> str:
+ return f"<RevOptions {self.vc_class.name}: rev={self.rev!r}>"
@property
- def arg_rev(self):
- # type: () -> Optional[str]
+ def arg_rev(self) -> Optional[str]:
if self.rev is None:
return self.vc_class.default_arg_rev
return self.rev
- def to_args(self):
- # type: () -> CommandArgs
+ def to_args(self) -> CommandArgs:
"""
Return the VCS-specific command arguments.
"""
- args = [] # type: CommandArgs
+ args: CommandArgs = []
rev = self.arg_rev
if rev is not None:
args += self.vc_class.get_base_rev_args(rev)
@@ -162,15 +172,13 @@ class RevOptions:
return args
- def to_display(self):
- # type: () -> str
+ def to_display(self) -> str:
if not self.rev:
- return ''
+ return ""
- return f' (to revision {self.rev})'
+ return f" (to revision {self.rev})"
- def make_new(self, rev):
- # type: (str) -> RevOptions
+ def make_new(self, rev: str) -> "RevOptions":
"""
Make a copy of the current instance, but with a new rev.
@@ -181,54 +189,46 @@ class RevOptions:
class VcsSupport:
- _registry = {} # type: Dict[str, VersionControl]
- schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn']
+ _registry: Dict[str, "VersionControl"] = {}
+ schemes = ["ssh", "git", "hg", "bzr", "sftp", "svn"]
- def __init__(self):
- # type: () -> None
+ def __init__(self) -> None:
# Register more schemes with urlparse for various version control
# systems
urllib.parse.uses_netloc.extend(self.schemes)
super().__init__()
- def __iter__(self):
- # type: () -> Iterator[str]
+ def __iter__(self) -> Iterator[str]:
return self._registry.__iter__()
@property
- def backends(self):
- # type: () -> List[VersionControl]
+ def backends(self) -> List["VersionControl"]:
return list(self._registry.values())
@property
- def dirnames(self):
- # type: () -> List[str]
+ def dirnames(self) -> List[str]:
return [backend.dirname for backend in self.backends]
@property
- def all_schemes(self):
- # type: () -> List[str]
- schemes = [] # type: List[str]
+ def all_schemes(self) -> List[str]:
+ schemes: List[str] = []
for backend in self.backends:
schemes.extend(backend.schemes)
return schemes
- def register(self, cls):
- # type: (Type[VersionControl]) -> None
- if not hasattr(cls, 'name'):
- logger.warning('Cannot register VCS %s', cls.__name__)
+ def register(self, cls: Type["VersionControl"]) -> None:
+ if not hasattr(cls, "name"):
+ logger.warning("Cannot register VCS %s", cls.__name__)
return
if cls.name not in self._registry:
self._registry[cls.name] = cls()
- logger.debug('Registered VCS backend: %s', cls.name)
+ logger.debug("Registered VCS backend: %s", cls.name)
- def unregister(self, name):
- # type: (str) -> None
+ def unregister(self, name: str) -> None:
if name in self._registry:
del self._registry[name]
- def get_backend_for_dir(self, location):
- # type: (str) -> Optional[VersionControl]
+ def get_backend_for_dir(self, location: str) -> Optional["VersionControl"]:
"""
Return a VersionControl object if a repository of that type is found
at the given directory.
@@ -238,8 +238,7 @@ class VcsSupport:
repo_path = vcs_backend.get_repository_root(location)
if not repo_path:
continue
- logger.debug('Determine that %s uses VCS: %s',
- location, vcs_backend.name)
+ logger.debug("Determine that %s uses VCS: %s", location, vcs_backend.name)
vcs_backends[repo_path] = vcs_backend
if not vcs_backends:
@@ -252,8 +251,7 @@ class VcsSupport:
inner_most_repo_path = max(vcs_backends, key=len)
return vcs_backends[inner_most_repo_path]
- def get_backend_for_scheme(self, scheme):
- # type: (str) -> Optional[VersionControl]
+ def get_backend_for_scheme(self, scheme: str) -> Optional["VersionControl"]:
"""
Return a VersionControl object or None.
"""
@@ -262,8 +260,7 @@ class VcsSupport:
return vcs_backend
return None
- def get_backend(self, name):
- # type: (str) -> Optional[VersionControl]
+ def get_backend(self, name: str) -> Optional["VersionControl"]:
"""
Return a VersionControl object or None.
"""
@@ -275,27 +272,25 @@ vcs = VcsSupport()
class VersionControl:
- name = ''
- dirname = ''
- repo_name = ''
+ name = ""
+ dirname = ""
+ repo_name = ""
# List of supported schemes for this Version Control
- schemes = () # type: Tuple[str, ...]
+ schemes: Tuple[str, ...] = ()
# Iterable of environment variable names to pass to call_subprocess().
- unset_environ = () # type: Tuple[str, ...]
- default_arg_rev = None # type: Optional[str]
+ unset_environ: Tuple[str, ...] = ()
+ default_arg_rev: Optional[str] = None
@classmethod
- def should_add_vcs_url_prefix(cls, remote_url):
- # type: (str) -> bool
+ def should_add_vcs_url_prefix(cls, remote_url: str) -> bool:
"""
Return whether the vcs prefix (e.g. "git+") should be added to a
repository's remote url when used in a requirement.
"""
- return not remote_url.lower().startswith(f'{cls.name}:')
+ return not remote_url.lower().startswith(f"{cls.name}:")
@classmethod
- def get_subdirectory(cls, location):
- # type: (str) -> Optional[str]
+ def get_subdirectory(cls, location: str) -> Optional[str]:
"""
Return the path to Python project root, relative to the repo root.
Return None if the project root is in the repo root.
@@ -303,16 +298,14 @@ class VersionControl:
return None
@classmethod
- def get_requirement_revision(cls, repo_dir):
- # type: (str) -> str
+ def get_requirement_revision(cls, repo_dir: str) -> str:
"""
Return the revision string that should be used in a requirement.
"""
return cls.get_revision(repo_dir)
@classmethod
- def get_src_requirement(cls, repo_dir, project_name):
- # type: (str, str) -> str
+ def get_src_requirement(cls, repo_dir: str, project_name: str) -> str:
"""
Return the requirement string to use to redownload the files
currently at the given repository directory.
@@ -327,18 +320,16 @@ class VersionControl:
repo_url = cls.get_remote_url(repo_dir)
if cls.should_add_vcs_url_prefix(repo_url):
- repo_url = f'{cls.name}+{repo_url}'
+ repo_url = f"{cls.name}+{repo_url}"
revision = cls.get_requirement_revision(repo_dir)
subdir = cls.get_subdirectory(repo_dir)
- req = make_vcs_requirement_url(repo_url, revision, project_name,
- subdir=subdir)
+ req = make_vcs_requirement_url(repo_url, revision, project_name, subdir=subdir)
return req
@staticmethod
- def get_base_rev_args(rev):
- # type: (str) -> List[str]
+ def get_base_rev_args(rev: str) -> List[str]:
"""
Return the base revision arguments for a vcs command.
@@ -347,8 +338,7 @@ class VersionControl:
"""
raise NotImplementedError
- def is_immutable_rev_checkout(self, url, dest):
- # type: (str, str) -> bool
+ def is_immutable_rev_checkout(self, url: str, dest: str) -> bool:
"""
Return true if the commit hash checked out at dest matches
the revision in url.
@@ -362,8 +352,9 @@ class VersionControl:
return False
@classmethod
- def make_rev_options(cls, rev=None, extra_args=None):
- # type: (Optional[str], Optional[CommandArgs]) -> RevOptions
+ def make_rev_options(
+ cls, rev: Optional[str] = None, extra_args: Optional[CommandArgs] = None
+ ) -> RevOptions:
"""
Return a RevOptions object.
@@ -374,18 +365,18 @@ class VersionControl:
return RevOptions(cls, rev, extra_args=extra_args)
@classmethod
- def _is_local_repository(cls, repo):
- # type: (str) -> bool
+ def _is_local_repository(cls, repo: str) -> bool:
"""
- posix absolute paths start with os.path.sep,
- win32 ones start with drive (like c:\\folder)
+ posix absolute paths start with os.path.sep,
+ win32 ones start with drive (like c:\\folder)
"""
drive, tail = os.path.splitdrive(repo)
return repo.startswith(os.path.sep) or bool(drive)
@classmethod
- def get_netloc_and_auth(cls, netloc, scheme):
- # type: (str, str) -> Tuple[str, Tuple[Optional[str], Optional[str]]]
+ def get_netloc_and_auth(
+ cls, netloc: str, scheme: str
+ ) -> Tuple[str, Tuple[Optional[str], Optional[str]]]:
"""
Parse the repository URL's netloc, and return the new netloc to use
along with auth information.
@@ -404,8 +395,7 @@ class VersionControl:
return netloc, (None, None)
@classmethod
- def get_url_rev_and_auth(cls, url):
- # type: (str) -> Tuple[str, Optional[str], AuthInfo]
+ def get_url_rev_and_auth(cls, url: str) -> Tuple[str, Optional[str], AuthInfo]:
"""
Parse the repository URL to use, and return the URL, revision,
and auth info to use.
@@ -413,44 +403,44 @@ class VersionControl:
Returns: (url, rev, (username, password)).
"""
scheme, netloc, path, query, frag = urllib.parse.urlsplit(url)
- if '+' not in scheme:
+ if "+" not in scheme:
raise ValueError(
"Sorry, {!r} is a malformed VCS url. "
"The format is <vcs>+<protocol>://<url>, "
"e.g. svn+http://myrepo/svn/MyApp#egg=MyApp".format(url)
)
# Remove the vcs prefix.
- scheme = scheme.split('+', 1)[1]
+ scheme = scheme.split("+", 1)[1]
netloc, user_pass = cls.get_netloc_and_auth(netloc, scheme)
rev = None
- if '@' in path:
- path, rev = path.rsplit('@', 1)
+ if "@" in path:
+ path, rev = path.rsplit("@", 1)
if not rev:
raise InstallationError(
"The URL {!r} has an empty revision (after @) "
"which is not supported. Include a revision after @ "
"or remove @ from the URL.".format(url)
)
- url = urllib.parse.urlunsplit((scheme, netloc, path, query, ''))
+ url = urllib.parse.urlunsplit((scheme, netloc, path, query, ""))
return url, rev, user_pass
@staticmethod
- def make_rev_args(username, password):
- # type: (Optional[str], Optional[HiddenText]) -> CommandArgs
+ def make_rev_args(
+ username: Optional[str], password: Optional[HiddenText]
+ ) -> CommandArgs:
"""
Return the RevOptions "extra arguments" to use in obtain().
"""
return []
- def get_url_rev_options(self, url):
- # type: (HiddenText) -> Tuple[HiddenText, RevOptions]
+ def get_url_rev_options(self, url: HiddenText) -> Tuple[HiddenText, RevOptions]:
"""
Return the URL and RevOptions object to use in obtain(),
as a tuple (url, rev_options).
"""
secret_url, rev, user_pass = self.get_url_rev_and_auth(url.secret)
username, secret_password = user_pass
- password = None # type: Optional[HiddenText]
+ password: Optional[HiddenText] = None
if secret_password is not None:
password = hide_value(secret_password)
extra_args = self.make_rev_args(username, password)
@@ -459,24 +449,23 @@ class VersionControl:
return hide_url(secret_url), rev_options
@staticmethod
- def normalize_url(url):
- # type: (str) -> str
+ def normalize_url(url: str) -> str:
"""
Normalize a URL for comparison by unquoting it and removing any
trailing slash.
"""
- return urllib.parse.unquote(url).rstrip('/')
+ return urllib.parse.unquote(url).rstrip("/")
@classmethod
- def compare_urls(cls, url1, url2):
- # type: (str, str) -> bool
+ def compare_urls(cls, url1: str, url2: str) -> bool:
"""
Compare two repo URLs for identity, ignoring incidental differences.
"""
- return (cls.normalize_url(url1) == cls.normalize_url(url2))
+ return cls.normalize_url(url1) == cls.normalize_url(url2)
- def fetch_new(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
+ def fetch_new(
+ self, dest: str, url: HiddenText, rev_options: RevOptions, verbosity: int
+ ) -> None:
"""
Fetch a revision from a repository, in the case that this is the
first fetch from the repository.
@@ -484,11 +473,11 @@ class VersionControl:
Args:
dest: the directory to fetch the repository to.
rev_options: a RevOptions object.
+ verbosity: verbosity level.
"""
raise NotImplementedError
- def switch(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
+ def switch(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
"""
Switch the repo at ``dest`` to point to ``URL``.
@@ -497,8 +486,7 @@ class VersionControl:
"""
raise NotImplementedError
- def update(self, dest, url, rev_options):
- # type: (str, HiddenText, RevOptions) -> None
+ def update(self, dest: str, url: HiddenText, rev_options: RevOptions) -> None:
"""
Update an already-existing repo to the given ``rev_options``.
@@ -508,8 +496,7 @@ class VersionControl:
raise NotImplementedError
@classmethod
- def is_commit_id_equal(cls, dest, name):
- # type: (str, Optional[str]) -> bool
+ def is_commit_id_equal(cls, dest: str, name: Optional[str]) -> bool:
"""
Return whether the id of the current commit equals the given name.
@@ -519,19 +506,19 @@ class VersionControl:
"""
raise NotImplementedError
- def obtain(self, dest, url):
- # type: (str, HiddenText) -> None
+ def obtain(self, dest: str, url: HiddenText, verbosity: int) -> None:
"""
Install or update in editable mode the package represented by this
VersionControl object.
:param dest: the repository directory in which to install or update.
:param url: the repository URL starting with a vcs prefix.
+ :param verbosity: verbosity level.
"""
url, rev_options = self.get_url_rev_options(url)
if not os.path.exists(dest):
- self.fetch_new(dest, url, rev_options)
+ self.fetch_new(dest, url, rev_options, verbosity=verbosity)
return
rev_display = rev_options.to_display()
@@ -539,73 +526,68 @@ class VersionControl:
existing_url = self.get_remote_url(dest)
if self.compare_urls(existing_url, url.secret):
logger.debug(
- '%s in %s exists, and has correct URL (%s)',
+ "%s in %s exists, and has correct URL (%s)",
self.repo_name.title(),
display_path(dest),
url,
)
if not self.is_commit_id_equal(dest, rev_options.rev):
logger.info(
- 'Updating %s %s%s',
+ "Updating %s %s%s",
display_path(dest),
self.repo_name,
rev_display,
)
self.update(dest, url, rev_options)
else:
- logger.info('Skipping because already up-to-date.')
+ logger.info("Skipping because already up-to-date.")
return
logger.warning(
- '%s %s in %s exists with URL %s',
+ "%s %s in %s exists with URL %s",
self.name,
self.repo_name,
display_path(dest),
existing_url,
)
- prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ',
- ('s', 'i', 'w', 'b'))
+ prompt = ("(s)witch, (i)gnore, (w)ipe, (b)ackup ", ("s", "i", "w", "b"))
else:
logger.warning(
- 'Directory %s already exists, and is not a %s %s.',
+ "Directory %s already exists, and is not a %s %s.",
dest,
self.name,
self.repo_name,
)
# https://github.com/python/mypy/issues/1174
- prompt = ('(i)gnore, (w)ipe, (b)ackup ', # type: ignore
- ('i', 'w', 'b'))
+ prompt = ("(i)gnore, (w)ipe, (b)ackup ", ("i", "w", "b")) # type: ignore
logger.warning(
- 'The plan is to install the %s repository %s',
+ "The plan is to install the %s repository %s",
self.name,
url,
)
- response = ask_path_exists('What to do? {}'.format(
- prompt[0]), prompt[1])
+ response = ask_path_exists("What to do? {}".format(prompt[0]), prompt[1])
- if response == 'a':
+ if response == "a":
sys.exit(-1)
- if response == 'w':
- logger.warning('Deleting %s', display_path(dest))
+ if response == "w":
+ logger.warning("Deleting %s", display_path(dest))
rmtree(dest)
- self.fetch_new(dest, url, rev_options)
+ self.fetch_new(dest, url, rev_options, verbosity=verbosity)
return
- if response == 'b':
+ if response == "b":
dest_dir = backup_dir(dest)
- logger.warning(
- 'Backing up %s to %s', display_path(dest), dest_dir,
- )
+ logger.warning("Backing up %s to %s", display_path(dest), dest_dir)
shutil.move(dest, dest_dir)
- self.fetch_new(dest, url, rev_options)
+ self.fetch_new(dest, url, rev_options, verbosity=verbosity)
return
# Do nothing if the response is "i".
- if response == 's':
+ if response == "s":
logger.info(
- 'Switching %s %s to %s%s',
+ "Switching %s %s to %s%s",
self.repo_name,
display_path(dest),
url,
@@ -613,21 +595,20 @@ class VersionControl:
)
self.switch(dest, url, rev_options)
- def unpack(self, location, url):
- # type: (str, HiddenText) -> None
+ def unpack(self, location: str, url: HiddenText, verbosity: int) -> None:
"""
Clean up current location and download the url repository
(and vcs infos) into location
:param url: the repository URL starting with a vcs prefix.
+ :param verbosity: verbosity level.
"""
if os.path.exists(location):
rmtree(location)
- self.obtain(location, url=url)
+ self.obtain(location, url=url, verbosity=verbosity)
@classmethod
- def get_remote_url(cls, location):
- # type: (str) -> str
+ def get_remote_url(cls, location: str) -> str:
"""
Return the url used at location
@@ -637,8 +618,7 @@ class VersionControl:
raise NotImplementedError
@classmethod
- def get_revision(cls, location):
- # type: (str) -> str
+ def get_revision(cls, location: str) -> str:
"""
Return the current commit id of the files at the given location.
"""
@@ -647,40 +627,46 @@ class VersionControl:
@classmethod
def run_command(
cls,
- cmd, # type: Union[List[str], CommandArgs]
- show_stdout=True, # type: bool
- cwd=None, # type: Optional[str]
- on_returncode='raise', # type: str
- extra_ok_returncodes=None, # type: Optional[Iterable[int]]
- command_desc=None, # type: Optional[str]
- extra_environ=None, # type: Optional[Mapping[str, Any]]
- spinner=None, # type: Optional[SpinnerInterface]
- log_failed_cmd=True, # type: bool
- stdout_only=False, # type: bool
- ):
- # type: (...) -> str
+ cmd: Union[List[str], CommandArgs],
+ show_stdout: bool = True,
+ cwd: Optional[str] = None,
+ on_returncode: 'Literal["raise", "warn", "ignore"]' = "raise",
+ extra_ok_returncodes: Optional[Iterable[int]] = None,
+ command_desc: Optional[str] = None,
+ extra_environ: Optional[Mapping[str, Any]] = None,
+ spinner: Optional[SpinnerInterface] = None,
+ log_failed_cmd: bool = True,
+ stdout_only: bool = False,
+ ) -> str:
"""
Run a VCS subcommand
This is simply a wrapper around call_subprocess that adds the VCS
command name, and checks that the VCS is available
"""
cmd = make_command(cls.name, *cmd)
+ if command_desc is None:
+ command_desc = format_command_args(cmd)
try:
- return call_subprocess(cmd, show_stdout, cwd,
- on_returncode=on_returncode,
- extra_ok_returncodes=extra_ok_returncodes,
- command_desc=command_desc,
- extra_environ=extra_environ,
- unset_environ=cls.unset_environ,
- spinner=spinner,
- log_failed_cmd=log_failed_cmd,
- stdout_only=stdout_only)
+ return call_subprocess(
+ cmd,
+ show_stdout,
+ cwd,
+ on_returncode=on_returncode,
+ extra_ok_returncodes=extra_ok_returncodes,
+ command_desc=command_desc,
+ extra_environ=extra_environ,
+ unset_environ=cls.unset_environ,
+ spinner=spinner,
+ log_failed_cmd=log_failed_cmd,
+ stdout_only=stdout_only,
+ )
except FileNotFoundError:
# errno.ENOENT = no such file or directory
# In other words, the VCS executable isn't available
raise BadCommand(
- f'Cannot find command {cls.name!r} - do you have '
- f'{cls.name!r} installed and in your PATH?')
+ f"Cannot find command {cls.name!r} - do you have "
+ f"{cls.name!r} installed and in your PATH?"
+ )
except PermissionError:
# errno.EACCES = Permission denied
# This error occurs, for instance, when the command is installed
@@ -695,18 +681,15 @@ class VersionControl:
)
@classmethod
- def is_repository_directory(cls, path):
- # type: (str) -> bool
+ def is_repository_directory(cls, path: str) -> bool:
"""
Return whether a directory path is a repository directory.
"""
- logger.debug('Checking in %s for %s (%s)...',
- path, cls.dirname, cls.name)
+ logger.debug("Checking in %s for %s (%s)...", path, cls.dirname, cls.name)
return os.path.exists(os.path.join(path, cls.dirname))
@classmethod
- def get_repository_root(cls, location):
- # type: (str) -> Optional[str]
+ def get_repository_root(cls, location: str) -> Optional[str]:
"""
Return the "root" (top-level) directory controlled by the vcs,
or `None` if the directory is not in any.
diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py
index 92f172bca..60db28e92 100644
--- a/src/pip/_internal/wheel_builder.py
+++ b/src/pip/_internal/wheel_builder.py
@@ -5,19 +5,21 @@ import logging
import os.path
import re
import shutil
-from typing import Any, Callable, Iterable, List, Optional, Tuple
+from typing import Callable, Iterable, List, Optional, Tuple
from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version
from pip._vendor.packaging.version import InvalidVersion, Version
from pip._internal.cache import WheelCache
from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel
-from pip._internal.metadata import get_wheel_distribution
+from pip._internal.metadata import FilesystemWheel, get_wheel_distribution
from pip._internal.models.link import Link
from pip._internal.models.wheel import Wheel
from pip._internal.operations.build.wheel import build_wheel_pep517
+from pip._internal.operations.build.wheel_editable import build_wheel_editable
from pip._internal.operations.build.wheel_legacy import build_wheel_legacy
from pip._internal.req.req_install import InstallRequirement
+from pip._internal.utils.deprecation import LegacyInstallReasonMissingWheelPackage
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import ensure_dir, hash_file, is_wheel_installed
from pip._internal.utils.setuptools_build import make_setuptools_clean_args
@@ -28,14 +30,13 @@ from pip._internal.vcs import vcs
logger = logging.getLogger(__name__)
-_egg_info_re = re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.IGNORECASE)
+_egg_info_re = re.compile(r"([a-z0-9_.]+)-([a-z0-9_.!+-]+)", re.IGNORECASE)
-BinaryAllowedPredicate = Callable[[InstallRequirement], bool]
+BdistWheelAllowedPredicate = Callable[[InstallRequirement], bool]
BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]]
-def _contains_egg_info(s):
- # type: (str) -> bool
+def _contains_egg_info(s: str) -> bool:
"""Determine whether the string looks like an egg_info.
:param s: The string to parse. E.g. foo-2.1
@@ -44,11 +45,10 @@ def _contains_egg_info(s):
def _should_build(
- req, # type: InstallRequirement
- need_wheel, # type: bool
- check_binary_allowed, # type: BinaryAllowedPredicate
-):
- # type: (...) -> bool
+ req: InstallRequirement,
+ need_wheel: bool,
+ check_bdist_wheel: Optional[BdistWheelAllowedPredicate] = None,
+) -> bool:
"""Return whether an InstallRequirement should be built into a wheel."""
if req.constraint:
# never build requirements that are merely constraints
@@ -56,7 +56,8 @@ def _should_build(
if req.is_wheel:
if need_wheel:
logger.info(
- 'Skipping %s, due to already being wheel.', req.name,
+ "Skipping %s, due to already being wheel.",
+ req.name,
)
return False
@@ -67,53 +68,50 @@ def _should_build(
# From this point, this concerns the pip install command only
# (need_wheel=False).
- if req.editable or not req.source_dir:
+ if not req.source_dir:
return False
+ if req.editable:
+ # we only build PEP 660 editable requirements
+ return req.supports_pyproject_editable()
+
if req.use_pep517:
return True
- if not check_binary_allowed(req):
+ assert check_bdist_wheel is not None
+ if not check_bdist_wheel(req):
logger.info(
- "Skipping wheel build for %s, due to binaries "
- "being disabled for it.", req.name,
+ "Skipping wheel build for %s, due to binaries being disabled for it.",
+ req.name,
)
return False
if not is_wheel_installed():
# we don't build legacy requirements if wheel is not installed
- logger.info(
- "Using legacy 'setup.py install' for %s, "
- "since package 'wheel' is not installed.", req.name,
- )
+ req.legacy_install_reason = LegacyInstallReasonMissingWheelPackage
return False
return True
def should_build_for_wheel_command(
- req, # type: InstallRequirement
-):
- # type: (...) -> bool
- return _should_build(
- req, need_wheel=True, check_binary_allowed=_always_true
- )
+ req: InstallRequirement,
+) -> bool:
+ return _should_build(req, need_wheel=True)
def should_build_for_install_command(
- req, # type: InstallRequirement
- check_binary_allowed, # type: BinaryAllowedPredicate
-):
- # type: (...) -> bool
+ req: InstallRequirement,
+ check_bdist_wheel_allowed: BdistWheelAllowedPredicate,
+) -> bool:
return _should_build(
- req, need_wheel=False, check_binary_allowed=check_binary_allowed
+ req, need_wheel=False, check_bdist_wheel=check_bdist_wheel_allowed
)
def _should_cache(
- req, # type: InstallRequirement
-):
- # type: (...) -> Optional[bool]
+ req: InstallRequirement,
+) -> Optional[bool]:
"""
Return whether a built InstallRequirement can be stored in the persistent
wheel cache, assuming the wheel cache is available, and _should_build()
@@ -144,10 +142,9 @@ def _should_cache(
def _get_cache_dir(
- req, # type: InstallRequirement
- wheel_cache, # type: WheelCache
-):
- # type: (...) -> str
+ req: InstallRequirement,
+ wheel_cache: WheelCache,
+) -> str:
"""Return the persistent or temporary cache directory where the built
wheel need to be stored.
"""
@@ -160,13 +157,7 @@ def _get_cache_dir(
return cache_dir
-def _always_true(_):
- # type: (Any) -> bool
- return True
-
-
-def _verify_one(req, wheel_path):
- # type: (InstallRequirement, str) -> None
+def _verify_one(req: InstallRequirement, wheel_path: str) -> None:
canonical_name = canonicalize_name(req.name or "")
w = Wheel(os.path.basename(wheel_path))
if canonicalize_name(w.name) != canonical_name:
@@ -174,7 +165,7 @@ def _verify_one(req, wheel_path):
"Wheel has unexpected file name: expected {!r}, "
"got {!r}".format(canonical_name, w.name),
)
- dist = get_wheel_distribution(wheel_path, canonical_name)
+ dist = get_wheel_distribution(FilesystemWheel(wheel_path), canonical_name)
dist_verstr = str(dist.version)
if canonicalize_version(dist_verstr) != canonicalize_version(w.version):
raise InvalidWheelFilename(
@@ -189,8 +180,7 @@ def _verify_one(req, wheel_path):
except InvalidVersion:
msg = f"Invalid Metadata-Version: {metadata_version_value}"
raise UnsupportedWheel(msg)
- if (metadata_version >= Version("1.2")
- and not isinstance(dist.version, Version)):
+ if metadata_version >= Version("1.2") and not isinstance(dist.version, Version):
raise UnsupportedWheel(
"Metadata 1.2 mandates PEP 440 version, "
"but {!r} is not".format(dist_verstr)
@@ -198,47 +188,50 @@ def _verify_one(req, wheel_path):
def _build_one(
- req, # type: InstallRequirement
- output_dir, # type: str
- verify, # type: bool
- build_options, # type: List[str]
- global_options, # type: List[str]
-):
- # type: (...) -> Optional[str]
+ req: InstallRequirement,
+ output_dir: str,
+ verify: bool,
+ build_options: List[str],
+ global_options: List[str],
+ editable: bool,
+) -> Optional[str]:
"""Build one wheel.
:return: The filename of the built wheel, or None if the build failed.
"""
+ artifact = "editable" if editable else "wheel"
try:
ensure_dir(output_dir)
except OSError as e:
logger.warning(
- "Building wheel for %s failed: %s",
- req.name, e,
+ "Building %s for %s failed: %s",
+ artifact,
+ req.name,
+ e,
)
return None
# Install build deps into temporary directory (PEP 518)
with req.build_env:
wheel_path = _build_one_inside_env(
- req, output_dir, build_options, global_options
+ req, output_dir, build_options, global_options, editable
)
if wheel_path and verify:
try:
_verify_one(req, wheel_path)
except (InvalidWheelFilename, UnsupportedWheel) as e:
- logger.warning("Built wheel for %s is invalid: %s", req.name, e)
+ logger.warning("Built %s for %s is invalid: %s", artifact, req.name, e)
return None
return wheel_path
def _build_one_inside_env(
- req, # type: InstallRequirement
- output_dir, # type: str
- build_options, # type: List[str]
- global_options, # type: List[str]
-):
- # type: (...) -> Optional[str]
+ req: InstallRequirement,
+ output_dir: str,
+ build_options: List[str],
+ global_options: List[str],
+ editable: bool,
+) -> Optional[str]:
with TempDirectory(kind="wheel") as temp_dir:
assert req.name
if req.use_pep517:
@@ -246,18 +239,26 @@ def _build_one_inside_env(
assert req.pep517_backend
if global_options:
logger.warning(
- 'Ignoring --global-option when building %s using PEP 517', req.name
+ "Ignoring --global-option when building %s using PEP 517", req.name
)
if build_options:
logger.warning(
- 'Ignoring --build-option when building %s using PEP 517', req.name
+ "Ignoring --build-option when building %s using PEP 517", req.name
+ )
+ if editable:
+ wheel_path = build_wheel_editable(
+ name=req.name,
+ backend=req.pep517_backend,
+ metadata_directory=req.metadata_directory,
+ tempd=temp_dir.path,
+ )
+ else:
+ wheel_path = build_wheel_pep517(
+ name=req.name,
+ backend=req.pep517_backend,
+ metadata_directory=req.metadata_directory,
+ tempd=temp_dir.path,
)
- wheel_path = build_wheel_pep517(
- name=req.name,
- backend=req.pep517_backend,
- metadata_directory=req.metadata_directory,
- tempd=temp_dir.path,
- )
else:
wheel_path = build_wheel_legacy(
name=req.name,
@@ -274,16 +275,20 @@ def _build_one_inside_env(
try:
wheel_hash, length = hash_file(wheel_path)
shutil.move(wheel_path, dest_path)
- logger.info('Created wheel for %s: '
- 'filename=%s size=%d sha256=%s',
- req.name, wheel_name, length,
- wheel_hash.hexdigest())
- logger.info('Stored in directory: %s', output_dir)
+ logger.info(
+ "Created wheel for %s: filename=%s size=%d sha256=%s",
+ req.name,
+ wheel_name,
+ length,
+ wheel_hash.hexdigest(),
+ )
+ logger.info("Stored in directory: %s", output_dir)
return dest_path
except Exception as e:
logger.warning(
"Building wheel for %s failed: %s",
- req.name, e,
+ req.name,
+ e,
)
# Ignore return, we can't do anything else useful.
if not req.use_pep517:
@@ -291,30 +296,30 @@ def _build_one_inside_env(
return None
-def _clean_one_legacy(req, global_options):
- # type: (InstallRequirement, List[str]) -> bool
+def _clean_one_legacy(req: InstallRequirement, global_options: List[str]) -> bool:
clean_args = make_setuptools_clean_args(
req.setup_py_path,
global_options=global_options,
)
- logger.info('Running setup.py clean for %s', req.name)
+ logger.info("Running setup.py clean for %s", req.name)
try:
- call_subprocess(clean_args, cwd=req.source_dir)
+ call_subprocess(
+ clean_args, command_desc="python setup.py clean", cwd=req.source_dir
+ )
return True
except Exception:
- logger.error('Failed cleaning build dir for %s', req.name)
+ logger.error("Failed cleaning build dir for %s", req.name)
return False
def build(
- requirements, # type: Iterable[InstallRequirement]
- wheel_cache, # type: WheelCache
- verify, # type: bool
- build_options, # type: List[str]
- global_options, # type: List[str]
-):
- # type: (...) -> BuildResult
+ requirements: Iterable[InstallRequirement],
+ wheel_cache: WheelCache,
+ verify: bool,
+ build_options: List[str],
+ global_options: List[str],
+) -> BuildResult:
"""Build wheels.
:return: The list of InstallRequirement that succeeded to build and
@@ -325,18 +330,30 @@ def build(
# Build the wheels.
logger.info(
- 'Building wheels for collected packages: %s',
- ', '.join(req.name for req in requirements), # type: ignore
+ "Building wheels for collected packages: %s",
+ ", ".join(req.name for req in requirements), # type: ignore
)
with indent_log():
build_successes, build_failures = [], []
for req in requirements:
+ assert req.name
cache_dir = _get_cache_dir(req, wheel_cache)
wheel_file = _build_one(
- req, cache_dir, verify, build_options, global_options
+ req,
+ cache_dir,
+ verify,
+ build_options,
+ global_options,
+ req.editable and req.permit_editable_wheels,
)
if wheel_file:
+ # Record the download origin in the cache
+ if req.download_info is not None:
+ # download_info is guaranteed to be set because when we build an
+ # InstallRequirement it has been through the preparer before, but
+ # let's be cautious.
+ wheel_cache.record_download_origin(cache_dir, req.download_info)
# Update the link for this.
req.link = Link(path_to_url(wheel_file))
req.local_file_path = req.link.file_path
@@ -348,13 +365,13 @@ def build(
# notify success/failure
if build_successes:
logger.info(
- 'Successfully built %s',
- ' '.join([req.name for req in build_successes]), # type: ignore
+ "Successfully built %s",
+ " ".join([req.name for req in build_successes]), # type: ignore
)
if build_failures:
logger.info(
- 'Failed to build %s',
- ' '.join([req.name for req in build_failures]), # type: ignore
+ "Failed to build %s",
+ " ".join([req.name for req in build_failures]), # type: ignore
)
# Return a list of requirements that failed to build
return build_successes, build_failures
diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst
index 12b421a04..077f1abf7 100644
--- a/src/pip/_vendor/README.rst
+++ b/src/pip/_vendor/README.rst
@@ -100,18 +100,16 @@ Modifications
* ``setuptools`` is completely stripped to only keep ``pkg_resources``.
* ``pkg_resources`` has been modified to import its dependencies from
- ``pip._vendor``.
+ ``pip._vendor``, and to use the vendored copy of ``platformdirs``
+ rather than ``appdirs``.
* ``packaging`` has been modified to import its dependencies from
``pip._vendor``.
-* ``html5lib`` has been modified to import six from ``pip._vendor``, to prefer
- importing from ``collections.abc`` instead of ``collections`` and does not
- import ``xml.etree.cElementTree`` on Python 3.
* ``CacheControl`` has been modified to import its dependencies from
``pip._vendor``.
* ``requests`` has been modified to import its other dependencies from
``pip._vendor`` and to *not* load ``simplejson`` (all platforms) and
``pyopenssl`` (Windows).
-
+* ``platformdirs`` has been modified to import its submodules from ``pip._vendor.platformdirs``.
Automatic Vendoring
===================
diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py
index 57e32dab1..b22f7abb9 100644
--- a/src/pip/_vendor/__init__.py
+++ b/src/pip/_vendor/__init__.py
@@ -58,13 +58,11 @@ if DEBUNDLED:
sys.path[:] = glob.glob(os.path.join(WHEEL_DIR, "*.whl")) + sys.path
# Actually alias all of our vendored dependencies.
- vendored("appdirs")
vendored("cachecontrol")
vendored("certifi")
vendored("colorama")
vendored("distlib")
vendored("distro")
- vendored("html5lib")
vendored("six")
vendored("six.moves")
vendored("six.moves.urllib")
@@ -74,6 +72,7 @@ if DEBUNDLED:
vendored("packaging.specifiers")
vendored("pep517")
vendored("pkg_resources")
+ vendored("platformdirs")
vendored("progress")
vendored("requests")
vendored("requests.exceptions")
@@ -106,6 +105,16 @@ if DEBUNDLED:
vendored("requests.packages.urllib3.util.timeout")
vendored("requests.packages.urllib3.util.url")
vendored("resolvelib")
+ vendored("rich")
+ vendored("rich.console")
+ vendored("rich.highlighter")
+ vendored("rich.logging")
+ vendored("rich.markup")
+ vendored("rich.progress")
+ vendored("rich.segment")
+ vendored("rich.style")
+ vendored("rich.text")
+ vendored("rich.traceback")
vendored("tenacity")
vendored("tomli")
vendored("urllib3")
diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py
deleted file mode 100644
index 33a3b7741..000000000
--- a/src/pip/_vendor/appdirs.py
+++ /dev/null
@@ -1,633 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-# Copyright (c) 2005-2010 ActiveState Software Inc.
-# Copyright (c) 2013 Eddy Petrișor
-
-"""Utilities for determining application-specific dirs.
-
-See <http://github.com/ActiveState/appdirs> for details and usage.
-"""
-# Dev Notes:
-# - MSDN on where to store app data files:
-# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120
-# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
-# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
-
-__version__ = "1.4.4"
-__version_info__ = tuple(int(segment) for segment in __version__.split("."))
-
-
-import sys
-import os
-
-PY3 = sys.version_info[0] == 3
-
-if PY3:
- unicode = str
-
-if sys.platform.startswith('java'):
- import platform
- os_name = platform.java_ver()[3][0]
- if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc.
- system = 'win32'
- elif os_name.startswith('Mac'): # "Mac OS X", etc.
- system = 'darwin'
- else: # "Linux", "SunOS", "FreeBSD", etc.
- # Setting this to "linux2" is not ideal, but only Windows or Mac
- # are actually checked for and the rest of the module expects
- # *sys.platform* style strings.
- system = 'linux2'
-elif sys.platform == 'cli' and os.name == 'nt':
- # Detect Windows in IronPython to match pip._internal.utils.compat.WINDOWS
- # Discussion: <https://github.com/pypa/pip/pull/7501>
- system = 'win32'
-else:
- system = sys.platform
-
-
-
-def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
- r"""Return full path to the user-specific data dir for this application.
-
- "appname" is the name of application.
- If None, just the system directory is returned.
- "appauthor" (only used on Windows) is the name of the
- appauthor or distributing body for this application. Typically
- it is the owning company name. This falls back to appname. You may
- pass False to disable it.
- "version" is an optional version path element to append to the
- path. You might want to use this if you want multiple versions
- of your app to be able to run independently. If used, this
- would typically be "<major>.<minor>".
- Only applied when appname is present.
- "roaming" (boolean, default False) can be set True to use the Windows
- roaming appdata directory. That means that for users on a Windows
- network setup for roaming profiles, this user data will be
- sync'd on login. See
- <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
- for a discussion of issues.
-
- Typical user data directories are:
- Mac OS X: ~/Library/Application Support/<AppName> # or ~/.config/<AppName>, if the other does not exist
- Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined
- Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName>
- Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>
- Win 7 (not roaming): C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>
- Win 7 (roaming): C:\Users\<username>\AppData\Roaming\<AppAuthor>\<AppName>
-
- For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
- That means, by default "~/.local/share/<AppName>".
- """
- if system == "win32":
- if appauthor is None:
- appauthor = appname
- const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA"
- path = os.path.normpath(_get_win_folder(const))
- if appname:
- if appauthor is not False:
- path = os.path.join(path, appauthor, appname)
- else:
- path = os.path.join(path, appname)
- elif system == 'darwin':
- path = os.path.expanduser('~/Library/Application Support/')
- if appname:
- path = os.path.join(path, appname)
- else:
- path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
- if appname:
- path = os.path.join(path, appname)
- if appname and version:
- path = os.path.join(path, version)
- return path
-
-
-def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
- r"""Return full path to the user-shared data dir for this application.
-
- "appname" is the name of application.
- If None, just the system directory is returned.
- "appauthor" (only used on Windows) is the name of the
- appauthor or distributing body for this application. Typically
- it is the owning company name. This falls back to appname. You may
- pass False to disable it.
- "version" is an optional version path element to append to the
- path. You might want to use this if you want multiple versions
- of your app to be able to run independently. If used, this
- would typically be "<major>.<minor>".
- Only applied when appname is present.
- "multipath" is an optional parameter only applicable to *nix
- which indicates that the entire list of data dirs should be
- returned. By default, the first item from XDG_DATA_DIRS is
- returned, or '/usr/local/share/<AppName>',
- if XDG_DATA_DIRS is not set
-
- Typical site data directories are:
- Mac OS X: /Library/Application Support/<AppName>
- Unix: /usr/local/share/<AppName> or /usr/share/<AppName>
- Win XP: C:\Documents and Settings\All Users\Application Data\<AppAuthor>\<AppName>
- Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
- Win 7: C:\ProgramData\<AppAuthor>\<AppName> # Hidden, but writeable on Win 7.
-
- For Unix, this is using the $XDG_DATA_DIRS[0] default.
-
- WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
- """
- if system == "win32":
- if appauthor is None:
- appauthor = appname
- path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA"))
- if appname:
- if appauthor is not False:
- path = os.path.join(path, appauthor, appname)
- else:
- path = os.path.join(path, appname)
- elif system == 'darwin':
- path = os.path.expanduser('/Library/Application Support')
- if appname:
- path = os.path.join(path, appname)
- else:
- # XDG default for $XDG_DATA_DIRS
- # only first, if multipath is False
- path = os.getenv('XDG_DATA_DIRS',
- os.pathsep.join(['/usr/local/share', '/usr/share']))
- pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
- if appname:
- if version:
- appname = os.path.join(appname, version)
- pathlist = [os.path.join(x, appname) for x in pathlist]
-
- if multipath:
- path = os.pathsep.join(pathlist)
- else:
- path = pathlist[0]
- return path
-
- if appname and version:
- path = os.path.join(path, version)
- return path
-
-
-def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
- r"""Return full path to the user-specific config dir for this application.
-
- "appname" is the name of application.
- If None, just the system directory is returned.
- "appauthor" (only used on Windows) is the name of the
- appauthor or distributing body for this application. Typically
- it is the owning company name. This falls back to appname. You may
- pass False to disable it.
- "version" is an optional version path element to append to the
- path. You might want to use this if you want multiple versions
- of your app to be able to run independently. If used, this
- would typically be "<major>.<minor>".
- Only applied when appname is present.
- "roaming" (boolean, default False) can be set True to use the Windows
- roaming appdata directory. That means that for users on a Windows
- network setup for roaming profiles, this user data will be
- sync'd on login. See
- <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
- for a discussion of issues.
-
- Typical user config directories are:
- Mac OS X: same as user_data_dir
- Unix: ~/.config/<AppName> # or in $XDG_CONFIG_HOME, if defined
- Win *: same as user_data_dir
-
- For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
- That means, by default "~/.config/<AppName>".
- """
- if system in ["win32", "darwin"]:
- path = user_data_dir(appname, appauthor, None, roaming)
- else:
- path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config"))
- if appname:
- path = os.path.join(path, appname)
- if appname and version:
- path = os.path.join(path, version)
- return path
-
-
-# for the discussion regarding site_config_dir locations
-# see <https://github.com/pypa/pip/issues/1733>
-def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
- r"""Return full path to the user-shared data dir for this application.
-
- "appname" is the name of application.
- If None, just the system directory is returned.
- "appauthor" (only used on Windows) is the name of the
- appauthor or distributing body for this application. Typically
- it is the owning company name. This falls back to appname. You may
- pass False to disable it.
- "version" is an optional version path element to append to the
- path. You might want to use this if you want multiple versions
- of your app to be able to run independently. If used, this
- would typically be "<major>.<minor>".
- Only applied when appname is present.
- "multipath" is an optional parameter only applicable to *nix
- which indicates that the entire list of config dirs should be
- returned. By default, the first item from XDG_CONFIG_DIRS is
- returned, or '/etc/xdg/<AppName>', if XDG_CONFIG_DIRS is not set
-
- Typical site config directories are:
- Mac OS X: same as site_data_dir
- Unix: /etc/xdg/<AppName> or $XDG_CONFIG_DIRS[i]/<AppName> for each value in
- $XDG_CONFIG_DIRS
- Win *: same as site_data_dir
- Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
-
- For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False
-
- WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
- """
- if system in ["win32", "darwin"]:
- path = site_data_dir(appname, appauthor)
- if appname and version:
- path = os.path.join(path, version)
- else:
- # XDG default for $XDG_CONFIG_DIRS (missing or empty)
- # see <https://github.com/pypa/pip/pull/7501#discussion_r360624829>
- # only first, if multipath is False
- path = os.getenv('XDG_CONFIG_DIRS') or '/etc/xdg'
- pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) if x]
- if appname:
- if version:
- appname = os.path.join(appname, version)
- pathlist = [os.path.join(x, appname) for x in pathlist]
-
- if multipath:
- path = os.pathsep.join(pathlist)
- else:
- path = pathlist[0]
- return path
-
-
-def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
- r"""Return full path to the user-specific cache dir for this application.
-
- "appname" is the name of application.
- If None, just the system directory is returned.
- "appauthor" (only used on Windows) is the name of the
- appauthor or distributing body for this application. Typically
- it is the owning company name. This falls back to appname. You may
- pass False to disable it.
- "version" is an optional version path element to append to the
- path. You might want to use this if you want multiple versions
- of your app to be able to run independently. If used, this
- would typically be "<major>.<minor>".
- Only applied when appname is present.
- "opinion" (boolean) can be False to disable the appending of
- "Cache" to the base app data dir for Windows. See
- discussion below.
-
- Typical user cache directories are:
- Mac OS X: ~/Library/Caches/<AppName>
- Unix: ~/.cache/<AppName> (XDG default)
- Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Cache
- Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Cache
-
- On Windows the only suggestion in the MSDN docs is that local settings go in
- the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming
- app data dir (the default returned by `user_data_dir` above). Apps typically
- put cache data somewhere *under* the given dir here. Some examples:
- ...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
- ...\Acme\SuperApp\Cache\1.0
- OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
- This can be disabled with the `opinion=False` option.
- """
- if system == "win32":
- if appauthor is None:
- appauthor = appname
- path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
- # When using Python 2, return paths as bytes on Windows like we do on
- # other operating systems. See helper function docs for more details.
- if not PY3 and isinstance(path, unicode):
- path = _win_path_to_bytes(path)
- if appname:
- if appauthor is not False:
- path = os.path.join(path, appauthor, appname)
- else:
- path = os.path.join(path, appname)
- if opinion:
- path = os.path.join(path, "Cache")
- elif system == 'darwin':
- path = os.path.expanduser('~/Library/Caches')
- if appname:
- path = os.path.join(path, appname)
- else:
- path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
- if appname:
- path = os.path.join(path, appname)
- if appname and version:
- path = os.path.join(path, version)
- return path
-
-
-def user_state_dir(appname=None, appauthor=None, version=None, roaming=False):
- r"""Return full path to the user-specific state dir for this application.
-
- "appname" is the name of application.
- If None, just the system directory is returned.
- "appauthor" (only used on Windows) is the name of the
- appauthor or distributing body for this application. Typically
- it is the owning company name. This falls back to appname. You may
- pass False to disable it.
- "version" is an optional version path element to append to the
- path. You might want to use this if you want multiple versions
- of your app to be able to run independently. If used, this
- would typically be "<major>.<minor>".
- Only applied when appname is present.
- "roaming" (boolean, default False) can be set True to use the Windows
- roaming appdata directory. That means that for users on a Windows
- network setup for roaming profiles, this user data will be
- sync'd on login. See
- <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
- for a discussion of issues.
-
- Typical user state directories are:
- Mac OS X: same as user_data_dir
- Unix: ~/.local/state/<AppName> # or in $XDG_STATE_HOME, if defined
- Win *: same as user_data_dir
-
- For Unix, we follow this Debian proposal <https://wiki.debian.org/XDGBaseDirectorySpecification#state>
- to extend the XDG spec and support $XDG_STATE_HOME.
-
- That means, by default "~/.local/state/<AppName>".
- """
- if system in ["win32", "darwin"]:
- path = user_data_dir(appname, appauthor, None, roaming)
- else:
- path = os.getenv('XDG_STATE_HOME', os.path.expanduser("~/.local/state"))
- if appname:
- path = os.path.join(path, appname)
- if appname and version:
- path = os.path.join(path, version)
- return path
-
-
-def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
- r"""Return full path to the user-specific log dir for this application.
-
- "appname" is the name of application.
- If None, just the system directory is returned.
- "appauthor" (only used on Windows) is the name of the
- appauthor or distributing body for this application. Typically
- it is the owning company name. This falls back to appname. You may
- pass False to disable it.
- "version" is an optional version path element to append to the
- path. You might want to use this if you want multiple versions
- of your app to be able to run independently. If used, this
- would typically be "<major>.<minor>".
- Only applied when appname is present.
- "opinion" (boolean) can be False to disable the appending of
- "Logs" to the base app data dir for Windows, and "log" to the
- base cache dir for Unix. See discussion below.
-
- Typical user log directories are:
- Mac OS X: ~/Library/Logs/<AppName>
- Unix: ~/.cache/<AppName>/log # or under $XDG_CACHE_HOME if defined
- Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Logs
- Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Logs
-
- On Windows the only suggestion in the MSDN docs is that local settings
- go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in
- examples of what some windows apps use for a logs dir.)
-
- OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA`
- value for Windows and appends "log" to the user cache dir for Unix.
- This can be disabled with the `opinion=False` option.
- """
- if system == "darwin":
- path = os.path.join(
- os.path.expanduser('~/Library/Logs'),
- appname)
- elif system == "win32":
- path = user_data_dir(appname, appauthor, version)
- version = False
- if opinion:
- path = os.path.join(path, "Logs")
- else:
- path = user_cache_dir(appname, appauthor, version)
- version = False
- if opinion:
- path = os.path.join(path, "log")
- if appname and version:
- path = os.path.join(path, version)
- return path
-
-
-class AppDirs(object):
- """Convenience wrapper for getting application dirs."""
- def __init__(self, appname=None, appauthor=None, version=None,
- roaming=False, multipath=False):
- self.appname = appname
- self.appauthor = appauthor
- self.version = version
- self.roaming = roaming
- self.multipath = multipath
-
- @property
- def user_data_dir(self):
- return user_data_dir(self.appname, self.appauthor,
- version=self.version, roaming=self.roaming)
-
- @property
- def site_data_dir(self):
- return site_data_dir(self.appname, self.appauthor,
- version=self.version, multipath=self.multipath)
-
- @property
- def user_config_dir(self):
- return user_config_dir(self.appname, self.appauthor,
- version=self.version, roaming=self.roaming)
-
- @property
- def site_config_dir(self):
- return site_config_dir(self.appname, self.appauthor,
- version=self.version, multipath=self.multipath)
-
- @property
- def user_cache_dir(self):
- return user_cache_dir(self.appname, self.appauthor,
- version=self.version)
-
- @property
- def user_state_dir(self):
- return user_state_dir(self.appname, self.appauthor,
- version=self.version)
-
- @property
- def user_log_dir(self):
- return user_log_dir(self.appname, self.appauthor,
- version=self.version)
-
-
-#---- internal support stuff
-
-def _get_win_folder_from_registry(csidl_name):
- """This is a fallback technique at best. I'm not sure if using the
- registry for this guarantees us the correct answer for all CSIDL_*
- names.
- """
- if PY3:
- import winreg as _winreg
- else:
- import _winreg
-
- shell_folder_name = {
- "CSIDL_APPDATA": "AppData",
- "CSIDL_COMMON_APPDATA": "Common AppData",
- "CSIDL_LOCAL_APPDATA": "Local AppData",
- }[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)
- return dir
-
-
-def _get_win_folder_with_pywin32(csidl_name):
- from win32com.shell import shellcon, shell
- dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0)
- # Try to make this a unicode path because SHGetFolderPath does
- # not return unicode strings when there is unicode data in the
- # path.
- try:
- dir = unicode(dir)
-
- # Downgrade to short path name if have highbit chars. See
- # <http://bugs.activestate.com/show_bug.cgi?id=85099>.
- has_high_char = False
- for c in dir:
- if ord(c) > 255:
- has_high_char = True
- break
- if has_high_char:
- try:
- import win32api
- dir = win32api.GetShortPathName(dir)
- except ImportError:
- pass
- except UnicodeError:
- pass
- return dir
-
-
-def _get_win_folder_with_ctypes(csidl_name):
- import ctypes
-
- csidl_const = {
- "CSIDL_APPDATA": 26,
- "CSIDL_COMMON_APPDATA": 35,
- "CSIDL_LOCAL_APPDATA": 28,
- }[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
- # <http://bugs.activestate.com/show_bug.cgi?id=85099>.
- 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_with_jna(csidl_name):
- import array
- from com.sun import jna
- from com.sun.jna.platform import win32
-
- buf_size = win32.WinDef.MAX_PATH * 2
- buf = array.zeros('c', buf_size)
- shell = win32.Shell32.INSTANCE
- shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf)
- dir = jna.Native.toString(buf.tostring()).rstrip("\0")
-
- # Downgrade to short path name if have highbit chars. See
- # <http://bugs.activestate.com/show_bug.cgi?id=85099>.
- has_high_char = False
- for c in dir:
- if ord(c) > 255:
- has_high_char = True
- break
- if has_high_char:
- buf = array.zeros('c', buf_size)
- kernel = win32.Kernel32.INSTANCE
- if kernel.GetShortPathName(dir, buf, buf_size):
- dir = jna.Native.toString(buf.tostring()).rstrip("\0")
-
- return dir
-
-if system == "win32":
- try:
- from ctypes import windll
- _get_win_folder = _get_win_folder_with_ctypes
- except ImportError:
- try:
- import com.sun.jna
- _get_win_folder = _get_win_folder_with_jna
- except ImportError:
- _get_win_folder = _get_win_folder_from_registry
-
-
-def _win_path_to_bytes(path):
- """Encode Windows paths to bytes. Only used on Python 2.
-
- Motivation is to be consistent with other operating systems where paths
- are also returned as bytes. This avoids problems mixing bytes and Unicode
- elsewhere in the codebase. For more details and discussion see
- <https://github.com/pypa/pip/issues/3463>.
-
- If encoding using ASCII and MBCS fails, return the original Unicode path.
- """
- for encoding in ('ASCII', 'MBCS'):
- try:
- return path.encode(encoding)
- except (UnicodeEncodeError, LookupError):
- pass
- return path
-
-
-#---- self test code
-
-if __name__ == "__main__":
- appname = "MyApp"
- appauthor = "MyCompany"
-
- props = ("user_data_dir",
- "user_config_dir",
- "user_cache_dir",
- "user_state_dir",
- "user_log_dir",
- "site_data_dir",
- "site_config_dir")
-
- print("-- app dirs %s --" % __version__)
-
- print("-- app dirs (with optional 'version')")
- dirs = AppDirs(appname, appauthor, version="1.0")
- for prop in props:
- print("%s: %s" % (prop, getattr(dirs, prop)))
-
- print("\n-- app dirs (without optional 'version')")
- dirs = AppDirs(appname, appauthor)
- for prop in props:
- print("%s: %s" % (prop, getattr(dirs, prop)))
-
- print("\n-- app dirs (without optional 'appauthor')")
- dirs = AppDirs(appname)
- for prop in props:
- print("%s: %s" % (prop, getattr(dirs, prop)))
-
- print("\n-- app dirs (with disabled 'appauthor')")
- dirs = AppDirs(appname, appauthor=False)
- for prop in props:
- print("%s: %s" % (prop, getattr(dirs, prop)))
diff --git a/src/pip/_vendor/cachecontrol/LICENSE.txt b/src/pip/_vendor/cachecontrol/LICENSE.txt
index 1ed31ac36..d8b3b56d3 100644
--- a/src/pip/_vendor/cachecontrol/LICENSE.txt
+++ b/src/pip/_vendor/cachecontrol/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright 2015 Eric Larson
+Copyright 2012-2021 Eric Larson
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -8,8 +8,6 @@ You may obtain a copy of the License at
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-implied.
-
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
diff --git a/src/pip/_vendor/cachecontrol/__init__.py b/src/pip/_vendor/cachecontrol/__init__.py
index a1bbbbe3b..f631ae6df 100644
--- a/src/pip/_vendor/cachecontrol/__init__.py
+++ b/src/pip/_vendor/cachecontrol/__init__.py
@@ -1,11 +1,18 @@
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
"""CacheControl import Interface.
Make it easy to import from cachecontrol without long namespaces.
"""
__author__ = "Eric Larson"
__email__ = "eric@ionrock.org"
-__version__ = "0.12.6"
+__version__ = "0.12.11"
from .wrapper import CacheControl
from .adapter import CacheControlAdapter
from .controller import CacheController
+
+import logging
+logging.getLogger(__name__).addHandler(logging.NullHandler())
diff --git a/src/pip/_vendor/cachecontrol/_cmd.py b/src/pip/_vendor/cachecontrol/_cmd.py
index f1e0ad94a..4266b5ee9 100644
--- a/src/pip/_vendor/cachecontrol/_cmd.py
+++ b/src/pip/_vendor/cachecontrol/_cmd.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
import logging
from pip._vendor import requests
diff --git a/src/pip/_vendor/cachecontrol/adapter.py b/src/pip/_vendor/cachecontrol/adapter.py
index 815650e81..94c75e1a0 100644
--- a/src/pip/_vendor/cachecontrol/adapter.py
+++ b/src/pip/_vendor/cachecontrol/adapter.py
@@ -1,16 +1,20 @@
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
import types
import functools
import zlib
from pip._vendor.requests.adapters import HTTPAdapter
-from .controller import CacheController
+from .controller import CacheController, PERMANENT_REDIRECT_STATUSES
from .cache import DictCache
from .filewrapper import CallbackFileWrapper
class CacheControlAdapter(HTTPAdapter):
- invalidating_methods = {"PUT", "DELETE"}
+ invalidating_methods = {"PUT", "PATCH", "DELETE"}
def __init__(
self,
@@ -93,7 +97,7 @@ class CacheControlAdapter(HTTPAdapter):
response = cached_response
# We always cache the 301 responses
- elif response.status == 301:
+ elif int(response.status) in PERMANENT_REDIRECT_STATUSES:
self.controller.cache_response(request, response)
else:
# Wrap the response file with a wrapper that will cache the
diff --git a/src/pip/_vendor/cachecontrol/cache.py b/src/pip/_vendor/cachecontrol/cache.py
index 94e07732d..2a965f595 100644
--- a/src/pip/_vendor/cachecontrol/cache.py
+++ b/src/pip/_vendor/cachecontrol/cache.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
"""
The cache object API for implementing caches. The default is a thread
safe in-memory dictionary.
@@ -10,7 +14,7 @@ class BaseCache(object):
def get(self, key):
raise NotImplementedError()
- def set(self, key, value):
+ def set(self, key, value, expires=None):
raise NotImplementedError()
def delete(self, key):
@@ -29,7 +33,7 @@ class DictCache(BaseCache):
def get(self, key):
return self.data.get(key, None)
- def set(self, key, value):
+ def set(self, key, value, expires=None):
with self.lock:
self.data.update({key: value})
@@ -37,3 +41,25 @@ class DictCache(BaseCache):
with self.lock:
if key in self.data:
self.data.pop(key)
+
+
+class SeparateBodyBaseCache(BaseCache):
+ """
+ In this variant, the body is not stored mixed in with the metadata, but is
+ passed in (as a bytes-like object) in a separate call to ``set_body()``.
+
+ That is, the expected interaction pattern is::
+
+ cache.set(key, serialized_metadata)
+ cache.set_body(key)
+
+ Similarly, the body should be loaded separately via ``get_body()``.
+ """
+ def set_body(self, key, body):
+ raise NotImplementedError()
+
+ def get_body(self, key):
+ """
+ Return the body as file-like object.
+ """
+ raise NotImplementedError()
diff --git a/src/pip/_vendor/cachecontrol/caches/__init__.py b/src/pip/_vendor/cachecontrol/caches/__init__.py
index 0e1658fa5..37827291f 100644
--- a/src/pip/_vendor/cachecontrol/caches/__init__.py
+++ b/src/pip/_vendor/cachecontrol/caches/__init__.py
@@ -1,2 +1,9 @@
-from .file_cache import FileCache # noqa
-from .redis_cache import RedisCache # noqa
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from .file_cache import FileCache, SeparateBodyFileCache
+from .redis_cache import RedisCache
+
+
+__all__ = ["FileCache", "SeparateBodyFileCache", "RedisCache"]
diff --git a/src/pip/_vendor/cachecontrol/caches/file_cache.py b/src/pip/_vendor/cachecontrol/caches/file_cache.py
index 607b94524..f1ddb2ebd 100644
--- a/src/pip/_vendor/cachecontrol/caches/file_cache.py
+++ b/src/pip/_vendor/cachecontrol/caches/file_cache.py
@@ -1,8 +1,12 @@
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
import hashlib
import os
from textwrap import dedent
-from ..cache import BaseCache
+from ..cache import BaseCache, SeparateBodyBaseCache
from ..controller import CacheController
try:
@@ -53,7 +57,8 @@ def _secure_open_write(filename, fmode):
raise
-class FileCache(BaseCache):
+class _FileCacheMixin:
+ """Shared implementation for both FileCache variants."""
def __init__(
self,
@@ -114,22 +119,27 @@ class FileCache(BaseCache):
except FileNotFoundError:
return None
- def set(self, key, value):
+ def set(self, key, value, expires=None):
name = self._fn(key)
+ self._write(name, value)
+ def _write(self, path, data: bytes):
+ """
+ Safely write the data to the given path.
+ """
# Make sure the directory exists
try:
- os.makedirs(os.path.dirname(name), self.dirmode)
+ os.makedirs(os.path.dirname(path), self.dirmode)
except (IOError, OSError):
pass
- with self.lock_class(name) as lock:
+ with self.lock_class(path) as lock:
# Write our actual file
with _secure_open_write(lock.path, self.filemode) as fh:
- fh.write(value)
+ fh.write(data)
- def delete(self, key):
- name = self._fn(key)
+ def _delete(self, key, suffix):
+ name = self._fn(key) + suffix
if not self.forever:
try:
os.remove(name)
@@ -137,6 +147,38 @@ class FileCache(BaseCache):
pass
+class FileCache(_FileCacheMixin, BaseCache):
+ """
+ Traditional FileCache: body is stored in memory, so not suitable for large
+ downloads.
+ """
+
+ def delete(self, key):
+ self._delete(key, "")
+
+
+class SeparateBodyFileCache(_FileCacheMixin, SeparateBodyBaseCache):
+ """
+ Memory-efficient FileCache: body is stored in a separate file, reducing
+ peak memory usage.
+ """
+
+ def get_body(self, key):
+ name = self._fn(key) + ".body"
+ try:
+ return open(name, "rb")
+ except FileNotFoundError:
+ return None
+
+ def set_body(self, key, body):
+ name = self._fn(key) + ".body"
+ self._write(name, body)
+
+ def delete(self, key):
+ self._delete(key, "")
+ self._delete(key, ".body")
+
+
def url_to_file_path(url, filecache):
"""Return the file cache path based on the URL.
diff --git a/src/pip/_vendor/cachecontrol/caches/redis_cache.py b/src/pip/_vendor/cachecontrol/caches/redis_cache.py
index ed705ce7d..2cba4b070 100644
--- a/src/pip/_vendor/cachecontrol/caches/redis_cache.py
+++ b/src/pip/_vendor/cachecontrol/caches/redis_cache.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
from __future__ import division
from datetime import datetime
@@ -15,9 +19,11 @@ class RedisCache(BaseCache):
def set(self, key, value, expires=None):
if not expires:
self.conn.set(key, value)
- else:
+ elif isinstance(expires, datetime):
expires = expires - datetime.utcnow()
self.conn.setex(key, int(expires.total_seconds()), value)
+ else:
+ self.conn.setex(key, expires, value)
def delete(self, key):
self.conn.delete(key)
diff --git a/src/pip/_vendor/cachecontrol/compat.py b/src/pip/_vendor/cachecontrol/compat.py
index 33b5aed0a..ccec9379d 100644
--- a/src/pip/_vendor/cachecontrol/compat.py
+++ b/src/pip/_vendor/cachecontrol/compat.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
try:
from urllib.parse import urljoin
except ImportError:
@@ -9,7 +13,6 @@ try:
except ImportError:
import pickle
-
# Handle the case where the requests module has been patched to not have
# urllib3 bundled as part of its source.
try:
diff --git a/src/pip/_vendor/cachecontrol/controller.py b/src/pip/_vendor/cachecontrol/controller.py
index dafe55ca7..7f23529f1 100644
--- a/src/pip/_vendor/cachecontrol/controller.py
+++ b/src/pip/_vendor/cachecontrol/controller.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
"""
The httplib2 algorithms ported for use with requests.
"""
@@ -9,7 +13,7 @@ from email.utils import parsedate_tz
from pip._vendor.requests.structures import CaseInsensitiveDict
-from .cache import DictCache
+from .cache import DictCache, SeparateBodyBaseCache
from .serialize import Serializer
@@ -17,19 +21,20 @@ logger = logging.getLogger(__name__)
URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?")
+PERMANENT_REDIRECT_STATUSES = (301, 308)
+
def parse_uri(uri):
"""Parses a URI using the regex given in Appendix B of RFC 3986.
- (scheme, authority, path, query, fragment) = parse_uri(uri)
+ (scheme, authority, path, query, fragment) = parse_uri(uri)
"""
groups = URI.match(uri).groups()
return (groups[1], groups[3], groups[4], groups[6], groups[8])
class CacheController(object):
- """An interface to see if request should cached or not.
- """
+ """An interface to see if request should cached or not."""
def __init__(
self, cache=None, cache_etags=True, serializer=None, status_codes=None
@@ -37,7 +42,7 @@ class CacheController(object):
self.cache = DictCache() if cache is None else cache
self.cache_etags = cache_etags
self.serializer = serializer or Serializer()
- self.cacheable_status_codes = status_codes or (200, 203, 300, 301)
+ self.cacheable_status_codes = status_codes or (200, 203, 300, 301, 308)
@classmethod
def _urlnorm(cls, uri):
@@ -141,23 +146,29 @@ class CacheController(object):
logger.debug("No cache entry available")
return False
+ if isinstance(self.cache, SeparateBodyBaseCache):
+ body_file = self.cache.get_body(cache_url)
+ else:
+ body_file = None
+
# Check whether it can be deserialized
- resp = self.serializer.loads(request, cache_data)
+ resp = self.serializer.loads(request, cache_data, body_file)
if not resp:
logger.warning("Cache entry deserialization failed, entry ignored")
return False
- # If we have a cached 301, return it immediately. We don't
- # need to test our response for other headers b/c it is
+ # If we have a cached permanent redirect, return it immediately. We
+ # don't need to test our response for other headers b/c it is
# intrinsically "cacheable" as it is Permanent.
+ #
# See:
# https://tools.ietf.org/html/rfc7231#section-6.4.2
#
# Client can try to refresh the value by repeating the request
# with cache busting headers as usual (ie no-cache).
- if resp.status == 301:
+ if int(resp.status) in PERMANENT_REDIRECT_STATUSES:
msg = (
- 'Returning cached "301 Moved Permanently" response '
+ "Returning cached permanent redirect response "
"(ignoring date and etag information)"
)
logger.debug(msg)
@@ -244,6 +255,26 @@ class CacheController(object):
return new_headers
+ def _cache_set(self, cache_url, request, response, body=None, expires_time=None):
+ """
+ Store the data in the cache.
+ """
+ if isinstance(self.cache, SeparateBodyBaseCache):
+ # We pass in the body separately; just put a placeholder empty
+ # string in the metadata.
+ self.cache.set(
+ cache_url,
+ self.serializer.dumps(request, response, b""),
+ expires=expires_time,
+ )
+ self.cache.set_body(cache_url, body)
+ else:
+ self.cache.set(
+ cache_url,
+ self.serializer.dumps(request, response, body),
+ expires=expires_time,
+ )
+
def cache_response(self, request, response, body=None, status_codes=None):
"""
Algorithm for caching requests.
@@ -261,6 +292,11 @@ class CacheController(object):
response_headers = CaseInsensitiveDict(response.headers)
+ if "date" in response_headers:
+ date = calendar.timegm(parsedate_tz(response_headers["date"]))
+ else:
+ date = 0
+
# If we've been given a body, our response has a Content-Length, that
# Content-Length is valid then we can check to see if the body we've
# been given matches the expected size, and if it doesn't we'll just
@@ -304,35 +340,62 @@ class CacheController(object):
# If we've been given an etag, then keep the response
if self.cache_etags and "etag" in response_headers:
+ expires_time = 0
+ if response_headers.get("expires"):
+ expires = parsedate_tz(response_headers["expires"])
+ if expires is not None:
+ expires_time = calendar.timegm(expires) - date
+
+ expires_time = max(expires_time, 14 * 86400)
+
+ logger.debug("etag object cached for {0} seconds".format(expires_time))
logger.debug("Caching due to etag")
- self.cache.set(
- cache_url, self.serializer.dumps(request, response, body=body)
- )
+ self._cache_set(cache_url, request, response, body, expires_time)
- # Add to the cache any 301s. We do this before looking that
- # the Date headers.
- elif response.status == 301:
- logger.debug("Caching permanant redirect")
- self.cache.set(cache_url, self.serializer.dumps(request, response))
+ # Add to the cache any permanent redirects. We do this before looking
+ # that the Date headers.
+ elif int(response.status) in PERMANENT_REDIRECT_STATUSES:
+ logger.debug("Caching permanent redirect")
+ self._cache_set(cache_url, request, response, b"")
# Add to the cache if the response headers demand it. If there
# is no date header then we can't do anything about expiring
# the cache.
elif "date" in response_headers:
+ date = calendar.timegm(parsedate_tz(response_headers["date"]))
# cache when there is a max-age > 0
if "max-age" in cc and cc["max-age"] > 0:
logger.debug("Caching b/c date exists and max-age > 0")
- self.cache.set(
- cache_url, self.serializer.dumps(request, response, body=body)
+ expires_time = cc["max-age"]
+ self._cache_set(
+ cache_url,
+ request,
+ response,
+ body,
+ expires_time,
)
# If the request can expire, it means we should cache it
# in the meantime.
elif "expires" in response_headers:
if response_headers["expires"]:
- logger.debug("Caching b/c of expires header")
- self.cache.set(
- cache_url, self.serializer.dumps(request, response, body=body)
+ expires = parsedate_tz(response_headers["expires"])
+ if expires is not None:
+ expires_time = calendar.timegm(expires) - date
+ else:
+ expires_time = None
+
+ logger.debug(
+ "Caching b/c of expires header. expires in {0} seconds".format(
+ expires_time
+ )
+ )
+ self._cache_set(
+ cache_url,
+ request,
+ response,
+ body,
+ expires_time,
)
def update_cached_response(self, request, response):
@@ -371,6 +434,6 @@ class CacheController(object):
cached_response.status = 200
# update our cache
- self.cache.set(cache_url, self.serializer.dumps(request, cached_response))
+ self._cache_set(cache_url, request, cached_response)
return cached_response
diff --git a/src/pip/_vendor/cachecontrol/filewrapper.py b/src/pip/_vendor/cachecontrol/filewrapper.py
index 30ed4c5a6..f5ed5f6f6 100644
--- a/src/pip/_vendor/cachecontrol/filewrapper.py
+++ b/src/pip/_vendor/cachecontrol/filewrapper.py
@@ -1,4 +1,9 @@
-from io import BytesIO
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
+from tempfile import NamedTemporaryFile
+import mmap
class CallbackFileWrapper(object):
@@ -11,10 +16,17 @@ class CallbackFileWrapper(object):
This class uses members with a double underscore (__) leading prefix so as
not to accidentally shadow an attribute.
+
+ The data is stored in a temporary file until it is all available. As long
+ as the temporary files directory is disk-based (sometimes it's a
+ memory-backed-``tmpfs`` on Linux), data will be unloaded to disk if memory
+ pressure is high. For small files the disk usually won't be used at all,
+ it'll all be in the filesystem memory cache, so there should be no
+ performance impact.
"""
def __init__(self, fp, callback):
- self.__buf = BytesIO()
+ self.__buf = NamedTemporaryFile("rb+", delete=True)
self.__fp = fp
self.__callback = callback
@@ -49,7 +61,19 @@ class CallbackFileWrapper(object):
def _close(self):
if self.__callback:
- self.__callback(self.__buf.getvalue())
+ if self.__buf.tell() == 0:
+ # Empty file:
+ result = b""
+ else:
+ # Return the data without actually loading it into memory,
+ # relying on Python's buffer API and mmap(). mmap() just gives
+ # a view directly into the filesystem's memory cache, so it
+ # doesn't result in duplicate memory use.
+ self.__buf.seek(0, 0)
+ result = memoryview(
+ mmap.mmap(self.__buf.fileno(), 0, access=mmap.ACCESS_READ)
+ )
+ self.__callback(result)
# We assign this to None here, because otherwise we can get into
# really tricky problems where the CPython interpreter dead locks
@@ -58,9 +82,16 @@ class CallbackFileWrapper(object):
# and allows the garbage collector to do it's thing normally.
self.__callback = None
+ # Closing the temporary file releases memory and frees disk space.
+ # Important when caching big files.
+ self.__buf.close()
+
def read(self, amt=None):
data = self.__fp.read(amt)
- self.__buf.write(data)
+ if data:
+ # We may be dealing with b'', a sign that things are over:
+ # it's passed e.g. after we've already closed self.__buf.
+ self.__buf.write(data)
if self.__is_fp_closed():
self._close()
diff --git a/src/pip/_vendor/cachecontrol/heuristics.py b/src/pip/_vendor/cachecontrol/heuristics.py
index 6c0e9790d..ebe4a96f5 100644
--- a/src/pip/_vendor/cachecontrol/heuristics.py
+++ b/src/pip/_vendor/cachecontrol/heuristics.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
import calendar
import time
diff --git a/src/pip/_vendor/cachecontrol/serialize.py b/src/pip/_vendor/cachecontrol/serialize.py
index 3b6ec2de1..7fe1a3e33 100644
--- a/src/pip/_vendor/cachecontrol/serialize.py
+++ b/src/pip/_vendor/cachecontrol/serialize.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
import base64
import io
import json
@@ -17,24 +21,18 @@ def _b64_decode_str(s):
return _b64_decode_bytes(s).decode("utf8")
-class Serializer(object):
+_default_body_read = object()
+
+class Serializer(object):
def dumps(self, request, response, body=None):
response_headers = CaseInsensitiveDict(response.headers)
if body is None:
+ # When a body isn't passed in, we'll read the response. We
+ # also update the response with a new file handler to be
+ # sure it acts as though it was never read.
body = response.read(decode_content=False)
-
- # NOTE: 99% sure this is dead code. I'm only leaving it
- # here b/c I don't have a test yet to prove
- # it. Basically, before using
- # `cachecontrol.filewrapper.CallbackFileWrapper`,
- # this made an effort to reset the file handle. The
- # `CallbackFileWrapper` short circuits this code by
- # setting the body as the content is consumed, the
- # result being a `body` argument is *always* passed
- # into cache_response, and in turn,
- # `Serializer.dump`.
response._fp = io.BytesIO(body)
# NOTE: This is all a bit weird, but it's really important that on
@@ -46,7 +44,7 @@ class Serializer(object):
# enough to have msgpack know the difference.
data = {
u"response": {
- u"body": body,
+ u"body": body, # Empty bytestring if body is stored separately
u"headers": dict(
(text_type(k), text_type(v)) for k, v in response.headers.items()
),
@@ -71,7 +69,7 @@ class Serializer(object):
return b",".join([b"cc=4", msgpack.dumps(data, use_bin_type=True)])
- def loads(self, request, data):
+ def loads(self, request, data, body_file=None):
# Short circuit if we've been given an empty set of data
if not data:
return
@@ -94,14 +92,14 @@ class Serializer(object):
# Dispatch to the actual load method for the given version
try:
- return getattr(self, "_loads_v{}".format(ver))(request, data)
+ return getattr(self, "_loads_v{}".format(ver))(request, data, body_file)
except AttributeError:
# This is a version we don't have a loads function for, so we'll
# just treat it as a miss and return None
return
- def prepare_response(self, request, cached):
+ def prepare_response(self, request, cached, body_file=None):
"""Verify our vary headers match and construct a real urllib3
HTTPResponse object.
"""
@@ -127,7 +125,10 @@ class Serializer(object):
cached["response"]["headers"] = headers
try:
- body = io.BytesIO(body_raw)
+ if body_file is None:
+ body = io.BytesIO(body_raw)
+ else:
+ body = body_file
except TypeError:
# This can happen if cachecontrol serialized to v1 format (pickle)
# using Python 2. A Python 2 str(byte string) will be unpickled as
@@ -139,21 +140,22 @@ class Serializer(object):
return HTTPResponse(body=body, preload_content=False, **cached["response"])
- def _loads_v0(self, request, data):
+ def _loads_v0(self, request, data, body_file=None):
# The original legacy cache data. This doesn't contain enough
# information to construct everything we need, so we'll treat this as
# a miss.
return
- def _loads_v1(self, request, data):
+ def _loads_v1(self, request, data, body_file=None):
try:
cached = pickle.loads(data)
except ValueError:
return
- return self.prepare_response(request, cached)
+ return self.prepare_response(request, cached, body_file)
- def _loads_v2(self, request, data):
+ def _loads_v2(self, request, data, body_file=None):
+ assert body_file is None
try:
cached = json.loads(zlib.decompress(data).decode("utf8"))
except (ValueError, zlib.error):
@@ -171,18 +173,18 @@ class Serializer(object):
for k, v in cached["vary"].items()
)
- return self.prepare_response(request, cached)
+ return self.prepare_response(request, cached, body_file)
- def _loads_v3(self, request, data):
+ def _loads_v3(self, request, data, body_file):
# Due to Python 2 encoding issues, it's impossible to know for sure
# exactly how to load v3 entries, thus we'll treat these as a miss so
# that they get rewritten out as v4 entries.
return
- def _loads_v4(self, request, data):
+ def _loads_v4(self, request, data, body_file=None):
try:
cached = msgpack.loads(data, raw=False)
except ValueError:
return
- return self.prepare_response(request, cached)
+ return self.prepare_response(request, cached, body_file)
diff --git a/src/pip/_vendor/cachecontrol/wrapper.py b/src/pip/_vendor/cachecontrol/wrapper.py
index d8e6fc6a9..b6ee7f203 100644
--- a/src/pip/_vendor/cachecontrol/wrapper.py
+++ b/src/pip/_vendor/cachecontrol/wrapper.py
@@ -1,3 +1,7 @@
+# SPDX-FileCopyrightText: 2015 Eric Larson
+#
+# SPDX-License-Identifier: Apache-2.0
+
from .adapter import CacheControlAdapter
from .cache import DictCache
diff --git a/src/pip/_vendor/certifi.pyi b/src/pip/_vendor/certifi.pyi
deleted file mode 100644
index e5c4d3d2a..000000000
--- a/src/pip/_vendor/certifi.pyi
+++ /dev/null
@@ -1 +0,0 @@
-from certifi import * \ No newline at end of file
diff --git a/src/pip/_vendor/certifi/__init__.py b/src/pip/_vendor/certifi/__init__.py
index eebdf8886..bdeb06bee 100644
--- a/src/pip/_vendor/certifi/__init__.py
+++ b/src/pip/_vendor/certifi/__init__.py
@@ -1,3 +1,4 @@
from .core import contents, where
-__version__ = "2021.05.30"
+__all__ = ["contents", "where"]
+__version__ = "2022.06.15"
diff --git a/src/pip/_vendor/certifi/cacert.pem b/src/pip/_vendor/certifi/cacert.pem
index 96e2fc65a..ee9be4cb3 100644
--- a/src/pip/_vendor/certifi/cacert.pem
+++ b/src/pip/_vendor/certifi/cacert.pem
@@ -28,36 +28,6 @@ DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME
HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
-----END CERTIFICATE-----
-# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2
-# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R2
-# Label: "GlobalSign Root CA - R2"
-# Serial: 4835703278459682885658125
-# MD5 Fingerprint: 94:14:77:7e:3e:5e:fd:8f:30:bd:41:b0:cf:e7:d0:30
-# SHA1 Fingerprint: 75:e0:ab:b6:13:85:12:27:1c:04:f8:5f:dd:de:38:e4:b7:24:2e:fe
-# SHA256 Fingerprint: ca:42:dd:41:74:5f:d0:b8:1e:b9:02:36:2c:f9:d8:bf:71:9d:a1:bd:1b:1e:fc:94:6f:5b:4c:99:f4:2c:1b:9e
------BEGIN CERTIFICATE-----
-MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
-A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
-Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
-MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
-A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
-hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
-v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
-eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
-tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
-C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
-zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
-mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
-V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
-bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
-3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
-J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
-291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
-ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
-AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
-TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
------END CERTIFICATE-----
-
# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited
# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited
# Label: "Entrust.net Premium 2048 Secure Server CA"
@@ -491,34 +461,6 @@ vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
+OkuE6N36B9K
-----END CERTIFICATE-----
-# Issuer: CN=DST Root CA X3 O=Digital Signature Trust Co.
-# Subject: CN=DST Root CA X3 O=Digital Signature Trust Co.
-# Label: "DST Root CA X3"
-# Serial: 91299735575339953335919266965803778155
-# MD5 Fingerprint: 41:03:52:dc:0f:f7:50:1b:16:f0:02:8e:ba:6f:45:c5
-# SHA1 Fingerprint: da:c9:02:4f:54:d8:f6:df:94:93:5f:b1:73:26:38:ca:6a:d7:7c:13
-# SHA256 Fingerprint: 06:87:26:03:31:a7:24:03:d9:09:f1:05:e6:9b:cf:0d:32:e1:bd:24:93:ff:c6:d9:20:6d:11:bc:d6:77:07:39
------BEGIN CERTIFICATE-----
-MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
-MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
-DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
-PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
-Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
-AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
-rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
-OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
-xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
-7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
-aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
-HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
-SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
-ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
-AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
-R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
-JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
-Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
------END CERTIFICATE-----
-
# Issuer: CN=SwissSign Gold CA - G2 O=SwissSign AG
# Subject: CN=SwissSign Gold CA - G2 O=SwissSign AG
# Label: "SwissSign Gold CA - G2"
@@ -779,36 +721,6 @@ t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw
WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
-----END CERTIFICATE-----
-# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc
-# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc
-# Label: "Cybertrust Global Root"
-# Serial: 4835703278459682877484360
-# MD5 Fingerprint: 72:e4:4a:87:e3:69:40:80:77:ea:bc:e3:f4:ff:f0:e1
-# SHA1 Fingerprint: 5f:43:e5:b1:bf:f8:78:8c:ac:1c:c7:ca:4a:9a:c6:22:2b:cc:34:c6
-# SHA256 Fingerprint: 96:0a:df:00:63:e9:63:56:75:0c:29:65:dd:0a:08:67:da:0b:9c:bd:6e:77:71:4a:ea:fb:23:49:ab:39:3d:a3
------BEGIN CERTIFICATE-----
-MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYG
-A1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2Jh
-bCBSb290MB4XDTA2MTIxNTA4MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UE
-ChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBS
-b290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+Mi8vRRQZhP/8NN5
-7CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW0ozS
-J8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2y
-HLtgwEZLAfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iP
-t3sMpTjr3kfb1V05/Iin89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNz
-FtApD0mpSPCzqrdsxacwOUBdrsTiXSZT8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAY
-XSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/
-MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2MDSgMqAw
-hi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3Js
-MB8GA1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUA
-A4IBAQBW7wojoFROlZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMj
-Wqd8BfP9IjsO0QbE2zZMcwSO5bAi5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUx
-XOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2hO0j9n0Hq0V+09+zv+mKts2o
-omcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+TX3EJIrduPuoc
-A06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW
-WL1WMRJOEcgh4LMRkWXbtKaIOM5V
------END CERTIFICATE-----
-
# Issuer: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority
# Subject: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority
# Label: "ePKI Root Certification Authority"
@@ -1450,39 +1362,6 @@ Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl
nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI=
-----END CERTIFICATE-----
-# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority
-# Subject: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority
-# Label: "Hellenic Academic and Research Institutions RootCA 2011"
-# Serial: 0
-# MD5 Fingerprint: 73:9f:4c:4b:73:5b:79:e9:fa:ba:1c:ef:6e:cb:d5:c9
-# SHA1 Fingerprint: fe:45:65:9b:79:03:5b:98:a1:61:b5:51:2e:ac:da:58:09:48:22:4d
-# SHA256 Fingerprint: bc:10:4f:15:a4:8b:e7:09:dc:a5:42:a7:e1:d4:b9:df:6f:05:45:27:e8:02:ea:a9:2d:59:54:44:25:8a:fe:71
------BEGIN CERTIFICATE-----
-MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix
-RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1
-dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p
-YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw
-NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK
-EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl
-cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl
-c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB
-BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz
-dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ
-fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns
-bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD
-75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP
-FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV
-HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp
-5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu
-b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA
-A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p
-6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8
-TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7
-dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys
-Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI
-l7WdmplNsDz4SgCbZN2fOUvRJ9e4
------END CERTIFICATE-----
-
# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967
# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967
# Label: "Actalis Authentication Root CA"
@@ -2342,27 +2221,6 @@ zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW
RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=
-----END CERTIFICATE-----
-# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4
-# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4
-# Label: "GlobalSign ECC Root CA - R4"
-# Serial: 14367148294922964480859022125800977897474
-# MD5 Fingerprint: 20:f0:27:68:d1:7e:a0:9d:0e:e6:2a:ca:df:5c:89:8e
-# SHA1 Fingerprint: 69:69:56:2e:40:80:f4:24:a1:e7:19:9f:14:ba:f3:ee:58:ab:6a:bb
-# SHA256 Fingerprint: be:c9:49:11:c2:95:56:76:db:6c:0a:55:09:86:d7:6e:3b:a0:05:66:7c:44:2c:97:62:b4:fb:b7:73:de:22:8c
------BEGIN CERTIFICATE-----
-MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEk
-MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpH
-bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX
-DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD
-QSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu
-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprlOQcJ
-FspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAw
-DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61F
-uOJAf/sKbvu+M8k8o4TVMAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGX
-kPoUVy0D7O48027KqGx2vKLeuwIgJ6iFJzWbVsaj8kfSt24bAgAXqmemFZHe+pTs
-ewv4n4Q=
------END CERTIFICATE-----
-
# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5
# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5
# Label: "GlobalSign ECC Root CA - R5"
@@ -3337,126 +3195,6 @@ rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV
Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9
-----END CERTIFICATE-----
-# Issuer: CN=GTS Root R1 O=Google Trust Services LLC
-# Subject: CN=GTS Root R1 O=Google Trust Services LLC
-# Label: "GTS Root R1"
-# Serial: 146587175971765017618439757810265552097
-# MD5 Fingerprint: 82:1a:ef:d4:d2:4a:f2:9f:e2:3d:97:06:14:70:72:85
-# SHA1 Fingerprint: e1:c9:50:e6:ef:22:f8:4c:56:45:72:8b:92:20:60:d7:d5:a7:a3:e8
-# SHA256 Fingerprint: 2a:57:54:71:e3:13:40:bc:21:58:1c:bd:2c:f1:3e:15:84:63:20:3e:ce:94:bc:f9:d3:cc:19:6b:f0:9a:54:72
------BEGIN CERTIFICATE-----
-MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBH
-MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM
-QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy
-MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl
-cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEB
-AQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM
-f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vX
-mX7wCl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7
-zUjwTcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0P
-fyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtc
-vfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4
-Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUsp
-zBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOO
-Rc92wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYW
-k70paDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+
-DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgF
-lQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
-HQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBADiW
-Cu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1
-d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6Z
-XPYfcX3v73svfuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZR
-gyFmxhE+885H7pwoHyXa/6xmld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3
-d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9bgsiG1eGZbYwE8na6SfZu6W0eX6Dv
-J4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq4BjFbkerQUIpm/Zg
-DdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWErtXvM
-+SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyy
-F62ARPBopY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9
-SQ98POyDGCBDTtWTurQ0sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdws
-E3PYJ/HQcu51OyLemGhmW/HGY0dVHLqlCFF1pkgl
------END CERTIFICATE-----
-
-# Issuer: CN=GTS Root R2 O=Google Trust Services LLC
-# Subject: CN=GTS Root R2 O=Google Trust Services LLC
-# Label: "GTS Root R2"
-# Serial: 146587176055767053814479386953112547951
-# MD5 Fingerprint: 44:ed:9a:0e:a4:09:3b:00:f2:ae:4c:a3:c6:61:b0:8b
-# SHA1 Fingerprint: d2:73:96:2a:2a:5e:39:9f:73:3f:e1:c7:1e:64:3f:03:38:34:fc:4d
-# SHA256 Fingerprint: c4:5d:7b:b0:8e:6d:67:e6:2e:42:35:11:0b:56:4e:5f:78:fd:92:ef:05:8c:84:0a:ea:4e:64:55:d7:58:5c:60
------BEGIN CERTIFICATE-----
-MIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBH
-MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM
-QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy
-MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl
-cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEB
-AQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv
-CvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3Kg
-GjSY6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9Bu
-XvAuMC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOd
-re7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXu
-PuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1
-mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K
-8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqj
-x5RWIr9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsR
-nTKaG73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0
-kzCqgc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9Ok
-twIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
-HQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBALZp
-8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT
-vhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiT
-z9D2PGcDFWEJ+YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiA
-pJiS4wGWAqoC7o87xdFtCjMwc3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvb
-pxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3DaWsYDQvTtN6LwG1BUSw7YhN4ZKJmB
-R64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5rn/WkhLx3+WuXrD5R
-RaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56GtmwfuNmsk
-0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC
-5AwiWVIQ7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiF
-izoHCBy69Y9Vmhh1fuXsgWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLn
-yOd/xCxgXS/Dr55FBcOEArf9LAhST4Ldo/DUhgkC
------END CERTIFICATE-----
-
-# Issuer: CN=GTS Root R3 O=Google Trust Services LLC
-# Subject: CN=GTS Root R3 O=Google Trust Services LLC
-# Label: "GTS Root R3"
-# Serial: 146587176140553309517047991083707763997
-# MD5 Fingerprint: 1a:79:5b:6b:04:52:9c:5d:c7:74:33:1b:25:9a:f9:25
-# SHA1 Fingerprint: 30:d4:24:6f:07:ff:db:91:89:8a:0b:e9:49:66:11:eb:8c:5e:46:e5
-# SHA256 Fingerprint: 15:d5:b8:77:46:19:ea:7d:54:ce:1c:a6:d0:b0:c4:03:e0:37:a9:17:f1:31:e8:a0:4e:1e:6b:7a:71:ba:bc:e5
------BEGIN CERTIFICATE-----
-MIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQsw
-CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
-MBIGA1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
-MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
-Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQA
-IgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout
-736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2A
-DDL24CejQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud
-DgQWBBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFuk
-fCPAlaUs3L6JbyO5o91lAFJekazInXJ0glMLfalAvWhgxeG4VDvBNhcl2MG9AjEA
-njWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOaKaqW04MjyaR7YbPMAuhd
------END CERTIFICATE-----
-
-# Issuer: CN=GTS Root R4 O=Google Trust Services LLC
-# Subject: CN=GTS Root R4 O=Google Trust Services LLC
-# Label: "GTS Root R4"
-# Serial: 146587176229350439916519468929765261721
-# MD5 Fingerprint: 5d:b6:6a:c4:60:17:24:6a:1a:99:a8:4b:ee:5e:b4:26
-# SHA1 Fingerprint: 2a:1d:60:27:d9:4a:b1:0a:1c:4d:91:5c:cd:33:a0:cb:3e:2d:54:cb
-# SHA256 Fingerprint: 71:cc:a5:39:1f:9e:79:4b:04:80:25:30:b3:63:e1:21:da:8a:30:43:bb:26:66:2f:ea:4d:ca:7f:c9:51:a4:bd
------BEGIN CERTIFICATE-----
-MIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQsw
-CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
-MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
-MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
-Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQA
-IgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu
-hXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/l
-xKvRHYqjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud
-DgQWBBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0
-CMRw3J5QdCHojXohw0+WbhXRIjVhLfoIN+4Zba3bssx9BzT1YBkstTTZbyACMANx
-sbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11xzPKwTdb+mciUqXWi4w==
------END CERTIFICATE-----
-
# Issuer: CN=UCA Global G2 Root O=UniTrust
# Subject: CN=UCA Global G2 Root O=UniTrust
# Label: "UCA Global G2 Root"
@@ -4255,3 +3993,693 @@ qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP
0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf
E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb
-----END CERTIFICATE-----
+
+# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique
+# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique
+# Label: "TunTrust Root CA"
+# Serial: 108534058042236574382096126452369648152337120275
+# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4
+# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb
+# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41
+-----BEGIN CERTIFICATE-----
+MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL
+BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg
+Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv
+b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG
+EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u
+IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ
+KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ
+n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd
+2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF
+VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ
+GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF
+li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU
+r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2
+eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb
+MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg
+jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB
+7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW
+5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE
+ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0
+90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z
+xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu
+QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4
+FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH
+22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP
+xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn
+dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5
+Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b
+nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ
+CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH
+u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj
+d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o=
+-----END CERTIFICATE-----
+
+# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA
+# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA
+# Label: "HARICA TLS RSA Root CA 2021"
+# Serial: 76817823531813593706434026085292783742
+# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91
+# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d
+# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d
+-----BEGIN CERTIFICATE-----
+MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs
+MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl
+c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg
+Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL
+MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl
+YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv
+b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l
+mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE
+4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv
+a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M
+pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw
+Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b
+LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY
+AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB
+AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq
+E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr
+W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ
+CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF
+MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE
+AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU
+X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3
+f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja
+H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP
+JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P
+zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt
+jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0
+/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT
+BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79
+aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW
+xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU
+63ZTGI0RmLo=
+-----END CERTIFICATE-----
+
+# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA
+# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA
+# Label: "HARICA TLS ECC Root CA 2021"
+# Serial: 137515985548005187474074462014555733966
+# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0
+# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48
+# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01
+-----BEGIN CERTIFICATE-----
+MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw
+CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh
+cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v
+dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG
+A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj
+aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg
+Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7
+KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y
+STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw
+AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD
+AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw
+SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN
+nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps
+-----END CERTIFICATE-----
+
+# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068
+# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068
+# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068"
+# Serial: 1977337328857672817
+# MD5 Fingerprint: 4e:6e:9b:54:4c:ca:b7:fa:48:e4:90:b1:15:4b:1c:a3
+# SHA1 Fingerprint: 0b:be:c2:27:22:49:cb:39:aa:db:35:5c:53:e3:8c:ae:78:ff:b6:fe
+# SHA256 Fingerprint: 57:de:05:83:ef:d2:b2:6e:03:61:da:99:da:9d:f4:64:8d:ef:7e:e8:44:1c:3b:72:8a:fa:9b:cd:e0:f9:b2:6a
+-----BEGIN CERTIFICATE-----
+MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE
+BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h
+cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1
+MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg
+Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9
+thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM
+cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG
+L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i
+NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h
+X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b
+m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy
+Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja
+EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T
+KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF
+6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh
+OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc
+tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd
+IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j
+b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC
+AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw
+ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m
+iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF
+Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ
+hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P
+Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE
+EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV
+1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t
+CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR
+5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw
+f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9
+ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK
+GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV
+-----END CERTIFICATE-----
+
+# Issuer: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd.
+# Subject: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd.
+# Label: "vTrus ECC Root CA"
+# Serial: 630369271402956006249506845124680065938238527194
+# MD5 Fingerprint: de:4b:c1:f5:52:8c:9b:43:e1:3e:8f:55:54:17:8d:85
+# SHA1 Fingerprint: f6:9c:db:b0:fc:f6:02:13:b6:52:32:a6:a3:91:3f:16:70:da:c3:e1
+# SHA256 Fingerprint: 30:fb:ba:2c:32:23:8e:2a:98:54:7a:f9:79:31:e5:50:42:8b:9b:3f:1c:8e:eb:66:33:dc:fa:86:c5:b2:7d:d3
+-----BEGIN CERTIFICATE-----
+MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw
+RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY
+BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz
+MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u
+LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF
+K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0
+v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd
+e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD
+VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw
+V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA
+AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG
+GJTO
+-----END CERTIFICATE-----
+
+# Issuer: CN=vTrus Root CA O=iTrusChina Co.,Ltd.
+# Subject: CN=vTrus Root CA O=iTrusChina Co.,Ltd.
+# Label: "vTrus Root CA"
+# Serial: 387574501246983434957692974888460947164905180485
+# MD5 Fingerprint: b8:c9:37:df:fa:6b:31:84:64:c5:ea:11:6a:1b:75:fc
+# SHA1 Fingerprint: 84:1a:69:fb:f5:cd:1a:25:34:13:3d:e3:f8:fc:b8:99:d0:c9:14:b7
+# SHA256 Fingerprint: 8a:71:de:65:59:33:6f:42:6c:26:e5:38:80:d0:0d:88:a1:8d:a4:c6:a9:1f:0d:cb:61:94:e2:06:c5:c9:63:87
+-----BEGIN CERTIFICATE-----
+MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL
+BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x
+FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx
+MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s
+THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD
+ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc
+IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU
+AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+
+GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9
+8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH
+flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt
+J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim
+0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN
+pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ
+UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW
+OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB
+AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E
+BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet
+8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd
+nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j
+bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM
+Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv
+TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS
+S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr
+I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9
+b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB
+UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P
+Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven
+sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s=
+-----END CERTIFICATE-----
+
+# Issuer: CN=ISRG Root X2 O=Internet Security Research Group
+# Subject: CN=ISRG Root X2 O=Internet Security Research Group
+# Label: "ISRG Root X2"
+# Serial: 87493402998870891108772069816698636114
+# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5
+# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af
+# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70
+-----BEGIN CERTIFICATE-----
+MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
+CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
+R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
+MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
+ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
+EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
+ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
+AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
+zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
+tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
+/q4AaOeMSQ+2b1tbFfLn
+-----END CERTIFICATE-----
+
+# Issuer: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd.
+# Subject: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd.
+# Label: "HiPKI Root CA - G1"
+# Serial: 60966262342023497858655262305426234976
+# MD5 Fingerprint: 69:45:df:16:65:4b:e8:68:9a:8f:76:5f:ff:80:9e:d3
+# SHA1 Fingerprint: 6a:92:e4:a8:ee:1b:ec:96:45:37:e3:29:57:49:cd:96:e3:e5:d2:60
+# SHA256 Fingerprint: f0:15:ce:3c:c2:39:bf:ef:06:4b:e9:f1:d2:c4:17:e1:a0:26:4a:0a:94:be:1f:0c:8d:12:18:64:eb:69:49:cc
+-----BEGIN CERTIFICATE-----
+MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP
+MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0
+ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa
+Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3
+YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw
+qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv
+Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6
+lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz
+Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ
+KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK
+FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj
+HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr
+y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ
+/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM
+a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6
+fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
+HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG
+SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi
+7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc
+SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza
+ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc
+XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg
+iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho
+L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF
+Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr
+kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+
+vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU
+YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ==
+-----END CERTIFICATE-----
+
+# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4
+# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4
+# Label: "GlobalSign ECC Root CA - R4"
+# Serial: 159662223612894884239637590694
+# MD5 Fingerprint: 26:29:f8:6d:e1:88:bf:a2:65:7f:aa:c4:cd:0f:7f:fc
+# SHA1 Fingerprint: 6b:a0:b0:98:e1:71:ef:5a:ad:fe:48:15:80:77:10:f4:bd:6f:0b:28
+# SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2
+-----BEGIN CERTIFICATE-----
+MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD
+VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh
+bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw
+MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g
+UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT
+BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx
+uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV
+HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/
++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147
+bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm
+-----END CERTIFICATE-----
+
+# Issuer: CN=GTS Root R1 O=Google Trust Services LLC
+# Subject: CN=GTS Root R1 O=Google Trust Services LLC
+# Label: "GTS Root R1"
+# Serial: 159662320309726417404178440727
+# MD5 Fingerprint: 05:fe:d0:bf:71:a8:a3:76:63:da:01:e0:d8:52:dc:40
+# SHA1 Fingerprint: e5:8c:1c:c4:91:3b:38:63:4b:e9:10:6e:e3:ad:8e:6b:9d:d9:81:4a
+# SHA256 Fingerprint: d9:47:43:2a:bd:e7:b7:fa:90:fc:2e:6b:59:10:1b:12:80:e0:e1:c7:e4:e4:0f:a3:c6:88:7f:ff:57:a7:f4:cf
+-----BEGIN CERTIFICATE-----
+MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw
+CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
+MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
+MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
+Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA
+A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo
+27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w
+Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw
+TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl
+qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH
+szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8
+Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk
+MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92
+wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p
+aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN
+VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID
+AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
+FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb
+C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe
+QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy
+h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4
+7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J
+ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef
+MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/
+Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT
+6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ
+0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm
+2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb
+bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c
+-----END CERTIFICATE-----
+
+# Issuer: CN=GTS Root R2 O=Google Trust Services LLC
+# Subject: CN=GTS Root R2 O=Google Trust Services LLC
+# Label: "GTS Root R2"
+# Serial: 159662449406622349769042896298
+# MD5 Fingerprint: 1e:39:c0:53:e6:1e:29:82:0b:ca:52:55:36:5d:57:dc
+# SHA1 Fingerprint: 9a:44:49:76:32:db:de:fa:d0:bc:fb:5a:7b:17:bd:9e:56:09:24:94
+# SHA256 Fingerprint: 8d:25:cd:97:22:9d:bf:70:35:6b:da:4e:b3:cc:73:40:31:e2:4c:f0:0f:af:cf:d3:2d:c7:6e:b5:84:1c:7e:a8
+-----BEGIN CERTIFICATE-----
+MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw
+CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
+MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
+MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
+Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA
+A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt
+nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY
+6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu
+MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k
+RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg
+f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV
++3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo
+dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW
+Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa
+G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq
+gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID
+AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
+FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H
+vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8
+0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC
+B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u
+NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg
+yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev
+HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6
+xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR
+TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg
+JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV
+7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl
+6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL
+-----END CERTIFICATE-----
+
+# Issuer: CN=GTS Root R3 O=Google Trust Services LLC
+# Subject: CN=GTS Root R3 O=Google Trust Services LLC
+# Label: "GTS Root R3"
+# Serial: 159662495401136852707857743206
+# MD5 Fingerprint: 3e:e7:9d:58:02:94:46:51:94:e5:e0:22:4a:8b:e7:73
+# SHA1 Fingerprint: ed:e5:71:80:2b:c8:92:b9:5b:83:3c:d2:32:68:3f:09:cd:a0:1e:46
+# SHA256 Fingerprint: 34:d8:a7:3e:e2:08:d9:bc:db:0d:95:65:20:93:4b:4e:40:e6:94:82:59:6e:8b:6f:73:c8:42:6b:01:0a:6f:48
+-----BEGIN CERTIFICATE-----
+MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD
+VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG
+A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw
+WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz
+IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
+AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G
+jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2
+4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
+BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7
+VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm
+ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X
+-----END CERTIFICATE-----
+
+# Issuer: CN=GTS Root R4 O=Google Trust Services LLC
+# Subject: CN=GTS Root R4 O=Google Trust Services LLC
+# Label: "GTS Root R4"
+# Serial: 159662532700760215368942768210
+# MD5 Fingerprint: 43:96:83:77:19:4d:76:b3:9d:65:52:e4:1d:22:a5:e8
+# SHA1 Fingerprint: 77:d3:03:67:b5:e0:0c:15:f6:0c:38:61:df:7c:e1:3b:92:46:4d:47
+# SHA256 Fingerprint: 34:9d:fa:40:58:c5:e2:63:12:3b:39:8a:e7:95:57:3c:4e:13:13:c8:3f:e6:8f:93:55:6c:d5:e8:03:1b:3c:7d
+-----BEGIN CERTIFICATE-----
+MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD
+VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG
+A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw
+WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz
+IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
+AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi
+QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR
+HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
+BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D
+9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8
+p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD
+-----END CERTIFICATE-----
+
+# Issuer: CN=Telia Root CA v2 O=Telia Finland Oyj
+# Subject: CN=Telia Root CA v2 O=Telia Finland Oyj
+# Label: "Telia Root CA v2"
+# Serial: 7288924052977061235122729490515358
+# MD5 Fingerprint: 0e:8f:ac:aa:82:df:85:b1:f4:dc:10:1c:fc:99:d9:48
+# SHA1 Fingerprint: b9:99:cd:d1:73:50:8a:c4:47:05:08:9c:8c:88:fb:be:a0:2b:40:cd
+# SHA256 Fingerprint: 24:2b:69:74:2f:cb:1e:5b:2a:bf:98:89:8b:94:57:21:87:54:4e:5b:4d:99:11:78:65:73:62:1f:6a:74:b8:2c
+-----BEGIN CERTIFICATE-----
+MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx
+CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE
+AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1
+NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ
+MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq
+AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9
+vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9
+lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD
+n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT
+7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o
+6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC
+TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6
+WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R
+DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI
+pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj
+YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy
+rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw
+AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ
+8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi
+0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM
+A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS
+SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K
+TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF
+6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er
+3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt
+Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT
+VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW
+ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA
+rBPuUBQemMc=
+-----END CERTIFICATE-----
+
+# Issuer: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH
+# Subject: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH
+# Label: "D-TRUST BR Root CA 1 2020"
+# Serial: 165870826978392376648679885835942448534
+# MD5 Fingerprint: b5:aa:4b:d5:ed:f7:e3:55:2e:8f:72:0a:f3:75:b8:ed
+# SHA1 Fingerprint: 1f:5b:98:f0:e3:b5:f7:74:3c:ed:e6:b0:36:7d:32:cd:f4:09:41:67
+# SHA256 Fingerprint: e5:9a:aa:81:60:09:c2:2b:ff:5b:25:ba:d3:7d:f3:06:f0:49:79:7c:1f:81:d8:5a:b0:89:e6:57:bd:8f:00:44
+-----BEGIN CERTIFICATE-----
+MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw
+CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS
+VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5
+NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG
+A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB
+BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS
+zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0
+QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/
+VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g
+PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf
+Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l
+dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1
+c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO
+PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW
+wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV
+dWNbFJWcHwHP2NVypw87
+-----END CERTIFICATE-----
+
+# Issuer: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH
+# Subject: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH
+# Label: "D-TRUST EV Root CA 1 2020"
+# Serial: 126288379621884218666039612629459926992
+# MD5 Fingerprint: 8c:2d:9d:70:9f:48:99:11:06:11:fb:e9:cb:30:c0:6e
+# SHA1 Fingerprint: 61:db:8c:21:59:69:03:90:d8:7c:9c:12:86:54:cf:9d:3d:f4:dd:07
+# SHA256 Fingerprint: 08:17:0d:1a:a3:64:53:90:1a:2f:95:92:45:e3:47:db:0c:8d:37:ab:aa:bc:56:b8:1a:a1:00:dc:95:89:70:db
+-----BEGIN CERTIFICATE-----
+MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw
+CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS
+VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5
+NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG
+A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB
+BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC
+/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD
+wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3
+OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g
+PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf
+Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l
+dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1
+c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO
+PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA
+y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb
+gfM0agPnIjhQW+0ZT0MW
+-----END CERTIFICATE-----
+
+# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc.
+# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc.
+# Label: "DigiCert TLS ECC P384 Root G5"
+# Serial: 13129116028163249804115411775095713523
+# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed
+# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee
+# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05
+-----BEGIN CERTIFICATE-----
+MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw
+CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp
+Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2
+MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
+bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG
+ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS
+7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp
+0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS
+B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49
+BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ
+LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4
+DXZDjC5Ty3zfDBeWUA==
+-----END CERTIFICATE-----
+
+# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc.
+# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc.
+# Label: "DigiCert TLS RSA4096 Root G5"
+# Serial: 11930366277458970227240571539258396554
+# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1
+# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35
+# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75
+-----BEGIN CERTIFICATE-----
+MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN
+MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT
+HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN
+NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs
+IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi
+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+
+ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0
+2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp
+wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM
+pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD
+nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po
+sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx
+Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd
+Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX
+KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe
+XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL
+tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv
+TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN
+AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw
+GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H
+PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF
+O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ
+REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik
+AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv
+/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+
+p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw
+MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF
+qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK
+ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+
+-----END CERTIFICATE-----
+
+# Issuer: CN=Certainly Root R1 O=Certainly
+# Subject: CN=Certainly Root R1 O=Certainly
+# Label: "Certainly Root R1"
+# Serial: 188833316161142517227353805653483829216
+# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12
+# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af
+# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0
+-----BEGIN CERTIFICATE-----
+MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw
+PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy
+dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9
+MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0
+YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2
+1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT
+vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed
+aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0
+1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5
+r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5
+cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ
+wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ
+6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA
+2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH
+Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR
+eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB
+/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u
+d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr
+PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d
+8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi
+1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd
+rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di
+taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7
+lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj
+yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn
+Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy
+yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n
+wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6
+OV+KmalBWQewLK8=
+-----END CERTIFICATE-----
+
+# Issuer: CN=Certainly Root E1 O=Certainly
+# Subject: CN=Certainly Root E1 O=Certainly
+# Label: "Certainly Root E1"
+# Serial: 8168531406727139161245376702891150584
+# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9
+# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b
+# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2
+-----BEGIN CERTIFICATE-----
+MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw
+CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu
+bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ
+BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s
+eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK
++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2
+QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E
+BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4
+hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm
+ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG
+BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR
+-----END CERTIFICATE-----
+
+# Issuer: CN=E-Tugra Global Root CA RSA v3 O=E-Tugra EBG A.S. OU=E-Tugra Trust Center
+# Subject: CN=E-Tugra Global Root CA RSA v3 O=E-Tugra EBG A.S. OU=E-Tugra Trust Center
+# Label: "E-Tugra Global Root CA RSA v3"
+# Serial: 75951268308633135324246244059508261641472512052
+# MD5 Fingerprint: 22:be:10:f6:c2:f8:03:88:73:5f:33:29:47:28:47:a4
+# SHA1 Fingerprint: e9:a8:5d:22:14:52:1c:5b:aa:0a:b4:be:24:6a:23:8a:c9:ba:e2:a9
+# SHA256 Fingerprint: ef:66:b0:b1:0a:3c:db:9f:2e:36:48:c7:6b:d2:af:18:ea:d2:bf:e6:f1:17:65:5e:28:c4:06:0d:a1:a3:f4:c2
+-----BEGIN CERTIFICATE-----
+MIIF8zCCA9ugAwIBAgIUDU3FzRYilZYIfrgLfxUGNPt5EDQwDQYJKoZIhvcNAQEL
+BQAwgYAxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHEwZBbmthcmExGTAXBgNVBAoTEEUt
+VHVncmEgRUJHIEEuUy4xHTAbBgNVBAsTFEUtVHVncmEgVHJ1c3QgQ2VudGVyMSYw
+JAYDVQQDEx1FLVR1Z3JhIEdsb2JhbCBSb290IENBIFJTQSB2MzAeFw0yMDAzMTgw
+OTA3MTdaFw00NTAzMTIwOTA3MTdaMIGAMQswCQYDVQQGEwJUUjEPMA0GA1UEBxMG
+QW5rYXJhMRkwFwYDVQQKExBFLVR1Z3JhIEVCRyBBLlMuMR0wGwYDVQQLExRFLVR1
+Z3JhIFRydXN0IENlbnRlcjEmMCQGA1UEAxMdRS1UdWdyYSBHbG9iYWwgUm9vdCBD
+QSBSU0EgdjMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCiZvCJt3J7
+7gnJY9LTQ91ew6aEOErxjYG7FL1H6EAX8z3DeEVypi6Q3po61CBxyryfHUuXCscx
+uj7X/iWpKo429NEvx7epXTPcMHD4QGxLsqYxYdE0PD0xesevxKenhOGXpOhL9hd8
+7jwH7eKKV9y2+/hDJVDqJ4GohryPUkqWOmAalrv9c/SF/YP9f4RtNGx/ardLAQO/
+rWm31zLZ9Vdq6YaCPqVmMbMWPcLzJmAy01IesGykNz709a/r4d+ABs8qQedmCeFL
+l+d3vSFtKbZnwy1+7dZ5ZdHPOrbRsV5WYVB6Ws5OUDGAA5hH5+QYfERaxqSzO8bG
+wzrwbMOLyKSRBfP12baqBqG3q+Sx6iEUXIOk/P+2UNOMEiaZdnDpwA+mdPy70Bt4
+znKS4iicvObpCdg604nmvi533wEKb5b25Y08TVJ2Glbhc34XrD2tbKNSEhhw5oBO
+M/J+JjKsBY04pOZ2PJ8QaQ5tndLBeSBrW88zjdGUdjXnXVXHt6woq0bM5zshtQoK
+5EpZ3IE1S0SVEgpnpaH/WwAH0sDM+T/8nzPyAPiMbIedBi3x7+PmBvrFZhNb/FAH
+nnGGstpvdDDPk1Po3CLW3iAfYY2jLqN4MpBs3KwytQXk9TwzDdbgh3cXTJ2w2Amo
+DVf3RIXwyAS+XF1a4xeOVGNpf0l0ZAWMowIDAQABo2MwYTAPBgNVHRMBAf8EBTAD
+AQH/MB8GA1UdIwQYMBaAFLK0ruYt9ybVqnUtdkvAG1Mh0EjvMB0GA1UdDgQWBBSy
+tK7mLfcm1ap1LXZLwBtTIdBI7zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEL
+BQADggIBAImocn+M684uGMQQgC0QDP/7FM0E4BQ8Tpr7nym/Ip5XuYJzEmMmtcyQ
+6dIqKe6cLcwsmb5FJ+Sxce3kOJUxQfJ9emN438o2Fi+CiJ+8EUdPdk3ILY7r3y18
+Tjvarvbj2l0Upq7ohUSdBm6O++96SmotKygY/r+QLHUWnw/qln0F7psTpURs+APQ
+3SPh/QMSEgj0GDSz4DcLdxEBSL9htLX4GdnLTeqjjO/98Aa1bZL0SmFQhO3sSdPk
+vmjmLuMxC1QLGpLWgti2omU8ZgT5Vdps+9u1FGZNlIM7zR6mK7L+d0CGq+ffCsn9
+9t2HVhjYsCxVYJb6CH5SkPVLpi6HfMsg2wY+oF0Dd32iPBMbKaITVaA9FCKvb7jQ
+mhty3QUBjYZgv6Rn7rWlDdF/5horYmbDB7rnoEgcOMPpRfunf/ztAmgayncSd6YA
+VSgU7NbHEqIbZULpkejLPoeJVF3Zr52XnGnnCv8PWniLYypMfUeUP95L6VPQMPHF
+9p5J3zugkaOj/s1YzOrfr28oO6Bpm4/srK4rVJ2bBLFHIK+WEj5jlB0E5y67hscM
+moi/dkfv97ALl2bSRM9gUgfh1SxKOidhd8rXj+eHDjD/DLsE4mHDosiXYY60MGo8
+bcIHX0pzLz/5FooBZu+6kcpSV3uu1OYP3Qt6f4ueJiDPO++BcYNZ
+-----END CERTIFICATE-----
+
+# Issuer: CN=E-Tugra Global Root CA ECC v3 O=E-Tugra EBG A.S. OU=E-Tugra Trust Center
+# Subject: CN=E-Tugra Global Root CA ECC v3 O=E-Tugra EBG A.S. OU=E-Tugra Trust Center
+# Label: "E-Tugra Global Root CA ECC v3"
+# Serial: 218504919822255052842371958738296604628416471745
+# MD5 Fingerprint: 46:bc:81:bb:f1:b5:1e:f7:4b:96:bc:14:e2:e7:27:64
+# SHA1 Fingerprint: 8a:2f:af:57:53:b1:b0:e6:a1:04:ec:5b:6a:69:71:6d:f6:1c:e2:84
+# SHA256 Fingerprint: 87:3f:46:85:fa:7f:56:36:25:25:2e:6d:36:bc:d7:f1:6f:c2:49:51:f2:64:e4:7e:1b:95:4f:49:08:cd:ca:13
+-----BEGIN CERTIFICATE-----
+MIICpTCCAiqgAwIBAgIUJkYZdzHhT28oNt45UYbm1JeIIsEwCgYIKoZIzj0EAwMw
+gYAxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHEwZBbmthcmExGTAXBgNVBAoTEEUtVHVn
+cmEgRUJHIEEuUy4xHTAbBgNVBAsTFEUtVHVncmEgVHJ1c3QgQ2VudGVyMSYwJAYD
+VQQDEx1FLVR1Z3JhIEdsb2JhbCBSb290IENBIEVDQyB2MzAeFw0yMDAzMTgwOTQ2
+NThaFw00NTAzMTIwOTQ2NThaMIGAMQswCQYDVQQGEwJUUjEPMA0GA1UEBxMGQW5r
+YXJhMRkwFwYDVQQKExBFLVR1Z3JhIEVCRyBBLlMuMR0wGwYDVQQLExRFLVR1Z3Jh
+IFRydXN0IENlbnRlcjEmMCQGA1UEAxMdRS1UdWdyYSBHbG9iYWwgUm9vdCBDQSBF
+Q0MgdjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASOmCm/xxAeJ9urA8woLNheSBkQ
+KczLWYHMjLiSF4mDKpL2w6QdTGLVn9agRtwcvHbB40fQWxPa56WzZkjnIZpKT4YK
+fWzqTTKACrJ6CZtpS5iB4i7sAnCWH/31Rs7K3IKjYzBhMA8GA1UdEwEB/wQFMAMB
+Af8wHwYDVR0jBBgwFoAU/4Ixcj75xGZsrTie0bBRiKWQzPUwHQYDVR0OBBYEFP+C
+MXI++cRmbK04ntGwUYilkMz1MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNp
+ADBmAjEA5gVYaWHlLcoNy/EZCL3W/VGSGn5jVASQkZo1kTmZ+gepZpO6yGjUij/6
+7W4WAie3AjEA3VoXK3YdZUKWpqxdinlW2Iob35reX8dQj7FbcQwm32pAAOwzkSFx
+vmjkI6TZraE3
+-----END CERTIFICATE-----
diff --git a/src/pip/_vendor/certifi/core.py b/src/pip/_vendor/certifi/core.py
index b8140cf1a..f34045b52 100644
--- a/src/pip/_vendor/certifi/core.py
+++ b/src/pip/_vendor/certifi/core.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
certifi.py
~~~~~~~~~~
@@ -7,6 +5,8 @@ certifi.py
This module returns the installation location of cacert.pem or its contents.
"""
import os
+import types
+from typing import Union
class _PipPatchedCertificate(Exception):
@@ -28,7 +28,7 @@ try:
_CACERT_CTX = None
_CACERT_PATH = None
- def where():
+ def where() -> str:
# This is slightly terrible, but we want to delay extracting the file
# in cases where we're inside of a zipimport situation until someone
# actually calls where(), but we don't want to re-extract the file
@@ -56,21 +56,29 @@ except _PipPatchedCertificate:
pass
except ImportError:
+ Package = Union[types.ModuleType, str]
+ Resource = Union[str, "os.PathLike"]
+
# This fallback will work for Python versions prior to 3.7 that lack the
# importlib.resources module but relies on the existing `where` function
# so won't address issues with environments like PyOxidizer that don't set
# __file__ on modules.
- def read_text(_module, _path, encoding="ascii"):
- with open(where(), "r", encoding=encoding) as data:
+ def read_text(
+ package: Package,
+ resource: Resource,
+ encoding: str = 'utf-8',
+ errors: str = 'strict'
+ ) -> str:
+ with open(where(), encoding=encoding) as data:
return data.read()
# If we don't have importlib.resources, then we will just do the old logic
# of assuming we're on the filesystem and munge the path directly.
- def where():
+ def where() -> str:
f = os.path.dirname(__file__)
return os.path.join(f, "cacert.pem")
-def contents():
+def contents() -> str:
return read_text("certifi", "cacert.pem", encoding="ascii")
diff --git a/src/pip/_vendor/certifi/py.typed b/src/pip/_vendor/certifi/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pip/_vendor/certifi/py.typed
diff --git a/src/pip/_vendor/chardet/LICENSE b/src/pip/_vendor/chardet/LICENSE
index 8add30ad5..4362b4915 100644
--- a/src/pip/_vendor/chardet/LICENSE
+++ b/src/pip/_vendor/chardet/LICENSE
@@ -1,8 +1,8 @@
- GNU LESSER GENERAL PUBLIC LICENSE
- Version 2.1, February 1999
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
- 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@@ -10,7 +10,7 @@
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
- Preamble
+ Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
@@ -112,7 +112,7 @@ modification follow. Pay close attention to the difference between a
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
- GNU LESSER GENERAL PUBLIC LICENSE
+ GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
@@ -146,7 +146,7 @@ such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
-
+
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
@@ -432,7 +432,7 @@ decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
- NO WARRANTY
+ NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
@@ -455,7 +455,7 @@ FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
- END OF TERMS AND CONDITIONS
+ END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
@@ -485,7 +485,7 @@ convey the exclusion of warranty; and each file should have at least the
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
@@ -500,5 +500,3 @@ necessary. Here is a sample; alter the names:
Ty Coon, President of Vice
That's all there is to it!
-
-
diff --git a/src/pip/_vendor/chardet/__init__.py b/src/pip/_vendor/chardet/__init__.py
index 80ad2546d..e91ad6182 100644
--- a/src/pip/_vendor/chardet/__init__.py
+++ b/src/pip/_vendor/chardet/__init__.py
@@ -15,13 +15,11 @@
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
-
-from .universaldetector import UniversalDetector
from .enums import InputState
-from .version import __version__, VERSION
-
+from .universaldetector import UniversalDetector
+from .version import VERSION, __version__
-__all__ = ['UniversalDetector', 'detect', 'detect_all', '__version__', 'VERSION']
+__all__ = ["UniversalDetector", "detect", "detect_all", "__version__", "VERSION"]
def detect(byte_str):
@@ -33,51 +31,63 @@ def detect(byte_str):
"""
if not isinstance(byte_str, bytearray):
if not isinstance(byte_str, bytes):
- raise TypeError('Expected object of type bytes or bytearray, got: '
- '{}'.format(type(byte_str)))
- else:
- byte_str = bytearray(byte_str)
+ raise TypeError(
+ f"Expected object of type bytes or bytearray, got: {type(byte_str)}"
+ )
+ byte_str = bytearray(byte_str)
detector = UniversalDetector()
detector.feed(byte_str)
return detector.close()
-def detect_all(byte_str):
+def detect_all(byte_str, ignore_threshold=False):
"""
Detect all the possible encodings of the given byte string.
- :param byte_str: The byte sequence to examine.
- :type byte_str: ``bytes`` or ``bytearray``
+ :param byte_str: The byte sequence to examine.
+ :type byte_str: ``bytes`` or ``bytearray``
+ :param ignore_threshold: Include encodings that are below
+ ``UniversalDetector.MINIMUM_THRESHOLD``
+ in results.
+ :type ignore_threshold: ``bool``
"""
if not isinstance(byte_str, bytearray):
if not isinstance(byte_str, bytes):
- raise TypeError('Expected object of type bytes or bytearray, got: '
- '{}'.format(type(byte_str)))
- else:
- byte_str = bytearray(byte_str)
+ raise TypeError(
+ f"Expected object of type bytes or bytearray, got: {type(byte_str)}"
+ )
+ byte_str = bytearray(byte_str)
detector = UniversalDetector()
detector.feed(byte_str)
detector.close()
- if detector._input_state == InputState.HIGH_BYTE:
+ if detector.input_state == InputState.HIGH_BYTE:
results = []
- for prober in detector._charset_probers:
- if prober.get_confidence() > detector.MINIMUM_THRESHOLD:
- charset_name = prober.charset_name
- lower_charset_name = prober.charset_name.lower()
+ probers = []
+ for prober in detector.charset_probers:
+ if hasattr(prober, "probers"):
+ probers.extend(p for p in prober.probers)
+ else:
+ probers.append(prober)
+ for prober in probers:
+ if ignore_threshold or prober.get_confidence() > detector.MINIMUM_THRESHOLD:
+ charset_name = prober.charset_name or ""
+ lower_charset_name = charset_name.lower()
# Use Windows encoding name instead of ISO-8859 if we saw any
# extra Windows-specific bytes
- if lower_charset_name.startswith('iso-8859'):
- if detector._has_win_bytes:
- charset_name = detector.ISO_WIN_MAP.get(lower_charset_name,
- charset_name)
- results.append({
- 'encoding': charset_name,
- 'confidence': prober.get_confidence(),
- 'language': prober.language,
- })
+ if lower_charset_name.startswith("iso-8859") and detector.has_win_bytes:
+ charset_name = detector.ISO_WIN_MAP.get(
+ lower_charset_name, charset_name
+ )
+ results.append(
+ {
+ "encoding": charset_name,
+ "confidence": prober.get_confidence(),
+ "language": prober.language,
+ }
+ )
if len(results) > 0:
- return sorted(results, key=lambda result: -result['confidence'])
+ return sorted(results, key=lambda result: -result["confidence"])
return [detector.result]
diff --git a/src/pip/_vendor/chardet/big5freq.py b/src/pip/_vendor/chardet/big5freq.py
index 38f32517a..87d9f972e 100644
--- a/src/pip/_vendor/chardet/big5freq.py
+++ b/src/pip/_vendor/chardet/big5freq.py
@@ -42,9 +42,9 @@
BIG5_TYPICAL_DISTRIBUTION_RATIO = 0.75
-#Char to FreqOrder table
+# Char to FreqOrder table
BIG5_TABLE_SIZE = 5376
-
+# fmt: off
BIG5_CHAR_TO_FREQ_ORDER = (
1,1801,1506, 255,1431, 198, 9, 82, 6,5008, 177, 202,3681,1256,2821, 110, # 16
3814, 33,3274, 261, 76, 44,2114, 16,2946,2187,1176, 659,3971, 26,3451,2653, # 32
@@ -383,4 +383,4 @@ BIG5_CHAR_TO_FREQ_ORDER = (
890,3669,3943,5791,1878,3798,3439,5792,2186,2358,3440,1652,5793,5794,5795, 941, # 5360
2299, 208,3546,4161,2020, 330,4438,3944,2906,2499,3799,4439,4811,5796,5797,5798, # 5376
)
-
+# fmt: on
diff --git a/src/pip/_vendor/chardet/big5prober.py b/src/pip/_vendor/chardet/big5prober.py
index 98f997012..e4dfa7aa0 100644
--- a/src/pip/_vendor/chardet/big5prober.py
+++ b/src/pip/_vendor/chardet/big5prober.py
@@ -25,15 +25,15 @@
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
-from .mbcharsetprober import MultiByteCharSetProber
-from .codingstatemachine import CodingStateMachine
from .chardistribution import Big5DistributionAnalysis
+from .codingstatemachine import CodingStateMachine
+from .mbcharsetprober import MultiByteCharSetProber
from .mbcssm import BIG5_SM_MODEL
class Big5Prober(MultiByteCharSetProber):
def __init__(self):
- super(Big5Prober, self).__init__()
+ super().__init__()
self.coding_sm = CodingStateMachine(BIG5_SM_MODEL)
self.distribution_analyzer = Big5DistributionAnalysis()
self.reset()
diff --git a/src/pip/_vendor/chardet/chardistribution.py b/src/pip/_vendor/chardet/chardistribution.py
index c0395f4a4..27b4a2939 100644
--- a/src/pip/_vendor/chardet/chardistribution.py
+++ b/src/pip/_vendor/chardet/chardistribution.py
@@ -25,19 +25,35 @@
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
-from .euctwfreq import (EUCTW_CHAR_TO_FREQ_ORDER, EUCTW_TABLE_SIZE,
- EUCTW_TYPICAL_DISTRIBUTION_RATIO)
-from .euckrfreq import (EUCKR_CHAR_TO_FREQ_ORDER, EUCKR_TABLE_SIZE,
- EUCKR_TYPICAL_DISTRIBUTION_RATIO)
-from .gb2312freq import (GB2312_CHAR_TO_FREQ_ORDER, GB2312_TABLE_SIZE,
- GB2312_TYPICAL_DISTRIBUTION_RATIO)
-from .big5freq import (BIG5_CHAR_TO_FREQ_ORDER, BIG5_TABLE_SIZE,
- BIG5_TYPICAL_DISTRIBUTION_RATIO)
-from .jisfreq import (JIS_CHAR_TO_FREQ_ORDER, JIS_TABLE_SIZE,
- JIS_TYPICAL_DISTRIBUTION_RATIO)
-
-
-class CharDistributionAnalysis(object):
+from .big5freq import (
+ BIG5_CHAR_TO_FREQ_ORDER,
+ BIG5_TABLE_SIZE,
+ BIG5_TYPICAL_DISTRIBUTION_RATIO,
+)
+from .euckrfreq import (
+ EUCKR_CHAR_TO_FREQ_ORDER,
+ EUCKR_TABLE_SIZE,
+ EUCKR_TYPICAL_DISTRIBUTION_RATIO,
+)
+from .euctwfreq import (
+ EUCTW_CHAR_TO_FREQ_ORDER,
+ EUCTW_TABLE_SIZE,
+ EUCTW_TYPICAL_DISTRIBUTION_RATIO,
+)
+from .gb2312freq import (
+ GB2312_CHAR_TO_FREQ_ORDER,
+ GB2312_TABLE_SIZE,
+ GB2312_TYPICAL_DISTRIBUTION_RATIO,
+)
+from .jisfreq import (
+ JIS_CHAR_TO_FREQ_ORDER,
+ JIS_TABLE_SIZE,
+ JIS_TYPICAL_DISTRIBUTION_RATIO,
+)
+from .johabfreq import JOHAB_TO_EUCKR_ORDER_TABLE
+
+
+class CharDistributionAnalysis:
ENOUGH_DATA_THRESHOLD = 1024
SURE_YES = 0.99
SURE_NO = 0.01
@@ -46,7 +62,7 @@ class CharDistributionAnalysis(object):
def __init__(self):
# Mapping table to get frequency order from char order (get from
# GetOrder())
- self._char_to_freq_order = None
+ self._char_to_freq_order = tuple()
self._table_size = None # Size of above table
# This is a constant value which varies from language to language,
# used in calculating confidence. See
@@ -89,8 +105,9 @@ class CharDistributionAnalysis(object):
return self.SURE_NO
if self._total_chars != self._freq_chars:
- r = (self._freq_chars / ((self._total_chars - self._freq_chars)
- * self.typical_distribution_ratio))
+ r = self._freq_chars / (
+ (self._total_chars - self._freq_chars) * self.typical_distribution_ratio
+ )
if r < self.SURE_YES:
return r
@@ -102,7 +119,7 @@ class CharDistributionAnalysis(object):
# For charset detection, certain amount of data is enough
return self._total_chars > self.ENOUGH_DATA_THRESHOLD
- def get_order(self, byte_str):
+ def get_order(self, _):
# We do not handle characters based on the original encoding string,
# but convert this encoding string to a number, here called order.
# This allows multiple encodings of a language to share one frequency
@@ -112,7 +129,7 @@ class CharDistributionAnalysis(object):
class EUCTWDistributionAnalysis(CharDistributionAnalysis):
def __init__(self):
- super(EUCTWDistributionAnalysis, self).__init__()
+ super().__init__()
self._char_to_freq_order = EUCTW_CHAR_TO_FREQ_ORDER
self._table_size = EUCTW_TABLE_SIZE
self.typical_distribution_ratio = EUCTW_TYPICAL_DISTRIBUTION_RATIO
@@ -125,13 +142,12 @@ class EUCTWDistributionAnalysis(CharDistributionAnalysis):
first_char = byte_str[0]
if first_char >= 0xC4:
return 94 * (first_char - 0xC4) + byte_str[1] - 0xA1
- else:
- return -1
+ return -1
class EUCKRDistributionAnalysis(CharDistributionAnalysis):
def __init__(self):
- super(EUCKRDistributionAnalysis, self).__init__()
+ super().__init__()
self._char_to_freq_order = EUCKR_CHAR_TO_FREQ_ORDER
self._table_size = EUCKR_TABLE_SIZE
self.typical_distribution_ratio = EUCKR_TYPICAL_DISTRIBUTION_RATIO
@@ -144,13 +160,27 @@ class EUCKRDistributionAnalysis(CharDistributionAnalysis):
first_char = byte_str[0]
if first_char >= 0xB0:
return 94 * (first_char - 0xB0) + byte_str[1] - 0xA1
- else:
- return -1
+ return -1
+
+
+class JOHABDistributionAnalysis(CharDistributionAnalysis):
+ def __init__(self):
+ super().__init__()
+ self._char_to_freq_order = EUCKR_CHAR_TO_FREQ_ORDER
+ self._table_size = EUCKR_TABLE_SIZE
+ self.typical_distribution_ratio = EUCKR_TYPICAL_DISTRIBUTION_RATIO
+
+ def get_order(self, byte_str):
+ first_char = byte_str[0]
+ if 0x88 <= first_char < 0xD4:
+ code = first_char * 256 + byte_str[1]
+ return JOHAB_TO_EUCKR_ORDER_TABLE.get(code, -1)
+ return -1
class GB2312DistributionAnalysis(CharDistributionAnalysis):
def __init__(self):
- super(GB2312DistributionAnalysis, self).__init__()
+ super().__init__()
self._char_to_freq_order = GB2312_CHAR_TO_FREQ_ORDER
self._table_size = GB2312_TABLE_SIZE
self.typical_distribution_ratio = GB2312_TYPICAL_DISTRIBUTION_RATIO
@@ -163,13 +193,12 @@ class GB2312DistributionAnalysis(CharDistributionAnalysis):
first_char, second_char = byte_str[0], byte_str[1]
if (first_char >= 0xB0) and (second_char >= 0xA1):
return 94 * (first_char - 0xB0) + second_char - 0xA1
- else:
- return -1
+ return -1
class Big5DistributionAnalysis(CharDistributionAnalysis):
def __init__(self):
- super(Big5DistributionAnalysis, self).__init__()
+ super().__init__()
self._char_to_freq_order = BIG5_CHAR_TO_FREQ_ORDER
self._table_size = BIG5_TABLE_SIZE
self.typical_distribution_ratio = BIG5_TYPICAL_DISTRIBUTION_RATIO
@@ -183,15 +212,13 @@ class Big5DistributionAnalysis(CharDistributionAnalysis):
if first_char >= 0xA4:
if second_char >= 0xA1:
return 157 * (first_char - 0xA4) + second_char - 0xA1 + 63
- else:
- return 157 * (first_char - 0xA4) + second_char - 0x40
- else:
- return -1
+ return 157 * (first_char - 0xA4) + second_char - 0x40
+ return -1
class SJISDistributionAnalysis(CharDistributionAnalysis):
def __init__(self):
- super(SJISDistributionAnalysis, self).__init__()
+ super().__init__()
self._char_to_freq_order = JIS_CHAR_TO_FREQ_ORDER
self._table_size = JIS_TABLE_SIZE
self.typical_distribution_ratio = JIS_TYPICAL_DISTRIBUTION_RATIO
@@ -202,9 +229,9 @@ class SJISDistributionAnalysis(CharDistributionAnalysis):
# second byte range: 0x40 -- 0x7e, 0x81 -- oxfe
# no validation needed here. State machine has done that
first_char, second_char = byte_str[0], byte_str[1]
- if (first_char >= 0x81) and (first_char <= 0x9F):
+ if 0x81 <= first_char <= 0x9F:
order = 188 * (first_char - 0x81)
- elif (first_char >= 0xE0) and (first_char <= 0xEF):
+ elif 0xE0 <= first_char <= 0xEF:
order = 188 * (first_char - 0xE0 + 31)
else:
return -1
@@ -216,7 +243,7 @@ class SJISDistributionAnalysis(CharDistributionAnalysis):
class EUCJPDistributionAnalysis(CharDistributionAnalysis):
def __init__(self):
- super(EUCJPDistributionAnalysis, self).__init__()
+ super().__init__()
self._char_to_freq_order = JIS_CHAR_TO_FREQ_ORDER
self._table_size = JIS_TABLE_SIZE
self.typical_distribution_ratio = JIS_TYPICAL_DISTRIBUTION_RATIO
@@ -228,6 +255,5 @@ class EUCJPDistributionAnalysis(CharDistributionAnalysis):
# no validation needed here. State machine has done that
char = byte_str[0]
if char >= 0xA0:
- return 94 * (char - 0xA1) + byte_str[1] - 0xa1
- else:
- return -1
+ return 94 * (char - 0xA1) + byte_str[1] - 0xA1
+ return -1
diff --git a/src/pip/_vendor/chardet/charsetgroupprober.py b/src/pip/_vendor/chardet/charsetgroupprober.py
index 5812cef0b..778ff332b 100644
--- a/src/pip/_vendor/chardet/charsetgroupprober.py
+++ b/src/pip/_vendor/chardet/charsetgroupprober.py
@@ -25,19 +25,19 @@
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
-from .enums import ProbingState
from .charsetprober import CharSetProber
+from .enums import ProbingState
class CharSetGroupProber(CharSetProber):
def __init__(self, lang_filter=None):
- super(CharSetGroupProber, self).__init__(lang_filter=lang_filter)
+ super().__init__(lang_filter=lang_filter)
self._active_num = 0
self.probers = []
self._best_guess_prober = None
def reset(self):
- super(CharSetGroupProber, self).reset()
+ super().reset()
self._active_num = 0
for prober in self.probers:
if prober:
@@ -75,7 +75,7 @@ class CharSetGroupProber(CharSetProber):
self._best_guess_prober = prober
self._state = ProbingState.FOUND_IT
return self.state
- elif state == ProbingState.NOT_ME:
+ if state == ProbingState.NOT_ME:
prober.active = False
self._active_num -= 1
if self._active_num <= 0:
@@ -87,7 +87,7 @@ class CharSetGroupProber(CharSetProber):
state = self.state
if state == ProbingState.FOUND_IT:
return 0.99
- elif state == ProbingState.NOT_ME:
+ if state == ProbingState.NOT_ME:
return 0.01
best_conf = 0.0
self._best_guess_prober = None
@@ -95,10 +95,12 @@ class CharSetGroupProber(CharSetProber):
if not prober:
continue
if not prober.active:
- self.logger.debug('%s not active', prober.charset_name)
+ self.logger.debug("%s not active", prober.charset_name)
continue
conf = prober.get_confidence()
- self.logger.debug('%s %s confidence = %s', prober.charset_name, prober.language, conf)
+ self.logger.debug(
+ "%s %s confidence = %s", prober.charset_name, prober.language, conf
+ )
if best_conf < conf:
best_conf = conf
self._best_guess_prober = prober
diff --git a/src/pip/_vendor/chardet/charsetprober.py b/src/pip/_vendor/chardet/charsetprober.py
index eac4e5986..9f1afd999 100644
--- a/src/pip/_vendor/chardet/charsetprober.py
+++ b/src/pip/_vendor/chardet/charsetprober.py
@@ -31,8 +31,12 @@ import re
from .enums import ProbingState
+INTERNATIONAL_WORDS_PATTERN = re.compile(
+ b"[a-zA-Z]*[\x80-\xFF]+[a-zA-Z]*[^a-zA-Z\x80-\xFF]?"
+)
-class CharSetProber(object):
+
+class CharSetProber:
SHORTCUT_THRESHOLD = 0.95
@@ -48,8 +52,8 @@ class CharSetProber(object):
def charset_name(self):
return None
- def feed(self, buf):
- pass
+ def feed(self, byte_str):
+ raise NotImplementedError
@property
def state(self):
@@ -60,7 +64,7 @@ class CharSetProber(object):
@staticmethod
def filter_high_byte_only(buf):
- buf = re.sub(b'([\x00-\x7F])+', b' ', buf)
+ buf = re.sub(b"([\x00-\x7F])+", b" ", buf)
return buf
@staticmethod
@@ -70,12 +74,10 @@ class CharSetProber(object):
alphabet: english alphabets [a-zA-Z]
international: international characters [\x80-\xFF]
marker: everything else [^a-zA-Z\x80-\xFF]
-
The input buffer can be thought to contain a series of words delimited
by markers. This function works to filter all words that contain at
least one international character. All contiguous sequences of markers
are replaced by a single space ascii character.
-
This filter applies to all scripts which do not use English characters.
"""
filtered = bytearray()
@@ -83,8 +85,7 @@ class CharSetProber(object):
# This regex expression filters out only words that have at-least one
# international character. The word may include one marker character at
# the end.
- words = re.findall(b'[a-zA-Z]*[\x80-\xFF]+[a-zA-Z]*[^a-zA-Z\x80-\xFF]?',
- buf)
+ words = INTERNATIONAL_WORDS_PATTERN.findall(buf)
for word in words:
filtered.extend(word[:-1])
@@ -94,20 +95,17 @@ class CharSetProber(object):
# similarly across all languages and may thus have similar
# frequencies).
last_char = word[-1:]
- if not last_char.isalpha() and last_char < b'\x80':
- last_char = b' '
+ if not last_char.isalpha() and last_char < b"\x80":
+ last_char = b" "
filtered.extend(last_char)
return filtered
@staticmethod
- def filter_with_english_letters(buf):
+ def remove_xml_tags(buf):
"""
Returns a copy of ``buf`` that retains only the sequences of English
alphabet and high byte characters that are not between <> characters.
- Also retains English alphabet and high byte characters immediately
- before occurrences of >.
-
This filter can be applied to all scripts which contain both English
characters and extended ASCII characters, but is currently only used by
``Latin1Prober``.
@@ -115,26 +113,21 @@ class CharSetProber(object):
filtered = bytearray()
in_tag = False
prev = 0
+ buf = memoryview(buf).cast("c")
- for curr in range(len(buf)):
- # Slice here to get bytes instead of an int with Python 3
- buf_char = buf[curr:curr + 1]
- # Check if we're coming out of or entering an HTML tag
- if buf_char == b'>':
+ for curr, buf_char in enumerate(buf):
+ # Check if we're coming out of or entering an XML tag
+ if buf_char == b">":
+ prev = curr + 1
in_tag = False
- elif buf_char == b'<':
- in_tag = True
-
- # If current character is not extended-ASCII and not alphabetic...
- if buf_char < b'\x80' and not buf_char.isalpha():
- # ...and we're not in a tag
+ elif buf_char == b"<":
if curr > prev and not in_tag:
# Keep everything after last non-extended-ASCII,
# non-alphabetic character
filtered.extend(buf[prev:curr])
# Output a space to delimit stretch we kept
- filtered.extend(b' ')
- prev = curr + 1
+ filtered.extend(b" ")
+ in_tag = True
# If we're not in a tag...
if not in_tag:
diff --git a/src/pip/_vendor/chardet/cli/__init__.py b/src/pip/_vendor/chardet/cli/__init__.py
index 8b1378917..e69de29bb 100644
--- a/src/pip/_vendor/chardet/cli/__init__.py
+++ b/src/pip/_vendor/chardet/cli/__init__.py
@@ -1 +0,0 @@
-
diff --git a/src/pip/_vendor/chardet/cli/chardetect.py b/src/pip/_vendor/chardet/cli/chardetect.py
index 6d6f93aab..7926fa37e 100644
--- a/src/pip/_vendor/chardet/cli/chardetect.py
+++ b/src/pip/_vendor/chardet/cli/chardetect.py
@@ -12,17 +12,15 @@ If no paths are provided, it takes its input from stdin.
"""
-from __future__ import absolute_import, print_function, unicode_literals
import argparse
import sys
-from pip._vendor.chardet import __version__
-from pip._vendor.chardet.compat import PY2
-from pip._vendor.chardet.universaldetector import UniversalDetector
+from .. import __version__
+from ..universaldetector import UniversalDetector
-def description_of(lines, name='stdin'):
+def description_of(lines, name="stdin"):
"""
Return a string describing the probable encoding of a file or
list of strings.
@@ -41,13 +39,9 @@ def description_of(lines, name='stdin'):
break
u.close()
result = u.result
- if PY2:
- name = name.decode(sys.getfilesystemencoding(), 'ignore')
- if result['encoding']:
- return '{}: {} with confidence {}'.format(name, result['encoding'],
- result['confidence'])
- else:
- return '{}: no result'.format(name)
+ if result["encoding"]:
+ return f'{name}: {result["encoding"]} with confidence {result["confidence"]}'
+ return f"{name}: no result"
def main(argv=None):
@@ -61,24 +55,32 @@ def main(argv=None):
# Get command line arguments
parser = argparse.ArgumentParser(
description="Takes one or more file paths and reports their detected \
- encodings")
- parser.add_argument('input',
- help='File whose encoding we would like to determine. \
- (default: stdin)',
- type=argparse.FileType('rb'), nargs='*',
- default=[sys.stdin if PY2 else sys.stdin.buffer])
- parser.add_argument('--version', action='version',
- version='%(prog)s {}'.format(__version__))
+ encodings"
+ )
+ parser.add_argument(
+ "input",
+ help="File whose encoding we would like to determine. \
+ (default: stdin)",
+ type=argparse.FileType("rb"),
+ nargs="*",
+ default=[sys.stdin.buffer],
+ )
+ parser.add_argument(
+ "--version", action="version", version=f"%(prog)s {__version__}"
+ )
args = parser.parse_args(argv)
for f in args.input:
if f.isatty():
- print("You are running chardetect interactively. Press " +
- "CTRL-D twice at the start of a blank line to signal the " +
- "end of your input. If you want help, run chardetect " +
- "--help\n", file=sys.stderr)
+ print(
+ "You are running chardetect interactively. Press "
+ "CTRL-D twice at the start of a blank line to signal the "
+ "end of your input. If you want help, run chardetect "
+ "--help\n",
+ file=sys.stderr,
+ )
print(description_of(f, f.name))
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/src/pip/_vendor/chardet/codingstatemachine.py b/src/pip/_vendor/chardet/codingstatemachine.py
index 68fba44f1..d3e3e825d 100644
--- a/src/pip/_vendor/chardet/codingstatemachine.py
+++ b/src/pip/_vendor/chardet/codingstatemachine.py
@@ -30,7 +30,7 @@ import logging
from .enums import MachineState
-class CodingStateMachine(object):
+class CodingStateMachine:
"""
A state machine to verify a byte sequence for a particular encoding. For
each byte the detector receives, it will feed that byte to every active
@@ -52,6 +52,7 @@ class CodingStateMachine(object):
negative answer for this encoding. Detector will exclude this
encoding from consideration from here on.
"""
+
def __init__(self, sm):
self._model = sm
self._curr_byte_pos = 0
@@ -66,14 +67,13 @@ class CodingStateMachine(object):
def next_state(self, c):
# for each byte we get its class
# if it is first byte, we also get byte length
- byte_class = self._model['class_table'][c]
+ byte_class = self._model["class_table"][c]
if self._curr_state == MachineState.START:
self._curr_byte_pos = 0
- self._curr_char_len = self._model['char_len_table'][byte_class]
+ self._curr_char_len = self._model["char_len_table"][byte_class]
# from byte's class and state_table, we get its next state
- curr_state = (self._curr_state * self._model['class_factor']
- + byte_class)
- self._curr_state = self._model['state_table'][curr_state]
+ curr_state = self._curr_state * self._model["class_factor"] + byte_class
+ self._curr_state = self._model["state_table"][curr_state]
self._curr_byte_pos += 1
return self._curr_state
@@ -81,8 +81,8 @@ class CodingStateMachine(object):
return self._curr_char_len
def get_coding_state_machine(self):
- return self._model['name']
+ return self._model["name"]
@property
def language(self):
- return self._model['language']
+ return self._model["language"]
diff --git a/src/pip/_vendor/chardet/cp949prober.py b/src/pip/_vendor/chardet/cp949prober.py
index efd793abc..28a1f3dbb 100644
--- a/src/pip/_vendor/chardet/cp949prober.py
+++ b/src/pip/_vendor/chardet/cp949prober.py
@@ -33,7 +33,7 @@ from .mbcssm import CP949_SM_MODEL
class CP949Prober(MultiByteCharSetProber):
def __init__(self):
- super(CP949Prober, self).__init__()
+ super().__init__()
self.coding_sm = CodingStateMachine(CP949_SM_MODEL)
# NOTE: CP949 is a superset of EUC-KR, so the distribution should be
# not different.
diff --git a/src/pip/_vendor/chardet/enums.py b/src/pip/_vendor/chardet/enums.py
index 045120722..32a77e76c 100644
--- a/src/pip/_vendor/chardet/enums.py
+++ b/src/pip/_vendor/chardet/enums.py
@@ -5,20 +5,22 @@ All of the Enums that are used throughout the chardet package.
"""
-class InputState(object):
+class InputState:
"""
This enum represents the different states a universal detector can be in.
"""
+
PURE_ASCII = 0
ESC_ASCII = 1
HIGH_BYTE = 2
-class LanguageFilter(object):
+class LanguageFilter:
"""
This enum represents the different language filters we can apply to a
``UniversalDetector``.
"""
+
CHINESE_SIMPLIFIED = 0x01
CHINESE_TRADITIONAL = 0x02
JAPANESE = 0x04
@@ -29,28 +31,31 @@ class LanguageFilter(object):
CJK = CHINESE | JAPANESE | KOREAN
-class ProbingState(object):
+class ProbingState:
"""
This enum represents the different states a prober can be in.
"""
+
DETECTING = 0
FOUND_IT = 1
NOT_ME = 2
-class MachineState(object):
+class MachineState:
"""
This enum represents the different states a state machine can be in.
"""
+
START = 0
ERROR = 1
ITS_ME = 2
-class SequenceLikelihood(object):
+class SequenceLikelihood:
"""
This enum represents the likelihood of a character following the previous one.
"""
+
NEGATIVE = 0
UNLIKELY = 1
LIKELY = 2
@@ -62,13 +67,14 @@ class SequenceLikelihood(object):
return 4
-class CharacterCategory(object):
+class CharacterCategory:
"""
This enum represents the different categories language models for
``SingleByteCharsetProber`` put characters into.
Anything less than CONTROL is considered a letter.
"""
+
UNDEFINED = 255
LINE_BREAK = 254
SYMBOL = 253
diff --git a/src/pip/_vendor/chardet/escprober.py b/src/pip/_vendor/chardet/escprober.py
index c70493f2b..d9926115d 100644
--- a/src/pip/_vendor/chardet/escprober.py
+++ b/src/pip/_vendor/chardet/escprober.py
@@ -27,9 +27,13 @@
from .charsetprober import CharSetProber
from .codingstatemachine import CodingStateMachine
-from .enums import LanguageFilter, ProbingState, MachineState
-from .escsm import (HZ_SM_MODEL, ISO2022CN_SM_MODEL, ISO2022JP_SM_MODEL,
- ISO2022KR_SM_MODEL)
+from .enums import LanguageFilter, MachineState, ProbingState
+from .escsm import (
+ HZ_SM_MODEL,
+ ISO2022CN_SM_MODEL,
+ ISO2022JP_SM_MODEL,
+ ISO2022KR_SM_MODEL,
+)
class EscCharSetProber(CharSetProber):
@@ -40,7 +44,7 @@ class EscCharSetProber(CharSetProber):
"""
def __init__(self, lang_filter=None):
- super(EscCharSetProber, self).__init__(lang_filter=lang_filter)
+ super().__init__(lang_filter=lang_filter)
self.coding_sm = []
if self.lang_filter & LanguageFilter.CHINESE_SIMPLIFIED:
self.coding_sm.append(CodingStateMachine(HZ_SM_MODEL))
@@ -56,7 +60,7 @@ class EscCharSetProber(CharSetProber):
self.reset()
def reset(self):
- super(EscCharSetProber, self).reset()
+ super().reset()
for coding_sm in self.coding_sm:
if not coding_sm:
continue
@@ -75,10 +79,7 @@ class EscCharSetProber(CharSetProber):
return self._detected_language
def get_confidence(self):
- if self._detected_charset:
- return 0.99
- else:
- return 0.00
+ return 0.99 if self._detected_charset else 0.00
def feed(self, byte_str):
for c in byte_str:
diff --git a/src/pip/_vendor/chardet/escsm.py b/src/pip/_vendor/chardet/escsm.py
index 0069523a0..3aa0f4d96 100644
--- a/src/pip/_vendor/chardet/escsm.py
+++ b/src/pip/_vendor/chardet/escsm.py
@@ -12,7 +12,7 @@
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
+# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -20,227 +20,241 @@
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
-# License along with this library; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
from .enums import MachineState
+# fmt: off
HZ_CLS = (
-1,0,0,0,0,0,0,0, # 00 - 07
-0,0,0,0,0,0,0,0, # 08 - 0f
-0,0,0,0,0,0,0,0, # 10 - 17
-0,0,0,1,0,0,0,0, # 18 - 1f
-0,0,0,0,0,0,0,0, # 20 - 27
-0,0,0,0,0,0,0,0, # 28 - 2f
-0,0,0,0,0,0,0,0, # 30 - 37
-0,0,0,0,0,0,0,0, # 38 - 3f
-0,0,0,0,0,0,0,0, # 40 - 47
-0,0,0,0,0,0,0,0, # 48 - 4f
-0,0,0,0,0,0,0,0, # 50 - 57
-0,0,0,0,0,0,0,0, # 58 - 5f
-0,0,0,0,0,0,0,0, # 60 - 67
-0,0,0,0,0,0,0,0, # 68 - 6f
-0,0,0,0,0,0,0,0, # 70 - 77
-0,0,0,4,0,5,2,0, # 78 - 7f
-1,1,1,1,1,1,1,1, # 80 - 87
-1,1,1,1,1,1,1,1, # 88 - 8f
-1,1,1,1,1,1,1,1, # 90 - 97
-1,1,1,1,1,1,1,1, # 98 - 9f
-1,1,1,1,1,1,1,1, # a0 - a7
-1,1,1,1,1,1,1,1, # a8 - af
-1,1,1,1,1,1,1,1, # b0 - b7
-1,1,1,1,1,1,1,1, # b8 - bf
-1,1,1,1,1,1,1,1, # c0 - c7
-1,1,1,1,1,1,1,1, # c8 - cf
-1,1,1,1,1,1,1,1, # d0 - d7
-1,1,1,1,1,1,1,1, # d8 - df
-1,1,1,1,1,1,1,1, # e0 - e7
-1,1,1,1,1,1,1,1, # e8 - ef
-1,1,1,1,1,1,1,1, # f0 - f7
-1,1,1,1,1,1,1,1, # f8 - ff
+ 1, 0, 0, 0, 0, 0, 0, 0, # 00 - 07
+ 0, 0, 0, 0, 0, 0, 0, 0, # 08 - 0f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 10 - 17
+ 0, 0, 0, 1, 0, 0, 0, 0, # 18 - 1f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 20 - 27
+ 0, 0, 0, 0, 0, 0, 0, 0, # 28 - 2f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 30 - 37
+ 0, 0, 0, 0, 0, 0, 0, 0, # 38 - 3f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 40 - 47
+ 0, 0, 0, 0, 0, 0, 0, 0, # 48 - 4f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 50 - 57
+ 0, 0, 0, 0, 0, 0, 0, 0, # 58 - 5f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 60 - 67
+ 0, 0, 0, 0, 0, 0, 0, 0, # 68 - 6f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 70 - 77
+ 0, 0, 0, 4, 0, 5, 2, 0, # 78 - 7f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 80 - 87
+ 1, 1, 1, 1, 1, 1, 1, 1, # 88 - 8f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 90 - 97
+ 1, 1, 1, 1, 1, 1, 1, 1, # 98 - 9f
+ 1, 1, 1, 1, 1, 1, 1, 1, # a0 - a7
+ 1, 1, 1, 1, 1, 1, 1, 1, # a8 - af
+ 1, 1, 1, 1, 1, 1, 1, 1, # b0 - b7
+ 1, 1, 1, 1, 1, 1, 1, 1, # b8 - bf
+ 1, 1, 1, 1, 1, 1, 1, 1, # c0 - c7
+ 1, 1, 1, 1, 1, 1, 1, 1, # c8 - cf
+ 1, 1, 1, 1, 1, 1, 1, 1, # d0 - d7
+ 1, 1, 1, 1, 1, 1, 1, 1, # d8 - df
+ 1, 1, 1, 1, 1, 1, 1, 1, # e0 - e7
+ 1, 1, 1, 1, 1, 1, 1, 1, # e8 - ef
+ 1, 1, 1, 1, 1, 1, 1, 1, # f0 - f7
+ 1, 1, 1, 1, 1, 1, 1, 1, # f8 - ff
)
HZ_ST = (
-MachineState.START,MachineState.ERROR, 3,MachineState.START,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,# 00-07
-MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,# 08-0f
-MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START, 4,MachineState.ERROR,# 10-17
- 5,MachineState.ERROR, 6,MachineState.ERROR, 5, 5, 4,MachineState.ERROR,# 18-1f
- 4,MachineState.ERROR, 4, 4, 4,MachineState.ERROR, 4,MachineState.ERROR,# 20-27
- 4,MachineState.ITS_ME,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,# 28-2f
+MachineState.START, MachineState.ERROR, 3, MachineState.START, MachineState.START, MachineState.START, MachineState.ERROR, MachineState.ERROR, # 00-07
+MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, # 08-0f
+MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, MachineState.START, MachineState.START, 4, MachineState.ERROR, # 10-17
+ 5, MachineState.ERROR, 6, MachineState.ERROR, 5, 5, 4, MachineState.ERROR, # 18-1f
+ 4, MachineState.ERROR, 4, 4, 4, MachineState.ERROR, 4, MachineState.ERROR, # 20-27
+ 4, MachineState.ITS_ME, MachineState.START, MachineState.START, MachineState.START, MachineState.START, MachineState.START, MachineState.START, # 28-2f
)
+# fmt: on
HZ_CHAR_LEN_TABLE = (0, 0, 0, 0, 0, 0)
-HZ_SM_MODEL = {'class_table': HZ_CLS,
- 'class_factor': 6,
- 'state_table': HZ_ST,
- 'char_len_table': HZ_CHAR_LEN_TABLE,
- 'name': "HZ-GB-2312",
- 'language': 'Chinese'}
+HZ_SM_MODEL = {
+ "class_table": HZ_CLS,
+ "class_factor": 6,
+ "state_table": HZ_ST,
+ "char_len_table": HZ_CHAR_LEN_TABLE,
+ "name": "HZ-GB-2312",
+ "language": "Chinese",
+}
+# fmt: off
ISO2022CN_CLS = (
-2,0,0,0,0,0,0,0, # 00 - 07
-0,0,0,0,0,0,0,0, # 08 - 0f
-0,0,0,0,0,0,0,0, # 10 - 17
-0,0,0,1,0,0,0,0, # 18 - 1f
-0,0,0,0,0,0,0,0, # 20 - 27
-0,3,0,0,0,0,0,0, # 28 - 2f
-0,0,0,0,0,0,0,0, # 30 - 37
-0,0,0,0,0,0,0,0, # 38 - 3f
-0,0,0,4,0,0,0,0, # 40 - 47
-0,0,0,0,0,0,0,0, # 48 - 4f
-0,0,0,0,0,0,0,0, # 50 - 57
-0,0,0,0,0,0,0,0, # 58 - 5f
-0,0,0,0,0,0,0,0, # 60 - 67
-0,0,0,0,0,0,0,0, # 68 - 6f
-0,0,0,0,0,0,0,0, # 70 - 77
-0,0,0,0,0,0,0,0, # 78 - 7f
-2,2,2,2,2,2,2,2, # 80 - 87
-2,2,2,2,2,2,2,2, # 88 - 8f
-2,2,2,2,2,2,2,2, # 90 - 97
-2,2,2,2,2,2,2,2, # 98 - 9f
-2,2,2,2,2,2,2,2, # a0 - a7
-2,2,2,2,2,2,2,2, # a8 - af
-2,2,2,2,2,2,2,2, # b0 - b7
-2,2,2,2,2,2,2,2, # b8 - bf
-2,2,2,2,2,2,2,2, # c0 - c7
-2,2,2,2,2,2,2,2, # c8 - cf
-2,2,2,2,2,2,2,2, # d0 - d7
-2,2,2,2,2,2,2,2, # d8 - df
-2,2,2,2,2,2,2,2, # e0 - e7
-2,2,2,2,2,2,2,2, # e8 - ef
-2,2,2,2,2,2,2,2, # f0 - f7
-2,2,2,2,2,2,2,2, # f8 - ff
+ 2, 0, 0, 0, 0, 0, 0, 0, # 00 - 07
+ 0, 0, 0, 0, 0, 0, 0, 0, # 08 - 0f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 10 - 17
+ 0, 0, 0, 1, 0, 0, 0, 0, # 18 - 1f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 20 - 27
+ 0, 3, 0, 0, 0, 0, 0, 0, # 28 - 2f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 30 - 37
+ 0, 0, 0, 0, 0, 0, 0, 0, # 38 - 3f
+ 0, 0, 0, 4, 0, 0, 0, 0, # 40 - 47
+ 0, 0, 0, 0, 0, 0, 0, 0, # 48 - 4f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 50 - 57
+ 0, 0, 0, 0, 0, 0, 0, 0, # 58 - 5f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 60 - 67
+ 0, 0, 0, 0, 0, 0, 0, 0, # 68 - 6f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 70 - 77
+ 0, 0, 0, 0, 0, 0, 0, 0, # 78 - 7f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 80 - 87
+ 2, 2, 2, 2, 2, 2, 2, 2, # 88 - 8f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 90 - 97
+ 2, 2, 2, 2, 2, 2, 2, 2, # 98 - 9f
+ 2, 2, 2, 2, 2, 2, 2, 2, # a0 - a7
+ 2, 2, 2, 2, 2, 2, 2, 2, # a8 - af
+ 2, 2, 2, 2, 2, 2, 2, 2, # b0 - b7
+ 2, 2, 2, 2, 2, 2, 2, 2, # b8 - bf
+ 2, 2, 2, 2, 2, 2, 2, 2, # c0 - c7
+ 2, 2, 2, 2, 2, 2, 2, 2, # c8 - cf
+ 2, 2, 2, 2, 2, 2, 2, 2, # d0 - d7
+ 2, 2, 2, 2, 2, 2, 2, 2, # d8 - df
+ 2, 2, 2, 2, 2, 2, 2, 2, # e0 - e7
+ 2, 2, 2, 2, 2, 2, 2, 2, # e8 - ef
+ 2, 2, 2, 2, 2, 2, 2, 2, # f0 - f7
+ 2, 2, 2, 2, 2, 2, 2, 2, # f8 - ff
)
ISO2022CN_ST = (
-MachineState.START, 3,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,# 00-07
-MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 08-0f
-MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,# 10-17
-MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 4,MachineState.ERROR,# 18-1f
-MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 20-27
- 5, 6,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 28-2f
-MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 30-37
-MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,MachineState.START,# 38-3f
+ MachineState.START, 3, MachineState.ERROR, MachineState.START, MachineState.START, MachineState.START, MachineState.START, MachineState.START, # 00-07
+ MachineState.START, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 08-0f
+ MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, # 10-17
+ MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, 4, MachineState.ERROR, # 18-1f
+ MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 20-27
+ 5, 6, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 28-2f
+ MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 30-37
+ MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ERROR, MachineState.START, # 38-3f
)
+# fmt: on
ISO2022CN_CHAR_LEN_TABLE = (0, 0, 0, 0, 0, 0, 0, 0, 0)
-ISO2022CN_SM_MODEL = {'class_table': ISO2022CN_CLS,
- 'class_factor': 9,
- 'state_table': ISO2022CN_ST,
- 'char_len_table': ISO2022CN_CHAR_LEN_TABLE,
- 'name': "ISO-2022-CN",
- 'language': 'Chinese'}
+ISO2022CN_SM_MODEL = {
+ "class_table": ISO2022CN_CLS,
+ "class_factor": 9,
+ "state_table": ISO2022CN_ST,
+ "char_len_table": ISO2022CN_CHAR_LEN_TABLE,
+ "name": "ISO-2022-CN",
+ "language": "Chinese",
+}
+# fmt: off
ISO2022JP_CLS = (
-2,0,0,0,0,0,0,0, # 00 - 07
-0,0,0,0,0,0,2,2, # 08 - 0f
-0,0,0,0,0,0,0,0, # 10 - 17
-0,0,0,1,0,0,0,0, # 18 - 1f
-0,0,0,0,7,0,0,0, # 20 - 27
-3,0,0,0,0,0,0,0, # 28 - 2f
-0,0,0,0,0,0,0,0, # 30 - 37
-0,0,0,0,0,0,0,0, # 38 - 3f
-6,0,4,0,8,0,0,0, # 40 - 47
-0,9,5,0,0,0,0,0, # 48 - 4f
-0,0,0,0,0,0,0,0, # 50 - 57
-0,0,0,0,0,0,0,0, # 58 - 5f
-0,0,0,0,0,0,0,0, # 60 - 67
-0,0,0,0,0,0,0,0, # 68 - 6f
-0,0,0,0,0,0,0,0, # 70 - 77
-0,0,0,0,0,0,0,0, # 78 - 7f
-2,2,2,2,2,2,2,2, # 80 - 87
-2,2,2,2,2,2,2,2, # 88 - 8f
-2,2,2,2,2,2,2,2, # 90 - 97
-2,2,2,2,2,2,2,2, # 98 - 9f
-2,2,2,2,2,2,2,2, # a0 - a7
-2,2,2,2,2,2,2,2, # a8 - af
-2,2,2,2,2,2,2,2, # b0 - b7
-2,2,2,2,2,2,2,2, # b8 - bf
-2,2,2,2,2,2,2,2, # c0 - c7
-2,2,2,2,2,2,2,2, # c8 - cf
-2,2,2,2,2,2,2,2, # d0 - d7
-2,2,2,2,2,2,2,2, # d8 - df
-2,2,2,2,2,2,2,2, # e0 - e7
-2,2,2,2,2,2,2,2, # e8 - ef
-2,2,2,2,2,2,2,2, # f0 - f7
-2,2,2,2,2,2,2,2, # f8 - ff
+ 2, 0, 0, 0, 0, 0, 0, 0, # 00 - 07
+ 0, 0, 0, 0, 0, 0, 2, 2, # 08 - 0f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 10 - 17
+ 0, 0, 0, 1, 0, 0, 0, 0, # 18 - 1f
+ 0, 0, 0, 0, 7, 0, 0, 0, # 20 - 27
+ 3, 0, 0, 0, 0, 0, 0, 0, # 28 - 2f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 30 - 37
+ 0, 0, 0, 0, 0, 0, 0, 0, # 38 - 3f
+ 6, 0, 4, 0, 8, 0, 0, 0, # 40 - 47
+ 0, 9, 5, 0, 0, 0, 0, 0, # 48 - 4f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 50 - 57
+ 0, 0, 0, 0, 0, 0, 0, 0, # 58 - 5f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 60 - 67
+ 0, 0, 0, 0, 0, 0, 0, 0, # 68 - 6f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 70 - 77
+ 0, 0, 0, 0, 0, 0, 0, 0, # 78 - 7f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 80 - 87
+ 2, 2, 2, 2, 2, 2, 2, 2, # 88 - 8f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 90 - 97
+ 2, 2, 2, 2, 2, 2, 2, 2, # 98 - 9f
+ 2, 2, 2, 2, 2, 2, 2, 2, # a0 - a7
+ 2, 2, 2, 2, 2, 2, 2, 2, # a8 - af
+ 2, 2, 2, 2, 2, 2, 2, 2, # b0 - b7
+ 2, 2, 2, 2, 2, 2, 2, 2, # b8 - bf
+ 2, 2, 2, 2, 2, 2, 2, 2, # c0 - c7
+ 2, 2, 2, 2, 2, 2, 2, 2, # c8 - cf
+ 2, 2, 2, 2, 2, 2, 2, 2, # d0 - d7
+ 2, 2, 2, 2, 2, 2, 2, 2, # d8 - df
+ 2, 2, 2, 2, 2, 2, 2, 2, # e0 - e7
+ 2, 2, 2, 2, 2, 2, 2, 2, # e8 - ef
+ 2, 2, 2, 2, 2, 2, 2, 2, # f0 - f7
+ 2, 2, 2, 2, 2, 2, 2, 2, # f8 - ff
)
ISO2022JP_ST = (
-MachineState.START, 3,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,# 00-07
-MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 08-0f
-MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,# 10-17
-MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,# 18-1f
-MachineState.ERROR, 5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 4,MachineState.ERROR,MachineState.ERROR,# 20-27
-MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 6,MachineState.ITS_ME,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,# 28-2f
-MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,# 30-37
-MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 38-3f
-MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,MachineState.START,MachineState.START,# 40-47
+ MachineState.START, 3, MachineState.ERROR, MachineState.START, MachineState.START, MachineState.START, MachineState.START, MachineState.START, # 00-07
+ MachineState.START, MachineState.START, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 08-0f
+ MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, # 10-17
+ MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, # 18-1f
+ MachineState.ERROR, 5, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, 4, MachineState.ERROR, MachineState.ERROR, # 20-27
+ MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, 6, MachineState.ITS_ME, MachineState.ERROR, MachineState.ITS_ME, MachineState.ERROR, # 28-2f
+ MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ITS_ME, # 30-37
+ MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 38-3f
+ MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ERROR, MachineState.START, MachineState.START, # 40-47
)
+# fmt: on
ISO2022JP_CHAR_LEN_TABLE = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
-ISO2022JP_SM_MODEL = {'class_table': ISO2022JP_CLS,
- 'class_factor': 10,
- 'state_table': ISO2022JP_ST,
- 'char_len_table': ISO2022JP_CHAR_LEN_TABLE,
- 'name': "ISO-2022-JP",
- 'language': 'Japanese'}
+ISO2022JP_SM_MODEL = {
+ "class_table": ISO2022JP_CLS,
+ "class_factor": 10,
+ "state_table": ISO2022JP_ST,
+ "char_len_table": ISO2022JP_CHAR_LEN_TABLE,
+ "name": "ISO-2022-JP",
+ "language": "Japanese",
+}
+# fmt: off
ISO2022KR_CLS = (
-2,0,0,0,0,0,0,0, # 00 - 07
-0,0,0,0,0,0,0,0, # 08 - 0f
-0,0,0,0,0,0,0,0, # 10 - 17
-0,0,0,1,0,0,0,0, # 18 - 1f
-0,0,0,0,3,0,0,0, # 20 - 27
-0,4,0,0,0,0,0,0, # 28 - 2f
-0,0,0,0,0,0,0,0, # 30 - 37
-0,0,0,0,0,0,0,0, # 38 - 3f
-0,0,0,5,0,0,0,0, # 40 - 47
-0,0,0,0,0,0,0,0, # 48 - 4f
-0,0,0,0,0,0,0,0, # 50 - 57
-0,0,0,0,0,0,0,0, # 58 - 5f
-0,0,0,0,0,0,0,0, # 60 - 67
-0,0,0,0,0,0,0,0, # 68 - 6f
-0,0,0,0,0,0,0,0, # 70 - 77
-0,0,0,0,0,0,0,0, # 78 - 7f
-2,2,2,2,2,2,2,2, # 80 - 87
-2,2,2,2,2,2,2,2, # 88 - 8f
-2,2,2,2,2,2,2,2, # 90 - 97
-2,2,2,2,2,2,2,2, # 98 - 9f
-2,2,2,2,2,2,2,2, # a0 - a7
-2,2,2,2,2,2,2,2, # a8 - af
-2,2,2,2,2,2,2,2, # b0 - b7
-2,2,2,2,2,2,2,2, # b8 - bf
-2,2,2,2,2,2,2,2, # c0 - c7
-2,2,2,2,2,2,2,2, # c8 - cf
-2,2,2,2,2,2,2,2, # d0 - d7
-2,2,2,2,2,2,2,2, # d8 - df
-2,2,2,2,2,2,2,2, # e0 - e7
-2,2,2,2,2,2,2,2, # e8 - ef
-2,2,2,2,2,2,2,2, # f0 - f7
-2,2,2,2,2,2,2,2, # f8 - ff
+ 2, 0, 0, 0, 0, 0, 0, 0, # 00 - 07
+ 0, 0, 0, 0, 0, 0, 0, 0, # 08 - 0f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 10 - 17
+ 0, 0, 0, 1, 0, 0, 0, 0, # 18 - 1f
+ 0, 0, 0, 0, 3, 0, 0, 0, # 20 - 27
+ 0, 4, 0, 0, 0, 0, 0, 0, # 28 - 2f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 30 - 37
+ 0, 0, 0, 0, 0, 0, 0, 0, # 38 - 3f
+ 0, 0, 0, 5, 0, 0, 0, 0, # 40 - 47
+ 0, 0, 0, 0, 0, 0, 0, 0, # 48 - 4f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 50 - 57
+ 0, 0, 0, 0, 0, 0, 0, 0, # 58 - 5f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 60 - 67
+ 0, 0, 0, 0, 0, 0, 0, 0, # 68 - 6f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 70 - 77
+ 0, 0, 0, 0, 0, 0, 0, 0, # 78 - 7f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 80 - 87
+ 2, 2, 2, 2, 2, 2, 2, 2, # 88 - 8f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 90 - 97
+ 2, 2, 2, 2, 2, 2, 2, 2, # 98 - 9f
+ 2, 2, 2, 2, 2, 2, 2, 2, # a0 - a7
+ 2, 2, 2, 2, 2, 2, 2, 2, # a8 - af
+ 2, 2, 2, 2, 2, 2, 2, 2, # b0 - b7
+ 2, 2, 2, 2, 2, 2, 2, 2, # b8 - bf
+ 2, 2, 2, 2, 2, 2, 2, 2, # c0 - c7
+ 2, 2, 2, 2, 2, 2, 2, 2, # c8 - cf
+ 2, 2, 2, 2, 2, 2, 2, 2, # d0 - d7
+ 2, 2, 2, 2, 2, 2, 2, 2, # d8 - df
+ 2, 2, 2, 2, 2, 2, 2, 2, # e0 - e7
+ 2, 2, 2, 2, 2, 2, 2, 2, # e8 - ef
+ 2, 2, 2, 2, 2, 2, 2, 2, # f0 - f7
+ 2, 2, 2, 2, 2, 2, 2, 2, # f8 - ff
)
ISO2022KR_ST = (
-MachineState.START, 3,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,# 00-07
-MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,# 08-0f
-MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 4,MachineState.ERROR,MachineState.ERROR,# 10-17
-MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,# 18-1f
-MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.START,MachineState.START,MachineState.START,MachineState.START,# 20-27
+ MachineState.START, 3, MachineState.ERROR, MachineState.START, MachineState.START, MachineState.START, MachineState.ERROR, MachineState.ERROR, # 00-07
+ MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ITS_ME, # 08-0f
+ MachineState.ITS_ME, MachineState.ITS_ME, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, 4, MachineState.ERROR, MachineState.ERROR, # 10-17
+ MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, 5, MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, # 18-1f
+ MachineState.ERROR, MachineState.ERROR, MachineState.ERROR, MachineState.ITS_ME, MachineState.START, MachineState.START, MachineState.START, MachineState.START, # 20-27
)
+# fmt: on
ISO2022KR_CHAR_LEN_TABLE = (0, 0, 0, 0, 0, 0)
-ISO2022KR_SM_MODEL = {'class_table': ISO2022KR_CLS,
- 'class_factor': 6,
- 'state_table': ISO2022KR_ST,
- 'char_len_table': ISO2022KR_CHAR_LEN_TABLE,
- 'name': "ISO-2022-KR",
- 'language': 'Korean'}
-
-
+ISO2022KR_SM_MODEL = {
+ "class_table": ISO2022KR_CLS,
+ "class_factor": 6,
+ "state_table": ISO2022KR_ST,
+ "char_len_table": ISO2022KR_CHAR_LEN_TABLE,
+ "name": "ISO-2022-KR",
+ "language": "Korean",
+}
diff --git a/src/pip/_vendor/chardet/eucjpprober.py b/src/pip/_vendor/chardet/eucjpprober.py
index 20ce8f7d1..abf2e66e2 100644
--- a/src/pip/_vendor/chardet/eucjpprober.py
+++ b/src/pip/_vendor/chardet/eucjpprober.py
@@ -25,24 +25,24 @@
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
-from .enums import ProbingState, MachineState
-from .mbcharsetprober import MultiByteCharSetProber
-from .codingstatemachine import CodingStateMachine
from .chardistribution import EUCJPDistributionAnalysis
+from .codingstatemachine import CodingStateMachine
+from .enums import MachineState, ProbingState
from .jpcntx import EUCJPContextAnalysis
+from .mbcharsetprober import MultiByteCharSetProber
from .mbcssm import EUCJP_SM_MODEL
class EUCJPProber(MultiByteCharSetProber):
def __init__(self):
- super(EUCJPProber, self).__init__()
+ super().__init__()
self.coding_sm = CodingStateMachine(EUCJP_SM_MODEL)
self.distribution_analyzer = EUCJPDistributionAnalysis()
self.context_analyzer = EUCJPContextAnalysis()
self.reset()
def reset(self):
- super(EUCJPProber, self).reset()
+ super().reset()
self.context_analyzer.reset()
@property
@@ -54,34 +54,37 @@ class EUCJPProber(MultiByteCharSetProber):
return "Japanese"
def feed(self, byte_str):
- for i in range(len(byte_str)):
- # PY3K: byte_str is a byte array, so byte_str[i] is an int, not a byte
- coding_state = self.coding_sm.next_state(byte_str[i])
+ for i, byte in enumerate(byte_str):
+ # PY3K: byte_str is a byte array, so byte is an int, not a byte
+ coding_state = self.coding_sm.next_state(byte)
if coding_state == MachineState.ERROR:
- self.logger.debug('%s %s prober hit error at byte %s',
- self.charset_name, self.language, i)
+ self.logger.debug(
+ "%s %s prober hit error at byte %s",
+ self.charset_name,
+ self.language,
+ i,
+ )
self._state = ProbingState.NOT_ME
break
- elif coding_state == MachineState.ITS_ME:
+ if coding_state == MachineState.ITS_ME:
self._state = ProbingState.FOUND_IT
break
- elif coding_state == MachineState.START:
+ if coding_state == MachineState.START:
char_len = self.coding_sm.get_current_charlen()
if i == 0:
- self._last_char[1] = byte_str[0]
+ self._last_char[1] = byte
self.context_analyzer.feed(self._last_char, char_len)
self.distribution_analyzer.feed(self._last_char, char_len)
else:
- self.context_analyzer.feed(byte_str[i - 1:i + 1],
- char_len)
- self.distribution_analyzer.feed(byte_str[i - 1:i + 1],
- char_len)
+ self.context_analyzer.feed(byte_str[i - 1 : i + 1], char_len)
+ self.distribution_analyzer.feed(byte_str[i - 1 : i + 1], char_len)
self._last_char[0] = byte_str[-1]
if self.state == ProbingState.DETECTING:
- if (self.context_analyzer.got_enough_data() and
- (self.get_confidence() > self.SHORTCUT_THRESHOLD)):
+ if self.context_analyzer.got_enough_data() and (
+ self.get_confidence() > self.SHORTCUT_THRESHOLD
+ ):
self._state = ProbingState.FOUND_IT
return self.state
diff --git a/src/pip/_vendor/chardet/euckrfreq.py b/src/pip/_vendor/chardet/euckrfreq.py
index b68078cb9..7dc3b1038 100644
--- a/src/pip/_vendor/chardet/euckrfreq.py
+++ b/src/pip/_vendor/chardet/euckrfreq.py
@@ -43,6 +43,7 @@ EUCKR_TYPICAL_DISTRIBUTION_RATIO = 6.0
EUCKR_TABLE_SIZE = 2352
# Char to FreqOrder table ,
+# fmt: off
EUCKR_CHAR_TO_FREQ_ORDER = (
13, 130, 120,1396, 481,1719,1720, 328, 609, 212,1721, 707, 400, 299,1722, 87,
1397,1723, 104, 536,1117,1203,1724,1267, 685,1268, 508,1725,1726,1727,1728,1398,
@@ -192,4 +193,4 @@ EUCKR_CHAR_TO_FREQ_ORDER = (
2629,2630,2631, 924, 648, 863, 603,2632,2633, 934,1540, 864, 865,2634, 642,1042,
670,1190,2635,2636,2637,2638, 168,2639, 652, 873, 542,1054,1541,2640,2641,2642, # 512, 256
)
-
+# fmt: on
diff --git a/src/pip/_vendor/chardet/euckrprober.py b/src/pip/_vendor/chardet/euckrprober.py
index 345a060d0..154a6d216 100644
--- a/src/pip/_vendor/chardet/euckrprober.py
+++ b/src/pip/_vendor/chardet/euckrprober.py
@@ -25,15 +25,15 @@
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
-from .mbcharsetprober import MultiByteCharSetProber
-from .codingstatemachine import CodingStateMachine
from .chardistribution import EUCKRDistributionAnalysis
+from .codingstatemachine import CodingStateMachine
+from .mbcharsetprober import MultiByteCharSetProber
from .mbcssm import EUCKR_SM_MODEL
class EUCKRProber(MultiByteCharSetProber):
def __init__(self):
- super(EUCKRProber, self).__init__()
+ super().__init__()
self.coding_sm = CodingStateMachine(EUCKR_SM_MODEL)
self.distribution_analyzer = EUCKRDistributionAnalysis()
self.reset()
diff --git a/src/pip/_vendor/chardet/euctwfreq.py b/src/pip/_vendor/chardet/euctwfreq.py
index ed7a995a3..4900ccc16 100644
--- a/src/pip/_vendor/chardet/euctwfreq.py
+++ b/src/pip/_vendor/chardet/euctwfreq.py
@@ -43,345 +43,346 @@
EUCTW_TYPICAL_DISTRIBUTION_RATIO = 0.75
-# Char to FreqOrder table ,
+# Char to FreqOrder table
EUCTW_TABLE_SIZE = 5376
+# fmt: off
EUCTW_CHAR_TO_FREQ_ORDER = (
- 1,1800,1506, 255,1431, 198, 9, 82, 6,7310, 177, 202,3615,1256,2808, 110, # 2742
-3735, 33,3241, 261, 76, 44,2113, 16,2931,2184,1176, 659,3868, 26,3404,2643, # 2758
-1198,3869,3313,4060, 410,2211, 302, 590, 361,1963, 8, 204, 58,4296,7311,1931, # 2774
- 63,7312,7313, 317,1614, 75, 222, 159,4061,2412,1480,7314,3500,3068, 224,2809, # 2790
-3616, 3, 10,3870,1471, 29,2774,1135,2852,1939, 873, 130,3242,1123, 312,7315, # 2806
-4297,2051, 507, 252, 682,7316, 142,1914, 124, 206,2932, 34,3501,3173, 64, 604, # 2822
-7317,2494,1976,1977, 155,1990, 645, 641,1606,7318,3405, 337, 72, 406,7319, 80, # 2838
- 630, 238,3174,1509, 263, 939,1092,2644, 756,1440,1094,3406, 449, 69,2969, 591, # 2854
- 179,2095, 471, 115,2034,1843, 60, 50,2970, 134, 806,1868, 734,2035,3407, 180, # 2870
- 995,1607, 156, 537,2893, 688,7320, 319,1305, 779,2144, 514,2374, 298,4298, 359, # 2886
-2495, 90,2707,1338, 663, 11, 906,1099,2545, 20,2436, 182, 532,1716,7321, 732, # 2902
-1376,4062,1311,1420,3175, 25,2312,1056, 113, 399, 382,1949, 242,3408,2467, 529, # 2918
-3243, 475,1447,3617,7322, 117, 21, 656, 810,1297,2295,2329,3502,7323, 126,4063, # 2934
- 706, 456, 150, 613,4299, 71,1118,2036,4064, 145,3069, 85, 835, 486,2114,1246, # 2950
-1426, 428, 727,1285,1015, 800, 106, 623, 303,1281,7324,2127,2354, 347,3736, 221, # 2966
-3503,3110,7325,1955,1153,4065, 83, 296,1199,3070, 192, 624, 93,7326, 822,1897, # 2982
-2810,3111, 795,2064, 991,1554,1542,1592, 27, 43,2853, 859, 139,1456, 860,4300, # 2998
- 437, 712,3871, 164,2392,3112, 695, 211,3017,2096, 195,3872,1608,3504,3505,3618, # 3014
-3873, 234, 811,2971,2097,3874,2229,1441,3506,1615,2375, 668,2076,1638, 305, 228, # 3030
-1664,4301, 467, 415,7327, 262,2098,1593, 239, 108, 300, 200,1033, 512,1247,2077, # 3046
-7328,7329,2173,3176,3619,2673, 593, 845,1062,3244, 88,1723,2037,3875,1950, 212, # 3062
- 266, 152, 149, 468,1898,4066,4302, 77, 187,7330,3018, 37, 5,2972,7331,3876, # 3078
-7332,7333, 39,2517,4303,2894,3177,2078, 55, 148, 74,4304, 545, 483,1474,1029, # 3094
-1665, 217,1869,1531,3113,1104,2645,4067, 24, 172,3507, 900,3877,3508,3509,4305, # 3110
- 32,1408,2811,1312, 329, 487,2355,2247,2708, 784,2674, 4,3019,3314,1427,1788, # 3126
- 188, 109, 499,7334,3620,1717,1789, 888,1217,3020,4306,7335,3510,7336,3315,1520, # 3142
-3621,3878, 196,1034, 775,7337,7338, 929,1815, 249, 439, 38,7339,1063,7340, 794, # 3158
-3879,1435,2296, 46, 178,3245,2065,7341,2376,7342, 214,1709,4307, 804, 35, 707, # 3174
- 324,3622,1601,2546, 140, 459,4068,7343,7344,1365, 839, 272, 978,2257,2572,3409, # 3190
-2128,1363,3623,1423, 697, 100,3071, 48, 70,1231, 495,3114,2193,7345,1294,7346, # 3206
-2079, 462, 586,1042,3246, 853, 256, 988, 185,2377,3410,1698, 434,1084,7347,3411, # 3222
- 314,2615,2775,4308,2330,2331, 569,2280, 637,1816,2518, 757,1162,1878,1616,3412, # 3238
- 287,1577,2115, 768,4309,1671,2854,3511,2519,1321,3737, 909,2413,7348,4069, 933, # 3254
-3738,7349,2052,2356,1222,4310, 765,2414,1322, 786,4311,7350,1919,1462,1677,2895, # 3270
-1699,7351,4312,1424,2437,3115,3624,2590,3316,1774,1940,3413,3880,4070, 309,1369, # 3286
-1130,2812, 364,2230,1653,1299,3881,3512,3882,3883,2646, 525,1085,3021, 902,2000, # 3302
-1475, 964,4313, 421,1844,1415,1057,2281, 940,1364,3116, 376,4314,4315,1381, 7, # 3318
-2520, 983,2378, 336,1710,2675,1845, 321,3414, 559,1131,3022,2742,1808,1132,1313, # 3334
- 265,1481,1857,7352, 352,1203,2813,3247, 167,1089, 420,2814, 776, 792,1724,3513, # 3350
-4071,2438,3248,7353,4072,7354, 446, 229, 333,2743, 901,3739,1200,1557,4316,2647, # 3366
-1920, 395,2744,2676,3740,4073,1835, 125, 916,3178,2616,4317,7355,7356,3741,7357, # 3382
-7358,7359,4318,3117,3625,1133,2547,1757,3415,1510,2313,1409,3514,7360,2145, 438, # 3398
-2591,2896,2379,3317,1068, 958,3023, 461, 311,2855,2677,4074,1915,3179,4075,1978, # 3414
- 383, 750,2745,2617,4076, 274, 539, 385,1278,1442,7361,1154,1964, 384, 561, 210, # 3430
- 98,1295,2548,3515,7362,1711,2415,1482,3416,3884,2897,1257, 129,7363,3742, 642, # 3446
- 523,2776,2777,2648,7364, 141,2231,1333, 68, 176, 441, 876, 907,4077, 603,2592, # 3462
- 710, 171,3417, 404, 549, 18,3118,2393,1410,3626,1666,7365,3516,4319,2898,4320, # 3478
-7366,2973, 368,7367, 146, 366, 99, 871,3627,1543, 748, 807,1586,1185, 22,2258, # 3494
- 379,3743,3180,7368,3181, 505,1941,2618,1991,1382,2314,7369, 380,2357, 218, 702, # 3510
-1817,1248,3418,3024,3517,3318,3249,7370,2974,3628, 930,3250,3744,7371, 59,7372, # 3526
- 585, 601,4078, 497,3419,1112,1314,4321,1801,7373,1223,1472,2174,7374, 749,1836, # 3542
- 690,1899,3745,1772,3885,1476, 429,1043,1790,2232,2116, 917,4079, 447,1086,1629, # 3558
-7375, 556,7376,7377,2020,1654, 844,1090, 105, 550, 966,1758,2815,1008,1782, 686, # 3574
-1095,7378,2282, 793,1602,7379,3518,2593,4322,4080,2933,2297,4323,3746, 980,2496, # 3590
- 544, 353, 527,4324, 908,2678,2899,7380, 381,2619,1942,1348,7381,1341,1252, 560, # 3606
-3072,7382,3420,2856,7383,2053, 973, 886,2080, 143,4325,7384,7385, 157,3886, 496, # 3622
-4081, 57, 840, 540,2038,4326,4327,3421,2117,1445, 970,2259,1748,1965,2081,4082, # 3638
-3119,1234,1775,3251,2816,3629, 773,1206,2129,1066,2039,1326,3887,1738,1725,4083, # 3654
- 279,3120, 51,1544,2594, 423,1578,2130,2066, 173,4328,1879,7386,7387,1583, 264, # 3670
- 610,3630,4329,2439, 280, 154,7388,7389,7390,1739, 338,1282,3073, 693,2857,1411, # 3686
-1074,3747,2440,7391,4330,7392,7393,1240, 952,2394,7394,2900,1538,2679, 685,1483, # 3702
-4084,2468,1436, 953,4085,2054,4331, 671,2395, 79,4086,2441,3252, 608, 567,2680, # 3718
-3422,4087,4088,1691, 393,1261,1791,2396,7395,4332,7396,7397,7398,7399,1383,1672, # 3734
-3748,3182,1464, 522,1119, 661,1150, 216, 675,4333,3888,1432,3519, 609,4334,2681, # 3750
-2397,7400,7401,7402,4089,3025, 0,7403,2469, 315, 231,2442, 301,3319,4335,2380, # 3766
-7404, 233,4090,3631,1818,4336,4337,7405, 96,1776,1315,2082,7406, 257,7407,1809, # 3782
-3632,2709,1139,1819,4091,2021,1124,2163,2778,1777,2649,7408,3074, 363,1655,3183, # 3798
-7409,2975,7410,7411,7412,3889,1567,3890, 718, 103,3184, 849,1443, 341,3320,2934, # 3814
-1484,7413,1712, 127, 67, 339,4092,2398, 679,1412, 821,7414,7415, 834, 738, 351, # 3830
-2976,2146, 846, 235,1497,1880, 418,1992,3749,2710, 186,1100,2147,2746,3520,1545, # 3846
-1355,2935,2858,1377, 583,3891,4093,2573,2977,7416,1298,3633,1078,2549,3634,2358, # 3862
- 78,3750,3751, 267,1289,2099,2001,1594,4094, 348, 369,1274,2194,2175,1837,4338, # 3878
-1820,2817,3635,2747,2283,2002,4339,2936,2748, 144,3321, 882,4340,3892,2749,3423, # 3894
-4341,2901,7417,4095,1726, 320,7418,3893,3026, 788,2978,7419,2818,1773,1327,2859, # 3910
-3894,2819,7420,1306,4342,2003,1700,3752,3521,2359,2650, 787,2022, 506, 824,3636, # 3926
- 534, 323,4343,1044,3322,2023,1900, 946,3424,7421,1778,1500,1678,7422,1881,4344, # 3942
- 165, 243,4345,3637,2521, 123, 683,4096, 764,4346, 36,3895,1792, 589,2902, 816, # 3958
- 626,1667,3027,2233,1639,1555,1622,3753,3896,7423,3897,2860,1370,1228,1932, 891, # 3974
-2083,2903, 304,4097,7424, 292,2979,2711,3522, 691,2100,4098,1115,4347, 118, 662, # 3990
-7425, 611,1156, 854,2381,1316,2861, 2, 386, 515,2904,7426,7427,3253, 868,2234, # 4006
-1486, 855,2651, 785,2212,3028,7428,1040,3185,3523,7429,3121, 448,7430,1525,7431, # 4022
-2164,4348,7432,3754,7433,4099,2820,3524,3122, 503, 818,3898,3123,1568, 814, 676, # 4038
-1444, 306,1749,7434,3755,1416,1030, 197,1428, 805,2821,1501,4349,7435,7436,7437, # 4054
-1993,7438,4350,7439,7440,2195, 13,2779,3638,2980,3124,1229,1916,7441,3756,2131, # 4070
-7442,4100,4351,2399,3525,7443,2213,1511,1727,1120,7444,7445, 646,3757,2443, 307, # 4086
-7446,7447,1595,3186,7448,7449,7450,3639,1113,1356,3899,1465,2522,2523,7451, 519, # 4102
-7452, 128,2132, 92,2284,1979,7453,3900,1512, 342,3125,2196,7454,2780,2214,1980, # 4118
-3323,7455, 290,1656,1317, 789, 827,2360,7456,3758,4352, 562, 581,3901,7457, 401, # 4134
-4353,2248, 94,4354,1399,2781,7458,1463,2024,4355,3187,1943,7459, 828,1105,4101, # 4150
-1262,1394,7460,4102, 605,4356,7461,1783,2862,7462,2822, 819,2101, 578,2197,2937, # 4166
-7463,1502, 436,3254,4103,3255,2823,3902,2905,3425,3426,7464,2712,2315,7465,7466, # 4182
-2332,2067, 23,4357, 193, 826,3759,2102, 699,1630,4104,3075, 390,1793,1064,3526, # 4198
-7467,1579,3076,3077,1400,7468,4105,1838,1640,2863,7469,4358,4359, 137,4106, 598, # 4214
-3078,1966, 780, 104, 974,2938,7470, 278, 899, 253, 402, 572, 504, 493,1339,7471, # 4230
-3903,1275,4360,2574,2550,7472,3640,3029,3079,2249, 565,1334,2713, 863, 41,7473, # 4246
-7474,4361,7475,1657,2333, 19, 463,2750,4107, 606,7476,2981,3256,1087,2084,1323, # 4262
-2652,2982,7477,1631,1623,1750,4108,2682,7478,2864, 791,2714,2653,2334, 232,2416, # 4278
-7479,2983,1498,7480,2654,2620, 755,1366,3641,3257,3126,2025,1609, 119,1917,3427, # 4294
- 862,1026,4109,7481,3904,3760,4362,3905,4363,2260,1951,2470,7482,1125, 817,4110, # 4310
-4111,3906,1513,1766,2040,1487,4112,3030,3258,2824,3761,3127,7483,7484,1507,7485, # 4326
-2683, 733, 40,1632,1106,2865, 345,4113, 841,2524, 230,4364,2984,1846,3259,3428, # 4342
-7486,1263, 986,3429,7487, 735, 879, 254,1137, 857, 622,1300,1180,1388,1562,3907, # 4358
-3908,2939, 967,2751,2655,1349, 592,2133,1692,3324,2985,1994,4114,1679,3909,1901, # 4374
-2185,7488, 739,3642,2715,1296,1290,7489,4115,2198,2199,1921,1563,2595,2551,1870, # 4390
-2752,2986,7490, 435,7491, 343,1108, 596, 17,1751,4365,2235,3430,3643,7492,4366, # 4406
- 294,3527,2940,1693, 477, 979, 281,2041,3528, 643,2042,3644,2621,2782,2261,1031, # 4422
-2335,2134,2298,3529,4367, 367,1249,2552,7493,3530,7494,4368,1283,3325,2004, 240, # 4438
-1762,3326,4369,4370, 836,1069,3128, 474,7495,2148,2525, 268,3531,7496,3188,1521, # 4454
-1284,7497,1658,1546,4116,7498,3532,3533,7499,4117,3327,2684,1685,4118, 961,1673, # 4470
-2622, 190,2005,2200,3762,4371,4372,7500, 570,2497,3645,1490,7501,4373,2623,3260, # 4486
-1956,4374, 584,1514, 396,1045,1944,7502,4375,1967,2444,7503,7504,4376,3910, 619, # 4502
-7505,3129,3261, 215,2006,2783,2553,3189,4377,3190,4378, 763,4119,3763,4379,7506, # 4518
-7507,1957,1767,2941,3328,3646,1174, 452,1477,4380,3329,3130,7508,2825,1253,2382, # 4534
-2186,1091,2285,4120, 492,7509, 638,1169,1824,2135,1752,3911, 648, 926,1021,1324, # 4550
-4381, 520,4382, 997, 847,1007, 892,4383,3764,2262,1871,3647,7510,2400,1784,4384, # 4566
-1952,2942,3080,3191,1728,4121,2043,3648,4385,2007,1701,3131,1551, 30,2263,4122, # 4582
-7511,2026,4386,3534,7512, 501,7513,4123, 594,3431,2165,1821,3535,3432,3536,3192, # 4598
- 829,2826,4124,7514,1680,3132,1225,4125,7515,3262,4387,4126,3133,2336,7516,4388, # 4614
-4127,7517,3912,3913,7518,1847,2383,2596,3330,7519,4389, 374,3914, 652,4128,4129, # 4630
- 375,1140, 798,7520,7521,7522,2361,4390,2264, 546,1659, 138,3031,2445,4391,7523, # 4646
-2250, 612,1848, 910, 796,3765,1740,1371, 825,3766,3767,7524,2906,2554,7525, 692, # 4662
- 444,3032,2624, 801,4392,4130,7526,1491, 244,1053,3033,4131,4132, 340,7527,3915, # 4678
-1041,2987, 293,1168, 87,1357,7528,1539, 959,7529,2236, 721, 694,4133,3768, 219, # 4694
-1478, 644,1417,3331,2656,1413,1401,1335,1389,3916,7530,7531,2988,2362,3134,1825, # 4710
- 730,1515, 184,2827, 66,4393,7532,1660,2943, 246,3332, 378,1457, 226,3433, 975, # 4726
-3917,2944,1264,3537, 674, 696,7533, 163,7534,1141,2417,2166, 713,3538,3333,4394, # 4742
-3918,7535,7536,1186, 15,7537,1079,1070,7538,1522,3193,3539, 276,1050,2716, 758, # 4758
-1126, 653,2945,3263,7539,2337, 889,3540,3919,3081,2989, 903,1250,4395,3920,3434, # 4774
-3541,1342,1681,1718, 766,3264, 286, 89,2946,3649,7540,1713,7541,2597,3334,2990, # 4790
-7542,2947,2215,3194,2866,7543,4396,2498,2526, 181, 387,1075,3921, 731,2187,3335, # 4806
-7544,3265, 310, 313,3435,2299, 770,4134, 54,3034, 189,4397,3082,3769,3922,7545, # 4822
-1230,1617,1849, 355,3542,4135,4398,3336, 111,4136,3650,1350,3135,3436,3035,4137, # 4838
-2149,3266,3543,7546,2784,3923,3924,2991, 722,2008,7547,1071, 247,1207,2338,2471, # 4854
-1378,4399,2009, 864,1437,1214,4400, 373,3770,1142,2216, 667,4401, 442,2753,2555, # 4870
-3771,3925,1968,4138,3267,1839, 837, 170,1107, 934,1336,1882,7548,7549,2118,4139, # 4886
-2828, 743,1569,7550,4402,4140, 582,2384,1418,3437,7551,1802,7552, 357,1395,1729, # 4902
-3651,3268,2418,1564,2237,7553,3083,3772,1633,4403,1114,2085,4141,1532,7554, 482, # 4918
-2446,4404,7555,7556,1492, 833,1466,7557,2717,3544,1641,2829,7558,1526,1272,3652, # 4934
-4142,1686,1794, 416,2556,1902,1953,1803,7559,3773,2785,3774,1159,2316,7560,2867, # 4950
-4405,1610,1584,3036,2419,2754, 443,3269,1163,3136,7561,7562,3926,7563,4143,2499, # 4966
-3037,4406,3927,3137,2103,1647,3545,2010,1872,4144,7564,4145, 431,3438,7565, 250, # 4982
- 97, 81,4146,7566,1648,1850,1558, 160, 848,7567, 866, 740,1694,7568,2201,2830, # 4998
-3195,4147,4407,3653,1687, 950,2472, 426, 469,3196,3654,3655,3928,7569,7570,1188, # 5014
- 424,1995, 861,3546,4148,3775,2202,2685, 168,1235,3547,4149,7571,2086,1674,4408, # 5030
-3337,3270, 220,2557,1009,7572,3776, 670,2992, 332,1208, 717,7573,7574,3548,2447, # 5046
-3929,3338,7575, 513,7576,1209,2868,3339,3138,4409,1080,7577,7578,7579,7580,2527, # 5062
-3656,3549, 815,1587,3930,3931,7581,3550,3439,3777,1254,4410,1328,3038,1390,3932, # 5078
-1741,3933,3778,3934,7582, 236,3779,2448,3271,7583,7584,3657,3780,1273,3781,4411, # 5094
-7585, 308,7586,4412, 245,4413,1851,2473,1307,2575, 430, 715,2136,2449,7587, 270, # 5110
- 199,2869,3935,7588,3551,2718,1753, 761,1754, 725,1661,1840,4414,3440,3658,7589, # 5126
-7590, 587, 14,3272, 227,2598, 326, 480,2265, 943,2755,3552, 291, 650,1883,7591, # 5142
-1702,1226, 102,1547, 62,3441, 904,4415,3442,1164,4150,7592,7593,1224,1548,2756, # 5158
- 391, 498,1493,7594,1386,1419,7595,2055,1177,4416, 813, 880,1081,2363, 566,1145, # 5174
-4417,2286,1001,1035,2558,2599,2238, 394,1286,7596,7597,2068,7598, 86,1494,1730, # 5190
-3936, 491,1588, 745, 897,2948, 843,3340,3937,2757,2870,3273,1768, 998,2217,2069, # 5206
- 397,1826,1195,1969,3659,2993,3341, 284,7599,3782,2500,2137,2119,1903,7600,3938, # 5222
-2150,3939,4151,1036,3443,1904, 114,2559,4152, 209,1527,7601,7602,2949,2831,2625, # 5238
-2385,2719,3139, 812,2560,7603,3274,7604,1559, 737,1884,3660,1210, 885, 28,2686, # 5254
-3553,3783,7605,4153,1004,1779,4418,7606, 346,1981,2218,2687,4419,3784,1742, 797, # 5270
-1642,3940,1933,1072,1384,2151, 896,3941,3275,3661,3197,2871,3554,7607,2561,1958, # 5286
-4420,2450,1785,7608,7609,7610,3942,4154,1005,1308,3662,4155,2720,4421,4422,1528, # 5302
-2600, 161,1178,4156,1982, 987,4423,1101,4157, 631,3943,1157,3198,2420,1343,1241, # 5318
-1016,2239,2562, 372, 877,2339,2501,1160, 555,1934, 911,3944,7611, 466,1170, 169, # 5334
-1051,2907,2688,3663,2474,2994,1182,2011,2563,1251,2626,7612, 992,2340,3444,1540, # 5350
-2721,1201,2070,2401,1996,2475,7613,4424, 528,1922,2188,1503,1873,1570,2364,3342, # 5366
-3276,7614, 557,1073,7615,1827,3445,2087,2266,3140,3039,3084, 767,3085,2786,4425, # 5382
-1006,4158,4426,2341,1267,2176,3664,3199, 778,3945,3200,2722,1597,2657,7616,4427, # 5398
-7617,3446,7618,7619,7620,3277,2689,1433,3278, 131, 95,1504,3946, 723,4159,3141, # 5414
-1841,3555,2758,2189,3947,2027,2104,3665,7621,2995,3948,1218,7622,3343,3201,3949, # 5430
-4160,2576, 248,1634,3785, 912,7623,2832,3666,3040,3786, 654, 53,7624,2996,7625, # 5446
-1688,4428, 777,3447,1032,3950,1425,7626, 191, 820,2120,2833, 971,4429, 931,3202, # 5462
- 135, 664, 783,3787,1997, 772,2908,1935,3951,3788,4430,2909,3203, 282,2723, 640, # 5478
-1372,3448,1127, 922, 325,3344,7627,7628, 711,2044,7629,7630,3952,2219,2787,1936, # 5494
-3953,3345,2220,2251,3789,2300,7631,4431,3790,1258,3279,3954,3204,2138,2950,3955, # 5510
-3956,7632,2221, 258,3205,4432, 101,1227,7633,3280,1755,7634,1391,3281,7635,2910, # 5526
-2056, 893,7636,7637,7638,1402,4161,2342,7639,7640,3206,3556,7641,7642, 878,1325, # 5542
-1780,2788,4433, 259,1385,2577, 744,1183,2267,4434,7643,3957,2502,7644, 684,1024, # 5558
-4162,7645, 472,3557,3449,1165,3282,3958,3959, 322,2152, 881, 455,1695,1152,1340, # 5574
- 660, 554,2153,4435,1058,4436,4163, 830,1065,3346,3960,4437,1923,7646,1703,1918, # 5590
-7647, 932,2268, 122,7648,4438, 947, 677,7649,3791,2627, 297,1905,1924,2269,4439, # 5606
-2317,3283,7650,7651,4164,7652,4165, 84,4166, 112, 989,7653, 547,1059,3961, 701, # 5622
-3558,1019,7654,4167,7655,3450, 942, 639, 457,2301,2451, 993,2951, 407, 851, 494, # 5638
-4440,3347, 927,7656,1237,7657,2421,3348, 573,4168, 680, 921,2911,1279,1874, 285, # 5654
- 790,1448,1983, 719,2167,7658,7659,4441,3962,3963,1649,7660,1541, 563,7661,1077, # 5670
-7662,3349,3041,3451, 511,2997,3964,3965,3667,3966,1268,2564,3350,3207,4442,4443, # 5686
-7663, 535,1048,1276,1189,2912,2028,3142,1438,1373,2834,2952,1134,2012,7664,4169, # 5702
-1238,2578,3086,1259,7665, 700,7666,2953,3143,3668,4170,7667,4171,1146,1875,1906, # 5718
-4444,2601,3967, 781,2422, 132,1589, 203, 147, 273,2789,2402, 898,1786,2154,3968, # 5734
-3969,7668,3792,2790,7669,7670,4445,4446,7671,3208,7672,1635,3793, 965,7673,1804, # 5750
-2690,1516,3559,1121,1082,1329,3284,3970,1449,3794, 65,1128,2835,2913,2759,1590, # 5766
-3795,7674,7675, 12,2658, 45, 976,2579,3144,4447, 517,2528,1013,1037,3209,7676, # 5782
-3796,2836,7677,3797,7678,3452,7679,2602, 614,1998,2318,3798,3087,2724,2628,7680, # 5798
-2580,4172, 599,1269,7681,1810,3669,7682,2691,3088, 759,1060, 489,1805,3351,3285, # 5814
-1358,7683,7684,2386,1387,1215,2629,2252, 490,7685,7686,4173,1759,2387,2343,7687, # 5830
-4448,3799,1907,3971,2630,1806,3210,4449,3453,3286,2760,2344, 874,7688,7689,3454, # 5846
-3670,1858, 91,2914,3671,3042,3800,4450,7690,3145,3972,2659,7691,3455,1202,1403, # 5862
-3801,2954,2529,1517,2503,4451,3456,2504,7692,4452,7693,2692,1885,1495,1731,3973, # 5878
-2365,4453,7694,2029,7695,7696,3974,2693,1216, 237,2581,4174,2319,3975,3802,4454, # 5894
-4455,2694,3560,3457, 445,4456,7697,7698,7699,7700,2761, 61,3976,3672,1822,3977, # 5910
-7701, 687,2045, 935, 925, 405,2660, 703,1096,1859,2725,4457,3978,1876,1367,2695, # 5926
-3352, 918,2105,1781,2476, 334,3287,1611,1093,4458, 564,3146,3458,3673,3353, 945, # 5942
-2631,2057,4459,7702,1925, 872,4175,7703,3459,2696,3089, 349,4176,3674,3979,4460, # 5958
-3803,4177,3675,2155,3980,4461,4462,4178,4463,2403,2046, 782,3981, 400, 251,4179, # 5974
-1624,7704,7705, 277,3676, 299,1265, 476,1191,3804,2121,4180,4181,1109, 205,7706, # 5990
-2582,1000,2156,3561,1860,7707,7708,7709,4464,7710,4465,2565, 107,2477,2157,3982, # 6006
-3460,3147,7711,1533, 541,1301, 158, 753,4182,2872,3562,7712,1696, 370,1088,4183, # 6022
-4466,3563, 579, 327, 440, 162,2240, 269,1937,1374,3461, 968,3043, 56,1396,3090, # 6038
-2106,3288,3354,7713,1926,2158,4467,2998,7714,3564,7715,7716,3677,4468,2478,7717, # 6054
-2791,7718,1650,4469,7719,2603,7720,7721,3983,2661,3355,1149,3356,3984,3805,3985, # 6070
-7722,1076, 49,7723, 951,3211,3289,3290, 450,2837, 920,7724,1811,2792,2366,4184, # 6086
-1908,1138,2367,3806,3462,7725,3212,4470,1909,1147,1518,2423,4471,3807,7726,4472, # 6102
-2388,2604, 260,1795,3213,7727,7728,3808,3291, 708,7729,3565,1704,7730,3566,1351, # 6118
-1618,3357,2999,1886, 944,4185,3358,4186,3044,3359,4187,7731,3678, 422, 413,1714, # 6134
-3292, 500,2058,2345,4188,2479,7732,1344,1910, 954,7733,1668,7734,7735,3986,2404, # 6150
-4189,3567,3809,4190,7736,2302,1318,2505,3091, 133,3092,2873,4473, 629, 31,2838, # 6166
-2697,3810,4474, 850, 949,4475,3987,2955,1732,2088,4191,1496,1852,7737,3988, 620, # 6182
-3214, 981,1242,3679,3360,1619,3680,1643,3293,2139,2452,1970,1719,3463,2168,7738, # 6198
-3215,7739,7740,3361,1828,7741,1277,4476,1565,2047,7742,1636,3568,3093,7743, 869, # 6214
-2839, 655,3811,3812,3094,3989,3000,3813,1310,3569,4477,7744,7745,7746,1733, 558, # 6230
-4478,3681, 335,1549,3045,1756,4192,3682,1945,3464,1829,1291,1192, 470,2726,2107, # 6246
-2793, 913,1054,3990,7747,1027,7748,3046,3991,4479, 982,2662,3362,3148,3465,3216, # 6262
-3217,1946,2794,7749, 571,4480,7750,1830,7751,3570,2583,1523,2424,7752,2089, 984, # 6278
-4481,3683,1959,7753,3684, 852, 923,2795,3466,3685, 969,1519, 999,2048,2320,1705, # 6294
-7754,3095, 615,1662, 151, 597,3992,2405,2321,1049, 275,4482,3686,4193, 568,3687, # 6310
-3571,2480,4194,3688,7755,2425,2270, 409,3218,7756,1566,2874,3467,1002, 769,2840, # 6326
- 194,2090,3149,3689,2222,3294,4195, 628,1505,7757,7758,1763,2177,3001,3993, 521, # 6342
-1161,2584,1787,2203,2406,4483,3994,1625,4196,4197, 412, 42,3096, 464,7759,2632, # 6358
-4484,3363,1760,1571,2875,3468,2530,1219,2204,3814,2633,2140,2368,4485,4486,3295, # 6374
-1651,3364,3572,7760,7761,3573,2481,3469,7762,3690,7763,7764,2271,2091, 460,7765, # 6390
-4487,7766,3002, 962, 588,3574, 289,3219,2634,1116, 52,7767,3047,1796,7768,7769, # 6406
-7770,1467,7771,1598,1143,3691,4198,1984,1734,1067,4488,1280,3365, 465,4489,1572, # 6422
- 510,7772,1927,2241,1812,1644,3575,7773,4490,3692,7774,7775,2663,1573,1534,7776, # 6438
-7777,4199, 536,1807,1761,3470,3815,3150,2635,7778,7779,7780,4491,3471,2915,1911, # 6454
-2796,7781,3296,1122, 377,3220,7782, 360,7783,7784,4200,1529, 551,7785,2059,3693, # 6470
-1769,2426,7786,2916,4201,3297,3097,2322,2108,2030,4492,1404, 136,1468,1479, 672, # 6486
-1171,3221,2303, 271,3151,7787,2762,7788,2049, 678,2727, 865,1947,4493,7789,2013, # 6502
-3995,2956,7790,2728,2223,1397,3048,3694,4494,4495,1735,2917,3366,3576,7791,3816, # 6518
- 509,2841,2453,2876,3817,7792,7793,3152,3153,4496,4202,2531,4497,2304,1166,1010, # 6534
- 552, 681,1887,7794,7795,2957,2958,3996,1287,1596,1861,3154, 358, 453, 736, 175, # 6550
- 478,1117, 905,1167,1097,7796,1853,1530,7797,1706,7798,2178,3472,2287,3695,3473, # 6566
-3577,4203,2092,4204,7799,3367,1193,2482,4205,1458,2190,2205,1862,1888,1421,3298, # 6582
-2918,3049,2179,3474, 595,2122,7800,3997,7801,7802,4206,1707,2636, 223,3696,1359, # 6598
- 751,3098, 183,3475,7803,2797,3003, 419,2369, 633, 704,3818,2389, 241,7804,7805, # 6614
-7806, 838,3004,3697,2272,2763,2454,3819,1938,2050,3998,1309,3099,2242,1181,7807, # 6630
-1136,2206,3820,2370,1446,4207,2305,4498,7808,7809,4208,1055,2605, 484,3698,7810, # 6646
-3999, 625,4209,2273,3368,1499,4210,4000,7811,4001,4211,3222,2274,2275,3476,7812, # 6662
-7813,2764, 808,2606,3699,3369,4002,4212,3100,2532, 526,3370,3821,4213, 955,7814, # 6678
-1620,4214,2637,2427,7815,1429,3700,1669,1831, 994, 928,7816,3578,1260,7817,7818, # 6694
-7819,1948,2288, 741,2919,1626,4215,2729,2455, 867,1184, 362,3371,1392,7820,7821, # 6710
-4003,4216,1770,1736,3223,2920,4499,4500,1928,2698,1459,1158,7822,3050,3372,2877, # 6726
-1292,1929,2506,2842,3701,1985,1187,2071,2014,2607,4217,7823,2566,2507,2169,3702, # 6742
-2483,3299,7824,3703,4501,7825,7826, 666,1003,3005,1022,3579,4218,7827,4502,1813, # 6758
-2253, 574,3822,1603, 295,1535, 705,3823,4219, 283, 858, 417,7828,7829,3224,4503, # 6774
-4504,3051,1220,1889,1046,2276,2456,4004,1393,1599, 689,2567, 388,4220,7830,2484, # 6790
- 802,7831,2798,3824,2060,1405,2254,7832,4505,3825,2109,1052,1345,3225,1585,7833, # 6806
- 809,7834,7835,7836, 575,2730,3477, 956,1552,1469,1144,2323,7837,2324,1560,2457, # 6822
-3580,3226,4005, 616,2207,3155,2180,2289,7838,1832,7839,3478,4506,7840,1319,3704, # 6838
-3705,1211,3581,1023,3227,1293,2799,7841,7842,7843,3826, 607,2306,3827, 762,2878, # 6854
-1439,4221,1360,7844,1485,3052,7845,4507,1038,4222,1450,2061,2638,4223,1379,4508, # 6870
-2585,7846,7847,4224,1352,1414,2325,2921,1172,7848,7849,3828,3829,7850,1797,1451, # 6886
-7851,7852,7853,7854,2922,4006,4007,2485,2346, 411,4008,4009,3582,3300,3101,4509, # 6902
-1561,2664,1452,4010,1375,7855,7856, 47,2959, 316,7857,1406,1591,2923,3156,7858, # 6918
-1025,2141,3102,3157, 354,2731, 884,2224,4225,2407, 508,3706, 726,3583, 996,2428, # 6934
-3584, 729,7859, 392,2191,1453,4011,4510,3707,7860,7861,2458,3585,2608,1675,2800, # 6950
- 919,2347,2960,2348,1270,4511,4012, 73,7862,7863, 647,7864,3228,2843,2255,1550, # 6966
-1346,3006,7865,1332, 883,3479,7866,7867,7868,7869,3301,2765,7870,1212, 831,1347, # 6982
-4226,4512,2326,3830,1863,3053, 720,3831,4513,4514,3832,7871,4227,7872,7873,4515, # 6998
-7874,7875,1798,4516,3708,2609,4517,3586,1645,2371,7876,7877,2924, 669,2208,2665, # 7014
-2429,7878,2879,7879,7880,1028,3229,7881,4228,2408,7882,2256,1353,7883,7884,4518, # 7030
-3158, 518,7885,4013,7886,4229,1960,7887,2142,4230,7888,7889,3007,2349,2350,3833, # 7046
- 516,1833,1454,4014,2699,4231,4519,2225,2610,1971,1129,3587,7890,2766,7891,2961, # 7062
-1422, 577,1470,3008,1524,3373,7892,7893, 432,4232,3054,3480,7894,2586,1455,2508, # 7078
-2226,1972,1175,7895,1020,2732,4015,3481,4520,7896,2733,7897,1743,1361,3055,3482, # 7094
-2639,4016,4233,4521,2290, 895, 924,4234,2170, 331,2243,3056, 166,1627,3057,1098, # 7110
-7898,1232,2880,2227,3374,4522, 657, 403,1196,2372, 542,3709,3375,1600,4235,3483, # 7126
-7899,4523,2767,3230, 576, 530,1362,7900,4524,2533,2666,3710,4017,7901, 842,3834, # 7142
-7902,2801,2031,1014,4018, 213,2700,3376, 665, 621,4236,7903,3711,2925,2430,7904, # 7158
-2431,3302,3588,3377,7905,4237,2534,4238,4525,3589,1682,4239,3484,1380,7906, 724, # 7174
-2277, 600,1670,7907,1337,1233,4526,3103,2244,7908,1621,4527,7909, 651,4240,7910, # 7190
-1612,4241,2611,7911,2844,7912,2734,2307,3058,7913, 716,2459,3059, 174,1255,2701, # 7206
-4019,3590, 548,1320,1398, 728,4020,1574,7914,1890,1197,3060,4021,7915,3061,3062, # 7222
-3712,3591,3713, 747,7916, 635,4242,4528,7917,7918,7919,4243,7920,7921,4529,7922, # 7238
-3378,4530,2432, 451,7923,3714,2535,2072,4244,2735,4245,4022,7924,1764,4531,7925, # 7254
-4246, 350,7926,2278,2390,2486,7927,4247,4023,2245,1434,4024, 488,4532, 458,4248, # 7270
-4025,3715, 771,1330,2391,3835,2568,3159,2159,2409,1553,2667,3160,4249,7928,2487, # 7286
-2881,2612,1720,2702,4250,3379,4533,7929,2536,4251,7930,3231,4252,2768,7931,2015, # 7302
-2736,7932,1155,1017,3716,3836,7933,3303,2308, 201,1864,4253,1430,7934,4026,7935, # 7318
-7936,7937,7938,7939,4254,1604,7940, 414,1865, 371,2587,4534,4535,3485,2016,3104, # 7334
-4536,1708, 960,4255, 887, 389,2171,1536,1663,1721,7941,2228,4027,2351,2926,1580, # 7350
-7942,7943,7944,1744,7945,2537,4537,4538,7946,4539,7947,2073,7948,7949,3592,3380, # 7366
-2882,4256,7950,4257,2640,3381,2802, 673,2703,2460, 709,3486,4028,3593,4258,7951, # 7382
-1148, 502, 634,7952,7953,1204,4540,3594,1575,4541,2613,3717,7954,3718,3105, 948, # 7398
-3232, 121,1745,3837,1110,7955,4259,3063,2509,3009,4029,3719,1151,1771,3838,1488, # 7414
-4030,1986,7956,2433,3487,7957,7958,2093,7959,4260,3839,1213,1407,2803, 531,2737, # 7430
-2538,3233,1011,1537,7960,2769,4261,3106,1061,7961,3720,3721,1866,2883,7962,2017, # 7446
- 120,4262,4263,2062,3595,3234,2309,3840,2668,3382,1954,4542,7963,7964,3488,1047, # 7462
-2704,1266,7965,1368,4543,2845, 649,3383,3841,2539,2738,1102,2846,2669,7966,7967, # 7478
-1999,7968,1111,3596,2962,7969,2488,3842,3597,2804,1854,3384,3722,7970,7971,3385, # 7494
-2410,2884,3304,3235,3598,7972,2569,7973,3599,2805,4031,1460, 856,7974,3600,7975, # 7510
-2885,2963,7976,2886,3843,7977,4264, 632,2510, 875,3844,1697,3845,2291,7978,7979, # 7526
-4544,3010,1239, 580,4545,4265,7980, 914, 936,2074,1190,4032,1039,2123,7981,7982, # 7542
-7983,3386,1473,7984,1354,4266,3846,7985,2172,3064,4033, 915,3305,4267,4268,3306, # 7558
-1605,1834,7986,2739, 398,3601,4269,3847,4034, 328,1912,2847,4035,3848,1331,4270, # 7574
-3011, 937,4271,7987,3602,4036,4037,3387,2160,4546,3388, 524, 742, 538,3065,1012, # 7590
-7988,7989,3849,2461,7990, 658,1103, 225,3850,7991,7992,4547,7993,4548,7994,3236, # 7606
-1243,7995,4038, 963,2246,4549,7996,2705,3603,3161,7997,7998,2588,2327,7999,4550, # 7622
-8000,8001,8002,3489,3307, 957,3389,2540,2032,1930,2927,2462, 870,2018,3604,1746, # 7638
-2770,2771,2434,2463,8003,3851,8004,3723,3107,3724,3490,3390,3725,8005,1179,3066, # 7654
-8006,3162,2373,4272,3726,2541,3163,3108,2740,4039,8007,3391,1556,2542,2292, 977, # 7670
-2887,2033,4040,1205,3392,8008,1765,3393,3164,2124,1271,1689, 714,4551,3491,8009, # 7686
-2328,3852, 533,4273,3605,2181, 617,8010,2464,3308,3492,2310,8011,8012,3165,8013, # 7702
-8014,3853,1987, 618, 427,2641,3493,3394,8015,8016,1244,1690,8017,2806,4274,4552, # 7718
-8018,3494,8019,8020,2279,1576, 473,3606,4275,3395, 972,8021,3607,8022,3067,8023, # 7734
-8024,4553,4554,8025,3727,4041,4042,8026, 153,4555, 356,8027,1891,2888,4276,2143, # 7750
- 408, 803,2352,8028,3854,8029,4277,1646,2570,2511,4556,4557,3855,8030,3856,4278, # 7766
-8031,2411,3396, 752,8032,8033,1961,2964,8034, 746,3012,2465,8035,4279,3728, 698, # 7782
-4558,1892,4280,3608,2543,4559,3609,3857,8036,3166,3397,8037,1823,1302,4043,2706, # 7798
-3858,1973,4281,8038,4282,3167, 823,1303,1288,1236,2848,3495,4044,3398, 774,3859, # 7814
-8039,1581,4560,1304,2849,3860,4561,8040,2435,2161,1083,3237,4283,4045,4284, 344, # 7830
-1173, 288,2311, 454,1683,8041,8042,1461,4562,4046,2589,8043,8044,4563, 985, 894, # 7846
-8045,3399,3168,8046,1913,2928,3729,1988,8047,2110,1974,8048,4047,8049,2571,1194, # 7862
- 425,8050,4564,3169,1245,3730,4285,8051,8052,2850,8053, 636,4565,1855,3861, 760, # 7878
-1799,8054,4286,2209,1508,4566,4048,1893,1684,2293,8055,8056,8057,4287,4288,2210, # 7894
- 479,8058,8059, 832,8060,4049,2489,8061,2965,2490,3731, 990,3109, 627,1814,2642, # 7910
-4289,1582,4290,2125,2111,3496,4567,8062, 799,4291,3170,8063,4568,2112,1737,3013, # 7926
-1018, 543, 754,4292,3309,1676,4569,4570,4050,8064,1489,8065,3497,8066,2614,2889, # 7942
-4051,8067,8068,2966,8069,8070,8071,8072,3171,4571,4572,2182,1722,8073,3238,3239, # 7958
-1842,3610,1715, 481, 365,1975,1856,8074,8075,1962,2491,4573,8076,2126,3611,3240, # 7974
- 433,1894,2063,2075,8077, 602,2741,8078,8079,8080,8081,8082,3014,1628,3400,8083, # 7990
-3172,4574,4052,2890,4575,2512,8084,2544,2772,8085,8086,8087,3310,4576,2891,8088, # 8006
-4577,8089,2851,4578,4579,1221,2967,4053,2513,8090,8091,8092,1867,1989,8093,8094, # 8022
-8095,1895,8096,8097,4580,1896,4054, 318,8098,2094,4055,4293,8099,8100, 485,8101, # 8038
- 938,3862, 553,2670, 116,8102,3863,3612,8103,3498,2671,2773,3401,3311,2807,8104, # 8054
-3613,2929,4056,1747,2930,2968,8105,8106, 207,8107,8108,2672,4581,2514,8109,3015, # 8070
- 890,3614,3864,8110,1877,3732,3402,8111,2183,2353,3403,1652,8112,8113,8114, 941, # 8086
-2294, 208,3499,4057,2019, 330,4294,3865,2892,2492,3733,4295,8115,8116,8117,8118, # 8102
+ 1, 1800, 1506, 255, 1431, 198, 9, 82, 6, 7310, 177, 202, 3615, 1256, 2808, 110, # 2742
+ 3735, 33, 3241, 261, 76, 44, 2113, 16, 2931, 2184, 1176, 659, 3868, 26, 3404, 2643, # 2758
+ 1198, 3869, 3313, 4060, 410, 2211, 302, 590, 361, 1963, 8, 204, 58, 4296, 7311, 1931, # 2774
+ 63, 7312, 7313, 317, 1614, 75, 222, 159, 4061, 2412, 1480, 7314, 3500, 3068, 224, 2809, # 2790
+ 3616, 3, 10, 3870, 1471, 29, 2774, 1135, 2852, 1939, 873, 130, 3242, 1123, 312, 7315, # 2806
+ 4297, 2051, 507, 252, 682, 7316, 142, 1914, 124, 206, 2932, 34, 3501, 3173, 64, 604, # 2822
+ 7317, 2494, 1976, 1977, 155, 1990, 645, 641, 1606, 7318, 3405, 337, 72, 406, 7319, 80, # 2838
+ 630, 238, 3174, 1509, 263, 939, 1092, 2644, 756, 1440, 1094, 3406, 449, 69, 2969, 591, # 2854
+ 179, 2095, 471, 115, 2034, 1843, 60, 50, 2970, 134, 806, 1868, 734, 2035, 3407, 180, # 2870
+ 995, 1607, 156, 537, 2893, 688, 7320, 319, 1305, 779, 2144, 514, 2374, 298, 4298, 359, # 2886
+ 2495, 90, 2707, 1338, 663, 11, 906, 1099, 2545, 20, 2436, 182, 532, 1716, 7321, 732, # 2902
+ 1376, 4062, 1311, 1420, 3175, 25, 2312, 1056, 113, 399, 382, 1949, 242, 3408, 2467, 529, # 2918
+ 3243, 475, 1447, 3617, 7322, 117, 21, 656, 810, 1297, 2295, 2329, 3502, 7323, 126, 4063, # 2934
+ 706, 456, 150, 613, 4299, 71, 1118, 2036, 4064, 145, 3069, 85, 835, 486, 2114, 1246, # 2950
+ 1426, 428, 727, 1285, 1015, 800, 106, 623, 303, 1281, 7324, 2127, 2354, 347, 3736, 221, # 2966
+ 3503, 3110, 7325, 1955, 1153, 4065, 83, 296, 1199, 3070, 192, 624, 93, 7326, 822, 1897, # 2982
+ 2810, 3111, 795, 2064, 991, 1554, 1542, 1592, 27, 43, 2853, 859, 139, 1456, 860, 4300, # 2998
+ 437, 712, 3871, 164, 2392, 3112, 695, 211, 3017, 2096, 195, 3872, 1608, 3504, 3505, 3618, # 3014
+ 3873, 234, 811, 2971, 2097, 3874, 2229, 1441, 3506, 1615, 2375, 668, 2076, 1638, 305, 228, # 3030
+ 1664, 4301, 467, 415, 7327, 262, 2098, 1593, 239, 108, 300, 200, 1033, 512, 1247, 2077, # 3046
+ 7328, 7329, 2173, 3176, 3619, 2673, 593, 845, 1062, 3244, 88, 1723, 2037, 3875, 1950, 212, # 3062
+ 266, 152, 149, 468, 1898, 4066, 4302, 77, 187, 7330, 3018, 37, 5, 2972, 7331, 3876, # 3078
+ 7332, 7333, 39, 2517, 4303, 2894, 3177, 2078, 55, 148, 74, 4304, 545, 483, 1474, 1029, # 3094
+ 1665, 217, 1869, 1531, 3113, 1104, 2645, 4067, 24, 172, 3507, 900, 3877, 3508, 3509, 4305, # 3110
+ 32, 1408, 2811, 1312, 329, 487, 2355, 2247, 2708, 784, 2674, 4, 3019, 3314, 1427, 1788, # 3126
+ 188, 109, 499, 7334, 3620, 1717, 1789, 888, 1217, 3020, 4306, 7335, 3510, 7336, 3315, 1520, # 3142
+ 3621, 3878, 196, 1034, 775, 7337, 7338, 929, 1815, 249, 439, 38, 7339, 1063, 7340, 794, # 3158
+ 3879, 1435, 2296, 46, 178, 3245, 2065, 7341, 2376, 7342, 214, 1709, 4307, 804, 35, 707, # 3174
+ 324, 3622, 1601, 2546, 140, 459, 4068, 7343, 7344, 1365, 839, 272, 978, 2257, 2572, 3409, # 3190
+ 2128, 1363, 3623, 1423, 697, 100, 3071, 48, 70, 1231, 495, 3114, 2193, 7345, 1294, 7346, # 3206
+ 2079, 462, 586, 1042, 3246, 853, 256, 988, 185, 2377, 3410, 1698, 434, 1084, 7347, 3411, # 3222
+ 314, 2615, 2775, 4308, 2330, 2331, 569, 2280, 637, 1816, 2518, 757, 1162, 1878, 1616, 3412, # 3238
+ 287, 1577, 2115, 768, 4309, 1671, 2854, 3511, 2519, 1321, 3737, 909, 2413, 7348, 4069, 933, # 3254
+ 3738, 7349, 2052, 2356, 1222, 4310, 765, 2414, 1322, 786, 4311, 7350, 1919, 1462, 1677, 2895, # 3270
+ 1699, 7351, 4312, 1424, 2437, 3115, 3624, 2590, 3316, 1774, 1940, 3413, 3880, 4070, 309, 1369, # 3286
+ 1130, 2812, 364, 2230, 1653, 1299, 3881, 3512, 3882, 3883, 2646, 525, 1085, 3021, 902, 2000, # 3302
+ 1475, 964, 4313, 421, 1844, 1415, 1057, 2281, 940, 1364, 3116, 376, 4314, 4315, 1381, 7, # 3318
+ 2520, 983, 2378, 336, 1710, 2675, 1845, 321, 3414, 559, 1131, 3022, 2742, 1808, 1132, 1313, # 3334
+ 265, 1481, 1857, 7352, 352, 1203, 2813, 3247, 167, 1089, 420, 2814, 776, 792, 1724, 3513, # 3350
+ 4071, 2438, 3248, 7353, 4072, 7354, 446, 229, 333, 2743, 901, 3739, 1200, 1557, 4316, 2647, # 3366
+ 1920, 395, 2744, 2676, 3740, 4073, 1835, 125, 916, 3178, 2616, 4317, 7355, 7356, 3741, 7357, # 3382
+ 7358, 7359, 4318, 3117, 3625, 1133, 2547, 1757, 3415, 1510, 2313, 1409, 3514, 7360, 2145, 438, # 3398
+ 2591, 2896, 2379, 3317, 1068, 958, 3023, 461, 311, 2855, 2677, 4074, 1915, 3179, 4075, 1978, # 3414
+ 383, 750, 2745, 2617, 4076, 274, 539, 385, 1278, 1442, 7361, 1154, 1964, 384, 561, 210, # 3430
+ 98, 1295, 2548, 3515, 7362, 1711, 2415, 1482, 3416, 3884, 2897, 1257, 129, 7363, 3742, 642, # 3446
+ 523, 2776, 2777, 2648, 7364, 141, 2231, 1333, 68, 176, 441, 876, 907, 4077, 603, 2592, # 3462
+ 710, 171, 3417, 404, 549, 18, 3118, 2393, 1410, 3626, 1666, 7365, 3516, 4319, 2898, 4320, # 3478
+ 7366, 2973, 368, 7367, 146, 366, 99, 871, 3627, 1543, 748, 807, 1586, 1185, 22, 2258, # 3494
+ 379, 3743, 3180, 7368, 3181, 505, 1941, 2618, 1991, 1382, 2314, 7369, 380, 2357, 218, 702, # 3510
+ 1817, 1248, 3418, 3024, 3517, 3318, 3249, 7370, 2974, 3628, 930, 3250, 3744, 7371, 59, 7372, # 3526
+ 585, 601, 4078, 497, 3419, 1112, 1314, 4321, 1801, 7373, 1223, 1472, 2174, 7374, 749, 1836, # 3542
+ 690, 1899, 3745, 1772, 3885, 1476, 429, 1043, 1790, 2232, 2116, 917, 4079, 447, 1086, 1629, # 3558
+ 7375, 556, 7376, 7377, 2020, 1654, 844, 1090, 105, 550, 966, 1758, 2815, 1008, 1782, 686, # 3574
+ 1095, 7378, 2282, 793, 1602, 7379, 3518, 2593, 4322, 4080, 2933, 2297, 4323, 3746, 980, 2496, # 3590
+ 544, 353, 527, 4324, 908, 2678, 2899, 7380, 381, 2619, 1942, 1348, 7381, 1341, 1252, 560, # 3606
+ 3072, 7382, 3420, 2856, 7383, 2053, 973, 886, 2080, 143, 4325, 7384, 7385, 157, 3886, 496, # 3622
+ 4081, 57, 840, 540, 2038, 4326, 4327, 3421, 2117, 1445, 970, 2259, 1748, 1965, 2081, 4082, # 3638
+ 3119, 1234, 1775, 3251, 2816, 3629, 773, 1206, 2129, 1066, 2039, 1326, 3887, 1738, 1725, 4083, # 3654
+ 279, 3120, 51, 1544, 2594, 423, 1578, 2130, 2066, 173, 4328, 1879, 7386, 7387, 1583, 264, # 3670
+ 610, 3630, 4329, 2439, 280, 154, 7388, 7389, 7390, 1739, 338, 1282, 3073, 693, 2857, 1411, # 3686
+ 1074, 3747, 2440, 7391, 4330, 7392, 7393, 1240, 952, 2394, 7394, 2900, 1538, 2679, 685, 1483, # 3702
+ 4084, 2468, 1436, 953, 4085, 2054, 4331, 671, 2395, 79, 4086, 2441, 3252, 608, 567, 2680, # 3718
+ 3422, 4087, 4088, 1691, 393, 1261, 1791, 2396, 7395, 4332, 7396, 7397, 7398, 7399, 1383, 1672, # 3734
+ 3748, 3182, 1464, 522, 1119, 661, 1150, 216, 675, 4333, 3888, 1432, 3519, 609, 4334, 2681, # 3750
+ 2397, 7400, 7401, 7402, 4089, 3025, 0, 7403, 2469, 315, 231, 2442, 301, 3319, 4335, 2380, # 3766
+ 7404, 233, 4090, 3631, 1818, 4336, 4337, 7405, 96, 1776, 1315, 2082, 7406, 257, 7407, 1809, # 3782
+ 3632, 2709, 1139, 1819, 4091, 2021, 1124, 2163, 2778, 1777, 2649, 7408, 3074, 363, 1655, 3183, # 3798
+ 7409, 2975, 7410, 7411, 7412, 3889, 1567, 3890, 718, 103, 3184, 849, 1443, 341, 3320, 2934, # 3814
+ 1484, 7413, 1712, 127, 67, 339, 4092, 2398, 679, 1412, 821, 7414, 7415, 834, 738, 351, # 3830
+ 2976, 2146, 846, 235, 1497, 1880, 418, 1992, 3749, 2710, 186, 1100, 2147, 2746, 3520, 1545, # 3846
+ 1355, 2935, 2858, 1377, 583, 3891, 4093, 2573, 2977, 7416, 1298, 3633, 1078, 2549, 3634, 2358, # 3862
+ 78, 3750, 3751, 267, 1289, 2099, 2001, 1594, 4094, 348, 369, 1274, 2194, 2175, 1837, 4338, # 3878
+ 1820, 2817, 3635, 2747, 2283, 2002, 4339, 2936, 2748, 144, 3321, 882, 4340, 3892, 2749, 3423, # 3894
+ 4341, 2901, 7417, 4095, 1726, 320, 7418, 3893, 3026, 788, 2978, 7419, 2818, 1773, 1327, 2859, # 3910
+ 3894, 2819, 7420, 1306, 4342, 2003, 1700, 3752, 3521, 2359, 2650, 787, 2022, 506, 824, 3636, # 3926
+ 534, 323, 4343, 1044, 3322, 2023, 1900, 946, 3424, 7421, 1778, 1500, 1678, 7422, 1881, 4344, # 3942
+ 165, 243, 4345, 3637, 2521, 123, 683, 4096, 764, 4346, 36, 3895, 1792, 589, 2902, 816, # 3958
+ 626, 1667, 3027, 2233, 1639, 1555, 1622, 3753, 3896, 7423, 3897, 2860, 1370, 1228, 1932, 891, # 3974
+ 2083, 2903, 304, 4097, 7424, 292, 2979, 2711, 3522, 691, 2100, 4098, 1115, 4347, 118, 662, # 3990
+ 7425, 611, 1156, 854, 2381, 1316, 2861, 2, 386, 515, 2904, 7426, 7427, 3253, 868, 2234, # 4006
+ 1486, 855, 2651, 785, 2212, 3028, 7428, 1040, 3185, 3523, 7429, 3121, 448, 7430, 1525, 7431, # 4022
+ 2164, 4348, 7432, 3754, 7433, 4099, 2820, 3524, 3122, 503, 818, 3898, 3123, 1568, 814, 676, # 4038
+ 1444, 306, 1749, 7434, 3755, 1416, 1030, 197, 1428, 805, 2821, 1501, 4349, 7435, 7436, 7437, # 4054
+ 1993, 7438, 4350, 7439, 7440, 2195, 13, 2779, 3638, 2980, 3124, 1229, 1916, 7441, 3756, 2131, # 4070
+ 7442, 4100, 4351, 2399, 3525, 7443, 2213, 1511, 1727, 1120, 7444, 7445, 646, 3757, 2443, 307, # 4086
+ 7446, 7447, 1595, 3186, 7448, 7449, 7450, 3639, 1113, 1356, 3899, 1465, 2522, 2523, 7451, 519, # 4102
+ 7452, 128, 2132, 92, 2284, 1979, 7453, 3900, 1512, 342, 3125, 2196, 7454, 2780, 2214, 1980, # 4118
+ 3323, 7455, 290, 1656, 1317, 789, 827, 2360, 7456, 3758, 4352, 562, 581, 3901, 7457, 401, # 4134
+ 4353, 2248, 94, 4354, 1399, 2781, 7458, 1463, 2024, 4355, 3187, 1943, 7459, 828, 1105, 4101, # 4150
+ 1262, 1394, 7460, 4102, 605, 4356, 7461, 1783, 2862, 7462, 2822, 819, 2101, 578, 2197, 2937, # 4166
+ 7463, 1502, 436, 3254, 4103, 3255, 2823, 3902, 2905, 3425, 3426, 7464, 2712, 2315, 7465, 7466, # 4182
+ 2332, 2067, 23, 4357, 193, 826, 3759, 2102, 699, 1630, 4104, 3075, 390, 1793, 1064, 3526, # 4198
+ 7467, 1579, 3076, 3077, 1400, 7468, 4105, 1838, 1640, 2863, 7469, 4358, 4359, 137, 4106, 598, # 4214
+ 3078, 1966, 780, 104, 974, 2938, 7470, 278, 899, 253, 402, 572, 504, 493, 1339, 7471, # 4230
+ 3903, 1275, 4360, 2574, 2550, 7472, 3640, 3029, 3079, 2249, 565, 1334, 2713, 863, 41, 7473, # 4246
+ 7474, 4361, 7475, 1657, 2333, 19, 463, 2750, 4107, 606, 7476, 2981, 3256, 1087, 2084, 1323, # 4262
+ 2652, 2982, 7477, 1631, 1623, 1750, 4108, 2682, 7478, 2864, 791, 2714, 2653, 2334, 232, 2416, # 4278
+ 7479, 2983, 1498, 7480, 2654, 2620, 755, 1366, 3641, 3257, 3126, 2025, 1609, 119, 1917, 3427, # 4294
+ 862, 1026, 4109, 7481, 3904, 3760, 4362, 3905, 4363, 2260, 1951, 2470, 7482, 1125, 817, 4110, # 4310
+ 4111, 3906, 1513, 1766, 2040, 1487, 4112, 3030, 3258, 2824, 3761, 3127, 7483, 7484, 1507, 7485, # 4326
+ 2683, 733, 40, 1632, 1106, 2865, 345, 4113, 841, 2524, 230, 4364, 2984, 1846, 3259, 3428, # 4342
+ 7486, 1263, 986, 3429, 7487, 735, 879, 254, 1137, 857, 622, 1300, 1180, 1388, 1562, 3907, # 4358
+ 3908, 2939, 967, 2751, 2655, 1349, 592, 2133, 1692, 3324, 2985, 1994, 4114, 1679, 3909, 1901, # 4374
+ 2185, 7488, 739, 3642, 2715, 1296, 1290, 7489, 4115, 2198, 2199, 1921, 1563, 2595, 2551, 1870, # 4390
+ 2752, 2986, 7490, 435, 7491, 343, 1108, 596, 17, 1751, 4365, 2235, 3430, 3643, 7492, 4366, # 4406
+ 294, 3527, 2940, 1693, 477, 979, 281, 2041, 3528, 643, 2042, 3644, 2621, 2782, 2261, 1031, # 4422
+ 2335, 2134, 2298, 3529, 4367, 367, 1249, 2552, 7493, 3530, 7494, 4368, 1283, 3325, 2004, 240, # 4438
+ 1762, 3326, 4369, 4370, 836, 1069, 3128, 474, 7495, 2148, 2525, 268, 3531, 7496, 3188, 1521, # 4454
+ 1284, 7497, 1658, 1546, 4116, 7498, 3532, 3533, 7499, 4117, 3327, 2684, 1685, 4118, 961, 1673, # 4470
+ 2622, 190, 2005, 2200, 3762, 4371, 4372, 7500, 570, 2497, 3645, 1490, 7501, 4373, 2623, 3260, # 4486
+ 1956, 4374, 584, 1514, 396, 1045, 1944, 7502, 4375, 1967, 2444, 7503, 7504, 4376, 3910, 619, # 4502
+ 7505, 3129, 3261, 215, 2006, 2783, 2553, 3189, 4377, 3190, 4378, 763, 4119, 3763, 4379, 7506, # 4518
+ 7507, 1957, 1767, 2941, 3328, 3646, 1174, 452, 1477, 4380, 3329, 3130, 7508, 2825, 1253, 2382, # 4534
+ 2186, 1091, 2285, 4120, 492, 7509, 638, 1169, 1824, 2135, 1752, 3911, 648, 926, 1021, 1324, # 4550
+ 4381, 520, 4382, 997, 847, 1007, 892, 4383, 3764, 2262, 1871, 3647, 7510, 2400, 1784, 4384, # 4566
+ 1952, 2942, 3080, 3191, 1728, 4121, 2043, 3648, 4385, 2007, 1701, 3131, 1551, 30, 2263, 4122, # 4582
+ 7511, 2026, 4386, 3534, 7512, 501, 7513, 4123, 594, 3431, 2165, 1821, 3535, 3432, 3536, 3192, # 4598
+ 829, 2826, 4124, 7514, 1680, 3132, 1225, 4125, 7515, 3262, 4387, 4126, 3133, 2336, 7516, 4388, # 4614
+ 4127, 7517, 3912, 3913, 7518, 1847, 2383, 2596, 3330, 7519, 4389, 374, 3914, 652, 4128, 4129, # 4630
+ 375, 1140, 798, 7520, 7521, 7522, 2361, 4390, 2264, 546, 1659, 138, 3031, 2445, 4391, 7523, # 4646
+ 2250, 612, 1848, 910, 796, 3765, 1740, 1371, 825, 3766, 3767, 7524, 2906, 2554, 7525, 692, # 4662
+ 444, 3032, 2624, 801, 4392, 4130, 7526, 1491, 244, 1053, 3033, 4131, 4132, 340, 7527, 3915, # 4678
+ 1041, 2987, 293, 1168, 87, 1357, 7528, 1539, 959, 7529, 2236, 721, 694, 4133, 3768, 219, # 4694
+ 1478, 644, 1417, 3331, 2656, 1413, 1401, 1335, 1389, 3916, 7530, 7531, 2988, 2362, 3134, 1825, # 4710
+ 730, 1515, 184, 2827, 66, 4393, 7532, 1660, 2943, 246, 3332, 378, 1457, 226, 3433, 975, # 4726
+ 3917, 2944, 1264, 3537, 674, 696, 7533, 163, 7534, 1141, 2417, 2166, 713, 3538, 3333, 4394, # 4742
+ 3918, 7535, 7536, 1186, 15, 7537, 1079, 1070, 7538, 1522, 3193, 3539, 276, 1050, 2716, 758, # 4758
+ 1126, 653, 2945, 3263, 7539, 2337, 889, 3540, 3919, 3081, 2989, 903, 1250, 4395, 3920, 3434, # 4774
+ 3541, 1342, 1681, 1718, 766, 3264, 286, 89, 2946, 3649, 7540, 1713, 7541, 2597, 3334, 2990, # 4790
+ 7542, 2947, 2215, 3194, 2866, 7543, 4396, 2498, 2526, 181, 387, 1075, 3921, 731, 2187, 3335, # 4806
+ 7544, 3265, 310, 313, 3435, 2299, 770, 4134, 54, 3034, 189, 4397, 3082, 3769, 3922, 7545, # 4822
+ 1230, 1617, 1849, 355, 3542, 4135, 4398, 3336, 111, 4136, 3650, 1350, 3135, 3436, 3035, 4137, # 4838
+ 2149, 3266, 3543, 7546, 2784, 3923, 3924, 2991, 722, 2008, 7547, 1071, 247, 1207, 2338, 2471, # 4854
+ 1378, 4399, 2009, 864, 1437, 1214, 4400, 373, 3770, 1142, 2216, 667, 4401, 442, 2753, 2555, # 4870
+ 3771, 3925, 1968, 4138, 3267, 1839, 837, 170, 1107, 934, 1336, 1882, 7548, 7549, 2118, 4139, # 4886
+ 2828, 743, 1569, 7550, 4402, 4140, 582, 2384, 1418, 3437, 7551, 1802, 7552, 357, 1395, 1729, # 4902
+ 3651, 3268, 2418, 1564, 2237, 7553, 3083, 3772, 1633, 4403, 1114, 2085, 4141, 1532, 7554, 482, # 4918
+ 2446, 4404, 7555, 7556, 1492, 833, 1466, 7557, 2717, 3544, 1641, 2829, 7558, 1526, 1272, 3652, # 4934
+ 4142, 1686, 1794, 416, 2556, 1902, 1953, 1803, 7559, 3773, 2785, 3774, 1159, 2316, 7560, 2867, # 4950
+ 4405, 1610, 1584, 3036, 2419, 2754, 443, 3269, 1163, 3136, 7561, 7562, 3926, 7563, 4143, 2499, # 4966
+ 3037, 4406, 3927, 3137, 2103, 1647, 3545, 2010, 1872, 4144, 7564, 4145, 431, 3438, 7565, 250, # 4982
+ 97, 81, 4146, 7566, 1648, 1850, 1558, 160, 848, 7567, 866, 740, 1694, 7568, 2201, 2830, # 4998
+ 3195, 4147, 4407, 3653, 1687, 950, 2472, 426, 469, 3196, 3654, 3655, 3928, 7569, 7570, 1188, # 5014
+ 424, 1995, 861, 3546, 4148, 3775, 2202, 2685, 168, 1235, 3547, 4149, 7571, 2086, 1674, 4408, # 5030
+ 3337, 3270, 220, 2557, 1009, 7572, 3776, 670, 2992, 332, 1208, 717, 7573, 7574, 3548, 2447, # 5046
+ 3929, 3338, 7575, 513, 7576, 1209, 2868, 3339, 3138, 4409, 1080, 7577, 7578, 7579, 7580, 2527, # 5062
+ 3656, 3549, 815, 1587, 3930, 3931, 7581, 3550, 3439, 3777, 1254, 4410, 1328, 3038, 1390, 3932, # 5078
+ 1741, 3933, 3778, 3934, 7582, 236, 3779, 2448, 3271, 7583, 7584, 3657, 3780, 1273, 3781, 4411, # 5094
+ 7585, 308, 7586, 4412, 245, 4413, 1851, 2473, 1307, 2575, 430, 715, 2136, 2449, 7587, 270, # 5110
+ 199, 2869, 3935, 7588, 3551, 2718, 1753, 761, 1754, 725, 1661, 1840, 4414, 3440, 3658, 7589, # 5126
+ 7590, 587, 14, 3272, 227, 2598, 326, 480, 2265, 943, 2755, 3552, 291, 650, 1883, 7591, # 5142
+ 1702, 1226, 102, 1547, 62, 3441, 904, 4415, 3442, 1164, 4150, 7592, 7593, 1224, 1548, 2756, # 5158
+ 391, 498, 1493, 7594, 1386, 1419, 7595, 2055, 1177, 4416, 813, 880, 1081, 2363, 566, 1145, # 5174
+ 4417, 2286, 1001, 1035, 2558, 2599, 2238, 394, 1286, 7596, 7597, 2068, 7598, 86, 1494, 1730, # 5190
+ 3936, 491, 1588, 745, 897, 2948, 843, 3340, 3937, 2757, 2870, 3273, 1768, 998, 2217, 2069, # 5206
+ 397, 1826, 1195, 1969, 3659, 2993, 3341, 284, 7599, 3782, 2500, 2137, 2119, 1903, 7600, 3938, # 5222
+ 2150, 3939, 4151, 1036, 3443, 1904, 114, 2559, 4152, 209, 1527, 7601, 7602, 2949, 2831, 2625, # 5238
+ 2385, 2719, 3139, 812, 2560, 7603, 3274, 7604, 1559, 737, 1884, 3660, 1210, 885, 28, 2686, # 5254
+ 3553, 3783, 7605, 4153, 1004, 1779, 4418, 7606, 346, 1981, 2218, 2687, 4419, 3784, 1742, 797, # 5270
+ 1642, 3940, 1933, 1072, 1384, 2151, 896, 3941, 3275, 3661, 3197, 2871, 3554, 7607, 2561, 1958, # 5286
+ 4420, 2450, 1785, 7608, 7609, 7610, 3942, 4154, 1005, 1308, 3662, 4155, 2720, 4421, 4422, 1528, # 5302
+ 2600, 161, 1178, 4156, 1982, 987, 4423, 1101, 4157, 631, 3943, 1157, 3198, 2420, 1343, 1241, # 5318
+ 1016, 2239, 2562, 372, 877, 2339, 2501, 1160, 555, 1934, 911, 3944, 7611, 466, 1170, 169, # 5334
+ 1051, 2907, 2688, 3663, 2474, 2994, 1182, 2011, 2563, 1251, 2626, 7612, 992, 2340, 3444, 1540, # 5350
+ 2721, 1201, 2070, 2401, 1996, 2475, 7613, 4424, 528, 1922, 2188, 1503, 1873, 1570, 2364, 3342, # 5366
+ 3276, 7614, 557, 1073, 7615, 1827, 3445, 2087, 2266, 3140, 3039, 3084, 767, 3085, 2786, 4425, # 5382
+ 1006, 4158, 4426, 2341, 1267, 2176, 3664, 3199, 778, 3945, 3200, 2722, 1597, 2657, 7616, 4427, # 5398
+ 7617, 3446, 7618, 7619, 7620, 3277, 2689, 1433, 3278, 131, 95, 1504, 3946, 723, 4159, 3141, # 5414
+ 1841, 3555, 2758, 2189, 3947, 2027, 2104, 3665, 7621, 2995, 3948, 1218, 7622, 3343, 3201, 3949, # 5430
+ 4160, 2576, 248, 1634, 3785, 912, 7623, 2832, 3666, 3040, 3786, 654, 53, 7624, 2996, 7625, # 5446
+ 1688, 4428, 777, 3447, 1032, 3950, 1425, 7626, 191, 820, 2120, 2833, 971, 4429, 931, 3202, # 5462
+ 135, 664, 783, 3787, 1997, 772, 2908, 1935, 3951, 3788, 4430, 2909, 3203, 282, 2723, 640, # 5478
+ 1372, 3448, 1127, 922, 325, 3344, 7627, 7628, 711, 2044, 7629, 7630, 3952, 2219, 2787, 1936, # 5494
+ 3953, 3345, 2220, 2251, 3789, 2300, 7631, 4431, 3790, 1258, 3279, 3954, 3204, 2138, 2950, 3955, # 5510
+ 3956, 7632, 2221, 258, 3205, 4432, 101, 1227, 7633, 3280, 1755, 7634, 1391, 3281, 7635, 2910, # 5526
+ 2056, 893, 7636, 7637, 7638, 1402, 4161, 2342, 7639, 7640, 3206, 3556, 7641, 7642, 878, 1325, # 5542
+ 1780, 2788, 4433, 259, 1385, 2577, 744, 1183, 2267, 4434, 7643, 3957, 2502, 7644, 684, 1024, # 5558
+ 4162, 7645, 472, 3557, 3449, 1165, 3282, 3958, 3959, 322, 2152, 881, 455, 1695, 1152, 1340, # 5574
+ 660, 554, 2153, 4435, 1058, 4436, 4163, 830, 1065, 3346, 3960, 4437, 1923, 7646, 1703, 1918, # 5590
+ 7647, 932, 2268, 122, 7648, 4438, 947, 677, 7649, 3791, 2627, 297, 1905, 1924, 2269, 4439, # 5606
+ 2317, 3283, 7650, 7651, 4164, 7652, 4165, 84, 4166, 112, 989, 7653, 547, 1059, 3961, 701, # 5622
+ 3558, 1019, 7654, 4167, 7655, 3450, 942, 639, 457, 2301, 2451, 993, 2951, 407, 851, 494, # 5638
+ 4440, 3347, 927, 7656, 1237, 7657, 2421, 3348, 573, 4168, 680, 921, 2911, 1279, 1874, 285, # 5654
+ 790, 1448, 1983, 719, 2167, 7658, 7659, 4441, 3962, 3963, 1649, 7660, 1541, 563, 7661, 1077, # 5670
+ 7662, 3349, 3041, 3451, 511, 2997, 3964, 3965, 3667, 3966, 1268, 2564, 3350, 3207, 4442, 4443, # 5686
+ 7663, 535, 1048, 1276, 1189, 2912, 2028, 3142, 1438, 1373, 2834, 2952, 1134, 2012, 7664, 4169, # 5702
+ 1238, 2578, 3086, 1259, 7665, 700, 7666, 2953, 3143, 3668, 4170, 7667, 4171, 1146, 1875, 1906, # 5718
+ 4444, 2601, 3967, 781, 2422, 132, 1589, 203, 147, 273, 2789, 2402, 898, 1786, 2154, 3968, # 5734
+ 3969, 7668, 3792, 2790, 7669, 7670, 4445, 4446, 7671, 3208, 7672, 1635, 3793, 965, 7673, 1804, # 5750
+ 2690, 1516, 3559, 1121, 1082, 1329, 3284, 3970, 1449, 3794, 65, 1128, 2835, 2913, 2759, 1590, # 5766
+ 3795, 7674, 7675, 12, 2658, 45, 976, 2579, 3144, 4447, 517, 2528, 1013, 1037, 3209, 7676, # 5782
+ 3796, 2836, 7677, 3797, 7678, 3452, 7679, 2602, 614, 1998, 2318, 3798, 3087, 2724, 2628, 7680, # 5798
+ 2580, 4172, 599, 1269, 7681, 1810, 3669, 7682, 2691, 3088, 759, 1060, 489, 1805, 3351, 3285, # 5814
+ 1358, 7683, 7684, 2386, 1387, 1215, 2629, 2252, 490, 7685, 7686, 4173, 1759, 2387, 2343, 7687, # 5830
+ 4448, 3799, 1907, 3971, 2630, 1806, 3210, 4449, 3453, 3286, 2760, 2344, 874, 7688, 7689, 3454, # 5846
+ 3670, 1858, 91, 2914, 3671, 3042, 3800, 4450, 7690, 3145, 3972, 2659, 7691, 3455, 1202, 1403, # 5862
+ 3801, 2954, 2529, 1517, 2503, 4451, 3456, 2504, 7692, 4452, 7693, 2692, 1885, 1495, 1731, 3973, # 5878
+ 2365, 4453, 7694, 2029, 7695, 7696, 3974, 2693, 1216, 237, 2581, 4174, 2319, 3975, 3802, 4454, # 5894
+ 4455, 2694, 3560, 3457, 445, 4456, 7697, 7698, 7699, 7700, 2761, 61, 3976, 3672, 1822, 3977, # 5910
+ 7701, 687, 2045, 935, 925, 405, 2660, 703, 1096, 1859, 2725, 4457, 3978, 1876, 1367, 2695, # 5926
+ 3352, 918, 2105, 1781, 2476, 334, 3287, 1611, 1093, 4458, 564, 3146, 3458, 3673, 3353, 945, # 5942
+ 2631, 2057, 4459, 7702, 1925, 872, 4175, 7703, 3459, 2696, 3089, 349, 4176, 3674, 3979, 4460, # 5958
+ 3803, 4177, 3675, 2155, 3980, 4461, 4462, 4178, 4463, 2403, 2046, 782, 3981, 400, 251, 4179, # 5974
+ 1624, 7704, 7705, 277, 3676, 299, 1265, 476, 1191, 3804, 2121, 4180, 4181, 1109, 205, 7706, # 5990
+ 2582, 1000, 2156, 3561, 1860, 7707, 7708, 7709, 4464, 7710, 4465, 2565, 107, 2477, 2157, 3982, # 6006
+ 3460, 3147, 7711, 1533, 541, 1301, 158, 753, 4182, 2872, 3562, 7712, 1696, 370, 1088, 4183, # 6022
+ 4466, 3563, 579, 327, 440, 162, 2240, 269, 1937, 1374, 3461, 968, 3043, 56, 1396, 3090, # 6038
+ 2106, 3288, 3354, 7713, 1926, 2158, 4467, 2998, 7714, 3564, 7715, 7716, 3677, 4468, 2478, 7717, # 6054
+ 2791, 7718, 1650, 4469, 7719, 2603, 7720, 7721, 3983, 2661, 3355, 1149, 3356, 3984, 3805, 3985, # 6070
+ 7722, 1076, 49, 7723, 951, 3211, 3289, 3290, 450, 2837, 920, 7724, 1811, 2792, 2366, 4184, # 6086
+ 1908, 1138, 2367, 3806, 3462, 7725, 3212, 4470, 1909, 1147, 1518, 2423, 4471, 3807, 7726, 4472, # 6102
+ 2388, 2604, 260, 1795, 3213, 7727, 7728, 3808, 3291, 708, 7729, 3565, 1704, 7730, 3566, 1351, # 6118
+ 1618, 3357, 2999, 1886, 944, 4185, 3358, 4186, 3044, 3359, 4187, 7731, 3678, 422, 413, 1714, # 6134
+ 3292, 500, 2058, 2345, 4188, 2479, 7732, 1344, 1910, 954, 7733, 1668, 7734, 7735, 3986, 2404, # 6150
+ 4189, 3567, 3809, 4190, 7736, 2302, 1318, 2505, 3091, 133, 3092, 2873, 4473, 629, 31, 2838, # 6166
+ 2697, 3810, 4474, 850, 949, 4475, 3987, 2955, 1732, 2088, 4191, 1496, 1852, 7737, 3988, 620, # 6182
+ 3214, 981, 1242, 3679, 3360, 1619, 3680, 1643, 3293, 2139, 2452, 1970, 1719, 3463, 2168, 7738, # 6198
+ 3215, 7739, 7740, 3361, 1828, 7741, 1277, 4476, 1565, 2047, 7742, 1636, 3568, 3093, 7743, 869, # 6214
+ 2839, 655, 3811, 3812, 3094, 3989, 3000, 3813, 1310, 3569, 4477, 7744, 7745, 7746, 1733, 558, # 6230
+ 4478, 3681, 335, 1549, 3045, 1756, 4192, 3682, 1945, 3464, 1829, 1291, 1192, 470, 2726, 2107, # 6246
+ 2793, 913, 1054, 3990, 7747, 1027, 7748, 3046, 3991, 4479, 982, 2662, 3362, 3148, 3465, 3216, # 6262
+ 3217, 1946, 2794, 7749, 571, 4480, 7750, 1830, 7751, 3570, 2583, 1523, 2424, 7752, 2089, 984, # 6278
+ 4481, 3683, 1959, 7753, 3684, 852, 923, 2795, 3466, 3685, 969, 1519, 999, 2048, 2320, 1705, # 6294
+ 7754, 3095, 615, 1662, 151, 597, 3992, 2405, 2321, 1049, 275, 4482, 3686, 4193, 568, 3687, # 6310
+ 3571, 2480, 4194, 3688, 7755, 2425, 2270, 409, 3218, 7756, 1566, 2874, 3467, 1002, 769, 2840, # 6326
+ 194, 2090, 3149, 3689, 2222, 3294, 4195, 628, 1505, 7757, 7758, 1763, 2177, 3001, 3993, 521, # 6342
+ 1161, 2584, 1787, 2203, 2406, 4483, 3994, 1625, 4196, 4197, 412, 42, 3096, 464, 7759, 2632, # 6358
+ 4484, 3363, 1760, 1571, 2875, 3468, 2530, 1219, 2204, 3814, 2633, 2140, 2368, 4485, 4486, 3295, # 6374
+ 1651, 3364, 3572, 7760, 7761, 3573, 2481, 3469, 7762, 3690, 7763, 7764, 2271, 2091, 460, 7765, # 6390
+ 4487, 7766, 3002, 962, 588, 3574, 289, 3219, 2634, 1116, 52, 7767, 3047, 1796, 7768, 7769, # 6406
+ 7770, 1467, 7771, 1598, 1143, 3691, 4198, 1984, 1734, 1067, 4488, 1280, 3365, 465, 4489, 1572, # 6422
+ 510, 7772, 1927, 2241, 1812, 1644, 3575, 7773, 4490, 3692, 7774, 7775, 2663, 1573, 1534, 7776, # 6438
+ 7777, 4199, 536, 1807, 1761, 3470, 3815, 3150, 2635, 7778, 7779, 7780, 4491, 3471, 2915, 1911, # 6454
+ 2796, 7781, 3296, 1122, 377, 3220, 7782, 360, 7783, 7784, 4200, 1529, 551, 7785, 2059, 3693, # 6470
+ 1769, 2426, 7786, 2916, 4201, 3297, 3097, 2322, 2108, 2030, 4492, 1404, 136, 1468, 1479, 672, # 6486
+ 1171, 3221, 2303, 271, 3151, 7787, 2762, 7788, 2049, 678, 2727, 865, 1947, 4493, 7789, 2013, # 6502
+ 3995, 2956, 7790, 2728, 2223, 1397, 3048, 3694, 4494, 4495, 1735, 2917, 3366, 3576, 7791, 3816, # 6518
+ 509, 2841, 2453, 2876, 3817, 7792, 7793, 3152, 3153, 4496, 4202, 2531, 4497, 2304, 1166, 1010, # 6534
+ 552, 681, 1887, 7794, 7795, 2957, 2958, 3996, 1287, 1596, 1861, 3154, 358, 453, 736, 175, # 6550
+ 478, 1117, 905, 1167, 1097, 7796, 1853, 1530, 7797, 1706, 7798, 2178, 3472, 2287, 3695, 3473, # 6566
+ 3577, 4203, 2092, 4204, 7799, 3367, 1193, 2482, 4205, 1458, 2190, 2205, 1862, 1888, 1421, 3298, # 6582
+ 2918, 3049, 2179, 3474, 595, 2122, 7800, 3997, 7801, 7802, 4206, 1707, 2636, 223, 3696, 1359, # 6598
+ 751, 3098, 183, 3475, 7803, 2797, 3003, 419, 2369, 633, 704, 3818, 2389, 241, 7804, 7805, # 6614
+ 7806, 838, 3004, 3697, 2272, 2763, 2454, 3819, 1938, 2050, 3998, 1309, 3099, 2242, 1181, 7807, # 6630
+ 1136, 2206, 3820, 2370, 1446, 4207, 2305, 4498, 7808, 7809, 4208, 1055, 2605, 484, 3698, 7810, # 6646
+ 3999, 625, 4209, 2273, 3368, 1499, 4210, 4000, 7811, 4001, 4211, 3222, 2274, 2275, 3476, 7812, # 6662
+ 7813, 2764, 808, 2606, 3699, 3369, 4002, 4212, 3100, 2532, 526, 3370, 3821, 4213, 955, 7814, # 6678
+ 1620, 4214, 2637, 2427, 7815, 1429, 3700, 1669, 1831, 994, 928, 7816, 3578, 1260, 7817, 7818, # 6694
+ 7819, 1948, 2288, 741, 2919, 1626, 4215, 2729, 2455, 867, 1184, 362, 3371, 1392, 7820, 7821, # 6710
+ 4003, 4216, 1770, 1736, 3223, 2920, 4499, 4500, 1928, 2698, 1459, 1158, 7822, 3050, 3372, 2877, # 6726
+ 1292, 1929, 2506, 2842, 3701, 1985, 1187, 2071, 2014, 2607, 4217, 7823, 2566, 2507, 2169, 3702, # 6742
+ 2483, 3299, 7824, 3703, 4501, 7825, 7826, 666, 1003, 3005, 1022, 3579, 4218, 7827, 4502, 1813, # 6758
+ 2253, 574, 3822, 1603, 295, 1535, 705, 3823, 4219, 283, 858, 417, 7828, 7829, 3224, 4503, # 6774
+ 4504, 3051, 1220, 1889, 1046, 2276, 2456, 4004, 1393, 1599, 689, 2567, 388, 4220, 7830, 2484, # 6790
+ 802, 7831, 2798, 3824, 2060, 1405, 2254, 7832, 4505, 3825, 2109, 1052, 1345, 3225, 1585, 7833, # 6806
+ 809, 7834, 7835, 7836, 575, 2730, 3477, 956, 1552, 1469, 1144, 2323, 7837, 2324, 1560, 2457, # 6822
+ 3580, 3226, 4005, 616, 2207, 3155, 2180, 2289, 7838, 1832, 7839, 3478, 4506, 7840, 1319, 3704, # 6838
+ 3705, 1211, 3581, 1023, 3227, 1293, 2799, 7841, 7842, 7843, 3826, 607, 2306, 3827, 762, 2878, # 6854
+ 1439, 4221, 1360, 7844, 1485, 3052, 7845, 4507, 1038, 4222, 1450, 2061, 2638, 4223, 1379, 4508, # 6870
+ 2585, 7846, 7847, 4224, 1352, 1414, 2325, 2921, 1172, 7848, 7849, 3828, 3829, 7850, 1797, 1451, # 6886
+ 7851, 7852, 7853, 7854, 2922, 4006, 4007, 2485, 2346, 411, 4008, 4009, 3582, 3300, 3101, 4509, # 6902
+ 1561, 2664, 1452, 4010, 1375, 7855, 7856, 47, 2959, 316, 7857, 1406, 1591, 2923, 3156, 7858, # 6918
+ 1025, 2141, 3102, 3157, 354, 2731, 884, 2224, 4225, 2407, 508, 3706, 726, 3583, 996, 2428, # 6934
+ 3584, 729, 7859, 392, 2191, 1453, 4011, 4510, 3707, 7860, 7861, 2458, 3585, 2608, 1675, 2800, # 6950
+ 919, 2347, 2960, 2348, 1270, 4511, 4012, 73, 7862, 7863, 647, 7864, 3228, 2843, 2255, 1550, # 6966
+ 1346, 3006, 7865, 1332, 883, 3479, 7866, 7867, 7868, 7869, 3301, 2765, 7870, 1212, 831, 1347, # 6982
+ 4226, 4512, 2326, 3830, 1863, 3053, 720, 3831, 4513, 4514, 3832, 7871, 4227, 7872, 7873, 4515, # 6998
+ 7874, 7875, 1798, 4516, 3708, 2609, 4517, 3586, 1645, 2371, 7876, 7877, 2924, 669, 2208, 2665, # 7014
+ 2429, 7878, 2879, 7879, 7880, 1028, 3229, 7881, 4228, 2408, 7882, 2256, 1353, 7883, 7884, 4518, # 7030
+ 3158, 518, 7885, 4013, 7886, 4229, 1960, 7887, 2142, 4230, 7888, 7889, 3007, 2349, 2350, 3833, # 7046
+ 516, 1833, 1454, 4014, 2699, 4231, 4519, 2225, 2610, 1971, 1129, 3587, 7890, 2766, 7891, 2961, # 7062
+ 1422, 577, 1470, 3008, 1524, 3373, 7892, 7893, 432, 4232, 3054, 3480, 7894, 2586, 1455, 2508, # 7078
+ 2226, 1972, 1175, 7895, 1020, 2732, 4015, 3481, 4520, 7896, 2733, 7897, 1743, 1361, 3055, 3482, # 7094
+ 2639, 4016, 4233, 4521, 2290, 895, 924, 4234, 2170, 331, 2243, 3056, 166, 1627, 3057, 1098, # 7110
+ 7898, 1232, 2880, 2227, 3374, 4522, 657, 403, 1196, 2372, 542, 3709, 3375, 1600, 4235, 3483, # 7126
+ 7899, 4523, 2767, 3230, 576, 530, 1362, 7900, 4524, 2533, 2666, 3710, 4017, 7901, 842, 3834, # 7142
+ 7902, 2801, 2031, 1014, 4018, 213, 2700, 3376, 665, 621, 4236, 7903, 3711, 2925, 2430, 7904, # 7158
+ 2431, 3302, 3588, 3377, 7905, 4237, 2534, 4238, 4525, 3589, 1682, 4239, 3484, 1380, 7906, 724, # 7174
+ 2277, 600, 1670, 7907, 1337, 1233, 4526, 3103, 2244, 7908, 1621, 4527, 7909, 651, 4240, 7910, # 7190
+ 1612, 4241, 2611, 7911, 2844, 7912, 2734, 2307, 3058, 7913, 716, 2459, 3059, 174, 1255, 2701, # 7206
+ 4019, 3590, 548, 1320, 1398, 728, 4020, 1574, 7914, 1890, 1197, 3060, 4021, 7915, 3061, 3062, # 7222
+ 3712, 3591, 3713, 747, 7916, 635, 4242, 4528, 7917, 7918, 7919, 4243, 7920, 7921, 4529, 7922, # 7238
+ 3378, 4530, 2432, 451, 7923, 3714, 2535, 2072, 4244, 2735, 4245, 4022, 7924, 1764, 4531, 7925, # 7254
+ 4246, 350, 7926, 2278, 2390, 2486, 7927, 4247, 4023, 2245, 1434, 4024, 488, 4532, 458, 4248, # 7270
+ 4025, 3715, 771, 1330, 2391, 3835, 2568, 3159, 2159, 2409, 1553, 2667, 3160, 4249, 7928, 2487, # 7286
+ 2881, 2612, 1720, 2702, 4250, 3379, 4533, 7929, 2536, 4251, 7930, 3231, 4252, 2768, 7931, 2015, # 7302
+ 2736, 7932, 1155, 1017, 3716, 3836, 7933, 3303, 2308, 201, 1864, 4253, 1430, 7934, 4026, 7935, # 7318
+ 7936, 7937, 7938, 7939, 4254, 1604, 7940, 414, 1865, 371, 2587, 4534, 4535, 3485, 2016, 3104, # 7334
+ 4536, 1708, 960, 4255, 887, 389, 2171, 1536, 1663, 1721, 7941, 2228, 4027, 2351, 2926, 1580, # 7350
+ 7942, 7943, 7944, 1744, 7945, 2537, 4537, 4538, 7946, 4539, 7947, 2073, 7948, 7949, 3592, 3380, # 7366
+ 2882, 4256, 7950, 4257, 2640, 3381, 2802, 673, 2703, 2460, 709, 3486, 4028, 3593, 4258, 7951, # 7382
+ 1148, 502, 634, 7952, 7953, 1204, 4540, 3594, 1575, 4541, 2613, 3717, 7954, 3718, 3105, 948, # 7398
+ 3232, 121, 1745, 3837, 1110, 7955, 4259, 3063, 2509, 3009, 4029, 3719, 1151, 1771, 3838, 1488, # 7414
+ 4030, 1986, 7956, 2433, 3487, 7957, 7958, 2093, 7959, 4260, 3839, 1213, 1407, 2803, 531, 2737, # 7430
+ 2538, 3233, 1011, 1537, 7960, 2769, 4261, 3106, 1061, 7961, 3720, 3721, 1866, 2883, 7962, 2017, # 7446
+ 120, 4262, 4263, 2062, 3595, 3234, 2309, 3840, 2668, 3382, 1954, 4542, 7963, 7964, 3488, 1047, # 7462
+ 2704, 1266, 7965, 1368, 4543, 2845, 649, 3383, 3841, 2539, 2738, 1102, 2846, 2669, 7966, 7967, # 7478
+ 1999, 7968, 1111, 3596, 2962, 7969, 2488, 3842, 3597, 2804, 1854, 3384, 3722, 7970, 7971, 3385, # 7494
+ 2410, 2884, 3304, 3235, 3598, 7972, 2569, 7973, 3599, 2805, 4031, 1460, 856, 7974, 3600, 7975, # 7510
+ 2885, 2963, 7976, 2886, 3843, 7977, 4264, 632, 2510, 875, 3844, 1697, 3845, 2291, 7978, 7979, # 7526
+ 4544, 3010, 1239, 580, 4545, 4265, 7980, 914, 936, 2074, 1190, 4032, 1039, 2123, 7981, 7982, # 7542
+ 7983, 3386, 1473, 7984, 1354, 4266, 3846, 7985, 2172, 3064, 4033, 915, 3305, 4267, 4268, 3306, # 7558
+ 1605, 1834, 7986, 2739, 398, 3601, 4269, 3847, 4034, 328, 1912, 2847, 4035, 3848, 1331, 4270, # 7574
+ 3011, 937, 4271, 7987, 3602, 4036, 4037, 3387, 2160, 4546, 3388, 524, 742, 538, 3065, 1012, # 7590
+ 7988, 7989, 3849, 2461, 7990, 658, 1103, 225, 3850, 7991, 7992, 4547, 7993, 4548, 7994, 3236, # 7606
+ 1243, 7995, 4038, 963, 2246, 4549, 7996, 2705, 3603, 3161, 7997, 7998, 2588, 2327, 7999, 4550, # 7622
+ 8000, 8001, 8002, 3489, 3307, 957, 3389, 2540, 2032, 1930, 2927, 2462, 870, 2018, 3604, 1746, # 7638
+ 2770, 2771, 2434, 2463, 8003, 3851, 8004, 3723, 3107, 3724, 3490, 3390, 3725, 8005, 1179, 3066, # 7654
+ 8006, 3162, 2373, 4272, 3726, 2541, 3163, 3108, 2740, 4039, 8007, 3391, 1556, 2542, 2292, 977, # 7670
+ 2887, 2033, 4040, 1205, 3392, 8008, 1765, 3393, 3164, 2124, 1271, 1689, 714, 4551, 3491, 8009, # 7686
+ 2328, 3852, 533, 4273, 3605, 2181, 617, 8010, 2464, 3308, 3492, 2310, 8011, 8012, 3165, 8013, # 7702
+ 8014, 3853, 1987, 618, 427, 2641, 3493, 3394, 8015, 8016, 1244, 1690, 8017, 2806, 4274, 4552, # 7718
+ 8018, 3494, 8019, 8020, 2279, 1576, 473, 3606, 4275, 3395, 972, 8021, 3607, 8022, 3067, 8023, # 7734
+ 8024, 4553, 4554, 8025, 3727, 4041, 4042, 8026, 153, 4555, 356, 8027, 1891, 2888, 4276, 2143, # 7750
+ 408, 803, 2352, 8028, 3854, 8029, 4277, 1646, 2570, 2511, 4556, 4557, 3855, 8030, 3856, 4278, # 7766
+ 8031, 2411, 3396, 752, 8032, 8033, 1961, 2964, 8034, 746, 3012, 2465, 8035, 4279, 3728, 698, # 7782
+ 4558, 1892, 4280, 3608, 2543, 4559, 3609, 3857, 8036, 3166, 3397, 8037, 1823, 1302, 4043, 2706, # 7798
+ 3858, 1973, 4281, 8038, 4282, 3167, 823, 1303, 1288, 1236, 2848, 3495, 4044, 3398, 774, 3859, # 7814
+ 8039, 1581, 4560, 1304, 2849, 3860, 4561, 8040, 2435, 2161, 1083, 3237, 4283, 4045, 4284, 344, # 7830
+ 1173, 288, 2311, 454, 1683, 8041, 8042, 1461, 4562, 4046, 2589, 8043, 8044, 4563, 985, 894, # 7846
+ 8045, 3399, 3168, 8046, 1913, 2928, 3729, 1988, 8047, 2110, 1974, 8048, 4047, 8049, 2571, 1194, # 7862
+ 425, 8050, 4564, 3169, 1245, 3730, 4285, 8051, 8052, 2850, 8053, 636, 4565, 1855, 3861, 760, # 7878
+ 1799, 8054, 4286, 2209, 1508, 4566, 4048, 1893, 1684, 2293, 8055, 8056, 8057, 4287, 4288, 2210, # 7894
+ 479, 8058, 8059, 832, 8060, 4049, 2489, 8061, 2965, 2490, 3731, 990, 3109, 627, 1814, 2642, # 7910
+ 4289, 1582, 4290, 2125, 2111, 3496, 4567, 8062, 799, 4291, 3170, 8063, 4568, 2112, 1737, 3013, # 7926
+ 1018, 543, 754, 4292, 3309, 1676, 4569, 4570, 4050, 8064, 1489, 8065, 3497, 8066, 2614, 2889, # 7942
+ 4051, 8067, 8068, 2966, 8069, 8070, 8071, 8072, 3171, 4571, 4572, 2182, 1722, 8073, 3238, 3239, # 7958
+ 1842, 3610, 1715, 481, 365, 1975, 1856, 8074, 8075, 1962, 2491, 4573, 8076, 2126, 3611, 3240, # 7974
+ 433, 1894, 2063, 2075, 8077, 602, 2741, 8078, 8079, 8080, 8081, 8082, 3014, 1628, 3400, 8083, # 7990
+ 3172, 4574, 4052, 2890, 4575, 2512, 8084, 2544, 2772, 8085, 8086, 8087, 3310, 4576, 2891, 8088, # 8006
+ 4577, 8089, 2851, 4578, 4579, 1221, 2967, 4053, 2513, 8090, 8091, 8092, 1867, 1989, 8093, 8094, # 8022
+ 8095, 1895, 8096, 8097, 4580, 1896, 4054, 318, 8098, 2094, 4055, 4293, 8099, 8100, 485, 8101, # 8038
+ 938, 3862, 553, 2670, 116, 8102, 3863, 3612, 8103, 3498, 2671, 2773, 3401, 3311, 2807, 8104, # 8054
+ 3613, 2929, 4056, 1747, 2930, 2968, 8105, 8106, 207, 8107, 8108, 2672, 4581, 2514, 8109, 3015, # 8070
+ 890, 3614, 3864, 8110, 1877, 3732, 3402, 8111, 2183, 2353, 3403, 1652, 8112, 8113, 8114, 941, # 8086
+ 2294, 208, 3499, 4057, 2019, 330, 4294, 3865, 2892, 2492, 3733, 4295, 8115, 8116, 8117, 8118, # 8102
)
-
+# fmt: on
diff --git a/src/pip/_vendor/chardet/euctwprober.py b/src/pip/_vendor/chardet/euctwprober.py
index 35669cc4d..ca10a23ca 100644
--- a/src/pip/_vendor/chardet/euctwprober.py
+++ b/src/pip/_vendor/chardet/euctwprober.py
@@ -25,14 +25,15 @@
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
-from .mbcharsetprober import MultiByteCharSetProber
-from .codingstatemachine import CodingStateMachine
from .chardistribution import EUCTWDistributionAnalysis
+from .codingstatemachine import CodingStateMachine
+from .mbcharsetprober import MultiByteCharSetProber
from .mbcssm import EUCTW_SM_MODEL
+
class EUCTWProber(MultiByteCharSetProber):
def __init__(self):
- super(EUCTWProber, self).__init__()
+ super().__init__()
self.coding_sm = CodingStateMachine(EUCTW_SM_MODEL)
self.distribution_analyzer = EUCTWDistributionAnalysis()
self.reset()
diff --git a/src/pip/_vendor/chardet/gb2312freq.py b/src/pip/_vendor/chardet/gb2312freq.py
index 697837bd9..b32bfc742 100644
--- a/src/pip/_vendor/chardet/gb2312freq.py
+++ b/src/pip/_vendor/chardet/gb2312freq.py
@@ -43,6 +43,7 @@ GB2312_TYPICAL_DISTRIBUTION_RATIO = 0.9
GB2312_TABLE_SIZE = 3760
+# fmt: off
GB2312_CHAR_TO_FREQ_ORDER = (
1671, 749,1443,2364,3924,3807,2330,3921,1704,3463,2691,1511,1515, 572,3191,2205,
2361, 224,2558, 479,1711, 963,3162, 440,4060,1905,2966,2947,3580,2647,3961,3842,
@@ -280,4 +281,4 @@ GB2312_CHAR_TO_FREQ_ORDER = (
381,1638,4592,1020, 516,3214, 458, 947,4575,1432, 211,1514,2926,1865,2142, 189,
852,1221,1400,1486, 882,2299,4036, 351, 28,1122, 700,6479,6480,6481,6482,6483, #last 512
)
-
+# fmt: on
diff --git a/src/pip/_vendor/chardet/gb2312prober.py b/src/pip/_vendor/chardet/gb2312prober.py
index 8446d2dd9..251c04295 100644
--- a/src/pip/_vendor/chardet/gb2312prober.py
+++ b/src/pip/_vendor/chardet/gb2312prober.py
@@ -25,14 +25,15 @@
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
-from .mbcharsetprober import MultiByteCharSetProber
-from .codingstatemachine import CodingStateMachine
from .chardistribution import GB2312DistributionAnalysis
+from .codingstatemachine import CodingStateMachine
+from .mbcharsetprober import MultiByteCharSetProber
from .mbcssm import GB2312_SM_MODEL
+
class GB2312Prober(MultiByteCharSetProber):
def __init__(self):
- super(GB2312Prober, self).__init__()
+ super().__init__()
self.coding_sm = CodingStateMachine(GB2312_SM_MODEL)
self.distribution_analyzer = GB2312DistributionAnalysis()
self.reset()
diff --git a/src/pip/_vendor/chardet/hebrewprober.py b/src/pip/_vendor/chardet/hebrewprober.py
index b0e1bf492..3ca634bf3 100644
--- a/src/pip/_vendor/chardet/hebrewprober.py
+++ b/src/pip/_vendor/chardet/hebrewprober.py
@@ -125,18 +125,19 @@ from .enums import ProbingState
# model probers scores. The answer is returned in the form of the name of the
# charset identified, either "windows-1255" or "ISO-8859-8".
+
class HebrewProber(CharSetProber):
# windows-1255 / ISO-8859-8 code points of interest
- FINAL_KAF = 0xea
- NORMAL_KAF = 0xeb
- FINAL_MEM = 0xed
- NORMAL_MEM = 0xee
- FINAL_NUN = 0xef
- NORMAL_NUN = 0xf0
- FINAL_PE = 0xf3
- NORMAL_PE = 0xf4
- FINAL_TSADI = 0xf5
- NORMAL_TSADI = 0xf6
+ FINAL_KAF = 0xEA
+ NORMAL_KAF = 0xEB
+ FINAL_MEM = 0xED
+ NORMAL_MEM = 0xEE
+ FINAL_NUN = 0xEF
+ NORMAL_NUN = 0xF0
+ FINAL_PE = 0xF3
+ NORMAL_PE = 0xF4
+ FINAL_TSADI = 0xF5
+ NORMAL_TSADI = 0xF6
# Minimum Visual vs Logical final letter score difference.
# If the difference is below this, don't rely solely on the final letter score
@@ -152,7 +153,7 @@ class HebrewProber(CharSetProber):
LOGICAL_HEBREW_NAME = "windows-1255"
def __init__(self):
- super(HebrewProber, self).__init__()
+ super().__init__()
self._final_char_logical_score = None
self._final_char_visual_score = None
self._prev = None
@@ -167,17 +168,22 @@ class HebrewProber(CharSetProber):
# The two last characters seen in the previous buffer,
# mPrev and mBeforePrev are initialized to space in order to simulate
# a word delimiter at the beginning of the data
- self._prev = ' '
- self._before_prev = ' '
+ self._prev = " "
+ self._before_prev = " "
# These probers are owned by the group prober.
- def set_model_probers(self, logicalProber, visualProber):
- self._logical_prober = logicalProber
- self._visual_prober = visualProber
+ def set_model_probers(self, logical_prober, visual_prober):
+ self._logical_prober = logical_prober
+ self._visual_prober = visual_prober
def is_final(self, c):
- return c in [self.FINAL_KAF, self.FINAL_MEM, self.FINAL_NUN,
- self.FINAL_PE, self.FINAL_TSADI]
+ return c in [
+ self.FINAL_KAF,
+ self.FINAL_MEM,
+ self.FINAL_NUN,
+ self.FINAL_PE,
+ self.FINAL_TSADI,
+ ]
def is_non_final(self, c):
# The normal Tsadi is not a good Non-Final letter due to words like
@@ -190,8 +196,7 @@ class HebrewProber(CharSetProber):
# for example legally end with a Non-Final Pe or Kaf. However, the
# benefit of these letters as Non-Final letters outweighs the damage
# since these words are quite rare.
- return c in [self.NORMAL_KAF, self.NORMAL_MEM,
- self.NORMAL_NUN, self.NORMAL_PE]
+ return c in [self.NORMAL_KAF, self.NORMAL_MEM, self.NORMAL_NUN, self.NORMAL_PE]
def feed(self, byte_str):
# Final letter analysis for logical-visual decision.
@@ -227,9 +232,9 @@ class HebrewProber(CharSetProber):
byte_str = self.filter_high_byte_only(byte_str)
for cur in byte_str:
- if cur == ' ':
+ if cur == " ":
# We stand on a space - a word just ended
- if self._before_prev != ' ':
+ if self._before_prev != " ":
# next-to-last char was not a space so self._prev is not a
# 1 letter word
if self.is_final(self._prev):
@@ -241,8 +246,11 @@ class HebrewProber(CharSetProber):
self._final_char_visual_score += 1
else:
# Not standing on a space
- if ((self._before_prev == ' ') and
- (self.is_final(self._prev)) and (cur != ' ')):
+ if (
+ (self._before_prev == " ")
+ and (self.is_final(self._prev))
+ and (cur != " ")
+ ):
# case (3) [-2:space][-1:final letter][cur:not space]
self._final_char_visual_score += 1
self._before_prev = self._prev
@@ -263,8 +271,9 @@ class HebrewProber(CharSetProber):
return self.VISUAL_HEBREW_NAME
# It's not dominant enough, try to rely on the model scores instead.
- modelsub = (self._logical_prober.get_confidence()
- - self._visual_prober.get_confidence())
+ modelsub = (
+ self._logical_prober.get_confidence() - self._visual_prober.get_confidence()
+ )
if modelsub > self.MIN_MODEL_DISTANCE:
return self.LOGICAL_HEBREW_NAME
if modelsub < -self.MIN_MODEL_DISTANCE:
@@ -281,12 +290,13 @@ class HebrewProber(CharSetProber):
@property
def language(self):
- return 'Hebrew'
+ return "Hebrew"
@property
def state(self):
# Remain active as long as any of the model probers are active.
- if (self._logical_prober.state == ProbingState.NOT_ME) and \
- (self._visual_prober.state == ProbingState.NOT_ME):
+ if (self._logical_prober.state == ProbingState.NOT_ME) and (
+ self._visual_prober.state == ProbingState.NOT_ME
+ ):
return ProbingState.NOT_ME
return ProbingState.DETECTING
diff --git a/src/pip/_vendor/chardet/jisfreq.py b/src/pip/_vendor/chardet/jisfreq.py
index 83fc082b5..3293576e0 100644
--- a/src/pip/_vendor/chardet/jisfreq.py
+++ b/src/pip/_vendor/chardet/jisfreq.py
@@ -46,6 +46,7 @@ JIS_TYPICAL_DISTRIBUTION_RATIO = 3.0
# Char to FreqOrder table ,
JIS_TABLE_SIZE = 4368
+# fmt: off
JIS_CHAR_TO_FREQ_ORDER = (
40, 1, 6, 182, 152, 180, 295,2127, 285, 381,3295,4304,3068,4606,3165,3510, # 16
3511,1822,2785,4607,1193,2226,5070,4608, 171,2996,1247, 18, 179,5071, 856,1661, # 32
@@ -321,5 +322,4 @@ JIS_CHAR_TO_FREQ_ORDER = (
1444,1698,2385,2251,3729,1365,2281,2235,1717,6188, 864,3841,2515, 444, 527,2767, # 4352
2922,3625, 544, 461,6189, 566, 209,2437,3398,2098,1065,2068,3331,3626,3257,2137, # 4368 #last 512
)
-
-
+# fmt: on
diff --git a/src/pip/_vendor/chardet/johabfreq.py b/src/pip/_vendor/chardet/johabfreq.py
new file mode 100644
index 000000000..c12969990
--- /dev/null
+++ b/src/pip/_vendor/chardet/johabfreq.py
@@ -0,0 +1,2382 @@
+######################## BEGIN LICENSE BLOCK ########################
+# The Original Code is Mozilla Communicator client code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Mark Pilgrim - port to Python
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+# 02110-1301 USA
+######################### END LICENSE BLOCK #########################
+
+# The frequency data itself is the same as euc-kr.
+# This is just a mapping table to euc-kr.
+
+JOHAB_TO_EUCKR_ORDER_TABLE = {
+ 0x8861: 0,
+ 0x8862: 1,
+ 0x8865: 2,
+ 0x8868: 3,
+ 0x8869: 4,
+ 0x886A: 5,
+ 0x886B: 6,
+ 0x8871: 7,
+ 0x8873: 8,
+ 0x8874: 9,
+ 0x8875: 10,
+ 0x8876: 11,
+ 0x8877: 12,
+ 0x8878: 13,
+ 0x8879: 14,
+ 0x887B: 15,
+ 0x887C: 16,
+ 0x887D: 17,
+ 0x8881: 18,
+ 0x8882: 19,
+ 0x8885: 20,
+ 0x8889: 21,
+ 0x8891: 22,
+ 0x8893: 23,
+ 0x8895: 24,
+ 0x8896: 25,
+ 0x8897: 26,
+ 0x88A1: 27,
+ 0x88A2: 28,
+ 0x88A5: 29,
+ 0x88A9: 30,
+ 0x88B5: 31,
+ 0x88B7: 32,
+ 0x88C1: 33,
+ 0x88C5: 34,
+ 0x88C9: 35,
+ 0x88E1: 36,
+ 0x88E2: 37,
+ 0x88E5: 38,
+ 0x88E8: 39,
+ 0x88E9: 40,
+ 0x88EB: 41,
+ 0x88F1: 42,
+ 0x88F3: 43,
+ 0x88F5: 44,
+ 0x88F6: 45,
+ 0x88F7: 46,
+ 0x88F8: 47,
+ 0x88FB: 48,
+ 0x88FC: 49,
+ 0x88FD: 50,
+ 0x8941: 51,
+ 0x8945: 52,
+ 0x8949: 53,
+ 0x8951: 54,
+ 0x8953: 55,
+ 0x8955: 56,
+ 0x8956: 57,
+ 0x8957: 58,
+ 0x8961: 59,
+ 0x8962: 60,
+ 0x8963: 61,
+ 0x8965: 62,
+ 0x8968: 63,
+ 0x8969: 64,
+ 0x8971: 65,
+ 0x8973: 66,
+ 0x8975: 67,
+ 0x8976: 68,
+ 0x8977: 69,
+ 0x897B: 70,
+ 0x8981: 71,
+ 0x8985: 72,
+ 0x8989: 73,
+ 0x8993: 74,
+ 0x8995: 75,
+ 0x89A1: 76,
+ 0x89A2: 77,
+ 0x89A5: 78,
+ 0x89A8: 79,
+ 0x89A9: 80,
+ 0x89AB: 81,
+ 0x89AD: 82,
+ 0x89B0: 83,
+ 0x89B1: 84,
+ 0x89B3: 85,
+ 0x89B5: 86,
+ 0x89B7: 87,
+ 0x89B8: 88,
+ 0x89C1: 89,
+ 0x89C2: 90,
+ 0x89C5: 91,
+ 0x89C9: 92,
+ 0x89CB: 93,
+ 0x89D1: 94,
+ 0x89D3: 95,
+ 0x89D5: 96,
+ 0x89D7: 97,
+ 0x89E1: 98,
+ 0x89E5: 99,
+ 0x89E9: 100,
+ 0x89F3: 101,
+ 0x89F6: 102,
+ 0x89F7: 103,
+ 0x8A41: 104,
+ 0x8A42: 105,
+ 0x8A45: 106,
+ 0x8A49: 107,
+ 0x8A51: 108,
+ 0x8A53: 109,
+ 0x8A55: 110,
+ 0x8A57: 111,
+ 0x8A61: 112,
+ 0x8A65: 113,
+ 0x8A69: 114,
+ 0x8A73: 115,
+ 0x8A75: 116,
+ 0x8A81: 117,
+ 0x8A82: 118,
+ 0x8A85: 119,
+ 0x8A88: 120,
+ 0x8A89: 121,
+ 0x8A8A: 122,
+ 0x8A8B: 123,
+ 0x8A90: 124,
+ 0x8A91: 125,
+ 0x8A93: 126,
+ 0x8A95: 127,
+ 0x8A97: 128,
+ 0x8A98: 129,
+ 0x8AA1: 130,
+ 0x8AA2: 131,
+ 0x8AA5: 132,
+ 0x8AA9: 133,
+ 0x8AB6: 134,
+ 0x8AB7: 135,
+ 0x8AC1: 136,
+ 0x8AD5: 137,
+ 0x8AE1: 138,
+ 0x8AE2: 139,
+ 0x8AE5: 140,
+ 0x8AE9: 141,
+ 0x8AF1: 142,
+ 0x8AF3: 143,
+ 0x8AF5: 144,
+ 0x8B41: 145,
+ 0x8B45: 146,
+ 0x8B49: 147,
+ 0x8B61: 148,
+ 0x8B62: 149,
+ 0x8B65: 150,
+ 0x8B68: 151,
+ 0x8B69: 152,
+ 0x8B6A: 153,
+ 0x8B71: 154,
+ 0x8B73: 155,
+ 0x8B75: 156,
+ 0x8B77: 157,
+ 0x8B81: 158,
+ 0x8BA1: 159,
+ 0x8BA2: 160,
+ 0x8BA5: 161,
+ 0x8BA8: 162,
+ 0x8BA9: 163,
+ 0x8BAB: 164,
+ 0x8BB1: 165,
+ 0x8BB3: 166,
+ 0x8BB5: 167,
+ 0x8BB7: 168,
+ 0x8BB8: 169,
+ 0x8BBC: 170,
+ 0x8C61: 171,
+ 0x8C62: 172,
+ 0x8C63: 173,
+ 0x8C65: 174,
+ 0x8C69: 175,
+ 0x8C6B: 176,
+ 0x8C71: 177,
+ 0x8C73: 178,
+ 0x8C75: 179,
+ 0x8C76: 180,
+ 0x8C77: 181,
+ 0x8C7B: 182,
+ 0x8C81: 183,
+ 0x8C82: 184,
+ 0x8C85: 185,
+ 0x8C89: 186,
+ 0x8C91: 187,
+ 0x8C93: 188,
+ 0x8C95: 189,
+ 0x8C96: 190,
+ 0x8C97: 191,
+ 0x8CA1: 192,
+ 0x8CA2: 193,
+ 0x8CA9: 194,
+ 0x8CE1: 195,
+ 0x8CE2: 196,
+ 0x8CE3: 197,
+ 0x8CE5: 198,
+ 0x8CE9: 199,
+ 0x8CF1: 200,
+ 0x8CF3: 201,
+ 0x8CF5: 202,
+ 0x8CF6: 203,
+ 0x8CF7: 204,
+ 0x8D41: 205,
+ 0x8D42: 206,
+ 0x8D45: 207,
+ 0x8D51: 208,
+ 0x8D55: 209,
+ 0x8D57: 210,
+ 0x8D61: 211,
+ 0x8D65: 212,
+ 0x8D69: 213,
+ 0x8D75: 214,
+ 0x8D76: 215,
+ 0x8D7B: 216,
+ 0x8D81: 217,
+ 0x8DA1: 218,
+ 0x8DA2: 219,
+ 0x8DA5: 220,
+ 0x8DA7: 221,
+ 0x8DA9: 222,
+ 0x8DB1: 223,
+ 0x8DB3: 224,
+ 0x8DB5: 225,
+ 0x8DB7: 226,
+ 0x8DB8: 227,
+ 0x8DB9: 228,
+ 0x8DC1: 229,
+ 0x8DC2: 230,
+ 0x8DC9: 231,
+ 0x8DD6: 232,
+ 0x8DD7: 233,
+ 0x8DE1: 234,
+ 0x8DE2: 235,
+ 0x8DF7: 236,
+ 0x8E41: 237,
+ 0x8E45: 238,
+ 0x8E49: 239,
+ 0x8E51: 240,
+ 0x8E53: 241,
+ 0x8E57: 242,
+ 0x8E61: 243,
+ 0x8E81: 244,
+ 0x8E82: 245,
+ 0x8E85: 246,
+ 0x8E89: 247,
+ 0x8E90: 248,
+ 0x8E91: 249,
+ 0x8E93: 250,
+ 0x8E95: 251,
+ 0x8E97: 252,
+ 0x8E98: 253,
+ 0x8EA1: 254,
+ 0x8EA9: 255,
+ 0x8EB6: 256,
+ 0x8EB7: 257,
+ 0x8EC1: 258,
+ 0x8EC2: 259,
+ 0x8EC5: 260,
+ 0x8EC9: 261,
+ 0x8ED1: 262,
+ 0x8ED3: 263,
+ 0x8ED6: 264,
+ 0x8EE1: 265,
+ 0x8EE5: 266,
+ 0x8EE9: 267,
+ 0x8EF1: 268,
+ 0x8EF3: 269,
+ 0x8F41: 270,
+ 0x8F61: 271,
+ 0x8F62: 272,
+ 0x8F65: 273,
+ 0x8F67: 274,
+ 0x8F69: 275,
+ 0x8F6B: 276,
+ 0x8F70: 277,
+ 0x8F71: 278,
+ 0x8F73: 279,
+ 0x8F75: 280,
+ 0x8F77: 281,
+ 0x8F7B: 282,
+ 0x8FA1: 283,
+ 0x8FA2: 284,
+ 0x8FA5: 285,
+ 0x8FA9: 286,
+ 0x8FB1: 287,
+ 0x8FB3: 288,
+ 0x8FB5: 289,
+ 0x8FB7: 290,
+ 0x9061: 291,
+ 0x9062: 292,
+ 0x9063: 293,
+ 0x9065: 294,
+ 0x9068: 295,
+ 0x9069: 296,
+ 0x906A: 297,
+ 0x906B: 298,
+ 0x9071: 299,
+ 0x9073: 300,
+ 0x9075: 301,
+ 0x9076: 302,
+ 0x9077: 303,
+ 0x9078: 304,
+ 0x9079: 305,
+ 0x907B: 306,
+ 0x907D: 307,
+ 0x9081: 308,
+ 0x9082: 309,
+ 0x9085: 310,
+ 0x9089: 311,
+ 0x9091: 312,
+ 0x9093: 313,
+ 0x9095: 314,
+ 0x9096: 315,
+ 0x9097: 316,
+ 0x90A1: 317,
+ 0x90A2: 318,
+ 0x90A5: 319,
+ 0x90A9: 320,
+ 0x90B1: 321,
+ 0x90B7: 322,
+ 0x90E1: 323,
+ 0x90E2: 324,
+ 0x90E4: 325,
+ 0x90E5: 326,
+ 0x90E9: 327,
+ 0x90EB: 328,
+ 0x90EC: 329,
+ 0x90F1: 330,
+ 0x90F3: 331,
+ 0x90F5: 332,
+ 0x90F6: 333,
+ 0x90F7: 334,
+ 0x90FD: 335,
+ 0x9141: 336,
+ 0x9142: 337,
+ 0x9145: 338,
+ 0x9149: 339,
+ 0x9151: 340,
+ 0x9153: 341,
+ 0x9155: 342,
+ 0x9156: 343,
+ 0x9157: 344,
+ 0x9161: 345,
+ 0x9162: 346,
+ 0x9165: 347,
+ 0x9169: 348,
+ 0x9171: 349,
+ 0x9173: 350,
+ 0x9176: 351,
+ 0x9177: 352,
+ 0x917A: 353,
+ 0x9181: 354,
+ 0x9185: 355,
+ 0x91A1: 356,
+ 0x91A2: 357,
+ 0x91A5: 358,
+ 0x91A9: 359,
+ 0x91AB: 360,
+ 0x91B1: 361,
+ 0x91B3: 362,
+ 0x91B5: 363,
+ 0x91B7: 364,
+ 0x91BC: 365,
+ 0x91BD: 366,
+ 0x91C1: 367,
+ 0x91C5: 368,
+ 0x91C9: 369,
+ 0x91D6: 370,
+ 0x9241: 371,
+ 0x9245: 372,
+ 0x9249: 373,
+ 0x9251: 374,
+ 0x9253: 375,
+ 0x9255: 376,
+ 0x9261: 377,
+ 0x9262: 378,
+ 0x9265: 379,
+ 0x9269: 380,
+ 0x9273: 381,
+ 0x9275: 382,
+ 0x9277: 383,
+ 0x9281: 384,
+ 0x9282: 385,
+ 0x9285: 386,
+ 0x9288: 387,
+ 0x9289: 388,
+ 0x9291: 389,
+ 0x9293: 390,
+ 0x9295: 391,
+ 0x9297: 392,
+ 0x92A1: 393,
+ 0x92B6: 394,
+ 0x92C1: 395,
+ 0x92E1: 396,
+ 0x92E5: 397,
+ 0x92E9: 398,
+ 0x92F1: 399,
+ 0x92F3: 400,
+ 0x9341: 401,
+ 0x9342: 402,
+ 0x9349: 403,
+ 0x9351: 404,
+ 0x9353: 405,
+ 0x9357: 406,
+ 0x9361: 407,
+ 0x9362: 408,
+ 0x9365: 409,
+ 0x9369: 410,
+ 0x936A: 411,
+ 0x936B: 412,
+ 0x9371: 413,
+ 0x9373: 414,
+ 0x9375: 415,
+ 0x9377: 416,
+ 0x9378: 417,
+ 0x937C: 418,
+ 0x9381: 419,
+ 0x9385: 420,
+ 0x9389: 421,
+ 0x93A1: 422,
+ 0x93A2: 423,
+ 0x93A5: 424,
+ 0x93A9: 425,
+ 0x93AB: 426,
+ 0x93B1: 427,
+ 0x93B3: 428,
+ 0x93B5: 429,
+ 0x93B7: 430,
+ 0x93BC: 431,
+ 0x9461: 432,
+ 0x9462: 433,
+ 0x9463: 434,
+ 0x9465: 435,
+ 0x9468: 436,
+ 0x9469: 437,
+ 0x946A: 438,
+ 0x946B: 439,
+ 0x946C: 440,
+ 0x9470: 441,
+ 0x9471: 442,
+ 0x9473: 443,
+ 0x9475: 444,
+ 0x9476: 445,
+ 0x9477: 446,
+ 0x9478: 447,
+ 0x9479: 448,
+ 0x947D: 449,
+ 0x9481: 450,
+ 0x9482: 451,
+ 0x9485: 452,
+ 0x9489: 453,
+ 0x9491: 454,
+ 0x9493: 455,
+ 0x9495: 456,
+ 0x9496: 457,
+ 0x9497: 458,
+ 0x94A1: 459,
+ 0x94E1: 460,
+ 0x94E2: 461,
+ 0x94E3: 462,
+ 0x94E5: 463,
+ 0x94E8: 464,
+ 0x94E9: 465,
+ 0x94EB: 466,
+ 0x94EC: 467,
+ 0x94F1: 468,
+ 0x94F3: 469,
+ 0x94F5: 470,
+ 0x94F7: 471,
+ 0x94F9: 472,
+ 0x94FC: 473,
+ 0x9541: 474,
+ 0x9542: 475,
+ 0x9545: 476,
+ 0x9549: 477,
+ 0x9551: 478,
+ 0x9553: 479,
+ 0x9555: 480,
+ 0x9556: 481,
+ 0x9557: 482,
+ 0x9561: 483,
+ 0x9565: 484,
+ 0x9569: 485,
+ 0x9576: 486,
+ 0x9577: 487,
+ 0x9581: 488,
+ 0x9585: 489,
+ 0x95A1: 490,
+ 0x95A2: 491,
+ 0x95A5: 492,
+ 0x95A8: 493,
+ 0x95A9: 494,
+ 0x95AB: 495,
+ 0x95AD: 496,
+ 0x95B1: 497,
+ 0x95B3: 498,
+ 0x95B5: 499,
+ 0x95B7: 500,
+ 0x95B9: 501,
+ 0x95BB: 502,
+ 0x95C1: 503,
+ 0x95C5: 504,
+ 0x95C9: 505,
+ 0x95E1: 506,
+ 0x95F6: 507,
+ 0x9641: 508,
+ 0x9645: 509,
+ 0x9649: 510,
+ 0x9651: 511,
+ 0x9653: 512,
+ 0x9655: 513,
+ 0x9661: 514,
+ 0x9681: 515,
+ 0x9682: 516,
+ 0x9685: 517,
+ 0x9689: 518,
+ 0x9691: 519,
+ 0x9693: 520,
+ 0x9695: 521,
+ 0x9697: 522,
+ 0x96A1: 523,
+ 0x96B6: 524,
+ 0x96C1: 525,
+ 0x96D7: 526,
+ 0x96E1: 527,
+ 0x96E5: 528,
+ 0x96E9: 529,
+ 0x96F3: 530,
+ 0x96F5: 531,
+ 0x96F7: 532,
+ 0x9741: 533,
+ 0x9745: 534,
+ 0x9749: 535,
+ 0x9751: 536,
+ 0x9757: 537,
+ 0x9761: 538,
+ 0x9762: 539,
+ 0x9765: 540,
+ 0x9768: 541,
+ 0x9769: 542,
+ 0x976B: 543,
+ 0x9771: 544,
+ 0x9773: 545,
+ 0x9775: 546,
+ 0x9777: 547,
+ 0x9781: 548,
+ 0x97A1: 549,
+ 0x97A2: 550,
+ 0x97A5: 551,
+ 0x97A8: 552,
+ 0x97A9: 553,
+ 0x97B1: 554,
+ 0x97B3: 555,
+ 0x97B5: 556,
+ 0x97B6: 557,
+ 0x97B7: 558,
+ 0x97B8: 559,
+ 0x9861: 560,
+ 0x9862: 561,
+ 0x9865: 562,
+ 0x9869: 563,
+ 0x9871: 564,
+ 0x9873: 565,
+ 0x9875: 566,
+ 0x9876: 567,
+ 0x9877: 568,
+ 0x987D: 569,
+ 0x9881: 570,
+ 0x9882: 571,
+ 0x9885: 572,
+ 0x9889: 573,
+ 0x9891: 574,
+ 0x9893: 575,
+ 0x9895: 576,
+ 0x9896: 577,
+ 0x9897: 578,
+ 0x98E1: 579,
+ 0x98E2: 580,
+ 0x98E5: 581,
+ 0x98E9: 582,
+ 0x98EB: 583,
+ 0x98EC: 584,
+ 0x98F1: 585,
+ 0x98F3: 586,
+ 0x98F5: 587,
+ 0x98F6: 588,
+ 0x98F7: 589,
+ 0x98FD: 590,
+ 0x9941: 591,
+ 0x9942: 592,
+ 0x9945: 593,
+ 0x9949: 594,
+ 0x9951: 595,
+ 0x9953: 596,
+ 0x9955: 597,
+ 0x9956: 598,
+ 0x9957: 599,
+ 0x9961: 600,
+ 0x9976: 601,
+ 0x99A1: 602,
+ 0x99A2: 603,
+ 0x99A5: 604,
+ 0x99A9: 605,
+ 0x99B7: 606,
+ 0x99C1: 607,
+ 0x99C9: 608,
+ 0x99E1: 609,
+ 0x9A41: 610,
+ 0x9A45: 611,
+ 0x9A81: 612,
+ 0x9A82: 613,
+ 0x9A85: 614,
+ 0x9A89: 615,
+ 0x9A90: 616,
+ 0x9A91: 617,
+ 0x9A97: 618,
+ 0x9AC1: 619,
+ 0x9AE1: 620,
+ 0x9AE5: 621,
+ 0x9AE9: 622,
+ 0x9AF1: 623,
+ 0x9AF3: 624,
+ 0x9AF7: 625,
+ 0x9B61: 626,
+ 0x9B62: 627,
+ 0x9B65: 628,
+ 0x9B68: 629,
+ 0x9B69: 630,
+ 0x9B71: 631,
+ 0x9B73: 632,
+ 0x9B75: 633,
+ 0x9B81: 634,
+ 0x9B85: 635,
+ 0x9B89: 636,
+ 0x9B91: 637,
+ 0x9B93: 638,
+ 0x9BA1: 639,
+ 0x9BA5: 640,
+ 0x9BA9: 641,
+ 0x9BB1: 642,
+ 0x9BB3: 643,
+ 0x9BB5: 644,
+ 0x9BB7: 645,
+ 0x9C61: 646,
+ 0x9C62: 647,
+ 0x9C65: 648,
+ 0x9C69: 649,
+ 0x9C71: 650,
+ 0x9C73: 651,
+ 0x9C75: 652,
+ 0x9C76: 653,
+ 0x9C77: 654,
+ 0x9C78: 655,
+ 0x9C7C: 656,
+ 0x9C7D: 657,
+ 0x9C81: 658,
+ 0x9C82: 659,
+ 0x9C85: 660,
+ 0x9C89: 661,
+ 0x9C91: 662,
+ 0x9C93: 663,
+ 0x9C95: 664,
+ 0x9C96: 665,
+ 0x9C97: 666,
+ 0x9CA1: 667,
+ 0x9CA2: 668,
+ 0x9CA5: 669,
+ 0x9CB5: 670,
+ 0x9CB7: 671,
+ 0x9CE1: 672,
+ 0x9CE2: 673,
+ 0x9CE5: 674,
+ 0x9CE9: 675,
+ 0x9CF1: 676,
+ 0x9CF3: 677,
+ 0x9CF5: 678,
+ 0x9CF6: 679,
+ 0x9CF7: 680,
+ 0x9CFD: 681,
+ 0x9D41: 682,
+ 0x9D42: 683,
+ 0x9D45: 684,
+ 0x9D49: 685,
+ 0x9D51: 686,
+ 0x9D53: 687,
+ 0x9D55: 688,
+ 0x9D57: 689,
+ 0x9D61: 690,
+ 0x9D62: 691,
+ 0x9D65: 692,
+ 0x9D69: 693,
+ 0x9D71: 694,
+ 0x9D73: 695,
+ 0x9D75: 696,
+ 0x9D76: 697,
+ 0x9D77: 698,
+ 0x9D81: 699,
+ 0x9D85: 700,
+ 0x9D93: 701,
+ 0x9D95: 702,
+ 0x9DA1: 703,
+ 0x9DA2: 704,
+ 0x9DA5: 705,
+ 0x9DA9: 706,
+ 0x9DB1: 707,
+ 0x9DB3: 708,
+ 0x9DB5: 709,
+ 0x9DB7: 710,
+ 0x9DC1: 711,
+ 0x9DC5: 712,
+ 0x9DD7: 713,
+ 0x9DF6: 714,
+ 0x9E41: 715,
+ 0x9E45: 716,
+ 0x9E49: 717,
+ 0x9E51: 718,
+ 0x9E53: 719,
+ 0x9E55: 720,
+ 0x9E57: 721,
+ 0x9E61: 722,
+ 0x9E65: 723,
+ 0x9E69: 724,
+ 0x9E73: 725,
+ 0x9E75: 726,
+ 0x9E77: 727,
+ 0x9E81: 728,
+ 0x9E82: 729,
+ 0x9E85: 730,
+ 0x9E89: 731,
+ 0x9E91: 732,
+ 0x9E93: 733,
+ 0x9E95: 734,
+ 0x9E97: 735,
+ 0x9EA1: 736,
+ 0x9EB6: 737,
+ 0x9EC1: 738,
+ 0x9EE1: 739,
+ 0x9EE2: 740,
+ 0x9EE5: 741,
+ 0x9EE9: 742,
+ 0x9EF1: 743,
+ 0x9EF5: 744,
+ 0x9EF7: 745,
+ 0x9F41: 746,
+ 0x9F42: 747,
+ 0x9F45: 748,
+ 0x9F49: 749,
+ 0x9F51: 750,
+ 0x9F53: 751,
+ 0x9F55: 752,
+ 0x9F57: 753,
+ 0x9F61: 754,
+ 0x9F62: 755,
+ 0x9F65: 756,
+ 0x9F69: 757,
+ 0x9F71: 758,
+ 0x9F73: 759,
+ 0x9F75: 760,
+ 0x9F77: 761,
+ 0x9F78: 762,
+ 0x9F7B: 763,
+ 0x9F7C: 764,
+ 0x9FA1: 765,
+ 0x9FA2: 766,
+ 0x9FA5: 767,
+ 0x9FA9: 768,
+ 0x9FB1: 769,
+ 0x9FB3: 770,
+ 0x9FB5: 771,
+ 0x9FB7: 772,
+ 0xA061: 773,
+ 0xA062: 774,
+ 0xA065: 775,
+ 0xA067: 776,
+ 0xA068: 777,
+ 0xA069: 778,
+ 0xA06A: 779,
+ 0xA06B: 780,
+ 0xA071: 781,
+ 0xA073: 782,
+ 0xA075: 783,
+ 0xA077: 784,
+ 0xA078: 785,
+ 0xA07B: 786,
+ 0xA07D: 787,
+ 0xA081: 788,
+ 0xA082: 789,
+ 0xA085: 790,
+ 0xA089: 791,
+ 0xA091: 792,
+ 0xA093: 793,
+ 0xA095: 794,
+ 0xA096: 795,
+ 0xA097: 796,
+ 0xA098: 797,
+ 0xA0A1: 798,
+ 0xA0A2: 799,
+ 0xA0A9: 800,
+ 0xA0B7: 801,
+ 0xA0E1: 802,
+ 0xA0E2: 803,
+ 0xA0E5: 804,
+ 0xA0E9: 805,
+ 0xA0EB: 806,
+ 0xA0F1: 807,
+ 0xA0F3: 808,
+ 0xA0F5: 809,
+ 0xA0F7: 810,
+ 0xA0F8: 811,
+ 0xA0FD: 812,
+ 0xA141: 813,
+ 0xA142: 814,
+ 0xA145: 815,
+ 0xA149: 816,
+ 0xA151: 817,
+ 0xA153: 818,
+ 0xA155: 819,
+ 0xA156: 820,
+ 0xA157: 821,
+ 0xA161: 822,
+ 0xA162: 823,
+ 0xA165: 824,
+ 0xA169: 825,
+ 0xA175: 826,
+ 0xA176: 827,
+ 0xA177: 828,
+ 0xA179: 829,
+ 0xA181: 830,
+ 0xA1A1: 831,
+ 0xA1A2: 832,
+ 0xA1A4: 833,
+ 0xA1A5: 834,
+ 0xA1A9: 835,
+ 0xA1AB: 836,
+ 0xA1B1: 837,
+ 0xA1B3: 838,
+ 0xA1B5: 839,
+ 0xA1B7: 840,
+ 0xA1C1: 841,
+ 0xA1C5: 842,
+ 0xA1D6: 843,
+ 0xA1D7: 844,
+ 0xA241: 845,
+ 0xA245: 846,
+ 0xA249: 847,
+ 0xA253: 848,
+ 0xA255: 849,
+ 0xA257: 850,
+ 0xA261: 851,
+ 0xA265: 852,
+ 0xA269: 853,
+ 0xA273: 854,
+ 0xA275: 855,
+ 0xA281: 856,
+ 0xA282: 857,
+ 0xA283: 858,
+ 0xA285: 859,
+ 0xA288: 860,
+ 0xA289: 861,
+ 0xA28A: 862,
+ 0xA28B: 863,
+ 0xA291: 864,
+ 0xA293: 865,
+ 0xA295: 866,
+ 0xA297: 867,
+ 0xA29B: 868,
+ 0xA29D: 869,
+ 0xA2A1: 870,
+ 0xA2A5: 871,
+ 0xA2A9: 872,
+ 0xA2B3: 873,
+ 0xA2B5: 874,
+ 0xA2C1: 875,
+ 0xA2E1: 876,
+ 0xA2E5: 877,
+ 0xA2E9: 878,
+ 0xA341: 879,
+ 0xA345: 880,
+ 0xA349: 881,
+ 0xA351: 882,
+ 0xA355: 883,
+ 0xA361: 884,
+ 0xA365: 885,
+ 0xA369: 886,
+ 0xA371: 887,
+ 0xA375: 888,
+ 0xA3A1: 889,
+ 0xA3A2: 890,
+ 0xA3A5: 891,
+ 0xA3A8: 892,
+ 0xA3A9: 893,
+ 0xA3AB: 894,
+ 0xA3B1: 895,
+ 0xA3B3: 896,
+ 0xA3B5: 897,
+ 0xA3B6: 898,
+ 0xA3B7: 899,
+ 0xA3B9: 900,
+ 0xA3BB: 901,
+ 0xA461: 902,
+ 0xA462: 903,
+ 0xA463: 904,
+ 0xA464: 905,
+ 0xA465: 906,
+ 0xA468: 907,
+ 0xA469: 908,
+ 0xA46A: 909,
+ 0xA46B: 910,
+ 0xA46C: 911,
+ 0xA471: 912,
+ 0xA473: 913,
+ 0xA475: 914,
+ 0xA477: 915,
+ 0xA47B: 916,
+ 0xA481: 917,
+ 0xA482: 918,
+ 0xA485: 919,
+ 0xA489: 920,
+ 0xA491: 921,
+ 0xA493: 922,
+ 0xA495: 923,
+ 0xA496: 924,
+ 0xA497: 925,
+ 0xA49B: 926,
+ 0xA4A1: 927,
+ 0xA4A2: 928,
+ 0xA4A5: 929,
+ 0xA4B3: 930,
+ 0xA4E1: 931,
+ 0xA4E2: 932,
+ 0xA4E5: 933,
+ 0xA4E8: 934,
+ 0xA4E9: 935,
+ 0xA4EB: 936,
+ 0xA4F1: 937,
+ 0xA4F3: 938,
+ 0xA4F5: 939,
+ 0xA4F7: 940,
+ 0xA4F8: 941,
+ 0xA541: 942,
+ 0xA542: 943,
+ 0xA545: 944,
+ 0xA548: 945,
+ 0xA549: 946,
+ 0xA551: 947,
+ 0xA553: 948,
+ 0xA555: 949,
+ 0xA556: 950,
+ 0xA557: 951,
+ 0xA561: 952,
+ 0xA562: 953,
+ 0xA565: 954,
+ 0xA569: 955,
+ 0xA573: 956,
+ 0xA575: 957,
+ 0xA576: 958,
+ 0xA577: 959,
+ 0xA57B: 960,
+ 0xA581: 961,
+ 0xA585: 962,
+ 0xA5A1: 963,
+ 0xA5A2: 964,
+ 0xA5A3: 965,
+ 0xA5A5: 966,
+ 0xA5A9: 967,
+ 0xA5B1: 968,
+ 0xA5B3: 969,
+ 0xA5B5: 970,
+ 0xA5B7: 971,
+ 0xA5C1: 972,
+ 0xA5C5: 973,
+ 0xA5D6: 974,
+ 0xA5E1: 975,
+ 0xA5F6: 976,
+ 0xA641: 977,
+ 0xA642: 978,
+ 0xA645: 979,
+ 0xA649: 980,
+ 0xA651: 981,
+ 0xA653: 982,
+ 0xA661: 983,
+ 0xA665: 984,
+ 0xA681: 985,
+ 0xA682: 986,
+ 0xA685: 987,
+ 0xA688: 988,
+ 0xA689: 989,
+ 0xA68A: 990,
+ 0xA68B: 991,
+ 0xA691: 992,
+ 0xA693: 993,
+ 0xA695: 994,
+ 0xA697: 995,
+ 0xA69B: 996,
+ 0xA69C: 997,
+ 0xA6A1: 998,
+ 0xA6A9: 999,
+ 0xA6B6: 1000,
+ 0xA6C1: 1001,
+ 0xA6E1: 1002,
+ 0xA6E2: 1003,
+ 0xA6E5: 1004,
+ 0xA6E9: 1005,
+ 0xA6F7: 1006,
+ 0xA741: 1007,
+ 0xA745: 1008,
+ 0xA749: 1009,
+ 0xA751: 1010,
+ 0xA755: 1011,
+ 0xA757: 1012,
+ 0xA761: 1013,
+ 0xA762: 1014,
+ 0xA765: 1015,
+ 0xA769: 1016,
+ 0xA771: 1017,
+ 0xA773: 1018,
+ 0xA775: 1019,
+ 0xA7A1: 1020,
+ 0xA7A2: 1021,
+ 0xA7A5: 1022,
+ 0xA7A9: 1023,
+ 0xA7AB: 1024,
+ 0xA7B1: 1025,
+ 0xA7B3: 1026,
+ 0xA7B5: 1027,
+ 0xA7B7: 1028,
+ 0xA7B8: 1029,
+ 0xA7B9: 1030,
+ 0xA861: 1031,
+ 0xA862: 1032,
+ 0xA865: 1033,
+ 0xA869: 1034,
+ 0xA86B: 1035,
+ 0xA871: 1036,
+ 0xA873: 1037,
+ 0xA875: 1038,
+ 0xA876: 1039,
+ 0xA877: 1040,
+ 0xA87D: 1041,
+ 0xA881: 1042,
+ 0xA882: 1043,
+ 0xA885: 1044,
+ 0xA889: 1045,
+ 0xA891: 1046,
+ 0xA893: 1047,
+ 0xA895: 1048,
+ 0xA896: 1049,
+ 0xA897: 1050,
+ 0xA8A1: 1051,
+ 0xA8A2: 1052,
+ 0xA8B1: 1053,
+ 0xA8E1: 1054,
+ 0xA8E2: 1055,
+ 0xA8E5: 1056,
+ 0xA8E8: 1057,
+ 0xA8E9: 1058,
+ 0xA8F1: 1059,
+ 0xA8F5: 1060,
+ 0xA8F6: 1061,
+ 0xA8F7: 1062,
+ 0xA941: 1063,
+ 0xA957: 1064,
+ 0xA961: 1065,
+ 0xA962: 1066,
+ 0xA971: 1067,
+ 0xA973: 1068,
+ 0xA975: 1069,
+ 0xA976: 1070,
+ 0xA977: 1071,
+ 0xA9A1: 1072,
+ 0xA9A2: 1073,
+ 0xA9A5: 1074,
+ 0xA9A9: 1075,
+ 0xA9B1: 1076,
+ 0xA9B3: 1077,
+ 0xA9B7: 1078,
+ 0xAA41: 1079,
+ 0xAA61: 1080,
+ 0xAA77: 1081,
+ 0xAA81: 1082,
+ 0xAA82: 1083,
+ 0xAA85: 1084,
+ 0xAA89: 1085,
+ 0xAA91: 1086,
+ 0xAA95: 1087,
+ 0xAA97: 1088,
+ 0xAB41: 1089,
+ 0xAB57: 1090,
+ 0xAB61: 1091,
+ 0xAB65: 1092,
+ 0xAB69: 1093,
+ 0xAB71: 1094,
+ 0xAB73: 1095,
+ 0xABA1: 1096,
+ 0xABA2: 1097,
+ 0xABA5: 1098,
+ 0xABA9: 1099,
+ 0xABB1: 1100,
+ 0xABB3: 1101,
+ 0xABB5: 1102,
+ 0xABB7: 1103,
+ 0xAC61: 1104,
+ 0xAC62: 1105,
+ 0xAC64: 1106,
+ 0xAC65: 1107,
+ 0xAC68: 1108,
+ 0xAC69: 1109,
+ 0xAC6A: 1110,
+ 0xAC6B: 1111,
+ 0xAC71: 1112,
+ 0xAC73: 1113,
+ 0xAC75: 1114,
+ 0xAC76: 1115,
+ 0xAC77: 1116,
+ 0xAC7B: 1117,
+ 0xAC81: 1118,
+ 0xAC82: 1119,
+ 0xAC85: 1120,
+ 0xAC89: 1121,
+ 0xAC91: 1122,
+ 0xAC93: 1123,
+ 0xAC95: 1124,
+ 0xAC96: 1125,
+ 0xAC97: 1126,
+ 0xACA1: 1127,
+ 0xACA2: 1128,
+ 0xACA5: 1129,
+ 0xACA9: 1130,
+ 0xACB1: 1131,
+ 0xACB3: 1132,
+ 0xACB5: 1133,
+ 0xACB7: 1134,
+ 0xACC1: 1135,
+ 0xACC5: 1136,
+ 0xACC9: 1137,
+ 0xACD1: 1138,
+ 0xACD7: 1139,
+ 0xACE1: 1140,
+ 0xACE2: 1141,
+ 0xACE3: 1142,
+ 0xACE4: 1143,
+ 0xACE5: 1144,
+ 0xACE8: 1145,
+ 0xACE9: 1146,
+ 0xACEB: 1147,
+ 0xACEC: 1148,
+ 0xACF1: 1149,
+ 0xACF3: 1150,
+ 0xACF5: 1151,
+ 0xACF6: 1152,
+ 0xACF7: 1153,
+ 0xACFC: 1154,
+ 0xAD41: 1155,
+ 0xAD42: 1156,
+ 0xAD45: 1157,
+ 0xAD49: 1158,
+ 0xAD51: 1159,
+ 0xAD53: 1160,
+ 0xAD55: 1161,
+ 0xAD56: 1162,
+ 0xAD57: 1163,
+ 0xAD61: 1164,
+ 0xAD62: 1165,
+ 0xAD65: 1166,
+ 0xAD69: 1167,
+ 0xAD71: 1168,
+ 0xAD73: 1169,
+ 0xAD75: 1170,
+ 0xAD76: 1171,
+ 0xAD77: 1172,
+ 0xAD81: 1173,
+ 0xAD85: 1174,
+ 0xAD89: 1175,
+ 0xAD97: 1176,
+ 0xADA1: 1177,
+ 0xADA2: 1178,
+ 0xADA3: 1179,
+ 0xADA5: 1180,
+ 0xADA9: 1181,
+ 0xADAB: 1182,
+ 0xADB1: 1183,
+ 0xADB3: 1184,
+ 0xADB5: 1185,
+ 0xADB7: 1186,
+ 0xADBB: 1187,
+ 0xADC1: 1188,
+ 0xADC2: 1189,
+ 0xADC5: 1190,
+ 0xADC9: 1191,
+ 0xADD7: 1192,
+ 0xADE1: 1193,
+ 0xADE5: 1194,
+ 0xADE9: 1195,
+ 0xADF1: 1196,
+ 0xADF5: 1197,
+ 0xADF6: 1198,
+ 0xAE41: 1199,
+ 0xAE45: 1200,
+ 0xAE49: 1201,
+ 0xAE51: 1202,
+ 0xAE53: 1203,
+ 0xAE55: 1204,
+ 0xAE61: 1205,
+ 0xAE62: 1206,
+ 0xAE65: 1207,
+ 0xAE69: 1208,
+ 0xAE71: 1209,
+ 0xAE73: 1210,
+ 0xAE75: 1211,
+ 0xAE77: 1212,
+ 0xAE81: 1213,
+ 0xAE82: 1214,
+ 0xAE85: 1215,
+ 0xAE88: 1216,
+ 0xAE89: 1217,
+ 0xAE91: 1218,
+ 0xAE93: 1219,
+ 0xAE95: 1220,
+ 0xAE97: 1221,
+ 0xAE99: 1222,
+ 0xAE9B: 1223,
+ 0xAE9C: 1224,
+ 0xAEA1: 1225,
+ 0xAEB6: 1226,
+ 0xAEC1: 1227,
+ 0xAEC2: 1228,
+ 0xAEC5: 1229,
+ 0xAEC9: 1230,
+ 0xAED1: 1231,
+ 0xAED7: 1232,
+ 0xAEE1: 1233,
+ 0xAEE2: 1234,
+ 0xAEE5: 1235,
+ 0xAEE9: 1236,
+ 0xAEF1: 1237,
+ 0xAEF3: 1238,
+ 0xAEF5: 1239,
+ 0xAEF7: 1240,
+ 0xAF41: 1241,
+ 0xAF42: 1242,
+ 0xAF49: 1243,
+ 0xAF51: 1244,
+ 0xAF55: 1245,
+ 0xAF57: 1246,
+ 0xAF61: 1247,
+ 0xAF62: 1248,
+ 0xAF65: 1249,
+ 0xAF69: 1250,
+ 0xAF6A: 1251,
+ 0xAF71: 1252,
+ 0xAF73: 1253,
+ 0xAF75: 1254,
+ 0xAF77: 1255,
+ 0xAFA1: 1256,
+ 0xAFA2: 1257,
+ 0xAFA5: 1258,
+ 0xAFA8: 1259,
+ 0xAFA9: 1260,
+ 0xAFB0: 1261,
+ 0xAFB1: 1262,
+ 0xAFB3: 1263,
+ 0xAFB5: 1264,
+ 0xAFB7: 1265,
+ 0xAFBC: 1266,
+ 0xB061: 1267,
+ 0xB062: 1268,
+ 0xB064: 1269,
+ 0xB065: 1270,
+ 0xB069: 1271,
+ 0xB071: 1272,
+ 0xB073: 1273,
+ 0xB076: 1274,
+ 0xB077: 1275,
+ 0xB07D: 1276,
+ 0xB081: 1277,
+ 0xB082: 1278,
+ 0xB085: 1279,
+ 0xB089: 1280,
+ 0xB091: 1281,
+ 0xB093: 1282,
+ 0xB096: 1283,
+ 0xB097: 1284,
+ 0xB0B7: 1285,
+ 0xB0E1: 1286,
+ 0xB0E2: 1287,
+ 0xB0E5: 1288,
+ 0xB0E9: 1289,
+ 0xB0EB: 1290,
+ 0xB0F1: 1291,
+ 0xB0F3: 1292,
+ 0xB0F6: 1293,
+ 0xB0F7: 1294,
+ 0xB141: 1295,
+ 0xB145: 1296,
+ 0xB149: 1297,
+ 0xB185: 1298,
+ 0xB1A1: 1299,
+ 0xB1A2: 1300,
+ 0xB1A5: 1301,
+ 0xB1A8: 1302,
+ 0xB1A9: 1303,
+ 0xB1AB: 1304,
+ 0xB1B1: 1305,
+ 0xB1B3: 1306,
+ 0xB1B7: 1307,
+ 0xB1C1: 1308,
+ 0xB1C2: 1309,
+ 0xB1C5: 1310,
+ 0xB1D6: 1311,
+ 0xB1E1: 1312,
+ 0xB1F6: 1313,
+ 0xB241: 1314,
+ 0xB245: 1315,
+ 0xB249: 1316,
+ 0xB251: 1317,
+ 0xB253: 1318,
+ 0xB261: 1319,
+ 0xB281: 1320,
+ 0xB282: 1321,
+ 0xB285: 1322,
+ 0xB289: 1323,
+ 0xB291: 1324,
+ 0xB293: 1325,
+ 0xB297: 1326,
+ 0xB2A1: 1327,
+ 0xB2B6: 1328,
+ 0xB2C1: 1329,
+ 0xB2E1: 1330,
+ 0xB2E5: 1331,
+ 0xB357: 1332,
+ 0xB361: 1333,
+ 0xB362: 1334,
+ 0xB365: 1335,
+ 0xB369: 1336,
+ 0xB36B: 1337,
+ 0xB370: 1338,
+ 0xB371: 1339,
+ 0xB373: 1340,
+ 0xB381: 1341,
+ 0xB385: 1342,
+ 0xB389: 1343,
+ 0xB391: 1344,
+ 0xB3A1: 1345,
+ 0xB3A2: 1346,
+ 0xB3A5: 1347,
+ 0xB3A9: 1348,
+ 0xB3B1: 1349,
+ 0xB3B3: 1350,
+ 0xB3B5: 1351,
+ 0xB3B7: 1352,
+ 0xB461: 1353,
+ 0xB462: 1354,
+ 0xB465: 1355,
+ 0xB466: 1356,
+ 0xB467: 1357,
+ 0xB469: 1358,
+ 0xB46A: 1359,
+ 0xB46B: 1360,
+ 0xB470: 1361,
+ 0xB471: 1362,
+ 0xB473: 1363,
+ 0xB475: 1364,
+ 0xB476: 1365,
+ 0xB477: 1366,
+ 0xB47B: 1367,
+ 0xB47C: 1368,
+ 0xB481: 1369,
+ 0xB482: 1370,
+ 0xB485: 1371,
+ 0xB489: 1372,
+ 0xB491: 1373,
+ 0xB493: 1374,
+ 0xB495: 1375,
+ 0xB496: 1376,
+ 0xB497: 1377,
+ 0xB4A1: 1378,
+ 0xB4A2: 1379,
+ 0xB4A5: 1380,
+ 0xB4A9: 1381,
+ 0xB4AC: 1382,
+ 0xB4B1: 1383,
+ 0xB4B3: 1384,
+ 0xB4B5: 1385,
+ 0xB4B7: 1386,
+ 0xB4BB: 1387,
+ 0xB4BD: 1388,
+ 0xB4C1: 1389,
+ 0xB4C5: 1390,
+ 0xB4C9: 1391,
+ 0xB4D3: 1392,
+ 0xB4E1: 1393,
+ 0xB4E2: 1394,
+ 0xB4E5: 1395,
+ 0xB4E6: 1396,
+ 0xB4E8: 1397,
+ 0xB4E9: 1398,
+ 0xB4EA: 1399,
+ 0xB4EB: 1400,
+ 0xB4F1: 1401,
+ 0xB4F3: 1402,
+ 0xB4F4: 1403,
+ 0xB4F5: 1404,
+ 0xB4F6: 1405,
+ 0xB4F7: 1406,
+ 0xB4F8: 1407,
+ 0xB4FA: 1408,
+ 0xB4FC: 1409,
+ 0xB541: 1410,
+ 0xB542: 1411,
+ 0xB545: 1412,
+ 0xB549: 1413,
+ 0xB551: 1414,
+ 0xB553: 1415,
+ 0xB555: 1416,
+ 0xB557: 1417,
+ 0xB561: 1418,
+ 0xB562: 1419,
+ 0xB563: 1420,
+ 0xB565: 1421,
+ 0xB569: 1422,
+ 0xB56B: 1423,
+ 0xB56C: 1424,
+ 0xB571: 1425,
+ 0xB573: 1426,
+ 0xB574: 1427,
+ 0xB575: 1428,
+ 0xB576: 1429,
+ 0xB577: 1430,
+ 0xB57B: 1431,
+ 0xB57C: 1432,
+ 0xB57D: 1433,
+ 0xB581: 1434,
+ 0xB585: 1435,
+ 0xB589: 1436,
+ 0xB591: 1437,
+ 0xB593: 1438,
+ 0xB595: 1439,
+ 0xB596: 1440,
+ 0xB5A1: 1441,
+ 0xB5A2: 1442,
+ 0xB5A5: 1443,
+ 0xB5A9: 1444,
+ 0xB5AA: 1445,
+ 0xB5AB: 1446,
+ 0xB5AD: 1447,
+ 0xB5B0: 1448,
+ 0xB5B1: 1449,
+ 0xB5B3: 1450,
+ 0xB5B5: 1451,
+ 0xB5B7: 1452,
+ 0xB5B9: 1453,
+ 0xB5C1: 1454,
+ 0xB5C2: 1455,
+ 0xB5C5: 1456,
+ 0xB5C9: 1457,
+ 0xB5D1: 1458,
+ 0xB5D3: 1459,
+ 0xB5D5: 1460,
+ 0xB5D6: 1461,
+ 0xB5D7: 1462,
+ 0xB5E1: 1463,
+ 0xB5E2: 1464,
+ 0xB5E5: 1465,
+ 0xB5F1: 1466,
+ 0xB5F5: 1467,
+ 0xB5F7: 1468,
+ 0xB641: 1469,
+ 0xB642: 1470,
+ 0xB645: 1471,
+ 0xB649: 1472,
+ 0xB651: 1473,
+ 0xB653: 1474,
+ 0xB655: 1475,
+ 0xB657: 1476,
+ 0xB661: 1477,
+ 0xB662: 1478,
+ 0xB665: 1479,
+ 0xB669: 1480,
+ 0xB671: 1481,
+ 0xB673: 1482,
+ 0xB675: 1483,
+ 0xB677: 1484,
+ 0xB681: 1485,
+ 0xB682: 1486,
+ 0xB685: 1487,
+ 0xB689: 1488,
+ 0xB68A: 1489,
+ 0xB68B: 1490,
+ 0xB691: 1491,
+ 0xB693: 1492,
+ 0xB695: 1493,
+ 0xB697: 1494,
+ 0xB6A1: 1495,
+ 0xB6A2: 1496,
+ 0xB6A5: 1497,
+ 0xB6A9: 1498,
+ 0xB6B1: 1499,
+ 0xB6B3: 1500,
+ 0xB6B6: 1501,
+ 0xB6B7: 1502,
+ 0xB6C1: 1503,
+ 0xB6C2: 1504,
+ 0xB6C5: 1505,
+ 0xB6C9: 1506,
+ 0xB6D1: 1507,
+ 0xB6D3: 1508,
+ 0xB6D7: 1509,
+ 0xB6E1: 1510,
+ 0xB6E2: 1511,
+ 0xB6E5: 1512,
+ 0xB6E9: 1513,
+ 0xB6F1: 1514,
+ 0xB6F3: 1515,
+ 0xB6F5: 1516,
+ 0xB6F7: 1517,
+ 0xB741: 1518,
+ 0xB742: 1519,
+ 0xB745: 1520,
+ 0xB749: 1521,
+ 0xB751: 1522,
+ 0xB753: 1523,
+ 0xB755: 1524,
+ 0xB757: 1525,
+ 0xB759: 1526,
+ 0xB761: 1527,
+ 0xB762: 1528,
+ 0xB765: 1529,
+ 0xB769: 1530,
+ 0xB76F: 1531,
+ 0xB771: 1532,
+ 0xB773: 1533,
+ 0xB775: 1534,
+ 0xB777: 1535,
+ 0xB778: 1536,
+ 0xB779: 1537,
+ 0xB77A: 1538,
+ 0xB77B: 1539,
+ 0xB77C: 1540,
+ 0xB77D: 1541,
+ 0xB781: 1542,
+ 0xB785: 1543,
+ 0xB789: 1544,
+ 0xB791: 1545,
+ 0xB795: 1546,
+ 0xB7A1: 1547,
+ 0xB7A2: 1548,
+ 0xB7A5: 1549,
+ 0xB7A9: 1550,
+ 0xB7AA: 1551,
+ 0xB7AB: 1552,
+ 0xB7B0: 1553,
+ 0xB7B1: 1554,
+ 0xB7B3: 1555,
+ 0xB7B5: 1556,
+ 0xB7B6: 1557,
+ 0xB7B7: 1558,
+ 0xB7B8: 1559,
+ 0xB7BC: 1560,
+ 0xB861: 1561,
+ 0xB862: 1562,
+ 0xB865: 1563,
+ 0xB867: 1564,
+ 0xB868: 1565,
+ 0xB869: 1566,
+ 0xB86B: 1567,
+ 0xB871: 1568,
+ 0xB873: 1569,
+ 0xB875: 1570,
+ 0xB876: 1571,
+ 0xB877: 1572,
+ 0xB878: 1573,
+ 0xB881: 1574,
+ 0xB882: 1575,
+ 0xB885: 1576,
+ 0xB889: 1577,
+ 0xB891: 1578,
+ 0xB893: 1579,
+ 0xB895: 1580,
+ 0xB896: 1581,
+ 0xB897: 1582,
+ 0xB8A1: 1583,
+ 0xB8A2: 1584,
+ 0xB8A5: 1585,
+ 0xB8A7: 1586,
+ 0xB8A9: 1587,
+ 0xB8B1: 1588,
+ 0xB8B7: 1589,
+ 0xB8C1: 1590,
+ 0xB8C5: 1591,
+ 0xB8C9: 1592,
+ 0xB8E1: 1593,
+ 0xB8E2: 1594,
+ 0xB8E5: 1595,
+ 0xB8E9: 1596,
+ 0xB8EB: 1597,
+ 0xB8F1: 1598,
+ 0xB8F3: 1599,
+ 0xB8F5: 1600,
+ 0xB8F7: 1601,
+ 0xB8F8: 1602,
+ 0xB941: 1603,
+ 0xB942: 1604,
+ 0xB945: 1605,
+ 0xB949: 1606,
+ 0xB951: 1607,
+ 0xB953: 1608,
+ 0xB955: 1609,
+ 0xB957: 1610,
+ 0xB961: 1611,
+ 0xB965: 1612,
+ 0xB969: 1613,
+ 0xB971: 1614,
+ 0xB973: 1615,
+ 0xB976: 1616,
+ 0xB977: 1617,
+ 0xB981: 1618,
+ 0xB9A1: 1619,
+ 0xB9A2: 1620,
+ 0xB9A5: 1621,
+ 0xB9A9: 1622,
+ 0xB9AB: 1623,
+ 0xB9B1: 1624,
+ 0xB9B3: 1625,
+ 0xB9B5: 1626,
+ 0xB9B7: 1627,
+ 0xB9B8: 1628,
+ 0xB9B9: 1629,
+ 0xB9BD: 1630,
+ 0xB9C1: 1631,
+ 0xB9C2: 1632,
+ 0xB9C9: 1633,
+ 0xB9D3: 1634,
+ 0xB9D5: 1635,
+ 0xB9D7: 1636,
+ 0xB9E1: 1637,
+ 0xB9F6: 1638,
+ 0xB9F7: 1639,
+ 0xBA41: 1640,
+ 0xBA45: 1641,
+ 0xBA49: 1642,
+ 0xBA51: 1643,
+ 0xBA53: 1644,
+ 0xBA55: 1645,
+ 0xBA57: 1646,
+ 0xBA61: 1647,
+ 0xBA62: 1648,
+ 0xBA65: 1649,
+ 0xBA77: 1650,
+ 0xBA81: 1651,
+ 0xBA82: 1652,
+ 0xBA85: 1653,
+ 0xBA89: 1654,
+ 0xBA8A: 1655,
+ 0xBA8B: 1656,
+ 0xBA91: 1657,
+ 0xBA93: 1658,
+ 0xBA95: 1659,
+ 0xBA97: 1660,
+ 0xBAA1: 1661,
+ 0xBAB6: 1662,
+ 0xBAC1: 1663,
+ 0xBAE1: 1664,
+ 0xBAE2: 1665,
+ 0xBAE5: 1666,
+ 0xBAE9: 1667,
+ 0xBAF1: 1668,
+ 0xBAF3: 1669,
+ 0xBAF5: 1670,
+ 0xBB41: 1671,
+ 0xBB45: 1672,
+ 0xBB49: 1673,
+ 0xBB51: 1674,
+ 0xBB61: 1675,
+ 0xBB62: 1676,
+ 0xBB65: 1677,
+ 0xBB69: 1678,
+ 0xBB71: 1679,
+ 0xBB73: 1680,
+ 0xBB75: 1681,
+ 0xBB77: 1682,
+ 0xBBA1: 1683,
+ 0xBBA2: 1684,
+ 0xBBA5: 1685,
+ 0xBBA8: 1686,
+ 0xBBA9: 1687,
+ 0xBBAB: 1688,
+ 0xBBB1: 1689,
+ 0xBBB3: 1690,
+ 0xBBB5: 1691,
+ 0xBBB7: 1692,
+ 0xBBB8: 1693,
+ 0xBBBB: 1694,
+ 0xBBBC: 1695,
+ 0xBC61: 1696,
+ 0xBC62: 1697,
+ 0xBC65: 1698,
+ 0xBC67: 1699,
+ 0xBC69: 1700,
+ 0xBC6C: 1701,
+ 0xBC71: 1702,
+ 0xBC73: 1703,
+ 0xBC75: 1704,
+ 0xBC76: 1705,
+ 0xBC77: 1706,
+ 0xBC81: 1707,
+ 0xBC82: 1708,
+ 0xBC85: 1709,
+ 0xBC89: 1710,
+ 0xBC91: 1711,
+ 0xBC93: 1712,
+ 0xBC95: 1713,
+ 0xBC96: 1714,
+ 0xBC97: 1715,
+ 0xBCA1: 1716,
+ 0xBCA5: 1717,
+ 0xBCB7: 1718,
+ 0xBCE1: 1719,
+ 0xBCE2: 1720,
+ 0xBCE5: 1721,
+ 0xBCE9: 1722,
+ 0xBCF1: 1723,
+ 0xBCF3: 1724,
+ 0xBCF5: 1725,
+ 0xBCF6: 1726,
+ 0xBCF7: 1727,
+ 0xBD41: 1728,
+ 0xBD57: 1729,
+ 0xBD61: 1730,
+ 0xBD76: 1731,
+ 0xBDA1: 1732,
+ 0xBDA2: 1733,
+ 0xBDA5: 1734,
+ 0xBDA9: 1735,
+ 0xBDB1: 1736,
+ 0xBDB3: 1737,
+ 0xBDB5: 1738,
+ 0xBDB7: 1739,
+ 0xBDB9: 1740,
+ 0xBDC1: 1741,
+ 0xBDC2: 1742,
+ 0xBDC9: 1743,
+ 0xBDD6: 1744,
+ 0xBDE1: 1745,
+ 0xBDF6: 1746,
+ 0xBE41: 1747,
+ 0xBE45: 1748,
+ 0xBE49: 1749,
+ 0xBE51: 1750,
+ 0xBE53: 1751,
+ 0xBE77: 1752,
+ 0xBE81: 1753,
+ 0xBE82: 1754,
+ 0xBE85: 1755,
+ 0xBE89: 1756,
+ 0xBE91: 1757,
+ 0xBE93: 1758,
+ 0xBE97: 1759,
+ 0xBEA1: 1760,
+ 0xBEB6: 1761,
+ 0xBEB7: 1762,
+ 0xBEE1: 1763,
+ 0xBF41: 1764,
+ 0xBF61: 1765,
+ 0xBF71: 1766,
+ 0xBF75: 1767,
+ 0xBF77: 1768,
+ 0xBFA1: 1769,
+ 0xBFA2: 1770,
+ 0xBFA5: 1771,
+ 0xBFA9: 1772,
+ 0xBFB1: 1773,
+ 0xBFB3: 1774,
+ 0xBFB7: 1775,
+ 0xBFB8: 1776,
+ 0xBFBD: 1777,
+ 0xC061: 1778,
+ 0xC062: 1779,
+ 0xC065: 1780,
+ 0xC067: 1781,
+ 0xC069: 1782,
+ 0xC071: 1783,
+ 0xC073: 1784,
+ 0xC075: 1785,
+ 0xC076: 1786,
+ 0xC077: 1787,
+ 0xC078: 1788,
+ 0xC081: 1789,
+ 0xC082: 1790,
+ 0xC085: 1791,
+ 0xC089: 1792,
+ 0xC091: 1793,
+ 0xC093: 1794,
+ 0xC095: 1795,
+ 0xC096: 1796,
+ 0xC097: 1797,
+ 0xC0A1: 1798,
+ 0xC0A5: 1799,
+ 0xC0A7: 1800,
+ 0xC0A9: 1801,
+ 0xC0B1: 1802,
+ 0xC0B7: 1803,
+ 0xC0E1: 1804,
+ 0xC0E2: 1805,
+ 0xC0E5: 1806,
+ 0xC0E9: 1807,
+ 0xC0F1: 1808,
+ 0xC0F3: 1809,
+ 0xC0F5: 1810,
+ 0xC0F6: 1811,
+ 0xC0F7: 1812,
+ 0xC141: 1813,
+ 0xC142: 1814,
+ 0xC145: 1815,
+ 0xC149: 1816,
+ 0xC151: 1817,
+ 0xC153: 1818,
+ 0xC155: 1819,
+ 0xC157: 1820,
+ 0xC161: 1821,
+ 0xC165: 1822,
+ 0xC176: 1823,
+ 0xC181: 1824,
+ 0xC185: 1825,
+ 0xC197: 1826,
+ 0xC1A1: 1827,
+ 0xC1A2: 1828,
+ 0xC1A5: 1829,
+ 0xC1A9: 1830,
+ 0xC1B1: 1831,
+ 0xC1B3: 1832,
+ 0xC1B5: 1833,
+ 0xC1B7: 1834,
+ 0xC1C1: 1835,
+ 0xC1C5: 1836,
+ 0xC1C9: 1837,
+ 0xC1D7: 1838,
+ 0xC241: 1839,
+ 0xC245: 1840,
+ 0xC249: 1841,
+ 0xC251: 1842,
+ 0xC253: 1843,
+ 0xC255: 1844,
+ 0xC257: 1845,
+ 0xC261: 1846,
+ 0xC271: 1847,
+ 0xC281: 1848,
+ 0xC282: 1849,
+ 0xC285: 1850,
+ 0xC289: 1851,
+ 0xC291: 1852,
+ 0xC293: 1853,
+ 0xC295: 1854,
+ 0xC297: 1855,
+ 0xC2A1: 1856,
+ 0xC2B6: 1857,
+ 0xC2C1: 1858,
+ 0xC2C5: 1859,
+ 0xC2E1: 1860,
+ 0xC2E5: 1861,
+ 0xC2E9: 1862,
+ 0xC2F1: 1863,
+ 0xC2F3: 1864,
+ 0xC2F5: 1865,
+ 0xC2F7: 1866,
+ 0xC341: 1867,
+ 0xC345: 1868,
+ 0xC349: 1869,
+ 0xC351: 1870,
+ 0xC357: 1871,
+ 0xC361: 1872,
+ 0xC362: 1873,
+ 0xC365: 1874,
+ 0xC369: 1875,
+ 0xC371: 1876,
+ 0xC373: 1877,
+ 0xC375: 1878,
+ 0xC377: 1879,
+ 0xC3A1: 1880,
+ 0xC3A2: 1881,
+ 0xC3A5: 1882,
+ 0xC3A8: 1883,
+ 0xC3A9: 1884,
+ 0xC3AA: 1885,
+ 0xC3B1: 1886,
+ 0xC3B3: 1887,
+ 0xC3B5: 1888,
+ 0xC3B7: 1889,
+ 0xC461: 1890,
+ 0xC462: 1891,
+ 0xC465: 1892,
+ 0xC469: 1893,
+ 0xC471: 1894,
+ 0xC473: 1895,
+ 0xC475: 1896,
+ 0xC477: 1897,
+ 0xC481: 1898,
+ 0xC482: 1899,
+ 0xC485: 1900,
+ 0xC489: 1901,
+ 0xC491: 1902,
+ 0xC493: 1903,
+ 0xC495: 1904,
+ 0xC496: 1905,
+ 0xC497: 1906,
+ 0xC4A1: 1907,
+ 0xC4A2: 1908,
+ 0xC4B7: 1909,
+ 0xC4E1: 1910,
+ 0xC4E2: 1911,
+ 0xC4E5: 1912,
+ 0xC4E8: 1913,
+ 0xC4E9: 1914,
+ 0xC4F1: 1915,
+ 0xC4F3: 1916,
+ 0xC4F5: 1917,
+ 0xC4F6: 1918,
+ 0xC4F7: 1919,
+ 0xC541: 1920,
+ 0xC542: 1921,
+ 0xC545: 1922,
+ 0xC549: 1923,
+ 0xC551: 1924,
+ 0xC553: 1925,
+ 0xC555: 1926,
+ 0xC557: 1927,
+ 0xC561: 1928,
+ 0xC565: 1929,
+ 0xC569: 1930,
+ 0xC571: 1931,
+ 0xC573: 1932,
+ 0xC575: 1933,
+ 0xC576: 1934,
+ 0xC577: 1935,
+ 0xC581: 1936,
+ 0xC5A1: 1937,
+ 0xC5A2: 1938,
+ 0xC5A5: 1939,
+ 0xC5A9: 1940,
+ 0xC5B1: 1941,
+ 0xC5B3: 1942,
+ 0xC5B5: 1943,
+ 0xC5B7: 1944,
+ 0xC5C1: 1945,
+ 0xC5C2: 1946,
+ 0xC5C5: 1947,
+ 0xC5C9: 1948,
+ 0xC5D1: 1949,
+ 0xC5D7: 1950,
+ 0xC5E1: 1951,
+ 0xC5F7: 1952,
+ 0xC641: 1953,
+ 0xC649: 1954,
+ 0xC661: 1955,
+ 0xC681: 1956,
+ 0xC682: 1957,
+ 0xC685: 1958,
+ 0xC689: 1959,
+ 0xC691: 1960,
+ 0xC693: 1961,
+ 0xC695: 1962,
+ 0xC697: 1963,
+ 0xC6A1: 1964,
+ 0xC6A5: 1965,
+ 0xC6A9: 1966,
+ 0xC6B7: 1967,
+ 0xC6C1: 1968,
+ 0xC6D7: 1969,
+ 0xC6E1: 1970,
+ 0xC6E2: 1971,
+ 0xC6E5: 1972,
+ 0xC6E9: 1973,
+ 0xC6F1: 1974,
+ 0xC6F3: 1975,
+ 0xC6F5: 1976,
+ 0xC6F7: 1977,
+ 0xC741: 1978,
+ 0xC745: 1979,
+ 0xC749: 1980,
+ 0xC751: 1981,
+ 0xC761: 1982,
+ 0xC762: 1983,
+ 0xC765: 1984,
+ 0xC769: 1985,
+ 0xC771: 1986,
+ 0xC773: 1987,
+ 0xC777: 1988,
+ 0xC7A1: 1989,
+ 0xC7A2: 1990,
+ 0xC7A5: 1991,
+ 0xC7A9: 1992,
+ 0xC7B1: 1993,
+ 0xC7B3: 1994,
+ 0xC7B5: 1995,
+ 0xC7B7: 1996,
+ 0xC861: 1997,
+ 0xC862: 1998,
+ 0xC865: 1999,
+ 0xC869: 2000,
+ 0xC86A: 2001,
+ 0xC871: 2002,
+ 0xC873: 2003,
+ 0xC875: 2004,
+ 0xC876: 2005,
+ 0xC877: 2006,
+ 0xC881: 2007,
+ 0xC882: 2008,
+ 0xC885: 2009,
+ 0xC889: 2010,
+ 0xC891: 2011,
+ 0xC893: 2012,
+ 0xC895: 2013,
+ 0xC896: 2014,
+ 0xC897: 2015,
+ 0xC8A1: 2016,
+ 0xC8B7: 2017,
+ 0xC8E1: 2018,
+ 0xC8E2: 2019,
+ 0xC8E5: 2020,
+ 0xC8E9: 2021,
+ 0xC8EB: 2022,
+ 0xC8F1: 2023,
+ 0xC8F3: 2024,
+ 0xC8F5: 2025,
+ 0xC8F6: 2026,
+ 0xC8F7: 2027,
+ 0xC941: 2028,
+ 0xC942: 2029,
+ 0xC945: 2030,
+ 0xC949: 2031,
+ 0xC951: 2032,
+ 0xC953: 2033,
+ 0xC955: 2034,
+ 0xC957: 2035,
+ 0xC961: 2036,
+ 0xC965: 2037,
+ 0xC976: 2038,
+ 0xC981: 2039,
+ 0xC985: 2040,
+ 0xC9A1: 2041,
+ 0xC9A2: 2042,
+ 0xC9A5: 2043,
+ 0xC9A9: 2044,
+ 0xC9B1: 2045,
+ 0xC9B3: 2046,
+ 0xC9B5: 2047,
+ 0xC9B7: 2048,
+ 0xC9BC: 2049,
+ 0xC9C1: 2050,
+ 0xC9C5: 2051,
+ 0xC9E1: 2052,
+ 0xCA41: 2053,
+ 0xCA45: 2054,
+ 0xCA55: 2055,
+ 0xCA57: 2056,
+ 0xCA61: 2057,
+ 0xCA81: 2058,
+ 0xCA82: 2059,
+ 0xCA85: 2060,
+ 0xCA89: 2061,
+ 0xCA91: 2062,
+ 0xCA93: 2063,
+ 0xCA95: 2064,
+ 0xCA97: 2065,
+ 0xCAA1: 2066,
+ 0xCAB6: 2067,
+ 0xCAC1: 2068,
+ 0xCAE1: 2069,
+ 0xCAE2: 2070,
+ 0xCAE5: 2071,
+ 0xCAE9: 2072,
+ 0xCAF1: 2073,
+ 0xCAF3: 2074,
+ 0xCAF7: 2075,
+ 0xCB41: 2076,
+ 0xCB45: 2077,
+ 0xCB49: 2078,
+ 0xCB51: 2079,
+ 0xCB57: 2080,
+ 0xCB61: 2081,
+ 0xCB62: 2082,
+ 0xCB65: 2083,
+ 0xCB68: 2084,
+ 0xCB69: 2085,
+ 0xCB6B: 2086,
+ 0xCB71: 2087,
+ 0xCB73: 2088,
+ 0xCB75: 2089,
+ 0xCB81: 2090,
+ 0xCB85: 2091,
+ 0xCB89: 2092,
+ 0xCB91: 2093,
+ 0xCB93: 2094,
+ 0xCBA1: 2095,
+ 0xCBA2: 2096,
+ 0xCBA5: 2097,
+ 0xCBA9: 2098,
+ 0xCBB1: 2099,
+ 0xCBB3: 2100,
+ 0xCBB5: 2101,
+ 0xCBB7: 2102,
+ 0xCC61: 2103,
+ 0xCC62: 2104,
+ 0xCC63: 2105,
+ 0xCC65: 2106,
+ 0xCC69: 2107,
+ 0xCC6B: 2108,
+ 0xCC71: 2109,
+ 0xCC73: 2110,
+ 0xCC75: 2111,
+ 0xCC76: 2112,
+ 0xCC77: 2113,
+ 0xCC7B: 2114,
+ 0xCC81: 2115,
+ 0xCC82: 2116,
+ 0xCC85: 2117,
+ 0xCC89: 2118,
+ 0xCC91: 2119,
+ 0xCC93: 2120,
+ 0xCC95: 2121,
+ 0xCC96: 2122,
+ 0xCC97: 2123,
+ 0xCCA1: 2124,
+ 0xCCA2: 2125,
+ 0xCCE1: 2126,
+ 0xCCE2: 2127,
+ 0xCCE5: 2128,
+ 0xCCE9: 2129,
+ 0xCCF1: 2130,
+ 0xCCF3: 2131,
+ 0xCCF5: 2132,
+ 0xCCF6: 2133,
+ 0xCCF7: 2134,
+ 0xCD41: 2135,
+ 0xCD42: 2136,
+ 0xCD45: 2137,
+ 0xCD49: 2138,
+ 0xCD51: 2139,
+ 0xCD53: 2140,
+ 0xCD55: 2141,
+ 0xCD57: 2142,
+ 0xCD61: 2143,
+ 0xCD65: 2144,
+ 0xCD69: 2145,
+ 0xCD71: 2146,
+ 0xCD73: 2147,
+ 0xCD76: 2148,
+ 0xCD77: 2149,
+ 0xCD81: 2150,
+ 0xCD89: 2151,
+ 0xCD93: 2152,
+ 0xCD95: 2153,
+ 0xCDA1: 2154,
+ 0xCDA2: 2155,
+ 0xCDA5: 2156,
+ 0xCDA9: 2157,
+ 0xCDB1: 2158,
+ 0xCDB3: 2159,
+ 0xCDB5: 2160,
+ 0xCDB7: 2161,
+ 0xCDC1: 2162,
+ 0xCDD7: 2163,
+ 0xCE41: 2164,
+ 0xCE45: 2165,
+ 0xCE61: 2166,
+ 0xCE65: 2167,
+ 0xCE69: 2168,
+ 0xCE73: 2169,
+ 0xCE75: 2170,
+ 0xCE81: 2171,
+ 0xCE82: 2172,
+ 0xCE85: 2173,
+ 0xCE88: 2174,
+ 0xCE89: 2175,
+ 0xCE8B: 2176,
+ 0xCE91: 2177,
+ 0xCE93: 2178,
+ 0xCE95: 2179,
+ 0xCE97: 2180,
+ 0xCEA1: 2181,
+ 0xCEB7: 2182,
+ 0xCEE1: 2183,
+ 0xCEE5: 2184,
+ 0xCEE9: 2185,
+ 0xCEF1: 2186,
+ 0xCEF5: 2187,
+ 0xCF41: 2188,
+ 0xCF45: 2189,
+ 0xCF49: 2190,
+ 0xCF51: 2191,
+ 0xCF55: 2192,
+ 0xCF57: 2193,
+ 0xCF61: 2194,
+ 0xCF65: 2195,
+ 0xCF69: 2196,
+ 0xCF71: 2197,
+ 0xCF73: 2198,
+ 0xCF75: 2199,
+ 0xCFA1: 2200,
+ 0xCFA2: 2201,
+ 0xCFA5: 2202,
+ 0xCFA9: 2203,
+ 0xCFB1: 2204,
+ 0xCFB3: 2205,
+ 0xCFB5: 2206,
+ 0xCFB7: 2207,
+ 0xD061: 2208,
+ 0xD062: 2209,
+ 0xD065: 2210,
+ 0xD069: 2211,
+ 0xD06E: 2212,
+ 0xD071: 2213,
+ 0xD073: 2214,
+ 0xD075: 2215,
+ 0xD077: 2216,
+ 0xD081: 2217,
+ 0xD082: 2218,
+ 0xD085: 2219,
+ 0xD089: 2220,
+ 0xD091: 2221,
+ 0xD093: 2222,
+ 0xD095: 2223,
+ 0xD096: 2224,
+ 0xD097: 2225,
+ 0xD0A1: 2226,
+ 0xD0B7: 2227,
+ 0xD0E1: 2228,
+ 0xD0E2: 2229,
+ 0xD0E5: 2230,
+ 0xD0E9: 2231,
+ 0xD0EB: 2232,
+ 0xD0F1: 2233,
+ 0xD0F3: 2234,
+ 0xD0F5: 2235,
+ 0xD0F7: 2236,
+ 0xD141: 2237,
+ 0xD142: 2238,
+ 0xD145: 2239,
+ 0xD149: 2240,
+ 0xD151: 2241,
+ 0xD153: 2242,
+ 0xD155: 2243,
+ 0xD157: 2244,
+ 0xD161: 2245,
+ 0xD162: 2246,
+ 0xD165: 2247,
+ 0xD169: 2248,
+ 0xD171: 2249,
+ 0xD173: 2250,
+ 0xD175: 2251,
+ 0xD176: 2252,
+ 0xD177: 2253,
+ 0xD181: 2254,
+ 0xD185: 2255,
+ 0xD189: 2256,
+ 0xD193: 2257,
+ 0xD1A1: 2258,
+ 0xD1A2: 2259,
+ 0xD1A5: 2260,
+ 0xD1A9: 2261,
+ 0xD1AE: 2262,
+ 0xD1B1: 2263,
+ 0xD1B3: 2264,
+ 0xD1B5: 2265,
+ 0xD1B7: 2266,
+ 0xD1BB: 2267,
+ 0xD1C1: 2268,
+ 0xD1C2: 2269,
+ 0xD1C5: 2270,
+ 0xD1C9: 2271,
+ 0xD1D5: 2272,
+ 0xD1D7: 2273,
+ 0xD1E1: 2274,
+ 0xD1E2: 2275,
+ 0xD1E5: 2276,
+ 0xD1F5: 2277,
+ 0xD1F7: 2278,
+ 0xD241: 2279,
+ 0xD242: 2280,
+ 0xD245: 2281,
+ 0xD249: 2282,
+ 0xD253: 2283,
+ 0xD255: 2284,
+ 0xD257: 2285,
+ 0xD261: 2286,
+ 0xD265: 2287,
+ 0xD269: 2288,
+ 0xD273: 2289,
+ 0xD275: 2290,
+ 0xD281: 2291,
+ 0xD282: 2292,
+ 0xD285: 2293,
+ 0xD289: 2294,
+ 0xD28E: 2295,
+ 0xD291: 2296,
+ 0xD295: 2297,
+ 0xD297: 2298,
+ 0xD2A1: 2299,
+ 0xD2A5: 2300,
+ 0xD2A9: 2301,
+ 0xD2B1: 2302,
+ 0xD2B7: 2303,
+ 0xD2C1: 2304,
+ 0xD2C2: 2305,
+ 0xD2C5: 2306,
+ 0xD2C9: 2307,
+ 0xD2D7: 2308,
+ 0xD2E1: 2309,
+ 0xD2E2: 2310,
+ 0xD2E5: 2311,
+ 0xD2E9: 2312,
+ 0xD2F1: 2313,
+ 0xD2F3: 2314,
+ 0xD2F5: 2315,
+ 0xD2F7: 2316,
+ 0xD341: 2317,
+ 0xD342: 2318,
+ 0xD345: 2319,
+ 0xD349: 2320,
+ 0xD351: 2321,
+ 0xD355: 2322,
+ 0xD357: 2323,
+ 0xD361: 2324,
+ 0xD362: 2325,
+ 0xD365: 2326,
+ 0xD367: 2327,
+ 0xD368: 2328,
+ 0xD369: 2329,
+ 0xD36A: 2330,
+ 0xD371: 2331,
+ 0xD373: 2332,
+ 0xD375: 2333,
+ 0xD377: 2334,
+ 0xD37B: 2335,
+ 0xD381: 2336,
+ 0xD385: 2337,
+ 0xD389: 2338,
+ 0xD391: 2339,
+ 0xD393: 2340,
+ 0xD397: 2341,
+ 0xD3A1: 2342,
+ 0xD3A2: 2343,
+ 0xD3A5: 2344,
+ 0xD3A9: 2345,
+ 0xD3B1: 2346,
+ 0xD3B3: 2347,
+ 0xD3B5: 2348,
+ 0xD3B7: 2349,
+}
diff --git a/src/pip/_vendor/chardet/compat.py b/src/pip/_vendor/chardet/johabprober.py
index 8941572b3..6f359d193 100644
--- a/src/pip/_vendor/chardet/compat.py
+++ b/src/pip/_vendor/chardet/johabprober.py
@@ -1,7 +1,13 @@
######################## BEGIN LICENSE BLOCK ########################
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Netscape Communications Corporation.
+# Portions created by the Initial Developer are Copyright (C) 1998
+# the Initial Developer. All Rights Reserved.
+#
# Contributor(s):
-# Dan Blanchard
-# Ian Cordasco
+# Mark Pilgrim - port to Python
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -19,18 +25,23 @@
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
-import sys
+from .chardistribution import JOHABDistributionAnalysis
+from .codingstatemachine import CodingStateMachine
+from .mbcharsetprober import MultiByteCharSetProber
+from .mbcssm import JOHAB_SM_MODEL
+
+
+class JOHABProber(MultiByteCharSetProber):
+ def __init__(self):
+ super().__init__()
+ self.coding_sm = CodingStateMachine(JOHAB_SM_MODEL)
+ self.distribution_analyzer = JOHABDistributionAnalysis()
+ self.reset()
+ @property
+ def charset_name(self):
+ return "Johab"
-if sys.version_info < (3, 0):
- PY2 = True
- PY3 = False
- string_types = (str, unicode)
- text_type = unicode
- iteritems = dict.iteritems
-else:
- PY2 = False
- PY3 = True
- string_types = (bytes, str)
- text_type = str
- iteritems = dict.items
+ @property
+ def language(self):
+ return "Korean"
diff --git a/src/pip/_vendor/chardet/jpcntx.py b/src/pip/_vendor/chardet/jpcntx.py
index 20044e4bc..7a8e5be06 100644
--- a/src/pip/_vendor/chardet/jpcntx.py
+++ b/src/pip/_vendor/chardet/jpcntx.py
@@ -27,93 +27,96 @@
# This is hiragana 2-char sequence table, the number in each cell represents its frequency category
-jp2CharContext = (
-(0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1),
-(2,4,0,4,0,3,0,4,0,3,4,4,4,2,4,3,3,4,3,2,3,3,4,2,3,3,3,2,4,1,4,3,3,1,5,4,3,4,3,4,3,5,3,0,3,5,4,2,0,3,1,0,3,3,0,3,3,0,1,1,0,4,3,0,3,3,0,4,0,2,0,3,5,5,5,5,4,0,4,1,0,3,4),
-(0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2),
-(0,4,0,5,0,5,0,4,0,4,5,4,4,3,5,3,5,1,5,3,4,3,4,4,3,4,3,3,4,3,5,4,4,3,5,5,3,5,5,5,3,5,5,3,4,5,5,3,1,3,2,0,3,4,0,4,2,0,4,2,1,5,3,2,3,5,0,4,0,2,0,5,4,4,5,4,5,0,4,0,0,4,4),
-(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
-(0,3,0,4,0,3,0,3,0,4,5,4,3,3,3,3,4,3,5,4,4,3,5,4,4,3,4,3,4,4,4,4,5,3,4,4,3,4,5,5,4,5,5,1,4,5,4,3,0,3,3,1,3,3,0,4,4,0,3,3,1,5,3,3,3,5,0,4,0,3,0,4,4,3,4,3,3,0,4,1,1,3,4),
-(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
-(0,4,0,3,0,3,0,4,0,3,4,4,3,2,2,1,2,1,3,1,3,3,3,3,3,4,3,1,3,3,5,3,3,0,4,3,0,5,4,3,3,5,4,4,3,4,4,5,0,1,2,0,1,2,0,2,2,0,1,0,0,5,2,2,1,4,0,3,0,1,0,4,4,3,5,4,3,0,2,1,0,4,3),
-(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
-(0,3,0,5,0,4,0,2,1,4,4,2,4,1,4,2,4,2,4,3,3,3,4,3,3,3,3,1,4,2,3,3,3,1,4,4,1,1,1,4,3,3,2,0,2,4,3,2,0,3,3,0,3,1,1,0,0,0,3,3,0,4,2,2,3,4,0,4,0,3,0,4,4,5,3,4,4,0,3,0,0,1,4),
-(1,4,0,4,0,4,0,4,0,3,5,4,4,3,4,3,5,4,3,3,4,3,5,4,4,4,4,3,4,2,4,3,3,1,5,4,3,2,4,5,4,5,5,4,4,5,4,4,0,3,2,2,3,3,0,4,3,1,3,2,1,4,3,3,4,5,0,3,0,2,0,4,5,5,4,5,4,0,4,0,0,5,4),
-(0,5,0,5,0,4,0,3,0,4,4,3,4,3,3,3,4,0,4,4,4,3,4,3,4,3,3,1,4,2,4,3,4,0,5,4,1,4,5,4,4,5,3,2,4,3,4,3,2,4,1,3,3,3,2,3,2,0,4,3,3,4,3,3,3,4,0,4,0,3,0,4,5,4,4,4,3,0,4,1,0,1,3),
-(0,3,1,4,0,3,0,2,0,3,4,4,3,1,4,2,3,3,4,3,4,3,4,3,4,4,3,2,3,1,5,4,4,1,4,4,3,5,4,4,3,5,5,4,3,4,4,3,1,2,3,1,2,2,0,3,2,0,3,1,0,5,3,3,3,4,3,3,3,3,4,4,4,4,5,4,2,0,3,3,2,4,3),
-(0,2,0,3,0,1,0,1,0,0,3,2,0,0,2,0,1,0,2,1,3,3,3,1,2,3,1,0,1,0,4,2,1,1,3,3,0,4,3,3,1,4,3,3,0,3,3,2,0,0,0,0,1,0,0,2,0,0,0,0,0,4,1,0,2,3,2,2,2,1,3,3,3,4,4,3,2,0,3,1,0,3,3),
-(0,4,0,4,0,3,0,3,0,4,4,4,3,3,3,3,3,3,4,3,4,2,4,3,4,3,3,2,4,3,4,5,4,1,4,5,3,5,4,5,3,5,4,0,3,5,5,3,1,3,3,2,2,3,0,3,4,1,3,3,2,4,3,3,3,4,0,4,0,3,0,4,5,4,4,5,3,0,4,1,0,3,4),
-(0,2,0,3,0,3,0,0,0,2,2,2,1,0,1,0,0,0,3,0,3,0,3,0,1,3,1,0,3,1,3,3,3,1,3,3,3,0,1,3,1,3,4,0,0,3,1,1,0,3,2,0,0,0,0,1,3,0,1,0,0,3,3,2,0,3,0,0,0,0,0,3,4,3,4,3,3,0,3,0,0,2,3),
-(2,3,0,3,0,2,0,1,0,3,3,4,3,1,3,1,1,1,3,1,4,3,4,3,3,3,0,0,3,1,5,4,3,1,4,3,2,5,5,4,4,4,4,3,3,4,4,4,0,2,1,1,3,2,0,1,2,0,0,1,0,4,1,3,3,3,0,3,0,1,0,4,4,4,5,5,3,0,2,0,0,4,4),
-(0,2,0,1,0,3,1,3,0,2,3,3,3,0,3,1,0,0,3,0,3,2,3,1,3,2,1,1,0,0,4,2,1,0,2,3,1,4,3,2,0,4,4,3,1,3,1,3,0,1,0,0,1,0,0,0,1,0,0,0,0,4,1,1,1,2,0,3,0,0,0,3,4,2,4,3,2,0,1,0,0,3,3),
-(0,1,0,4,0,5,0,4,0,2,4,4,2,3,3,2,3,3,5,3,3,3,4,3,4,2,3,0,4,3,3,3,4,1,4,3,2,1,5,5,3,4,5,1,3,5,4,2,0,3,3,0,1,3,0,4,2,0,1,3,1,4,3,3,3,3,0,3,0,1,0,3,4,4,4,5,5,0,3,0,1,4,5),
-(0,2,0,3,0,3,0,0,0,2,3,1,3,0,4,0,1,1,3,0,3,4,3,2,3,1,0,3,3,2,3,1,3,0,2,3,0,2,1,4,1,2,2,0,0,3,3,0,0,2,0,0,0,1,0,0,0,0,2,2,0,3,2,1,3,3,0,2,0,2,0,0,3,3,1,2,4,0,3,0,2,2,3),
-(2,4,0,5,0,4,0,4,0,2,4,4,4,3,4,3,3,3,1,2,4,3,4,3,4,4,5,0,3,3,3,3,2,0,4,3,1,4,3,4,1,4,4,3,3,4,4,3,1,2,3,0,4,2,0,4,1,0,3,3,0,4,3,3,3,4,0,4,0,2,0,3,5,3,4,5,2,0,3,0,0,4,5),
-(0,3,0,4,0,1,0,1,0,1,3,2,2,1,3,0,3,0,2,0,2,0,3,0,2,0,0,0,1,0,1,1,0,0,3,1,0,0,0,4,0,3,1,0,2,1,3,0,0,0,0,0,0,3,0,0,0,0,0,0,0,4,2,2,3,1,0,3,0,0,0,1,4,4,4,3,0,0,4,0,0,1,4),
-(1,4,1,5,0,3,0,3,0,4,5,4,4,3,5,3,3,4,4,3,4,1,3,3,3,3,2,1,4,1,5,4,3,1,4,4,3,5,4,4,3,5,4,3,3,4,4,4,0,3,3,1,2,3,0,3,1,0,3,3,0,5,4,4,4,4,4,4,3,3,5,4,4,3,3,5,4,0,3,2,0,4,4),
-(0,2,0,3,0,1,0,0,0,1,3,3,3,2,4,1,3,0,3,1,3,0,2,2,1,1,0,0,2,0,4,3,1,0,4,3,0,4,4,4,1,4,3,1,1,3,3,1,0,2,0,0,1,3,0,0,0,0,2,0,0,4,3,2,4,3,5,4,3,3,3,4,3,3,4,3,3,0,2,1,0,3,3),
-(0,2,0,4,0,3,0,2,0,2,5,5,3,4,4,4,4,1,4,3,3,0,4,3,4,3,1,3,3,2,4,3,0,3,4,3,0,3,4,4,2,4,4,0,4,5,3,3,2,2,1,1,1,2,0,1,5,0,3,3,2,4,3,3,3,4,0,3,0,2,0,4,4,3,5,5,0,0,3,0,2,3,3),
-(0,3,0,4,0,3,0,1,0,3,4,3,3,1,3,3,3,0,3,1,3,0,4,3,3,1,1,0,3,0,3,3,0,0,4,4,0,1,5,4,3,3,5,0,3,3,4,3,0,2,0,1,1,1,0,1,3,0,1,2,1,3,3,2,3,3,0,3,0,1,0,1,3,3,4,4,1,0,1,2,2,1,3),
-(0,1,0,4,0,4,0,3,0,1,3,3,3,2,3,1,1,0,3,0,3,3,4,3,2,4,2,0,1,0,4,3,2,0,4,3,0,5,3,3,2,4,4,4,3,3,3,4,0,1,3,0,0,1,0,0,1,0,0,0,0,4,2,3,3,3,0,3,0,0,0,4,4,4,5,3,2,0,3,3,0,3,5),
-(0,2,0,3,0,0,0,3,0,1,3,0,2,0,0,0,1,0,3,1,1,3,3,0,0,3,0,0,3,0,2,3,1,0,3,1,0,3,3,2,0,4,2,2,0,2,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,2,1,2,0,1,0,1,0,0,0,1,3,1,2,0,0,0,1,0,0,1,4),
-(0,3,0,3,0,5,0,1,0,2,4,3,1,3,3,2,1,1,5,2,1,0,5,1,2,0,0,0,3,3,2,2,3,2,4,3,0,0,3,3,1,3,3,0,2,5,3,4,0,3,3,0,1,2,0,2,2,0,3,2,0,2,2,3,3,3,0,2,0,1,0,3,4,4,2,5,4,0,3,0,0,3,5),
-(0,3,0,3,0,3,0,1,0,3,3,3,3,0,3,0,2,0,2,1,1,0,2,0,1,0,0,0,2,1,0,0,1,0,3,2,0,0,3,3,1,2,3,1,0,3,3,0,0,1,0,0,0,0,0,2,0,0,0,0,0,2,3,1,2,3,0,3,0,1,0,3,2,1,0,4,3,0,1,1,0,3,3),
-(0,4,0,5,0,3,0,3,0,4,5,5,4,3,5,3,4,3,5,3,3,2,5,3,4,4,4,3,4,3,4,5,5,3,4,4,3,4,4,5,4,4,4,3,4,5,5,4,2,3,4,2,3,4,0,3,3,1,4,3,2,4,3,3,5,5,0,3,0,3,0,5,5,5,5,4,4,0,4,0,1,4,4),
-(0,4,0,4,0,3,0,3,0,3,5,4,4,2,3,2,5,1,3,2,5,1,4,2,3,2,3,3,4,3,3,3,3,2,5,4,1,3,3,5,3,4,4,0,4,4,3,1,1,3,1,0,2,3,0,2,3,0,3,0,0,4,3,1,3,4,0,3,0,2,0,4,4,4,3,4,5,0,4,0,0,3,4),
-(0,3,0,3,0,3,1,2,0,3,4,4,3,3,3,0,2,2,4,3,3,1,3,3,3,1,1,0,3,1,4,3,2,3,4,4,2,4,4,4,3,4,4,3,2,4,4,3,1,3,3,1,3,3,0,4,1,0,2,2,1,4,3,2,3,3,5,4,3,3,5,4,4,3,3,0,4,0,3,2,2,4,4),
-(0,2,0,1,0,0,0,0,0,1,2,1,3,0,0,0,0,0,2,0,1,2,1,0,0,1,0,0,0,0,3,0,0,1,0,1,1,3,1,0,0,0,1,1,0,1,1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,1,1,2,2,0,3,4,0,0,0,1,1,0,0,1,0,0,0,0,0,1,1),
-(0,1,0,0,0,1,0,0,0,0,4,0,4,1,4,0,3,0,4,0,3,0,4,0,3,0,3,0,4,1,5,1,4,0,0,3,0,5,0,5,2,0,1,0,0,0,2,1,4,0,1,3,0,0,3,0,0,3,1,1,4,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0),
-(1,4,0,5,0,3,0,2,0,3,5,4,4,3,4,3,5,3,4,3,3,0,4,3,3,3,3,3,3,2,4,4,3,1,3,4,4,5,4,4,3,4,4,1,3,5,4,3,3,3,1,2,2,3,3,1,3,1,3,3,3,5,3,3,4,5,0,3,0,3,0,3,4,3,4,4,3,0,3,0,2,4,3),
-(0,1,0,4,0,0,0,0,0,1,4,0,4,1,4,2,4,0,3,0,1,0,1,0,0,0,0,0,2,0,3,1,1,1,0,3,0,0,0,1,2,1,0,0,1,1,1,1,0,1,0,0,0,1,0,0,3,0,0,0,0,3,2,0,2,2,0,1,0,0,0,2,3,2,3,3,0,0,0,0,2,1,0),
-(0,5,1,5,0,3,0,3,0,5,4,4,5,1,5,3,3,0,4,3,4,3,5,3,4,3,3,2,4,3,4,3,3,0,3,3,1,4,4,3,4,4,4,3,4,5,5,3,2,3,1,1,3,3,1,3,1,1,3,3,2,4,5,3,3,5,0,4,0,3,0,4,4,3,5,3,3,0,3,4,0,4,3),
-(0,5,0,5,0,3,0,2,0,4,4,3,5,2,4,3,3,3,4,4,4,3,5,3,5,3,3,1,4,0,4,3,3,0,3,3,0,4,4,4,4,5,4,3,3,5,5,3,2,3,1,2,3,2,0,1,0,0,3,2,2,4,4,3,1,5,0,4,0,3,0,4,3,1,3,2,1,0,3,3,0,3,3),
-(0,4,0,5,0,5,0,4,0,4,5,5,5,3,4,3,3,2,5,4,4,3,5,3,5,3,4,0,4,3,4,4,3,2,4,4,3,4,5,4,4,5,5,0,3,5,5,4,1,3,3,2,3,3,1,3,1,0,4,3,1,4,4,3,4,5,0,4,0,2,0,4,3,4,4,3,3,0,4,0,0,5,5),
-(0,4,0,4,0,5,0,1,1,3,3,4,4,3,4,1,3,0,5,1,3,0,3,1,3,1,1,0,3,0,3,3,4,0,4,3,0,4,4,4,3,4,4,0,3,5,4,1,0,3,0,0,2,3,0,3,1,0,3,1,0,3,2,1,3,5,0,3,0,1,0,3,2,3,3,4,4,0,2,2,0,4,4),
-(2,4,0,5,0,4,0,3,0,4,5,5,4,3,5,3,5,3,5,3,5,2,5,3,4,3,3,4,3,4,5,3,2,1,5,4,3,2,3,4,5,3,4,1,2,5,4,3,0,3,3,0,3,2,0,2,3,0,4,1,0,3,4,3,3,5,0,3,0,1,0,4,5,5,5,4,3,0,4,2,0,3,5),
-(0,5,0,4,0,4,0,2,0,5,4,3,4,3,4,3,3,3,4,3,4,2,5,3,5,3,4,1,4,3,4,4,4,0,3,5,0,4,4,4,4,5,3,1,3,4,5,3,3,3,3,3,3,3,0,2,2,0,3,3,2,4,3,3,3,5,3,4,1,3,3,5,3,2,0,0,0,0,4,3,1,3,3),
-(0,1,0,3,0,3,0,1,0,1,3,3,3,2,3,3,3,0,3,0,0,0,3,1,3,0,0,0,2,2,2,3,0,0,3,2,0,1,2,4,1,3,3,0,0,3,3,3,0,1,0,0,2,1,0,0,3,0,3,1,0,3,0,0,1,3,0,2,0,1,0,3,3,1,3,3,0,0,1,1,0,3,3),
-(0,2,0,3,0,2,1,4,0,2,2,3,1,1,3,1,1,0,2,0,3,1,2,3,1,3,0,0,1,0,4,3,2,3,3,3,1,4,2,3,3,3,3,1,0,3,1,4,0,1,1,0,1,2,0,1,1,0,1,1,0,3,1,3,2,2,0,1,0,0,0,2,3,3,3,1,0,0,0,0,0,2,3),
-(0,5,0,4,0,5,0,2,0,4,5,5,3,3,4,3,3,1,5,4,4,2,4,4,4,3,4,2,4,3,5,5,4,3,3,4,3,3,5,5,4,5,5,1,3,4,5,3,1,4,3,1,3,3,0,3,3,1,4,3,1,4,5,3,3,5,0,4,0,3,0,5,3,3,1,4,3,0,4,0,1,5,3),
-(0,5,0,5,0,4,0,2,0,4,4,3,4,3,3,3,3,3,5,4,4,4,4,4,4,5,3,3,5,2,4,4,4,3,4,4,3,3,4,4,5,5,3,3,4,3,4,3,3,4,3,3,3,3,1,2,2,1,4,3,3,5,4,4,3,4,0,4,0,3,0,4,4,4,4,4,1,0,4,2,0,2,4),
-(0,4,0,4,0,3,0,1,0,3,5,2,3,0,3,0,2,1,4,2,3,3,4,1,4,3,3,2,4,1,3,3,3,0,3,3,0,0,3,3,3,5,3,3,3,3,3,2,0,2,0,0,2,0,0,2,0,0,1,0,0,3,1,2,2,3,0,3,0,2,0,4,4,3,3,4,1,0,3,0,0,2,4),
-(0,0,0,4,0,0,0,0,0,0,1,0,1,0,2,0,0,0,0,0,1,0,2,0,1,0,0,0,0,0,3,1,3,0,3,2,0,0,0,1,0,3,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,4,0,2,0,0,0,0,0,0,2),
-(0,2,1,3,0,2,0,2,0,3,3,3,3,1,3,1,3,3,3,3,3,3,4,2,2,1,2,1,4,0,4,3,1,3,3,3,2,4,3,5,4,3,3,3,3,3,3,3,0,1,3,0,2,0,0,1,0,0,1,0,0,4,2,0,2,3,0,3,3,0,3,3,4,2,3,1,4,0,1,2,0,2,3),
-(0,3,0,3,0,1,0,3,0,2,3,3,3,0,3,1,2,0,3,3,2,3,3,2,3,2,3,1,3,0,4,3,2,0,3,3,1,4,3,3,2,3,4,3,1,3,3,1,1,0,1,1,0,1,0,1,0,1,0,0,0,4,1,1,0,3,0,3,1,0,2,3,3,3,3,3,1,0,0,2,0,3,3),
-(0,0,0,0,0,0,0,0,0,0,3,0,2,0,3,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,3,0,3,0,3,1,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,2,0,2,3,0,0,0,0,0,0,0,0,3),
-(0,2,0,3,1,3,0,3,0,2,3,3,3,1,3,1,3,1,3,1,3,3,3,1,3,0,2,3,1,1,4,3,3,2,3,3,1,2,2,4,1,3,3,0,1,4,2,3,0,1,3,0,3,0,0,1,3,0,2,0,0,3,3,2,1,3,0,3,0,2,0,3,4,4,4,3,1,0,3,0,0,3,3),
-(0,2,0,1,0,2,0,0,0,1,3,2,2,1,3,0,1,1,3,0,3,2,3,1,2,0,2,0,1,1,3,3,3,0,3,3,1,1,2,3,2,3,3,1,2,3,2,0,0,1,0,0,0,0,0,0,3,0,1,0,0,2,1,2,1,3,0,3,0,0,0,3,4,4,4,3,2,0,2,0,0,2,4),
-(0,0,0,1,0,1,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,2,2,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,3,1,0,0,0,0,0,0,0,3),
-(0,3,0,3,0,2,0,3,0,3,3,3,2,3,2,2,2,0,3,1,3,3,3,2,3,3,0,0,3,0,3,2,2,0,2,3,1,4,3,4,3,3,2,3,1,5,4,4,0,3,1,2,1,3,0,3,1,1,2,0,2,3,1,3,1,3,0,3,0,1,0,3,3,4,4,2,1,0,2,1,0,2,4),
-(0,1,0,3,0,1,0,2,0,1,4,2,5,1,4,0,2,0,2,1,3,1,4,0,2,1,0,0,2,1,4,1,1,0,3,3,0,5,1,3,2,3,3,1,0,3,2,3,0,1,0,0,0,0,0,0,1,0,0,0,0,4,0,1,0,3,0,2,0,1,0,3,3,3,4,3,3,0,0,0,0,2,3),
-(0,0,0,1,0,0,0,0,0,0,2,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,0,1,0,0,0,0,0,3),
-(0,1,0,3,0,4,0,3,0,2,4,3,1,0,3,2,2,1,3,1,2,2,3,1,1,1,2,1,3,0,1,2,0,1,3,2,1,3,0,5,5,1,0,0,1,3,2,1,0,3,0,0,1,0,0,0,0,0,3,4,0,1,1,1,3,2,0,2,0,1,0,2,3,3,1,2,3,0,1,0,1,0,4),
-(0,0,0,1,0,3,0,3,0,2,2,1,0,0,4,0,3,0,3,1,3,0,3,0,3,0,1,0,3,0,3,1,3,0,3,3,0,0,1,2,1,1,1,0,1,2,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,2,2,1,2,0,0,2,0,0,0,0,2,3,3,3,3,0,0,0,0,1,4),
-(0,0,0,3,0,3,0,0,0,0,3,1,1,0,3,0,1,0,2,0,1,0,0,0,0,0,0,0,1,0,3,0,2,0,2,3,0,0,2,2,3,1,2,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,2,0,0,0,0,2,3),
-(2,4,0,5,0,5,0,4,0,3,4,3,3,3,4,3,3,3,4,3,4,4,5,4,5,5,5,2,3,0,5,5,4,1,5,4,3,1,5,4,3,4,4,3,3,4,3,3,0,3,2,0,2,3,0,3,0,0,3,3,0,5,3,2,3,3,0,3,0,3,0,3,4,5,4,5,3,0,4,3,0,3,4),
-(0,3,0,3,0,3,0,3,0,3,3,4,3,2,3,2,3,0,4,3,3,3,3,3,3,3,3,0,3,2,4,3,3,1,3,4,3,4,4,4,3,4,4,3,2,4,4,1,0,2,0,0,1,1,0,2,0,0,3,1,0,5,3,2,1,3,0,3,0,1,2,4,3,2,4,3,3,0,3,2,0,4,4),
-(0,3,0,3,0,1,0,0,0,1,4,3,3,2,3,1,3,1,4,2,3,2,4,2,3,4,3,0,2,2,3,3,3,0,3,3,3,0,3,4,1,3,3,0,3,4,3,3,0,1,1,0,1,0,0,0,4,0,3,0,0,3,1,2,1,3,0,4,0,1,0,4,3,3,4,3,3,0,2,0,0,3,3),
-(0,3,0,4,0,1,0,3,0,3,4,3,3,0,3,3,3,1,3,1,3,3,4,3,3,3,0,0,3,1,5,3,3,1,3,3,2,5,4,3,3,4,5,3,2,5,3,4,0,1,0,0,0,0,0,2,0,0,1,1,0,4,2,2,1,3,0,3,0,2,0,4,4,3,5,3,2,0,1,1,0,3,4),
-(0,5,0,4,0,5,0,2,0,4,4,3,3,2,3,3,3,1,4,3,4,1,5,3,4,3,4,0,4,2,4,3,4,1,5,4,0,4,4,4,4,5,4,1,3,5,4,2,1,4,1,1,3,2,0,3,1,0,3,2,1,4,3,3,3,4,0,4,0,3,0,4,4,4,3,3,3,0,4,2,0,3,4),
-(1,4,0,4,0,3,0,1,0,3,3,3,1,1,3,3,2,2,3,3,1,0,3,2,2,1,2,0,3,1,2,1,2,0,3,2,0,2,2,3,3,4,3,0,3,3,1,2,0,1,1,3,1,2,0,0,3,0,1,1,0,3,2,2,3,3,0,3,0,0,0,2,3,3,4,3,3,0,1,0,0,1,4),
-(0,4,0,4,0,4,0,0,0,3,4,4,3,1,4,2,3,2,3,3,3,1,4,3,4,0,3,0,4,2,3,3,2,2,5,4,2,1,3,4,3,4,3,1,3,3,4,2,0,2,1,0,3,3,0,0,2,0,3,1,0,4,4,3,4,3,0,4,0,1,0,2,4,4,4,4,4,0,3,2,0,3,3),
-(0,0,0,1,0,4,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,3,2,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,2),
-(0,2,0,3,0,4,0,4,0,1,3,3,3,0,4,0,2,1,2,1,1,1,2,0,3,1,1,0,1,0,3,1,0,0,3,3,2,0,1,1,0,0,0,0,0,1,0,2,0,2,2,0,3,1,0,0,1,0,1,1,0,1,2,0,3,0,0,0,0,1,0,0,3,3,4,3,1,0,1,0,3,0,2),
-(0,0,0,3,0,5,0,0,0,0,1,0,2,0,3,1,0,1,3,0,0,0,2,0,0,0,1,0,0,0,1,1,0,0,4,0,0,0,2,3,0,1,4,1,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,1,0,0,0,0,0,0,0,2,0,0,3,0,0,0,0,0,3),
-(0,2,0,5,0,5,0,1,0,2,4,3,3,2,5,1,3,2,3,3,3,0,4,1,2,0,3,0,4,0,2,2,1,1,5,3,0,0,1,4,2,3,2,0,3,3,3,2,0,2,4,1,1,2,0,1,1,0,3,1,0,1,3,1,2,3,0,2,0,0,0,1,3,5,4,4,4,0,3,0,0,1,3),
-(0,4,0,5,0,4,0,4,0,4,5,4,3,3,4,3,3,3,4,3,4,4,5,3,4,5,4,2,4,2,3,4,3,1,4,4,1,3,5,4,4,5,5,4,4,5,5,5,2,3,3,1,4,3,1,3,3,0,3,3,1,4,3,4,4,4,0,3,0,4,0,3,3,4,4,5,0,0,4,3,0,4,5),
-(0,4,0,4,0,3,0,3,0,3,4,4,4,3,3,2,4,3,4,3,4,3,5,3,4,3,2,1,4,2,4,4,3,1,3,4,2,4,5,5,3,4,5,4,1,5,4,3,0,3,2,2,3,2,1,3,1,0,3,3,3,5,3,3,3,5,4,4,2,3,3,4,3,3,3,2,1,0,3,2,1,4,3),
-(0,4,0,5,0,4,0,3,0,3,5,5,3,2,4,3,4,0,5,4,4,1,4,4,4,3,3,3,4,3,5,5,2,3,3,4,1,2,5,5,3,5,5,2,3,5,5,4,0,3,2,0,3,3,1,1,5,1,4,1,0,4,3,2,3,5,0,4,0,3,0,5,4,3,4,3,0,0,4,1,0,4,4),
-(1,3,0,4,0,2,0,2,0,2,5,5,3,3,3,3,3,0,4,2,3,4,4,4,3,4,0,0,3,4,5,4,3,3,3,3,2,5,5,4,5,5,5,4,3,5,5,5,1,3,1,0,1,0,0,3,2,0,4,2,0,5,2,3,2,4,1,3,0,3,0,4,5,4,5,4,3,0,4,2,0,5,4),
-(0,3,0,4,0,5,0,3,0,3,4,4,3,2,3,2,3,3,3,3,3,2,4,3,3,2,2,0,3,3,3,3,3,1,3,3,3,0,4,4,3,4,4,1,1,4,4,2,0,3,1,0,1,1,0,4,1,0,2,3,1,3,3,1,3,4,0,3,0,1,0,3,1,3,0,0,1,0,2,0,0,4,4),
-(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
-(0,3,0,3,0,2,0,3,0,1,5,4,3,3,3,1,4,2,1,2,3,4,4,2,4,4,5,0,3,1,4,3,4,0,4,3,3,3,2,3,2,5,3,4,3,2,2,3,0,0,3,0,2,1,0,1,2,0,0,0,0,2,1,1,3,1,0,2,0,4,0,3,4,4,4,5,2,0,2,0,0,1,3),
-(0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,1,0,0,1,1,0,0,0,4,2,1,1,0,1,0,3,2,0,0,3,1,1,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,1,0,0,0,2,0,0,0,1,4,0,4,2,1,0,0,0,0,0,1),
-(0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,1,0,0,0,0,0,0,1,0,1,0,0,0,0,3,1,0,0,0,2,0,2,1,0,0,1,2,1,0,1,1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,1,3,1,0,0,0,0,0,1,0,0,2,1,0,0,0,0,0,0,0,0,2),
-(0,4,0,4,0,4,0,3,0,4,4,3,4,2,4,3,2,0,4,4,4,3,5,3,5,3,3,2,4,2,4,3,4,3,1,4,0,2,3,4,4,4,3,3,3,4,4,4,3,4,1,3,4,3,2,1,2,1,3,3,3,4,4,3,3,5,0,4,0,3,0,4,3,3,3,2,1,0,3,0,0,3,3),
-(0,4,0,3,0,3,0,3,0,3,5,5,3,3,3,3,4,3,4,3,3,3,4,4,4,3,3,3,3,4,3,5,3,3,1,3,2,4,5,5,5,5,4,3,4,5,5,3,2,2,3,3,3,3,2,3,3,1,2,3,2,4,3,3,3,4,0,4,0,2,0,4,3,2,2,1,2,0,3,0,0,4,1),
+# fmt: off
+jp2_char_context = (
+ (0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
+ (2, 4, 0, 4, 0, 3, 0, 4, 0, 3, 4, 4, 4, 2, 4, 3, 3, 4, 3, 2, 3, 3, 4, 2, 3, 3, 3, 2, 4, 1, 4, 3, 3, 1, 5, 4, 3, 4, 3, 4, 3, 5, 3, 0, 3, 5, 4, 2, 0, 3, 1, 0, 3, 3, 0, 3, 3, 0, 1, 1, 0, 4, 3, 0, 3, 3, 0, 4, 0, 2, 0, 3, 5, 5, 5, 5, 4, 0, 4, 1, 0, 3, 4),
+ (0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2),
+ (0, 4, 0, 5, 0, 5, 0, 4, 0, 4, 5, 4, 4, 3, 5, 3, 5, 1, 5, 3, 4, 3, 4, 4, 3, 4, 3, 3, 4, 3, 5, 4, 4, 3, 5, 5, 3, 5, 5, 5, 3, 5, 5, 3, 4, 5, 5, 3, 1, 3, 2, 0, 3, 4, 0, 4, 2, 0, 4, 2, 1, 5, 3, 2, 3, 5, 0, 4, 0, 2, 0, 5, 4, 4, 5, 4, 5, 0, 4, 0, 0, 4, 4),
+ (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+ (0, 3, 0, 4, 0, 3, 0, 3, 0, 4, 5, 4, 3, 3, 3, 3, 4, 3, 5, 4, 4, 3, 5, 4, 4, 3, 4, 3, 4, 4, 4, 4, 5, 3, 4, 4, 3, 4, 5, 5, 4, 5, 5, 1, 4, 5, 4, 3, 0, 3, 3, 1, 3, 3, 0, 4, 4, 0, 3, 3, 1, 5, 3, 3, 3, 5, 0, 4, 0, 3, 0, 4, 4, 3, 4, 3, 3, 0, 4, 1, 1, 3, 4),
+ (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+ (0, 4, 0, 3, 0, 3, 0, 4, 0, 3, 4, 4, 3, 2, 2, 1, 2, 1, 3, 1, 3, 3, 3, 3, 3, 4, 3, 1, 3, 3, 5, 3, 3, 0, 4, 3, 0, 5, 4, 3, 3, 5, 4, 4, 3, 4, 4, 5, 0, 1, 2, 0, 1, 2, 0, 2, 2, 0, 1, 0, 0, 5, 2, 2, 1, 4, 0, 3, 0, 1, 0, 4, 4, 3, 5, 4, 3, 0, 2, 1, 0, 4, 3),
+ (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+ (0, 3, 0, 5, 0, 4, 0, 2, 1, 4, 4, 2, 4, 1, 4, 2, 4, 2, 4, 3, 3, 3, 4, 3, 3, 3, 3, 1, 4, 2, 3, 3, 3, 1, 4, 4, 1, 1, 1, 4, 3, 3, 2, 0, 2, 4, 3, 2, 0, 3, 3, 0, 3, 1, 1, 0, 0, 0, 3, 3, 0, 4, 2, 2, 3, 4, 0, 4, 0, 3, 0, 4, 4, 5, 3, 4, 4, 0, 3, 0, 0, 1, 4),
+ (1, 4, 0, 4, 0, 4, 0, 4, 0, 3, 5, 4, 4, 3, 4, 3, 5, 4, 3, 3, 4, 3, 5, 4, 4, 4, 4, 3, 4, 2, 4, 3, 3, 1, 5, 4, 3, 2, 4, 5, 4, 5, 5, 4, 4, 5, 4, 4, 0, 3, 2, 2, 3, 3, 0, 4, 3, 1, 3, 2, 1, 4, 3, 3, 4, 5, 0, 3, 0, 2, 0, 4, 5, 5, 4, 5, 4, 0, 4, 0, 0, 5, 4),
+ (0, 5, 0, 5, 0, 4, 0, 3, 0, 4, 4, 3, 4, 3, 3, 3, 4, 0, 4, 4, 4, 3, 4, 3, 4, 3, 3, 1, 4, 2, 4, 3, 4, 0, 5, 4, 1, 4, 5, 4, 4, 5, 3, 2, 4, 3, 4, 3, 2, 4, 1, 3, 3, 3, 2, 3, 2, 0, 4, 3, 3, 4, 3, 3, 3, 4, 0, 4, 0, 3, 0, 4, 5, 4, 4, 4, 3, 0, 4, 1, 0, 1, 3),
+ (0, 3, 1, 4, 0, 3, 0, 2, 0, 3, 4, 4, 3, 1, 4, 2, 3, 3, 4, 3, 4, 3, 4, 3, 4, 4, 3, 2, 3, 1, 5, 4, 4, 1, 4, 4, 3, 5, 4, 4, 3, 5, 5, 4, 3, 4, 4, 3, 1, 2, 3, 1, 2, 2, 0, 3, 2, 0, 3, 1, 0, 5, 3, 3, 3, 4, 3, 3, 3, 3, 4, 4, 4, 4, 5, 4, 2, 0, 3, 3, 2, 4, 3),
+ (0, 2, 0, 3, 0, 1, 0, 1, 0, 0, 3, 2, 0, 0, 2, 0, 1, 0, 2, 1, 3, 3, 3, 1, 2, 3, 1, 0, 1, 0, 4, 2, 1, 1, 3, 3, 0, 4, 3, 3, 1, 4, 3, 3, 0, 3, 3, 2, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 4, 1, 0, 2, 3, 2, 2, 2, 1, 3, 3, 3, 4, 4, 3, 2, 0, 3, 1, 0, 3, 3),
+ (0, 4, 0, 4, 0, 3, 0, 3, 0, 4, 4, 4, 3, 3, 3, 3, 3, 3, 4, 3, 4, 2, 4, 3, 4, 3, 3, 2, 4, 3, 4, 5, 4, 1, 4, 5, 3, 5, 4, 5, 3, 5, 4, 0, 3, 5, 5, 3, 1, 3, 3, 2, 2, 3, 0, 3, 4, 1, 3, 3, 2, 4, 3, 3, 3, 4, 0, 4, 0, 3, 0, 4, 5, 4, 4, 5, 3, 0, 4, 1, 0, 3, 4),
+ (0, 2, 0, 3, 0, 3, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 3, 0, 3, 0, 3, 0, 1, 3, 1, 0, 3, 1, 3, 3, 3, 1, 3, 3, 3, 0, 1, 3, 1, 3, 4, 0, 0, 3, 1, 1, 0, 3, 2, 0, 0, 0, 0, 1, 3, 0, 1, 0, 0, 3, 3, 2, 0, 3, 0, 0, 0, 0, 0, 3, 4, 3, 4, 3, 3, 0, 3, 0, 0, 2, 3),
+ (2, 3, 0, 3, 0, 2, 0, 1, 0, 3, 3, 4, 3, 1, 3, 1, 1, 1, 3, 1, 4, 3, 4, 3, 3, 3, 0, 0, 3, 1, 5, 4, 3, 1, 4, 3, 2, 5, 5, 4, 4, 4, 4, 3, 3, 4, 4, 4, 0, 2, 1, 1, 3, 2, 0, 1, 2, 0, 0, 1, 0, 4, 1, 3, 3, 3, 0, 3, 0, 1, 0, 4, 4, 4, 5, 5, 3, 0, 2, 0, 0, 4, 4),
+ (0, 2, 0, 1, 0, 3, 1, 3, 0, 2, 3, 3, 3, 0, 3, 1, 0, 0, 3, 0, 3, 2, 3, 1, 3, 2, 1, 1, 0, 0, 4, 2, 1, 0, 2, 3, 1, 4, 3, 2, 0, 4, 4, 3, 1, 3, 1, 3, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 4, 1, 1, 1, 2, 0, 3, 0, 0, 0, 3, 4, 2, 4, 3, 2, 0, 1, 0, 0, 3, 3),
+ (0, 1, 0, 4, 0, 5, 0, 4, 0, 2, 4, 4, 2, 3, 3, 2, 3, 3, 5, 3, 3, 3, 4, 3, 4, 2, 3, 0, 4, 3, 3, 3, 4, 1, 4, 3, 2, 1, 5, 5, 3, 4, 5, 1, 3, 5, 4, 2, 0, 3, 3, 0, 1, 3, 0, 4, 2, 0, 1, 3, 1, 4, 3, 3, 3, 3, 0, 3, 0, 1, 0, 3, 4, 4, 4, 5, 5, 0, 3, 0, 1, 4, 5),
+ (0, 2, 0, 3, 0, 3, 0, 0, 0, 2, 3, 1, 3, 0, 4, 0, 1, 1, 3, 0, 3, 4, 3, 2, 3, 1, 0, 3, 3, 2, 3, 1, 3, 0, 2, 3, 0, 2, 1, 4, 1, 2, 2, 0, 0, 3, 3, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 2, 2, 0, 3, 2, 1, 3, 3, 0, 2, 0, 2, 0, 0, 3, 3, 1, 2, 4, 0, 3, 0, 2, 2, 3),
+ (2, 4, 0, 5, 0, 4, 0, 4, 0, 2, 4, 4, 4, 3, 4, 3, 3, 3, 1, 2, 4, 3, 4, 3, 4, 4, 5, 0, 3, 3, 3, 3, 2, 0, 4, 3, 1, 4, 3, 4, 1, 4, 4, 3, 3, 4, 4, 3, 1, 2, 3, 0, 4, 2, 0, 4, 1, 0, 3, 3, 0, 4, 3, 3, 3, 4, 0, 4, 0, 2, 0, 3, 5, 3, 4, 5, 2, 0, 3, 0, 0, 4, 5),
+ (0, 3, 0, 4, 0, 1, 0, 1, 0, 1, 3, 2, 2, 1, 3, 0, 3, 0, 2, 0, 2, 0, 3, 0, 2, 0, 0, 0, 1, 0, 1, 1, 0, 0, 3, 1, 0, 0, 0, 4, 0, 3, 1, 0, 2, 1, 3, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 4, 2, 2, 3, 1, 0, 3, 0, 0, 0, 1, 4, 4, 4, 3, 0, 0, 4, 0, 0, 1, 4),
+ (1, 4, 1, 5, 0, 3, 0, 3, 0, 4, 5, 4, 4, 3, 5, 3, 3, 4, 4, 3, 4, 1, 3, 3, 3, 3, 2, 1, 4, 1, 5, 4, 3, 1, 4, 4, 3, 5, 4, 4, 3, 5, 4, 3, 3, 4, 4, 4, 0, 3, 3, 1, 2, 3, 0, 3, 1, 0, 3, 3, 0, 5, 4, 4, 4, 4, 4, 4, 3, 3, 5, 4, 4, 3, 3, 5, 4, 0, 3, 2, 0, 4, 4),
+ (0, 2, 0, 3, 0, 1, 0, 0, 0, 1, 3, 3, 3, 2, 4, 1, 3, 0, 3, 1, 3, 0, 2, 2, 1, 1, 0, 0, 2, 0, 4, 3, 1, 0, 4, 3, 0, 4, 4, 4, 1, 4, 3, 1, 1, 3, 3, 1, 0, 2, 0, 0, 1, 3, 0, 0, 0, 0, 2, 0, 0, 4, 3, 2, 4, 3, 5, 4, 3, 3, 3, 4, 3, 3, 4, 3, 3, 0, 2, 1, 0, 3, 3),
+ (0, 2, 0, 4, 0, 3, 0, 2, 0, 2, 5, 5, 3, 4, 4, 4, 4, 1, 4, 3, 3, 0, 4, 3, 4, 3, 1, 3, 3, 2, 4, 3, 0, 3, 4, 3, 0, 3, 4, 4, 2, 4, 4, 0, 4, 5, 3, 3, 2, 2, 1, 1, 1, 2, 0, 1, 5, 0, 3, 3, 2, 4, 3, 3, 3, 4, 0, 3, 0, 2, 0, 4, 4, 3, 5, 5, 0, 0, 3, 0, 2, 3, 3),
+ (0, 3, 0, 4, 0, 3, 0, 1, 0, 3, 4, 3, 3, 1, 3, 3, 3, 0, 3, 1, 3, 0, 4, 3, 3, 1, 1, 0, 3, 0, 3, 3, 0, 0, 4, 4, 0, 1, 5, 4, 3, 3, 5, 0, 3, 3, 4, 3, 0, 2, 0, 1, 1, 1, 0, 1, 3, 0, 1, 2, 1, 3, 3, 2, 3, 3, 0, 3, 0, 1, 0, 1, 3, 3, 4, 4, 1, 0, 1, 2, 2, 1, 3),
+ (0, 1, 0, 4, 0, 4, 0, 3, 0, 1, 3, 3, 3, 2, 3, 1, 1, 0, 3, 0, 3, 3, 4, 3, 2, 4, 2, 0, 1, 0, 4, 3, 2, 0, 4, 3, 0, 5, 3, 3, 2, 4, 4, 4, 3, 3, 3, 4, 0, 1, 3, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 4, 2, 3, 3, 3, 0, 3, 0, 0, 0, 4, 4, 4, 5, 3, 2, 0, 3, 3, 0, 3, 5),
+ (0, 2, 0, 3, 0, 0, 0, 3, 0, 1, 3, 0, 2, 0, 0, 0, 1, 0, 3, 1, 1, 3, 3, 0, 0, 3, 0, 0, 3, 0, 2, 3, 1, 0, 3, 1, 0, 3, 3, 2, 0, 4, 2, 2, 0, 2, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 2, 0, 1, 0, 1, 0, 0, 0, 1, 3, 1, 2, 0, 0, 0, 1, 0, 0, 1, 4),
+ (0, 3, 0, 3, 0, 5, 0, 1, 0, 2, 4, 3, 1, 3, 3, 2, 1, 1, 5, 2, 1, 0, 5, 1, 2, 0, 0, 0, 3, 3, 2, 2, 3, 2, 4, 3, 0, 0, 3, 3, 1, 3, 3, 0, 2, 5, 3, 4, 0, 3, 3, 0, 1, 2, 0, 2, 2, 0, 3, 2, 0, 2, 2, 3, 3, 3, 0, 2, 0, 1, 0, 3, 4, 4, 2, 5, 4, 0, 3, 0, 0, 3, 5),
+ (0, 3, 0, 3, 0, 3, 0, 1, 0, 3, 3, 3, 3, 0, 3, 0, 2, 0, 2, 1, 1, 0, 2, 0, 1, 0, 0, 0, 2, 1, 0, 0, 1, 0, 3, 2, 0, 0, 3, 3, 1, 2, 3, 1, 0, 3, 3, 0, 0, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 2, 3, 1, 2, 3, 0, 3, 0, 1, 0, 3, 2, 1, 0, 4, 3, 0, 1, 1, 0, 3, 3),
+ (0, 4, 0, 5, 0, 3, 0, 3, 0, 4, 5, 5, 4, 3, 5, 3, 4, 3, 5, 3, 3, 2, 5, 3, 4, 4, 4, 3, 4, 3, 4, 5, 5, 3, 4, 4, 3, 4, 4, 5, 4, 4, 4, 3, 4, 5, 5, 4, 2, 3, 4, 2, 3, 4, 0, 3, 3, 1, 4, 3, 2, 4, 3, 3, 5, 5, 0, 3, 0, 3, 0, 5, 5, 5, 5, 4, 4, 0, 4, 0, 1, 4, 4),
+ (0, 4, 0, 4, 0, 3, 0, 3, 0, 3, 5, 4, 4, 2, 3, 2, 5, 1, 3, 2, 5, 1, 4, 2, 3, 2, 3, 3, 4, 3, 3, 3, 3, 2, 5, 4, 1, 3, 3, 5, 3, 4, 4, 0, 4, 4, 3, 1, 1, 3, 1, 0, 2, 3, 0, 2, 3, 0, 3, 0, 0, 4, 3, 1, 3, 4, 0, 3, 0, 2, 0, 4, 4, 4, 3, 4, 5, 0, 4, 0, 0, 3, 4),
+ (0, 3, 0, 3, 0, 3, 1, 2, 0, 3, 4, 4, 3, 3, 3, 0, 2, 2, 4, 3, 3, 1, 3, 3, 3, 1, 1, 0, 3, 1, 4, 3, 2, 3, 4, 4, 2, 4, 4, 4, 3, 4, 4, 3, 2, 4, 4, 3, 1, 3, 3, 1, 3, 3, 0, 4, 1, 0, 2, 2, 1, 4, 3, 2, 3, 3, 5, 4, 3, 3, 5, 4, 4, 3, 3, 0, 4, 0, 3, 2, 2, 4, 4),
+ (0, 2, 0, 1, 0, 0, 0, 0, 0, 1, 2, 1, 3, 0, 0, 0, 0, 0, 2, 0, 1, 2, 1, 0, 0, 1, 0, 0, 0, 0, 3, 0, 0, 1, 0, 1, 1, 3, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 0, 3, 4, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1),
+ (0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 4, 1, 4, 0, 3, 0, 4, 0, 3, 0, 4, 0, 3, 0, 3, 0, 4, 1, 5, 1, 4, 0, 0, 3, 0, 5, 0, 5, 2, 0, 1, 0, 0, 0, 2, 1, 4, 0, 1, 3, 0, 0, 3, 0, 0, 3, 1, 1, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0),
+ (1, 4, 0, 5, 0, 3, 0, 2, 0, 3, 5, 4, 4, 3, 4, 3, 5, 3, 4, 3, 3, 0, 4, 3, 3, 3, 3, 3, 3, 2, 4, 4, 3, 1, 3, 4, 4, 5, 4, 4, 3, 4, 4, 1, 3, 5, 4, 3, 3, 3, 1, 2, 2, 3, 3, 1, 3, 1, 3, 3, 3, 5, 3, 3, 4, 5, 0, 3, 0, 3, 0, 3, 4, 3, 4, 4, 3, 0, 3, 0, 2, 4, 3),
+ (0, 1, 0, 4, 0, 0, 0, 0, 0, 1, 4, 0, 4, 1, 4, 2, 4, 0, 3, 0, 1, 0, 1, 0, 0, 0, 0, 0, 2, 0, 3, 1, 1, 1, 0, 3, 0, 0, 0, 1, 2, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 3, 0, 0, 0, 0, 3, 2, 0, 2, 2, 0, 1, 0, 0, 0, 2, 3, 2, 3, 3, 0, 0, 0, 0, 2, 1, 0),
+ (0, 5, 1, 5, 0, 3, 0, 3, 0, 5, 4, 4, 5, 1, 5, 3, 3, 0, 4, 3, 4, 3, 5, 3, 4, 3, 3, 2, 4, 3, 4, 3, 3, 0, 3, 3, 1, 4, 4, 3, 4, 4, 4, 3, 4, 5, 5, 3, 2, 3, 1, 1, 3, 3, 1, 3, 1, 1, 3, 3, 2, 4, 5, 3, 3, 5, 0, 4, 0, 3, 0, 4, 4, 3, 5, 3, 3, 0, 3, 4, 0, 4, 3),
+ (0, 5, 0, 5, 0, 3, 0, 2, 0, 4, 4, 3, 5, 2, 4, 3, 3, 3, 4, 4, 4, 3, 5, 3, 5, 3, 3, 1, 4, 0, 4, 3, 3, 0, 3, 3, 0, 4, 4, 4, 4, 5, 4, 3, 3, 5, 5, 3, 2, 3, 1, 2, 3, 2, 0, 1, 0, 0, 3, 2, 2, 4, 4, 3, 1, 5, 0, 4, 0, 3, 0, 4, 3, 1, 3, 2, 1, 0, 3, 3, 0, 3, 3),
+ (0, 4, 0, 5, 0, 5, 0, 4, 0, 4, 5, 5, 5, 3, 4, 3, 3, 2, 5, 4, 4, 3, 5, 3, 5, 3, 4, 0, 4, 3, 4, 4, 3, 2, 4, 4, 3, 4, 5, 4, 4, 5, 5, 0, 3, 5, 5, 4, 1, 3, 3, 2, 3, 3, 1, 3, 1, 0, 4, 3, 1, 4, 4, 3, 4, 5, 0, 4, 0, 2, 0, 4, 3, 4, 4, 3, 3, 0, 4, 0, 0, 5, 5),
+ (0, 4, 0, 4, 0, 5, 0, 1, 1, 3, 3, 4, 4, 3, 4, 1, 3, 0, 5, 1, 3, 0, 3, 1, 3, 1, 1, 0, 3, 0, 3, 3, 4, 0, 4, 3, 0, 4, 4, 4, 3, 4, 4, 0, 3, 5, 4, 1, 0, 3, 0, 0, 2, 3, 0, 3, 1, 0, 3, 1, 0, 3, 2, 1, 3, 5, 0, 3, 0, 1, 0, 3, 2, 3, 3, 4, 4, 0, 2, 2, 0, 4, 4),
+ (2, 4, 0, 5, 0, 4, 0, 3, 0, 4, 5, 5, 4, 3, 5, 3, 5, 3, 5, 3, 5, 2, 5, 3, 4, 3, 3, 4, 3, 4, 5, 3, 2, 1, 5, 4, 3, 2, 3, 4, 5, 3, 4, 1, 2, 5, 4, 3, 0, 3, 3, 0, 3, 2, 0, 2, 3, 0, 4, 1, 0, 3, 4, 3, 3, 5, 0, 3, 0, 1, 0, 4, 5, 5, 5, 4, 3, 0, 4, 2, 0, 3, 5),
+ (0, 5, 0, 4, 0, 4, 0, 2, 0, 5, 4, 3, 4, 3, 4, 3, 3, 3, 4, 3, 4, 2, 5, 3, 5, 3, 4, 1, 4, 3, 4, 4, 4, 0, 3, 5, 0, 4, 4, 4, 4, 5, 3, 1, 3, 4, 5, 3, 3, 3, 3, 3, 3, 3, 0, 2, 2, 0, 3, 3, 2, 4, 3, 3, 3, 5, 3, 4, 1, 3, 3, 5, 3, 2, 0, 0, 0, 0, 4, 3, 1, 3, 3),
+ (0, 1, 0, 3, 0, 3, 0, 1, 0, 1, 3, 3, 3, 2, 3, 3, 3, 0, 3, 0, 0, 0, 3, 1, 3, 0, 0, 0, 2, 2, 2, 3, 0, 0, 3, 2, 0, 1, 2, 4, 1, 3, 3, 0, 0, 3, 3, 3, 0, 1, 0, 0, 2, 1, 0, 0, 3, 0, 3, 1, 0, 3, 0, 0, 1, 3, 0, 2, 0, 1, 0, 3, 3, 1, 3, 3, 0, 0, 1, 1, 0, 3, 3),
+ (0, 2, 0, 3, 0, 2, 1, 4, 0, 2, 2, 3, 1, 1, 3, 1, 1, 0, 2, 0, 3, 1, 2, 3, 1, 3, 0, 0, 1, 0, 4, 3, 2, 3, 3, 3, 1, 4, 2, 3, 3, 3, 3, 1, 0, 3, 1, 4, 0, 1, 1, 0, 1, 2, 0, 1, 1, 0, 1, 1, 0, 3, 1, 3, 2, 2, 0, 1, 0, 0, 0, 2, 3, 3, 3, 1, 0, 0, 0, 0, 0, 2, 3),
+ (0, 5, 0, 4, 0, 5, 0, 2, 0, 4, 5, 5, 3, 3, 4, 3, 3, 1, 5, 4, 4, 2, 4, 4, 4, 3, 4, 2, 4, 3, 5, 5, 4, 3, 3, 4, 3, 3, 5, 5, 4, 5, 5, 1, 3, 4, 5, 3, 1, 4, 3, 1, 3, 3, 0, 3, 3, 1, 4, 3, 1, 4, 5, 3, 3, 5, 0, 4, 0, 3, 0, 5, 3, 3, 1, 4, 3, 0, 4, 0, 1, 5, 3),
+ (0, 5, 0, 5, 0, 4, 0, 2, 0, 4, 4, 3, 4, 3, 3, 3, 3, 3, 5, 4, 4, 4, 4, 4, 4, 5, 3, 3, 5, 2, 4, 4, 4, 3, 4, 4, 3, 3, 4, 4, 5, 5, 3, 3, 4, 3, 4, 3, 3, 4, 3, 3, 3, 3, 1, 2, 2, 1, 4, 3, 3, 5, 4, 4, 3, 4, 0, 4, 0, 3, 0, 4, 4, 4, 4, 4, 1, 0, 4, 2, 0, 2, 4),
+ (0, 4, 0, 4, 0, 3, 0, 1, 0, 3, 5, 2, 3, 0, 3, 0, 2, 1, 4, 2, 3, 3, 4, 1, 4, 3, 3, 2, 4, 1, 3, 3, 3, 0, 3, 3, 0, 0, 3, 3, 3, 5, 3, 3, 3, 3, 3, 2, 0, 2, 0, 0, 2, 0, 0, 2, 0, 0, 1, 0, 0, 3, 1, 2, 2, 3, 0, 3, 0, 2, 0, 4, 4, 3, 3, 4, 1, 0, 3, 0, 0, 2, 4),
+ (0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 2, 0, 0, 0, 0, 0, 1, 0, 2, 0, 1, 0, 0, 0, 0, 0, 3, 1, 3, 0, 3, 2, 0, 0, 0, 1, 0, 3, 2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 2, 0, 0, 0, 0, 0, 0, 2),
+ (0, 2, 1, 3, 0, 2, 0, 2, 0, 3, 3, 3, 3, 1, 3, 1, 3, 3, 3, 3, 3, 3, 4, 2, 2, 1, 2, 1, 4, 0, 4, 3, 1, 3, 3, 3, 2, 4, 3, 5, 4, 3, 3, 3, 3, 3, 3, 3, 0, 1, 3, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 4, 2, 0, 2, 3, 0, 3, 3, 0, 3, 3, 4, 2, 3, 1, 4, 0, 1, 2, 0, 2, 3),
+ (0, 3, 0, 3, 0, 1, 0, 3, 0, 2, 3, 3, 3, 0, 3, 1, 2, 0, 3, 3, 2, 3, 3, 2, 3, 2, 3, 1, 3, 0, 4, 3, 2, 0, 3, 3, 1, 4, 3, 3, 2, 3, 4, 3, 1, 3, 3, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 4, 1, 1, 0, 3, 0, 3, 1, 0, 2, 3, 3, 3, 3, 3, 1, 0, 0, 2, 0, 3, 3),
+ (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 3, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 2, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 3),
+ (0, 2, 0, 3, 1, 3, 0, 3, 0, 2, 3, 3, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 1, 3, 0, 2, 3, 1, 1, 4, 3, 3, 2, 3, 3, 1, 2, 2, 4, 1, 3, 3, 0, 1, 4, 2, 3, 0, 1, 3, 0, 3, 0, 0, 1, 3, 0, 2, 0, 0, 3, 3, 2, 1, 3, 0, 3, 0, 2, 0, 3, 4, 4, 4, 3, 1, 0, 3, 0, 0, 3, 3),
+ (0, 2, 0, 1, 0, 2, 0, 0, 0, 1, 3, 2, 2, 1, 3, 0, 1, 1, 3, 0, 3, 2, 3, 1, 2, 0, 2, 0, 1, 1, 3, 3, 3, 0, 3, 3, 1, 1, 2, 3, 2, 3, 3, 1, 2, 3, 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 0, 2, 1, 2, 1, 3, 0, 3, 0, 0, 0, 3, 4, 4, 4, 3, 2, 0, 2, 0, 0, 2, 4),
+ (0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 3, 1, 0, 0, 0, 0, 0, 0, 0, 3),
+ (0, 3, 0, 3, 0, 2, 0, 3, 0, 3, 3, 3, 2, 3, 2, 2, 2, 0, 3, 1, 3, 3, 3, 2, 3, 3, 0, 0, 3, 0, 3, 2, 2, 0, 2, 3, 1, 4, 3, 4, 3, 3, 2, 3, 1, 5, 4, 4, 0, 3, 1, 2, 1, 3, 0, 3, 1, 1, 2, 0, 2, 3, 1, 3, 1, 3, 0, 3, 0, 1, 0, 3, 3, 4, 4, 2, 1, 0, 2, 1, 0, 2, 4),
+ (0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 4, 2, 5, 1, 4, 0, 2, 0, 2, 1, 3, 1, 4, 0, 2, 1, 0, 0, 2, 1, 4, 1, 1, 0, 3, 3, 0, 5, 1, 3, 2, 3, 3, 1, 0, 3, 2, 3, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 1, 0, 3, 0, 2, 0, 1, 0, 3, 3, 3, 4, 3, 3, 0, 0, 0, 0, 2, 3),
+ (0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 1, 0, 0, 0, 0, 0, 3),
+ (0, 1, 0, 3, 0, 4, 0, 3, 0, 2, 4, 3, 1, 0, 3, 2, 2, 1, 3, 1, 2, 2, 3, 1, 1, 1, 2, 1, 3, 0, 1, 2, 0, 1, 3, 2, 1, 3, 0, 5, 5, 1, 0, 0, 1, 3, 2, 1, 0, 3, 0, 0, 1, 0, 0, 0, 0, 0, 3, 4, 0, 1, 1, 1, 3, 2, 0, 2, 0, 1, 0, 2, 3, 3, 1, 2, 3, 0, 1, 0, 1, 0, 4),
+ (0, 0, 0, 1, 0, 3, 0, 3, 0, 2, 2, 1, 0, 0, 4, 0, 3, 0, 3, 1, 3, 0, 3, 0, 3, 0, 1, 0, 3, 0, 3, 1, 3, 0, 3, 3, 0, 0, 1, 2, 1, 1, 1, 0, 1, 2, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1, 2, 0, 0, 2, 0, 0, 0, 0, 2, 3, 3, 3, 3, 0, 0, 0, 0, 1, 4),
+ (0, 0, 0, 3, 0, 3, 0, 0, 0, 0, 3, 1, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 3, 0, 2, 0, 2, 3, 0, 0, 2, 2, 3, 1, 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 2, 0, 0, 0, 0, 2, 3),
+ (2, 4, 0, 5, 0, 5, 0, 4, 0, 3, 4, 3, 3, 3, 4, 3, 3, 3, 4, 3, 4, 4, 5, 4, 5, 5, 5, 2, 3, 0, 5, 5, 4, 1, 5, 4, 3, 1, 5, 4, 3, 4, 4, 3, 3, 4, 3, 3, 0, 3, 2, 0, 2, 3, 0, 3, 0, 0, 3, 3, 0, 5, 3, 2, 3, 3, 0, 3, 0, 3, 0, 3, 4, 5, 4, 5, 3, 0, 4, 3, 0, 3, 4),
+ (0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 3, 4, 3, 2, 3, 2, 3, 0, 4, 3, 3, 3, 3, 3, 3, 3, 3, 0, 3, 2, 4, 3, 3, 1, 3, 4, 3, 4, 4, 4, 3, 4, 4, 3, 2, 4, 4, 1, 0, 2, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0, 5, 3, 2, 1, 3, 0, 3, 0, 1, 2, 4, 3, 2, 4, 3, 3, 0, 3, 2, 0, 4, 4),
+ (0, 3, 0, 3, 0, 1, 0, 0, 0, 1, 4, 3, 3, 2, 3, 1, 3, 1, 4, 2, 3, 2, 4, 2, 3, 4, 3, 0, 2, 2, 3, 3, 3, 0, 3, 3, 3, 0, 3, 4, 1, 3, 3, 0, 3, 4, 3, 3, 0, 1, 1, 0, 1, 0, 0, 0, 4, 0, 3, 0, 0, 3, 1, 2, 1, 3, 0, 4, 0, 1, 0, 4, 3, 3, 4, 3, 3, 0, 2, 0, 0, 3, 3),
+ (0, 3, 0, 4, 0, 1, 0, 3, 0, 3, 4, 3, 3, 0, 3, 3, 3, 1, 3, 1, 3, 3, 4, 3, 3, 3, 0, 0, 3, 1, 5, 3, 3, 1, 3, 3, 2, 5, 4, 3, 3, 4, 5, 3, 2, 5, 3, 4, 0, 1, 0, 0, 0, 0, 0, 2, 0, 0, 1, 1, 0, 4, 2, 2, 1, 3, 0, 3, 0, 2, 0, 4, 4, 3, 5, 3, 2, 0, 1, 1, 0, 3, 4),
+ (0, 5, 0, 4, 0, 5, 0, 2, 0, 4, 4, 3, 3, 2, 3, 3, 3, 1, 4, 3, 4, 1, 5, 3, 4, 3, 4, 0, 4, 2, 4, 3, 4, 1, 5, 4, 0, 4, 4, 4, 4, 5, 4, 1, 3, 5, 4, 2, 1, 4, 1, 1, 3, 2, 0, 3, 1, 0, 3, 2, 1, 4, 3, 3, 3, 4, 0, 4, 0, 3, 0, 4, 4, 4, 3, 3, 3, 0, 4, 2, 0, 3, 4),
+ (1, 4, 0, 4, 0, 3, 0, 1, 0, 3, 3, 3, 1, 1, 3, 3, 2, 2, 3, 3, 1, 0, 3, 2, 2, 1, 2, 0, 3, 1, 2, 1, 2, 0, 3, 2, 0, 2, 2, 3, 3, 4, 3, 0, 3, 3, 1, 2, 0, 1, 1, 3, 1, 2, 0, 0, 3, 0, 1, 1, 0, 3, 2, 2, 3, 3, 0, 3, 0, 0, 0, 2, 3, 3, 4, 3, 3, 0, 1, 0, 0, 1, 4),
+ (0, 4, 0, 4, 0, 4, 0, 0, 0, 3, 4, 4, 3, 1, 4, 2, 3, 2, 3, 3, 3, 1, 4, 3, 4, 0, 3, 0, 4, 2, 3, 3, 2, 2, 5, 4, 2, 1, 3, 4, 3, 4, 3, 1, 3, 3, 4, 2, 0, 2, 1, 0, 3, 3, 0, 0, 2, 0, 3, 1, 0, 4, 4, 3, 4, 3, 0, 4, 0, 1, 0, 2, 4, 4, 4, 4, 4, 0, 3, 2, 0, 3, 3),
+ (0, 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 3, 2, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2),
+ (0, 2, 0, 3, 0, 4, 0, 4, 0, 1, 3, 3, 3, 0, 4, 0, 2, 1, 2, 1, 1, 1, 2, 0, 3, 1, 1, 0, 1, 0, 3, 1, 0, 0, 3, 3, 2, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 2, 0, 2, 2, 0, 3, 1, 0, 0, 1, 0, 1, 1, 0, 1, 2, 0, 3, 0, 0, 0, 0, 1, 0, 0, 3, 3, 4, 3, 1, 0, 1, 0, 3, 0, 2),
+ (0, 0, 0, 3, 0, 5, 0, 0, 0, 0, 1, 0, 2, 0, 3, 1, 0, 1, 3, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 4, 0, 0, 0, 2, 3, 0, 1, 4, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3, 0, 0, 0, 0, 0, 3),
+ (0, 2, 0, 5, 0, 5, 0, 1, 0, 2, 4, 3, 3, 2, 5, 1, 3, 2, 3, 3, 3, 0, 4, 1, 2, 0, 3, 0, 4, 0, 2, 2, 1, 1, 5, 3, 0, 0, 1, 4, 2, 3, 2, 0, 3, 3, 3, 2, 0, 2, 4, 1, 1, 2, 0, 1, 1, 0, 3, 1, 0, 1, 3, 1, 2, 3, 0, 2, 0, 0, 0, 1, 3, 5, 4, 4, 4, 0, 3, 0, 0, 1, 3),
+ (0, 4, 0, 5, 0, 4, 0, 4, 0, 4, 5, 4, 3, 3, 4, 3, 3, 3, 4, 3, 4, 4, 5, 3, 4, 5, 4, 2, 4, 2, 3, 4, 3, 1, 4, 4, 1, 3, 5, 4, 4, 5, 5, 4, 4, 5, 5, 5, 2, 3, 3, 1, 4, 3, 1, 3, 3, 0, 3, 3, 1, 4, 3, 4, 4, 4, 0, 3, 0, 4, 0, 3, 3, 4, 4, 5, 0, 0, 4, 3, 0, 4, 5),
+ (0, 4, 0, 4, 0, 3, 0, 3, 0, 3, 4, 4, 4, 3, 3, 2, 4, 3, 4, 3, 4, 3, 5, 3, 4, 3, 2, 1, 4, 2, 4, 4, 3, 1, 3, 4, 2, 4, 5, 5, 3, 4, 5, 4, 1, 5, 4, 3, 0, 3, 2, 2, 3, 2, 1, 3, 1, 0, 3, 3, 3, 5, 3, 3, 3, 5, 4, 4, 2, 3, 3, 4, 3, 3, 3, 2, 1, 0, 3, 2, 1, 4, 3),
+ (0, 4, 0, 5, 0, 4, 0, 3, 0, 3, 5, 5, 3, 2, 4, 3, 4, 0, 5, 4, 4, 1, 4, 4, 4, 3, 3, 3, 4, 3, 5, 5, 2, 3, 3, 4, 1, 2, 5, 5, 3, 5, 5, 2, 3, 5, 5, 4, 0, 3, 2, 0, 3, 3, 1, 1, 5, 1, 4, 1, 0, 4, 3, 2, 3, 5, 0, 4, 0, 3, 0, 5, 4, 3, 4, 3, 0, 0, 4, 1, 0, 4, 4),
+ (1, 3, 0, 4, 0, 2, 0, 2, 0, 2, 5, 5, 3, 3, 3, 3, 3, 0, 4, 2, 3, 4, 4, 4, 3, 4, 0, 0, 3, 4, 5, 4, 3, 3, 3, 3, 2, 5, 5, 4, 5, 5, 5, 4, 3, 5, 5, 5, 1, 3, 1, 0, 1, 0, 0, 3, 2, 0, 4, 2, 0, 5, 2, 3, 2, 4, 1, 3, 0, 3, 0, 4, 5, 4, 5, 4, 3, 0, 4, 2, 0, 5, 4),
+ (0, 3, 0, 4, 0, 5, 0, 3, 0, 3, 4, 4, 3, 2, 3, 2, 3, 3, 3, 3, 3, 2, 4, 3, 3, 2, 2, 0, 3, 3, 3, 3, 3, 1, 3, 3, 3, 0, 4, 4, 3, 4, 4, 1, 1, 4, 4, 2, 0, 3, 1, 0, 1, 1, 0, 4, 1, 0, 2, 3, 1, 3, 3, 1, 3, 4, 0, 3, 0, 1, 0, 3, 1, 3, 0, 0, 1, 0, 2, 0, 0, 4, 4),
+ (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+ (0, 3, 0, 3, 0, 2, 0, 3, 0, 1, 5, 4, 3, 3, 3, 1, 4, 2, 1, 2, 3, 4, 4, 2, 4, 4, 5, 0, 3, 1, 4, 3, 4, 0, 4, 3, 3, 3, 2, 3, 2, 5, 3, 4, 3, 2, 2, 3, 0, 0, 3, 0, 2, 1, 0, 1, 2, 0, 0, 0, 0, 2, 1, 1, 3, 1, 0, 2, 0, 4, 0, 3, 4, 4, 4, 5, 2, 0, 2, 0, 0, 1, 3),
+ (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 4, 2, 1, 1, 0, 1, 0, 3, 2, 0, 0, 3, 1, 1, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 4, 0, 4, 2, 1, 0, 0, 0, 0, 0, 1),
+ (0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 2, 0, 2, 1, 0, 0, 1, 2, 1, 0, 1, 1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 1, 0, 0, 0, 0, 0, 1, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2),
+ (0, 4, 0, 4, 0, 4, 0, 3, 0, 4, 4, 3, 4, 2, 4, 3, 2, 0, 4, 4, 4, 3, 5, 3, 5, 3, 3, 2, 4, 2, 4, 3, 4, 3, 1, 4, 0, 2, 3, 4, 4, 4, 3, 3, 3, 4, 4, 4, 3, 4, 1, 3, 4, 3, 2, 1, 2, 1, 3, 3, 3, 4, 4, 3, 3, 5, 0, 4, 0, 3, 0, 4, 3, 3, 3, 2, 1, 0, 3, 0, 0, 3, 3),
+ (0, 4, 0, 3, 0, 3, 0, 3, 0, 3, 5, 5, 3, 3, 3, 3, 4, 3, 4, 3, 3, 3, 4, 4, 4, 3, 3, 3, 3, 4, 3, 5, 3, 3, 1, 3, 2, 4, 5, 5, 5, 5, 4, 3, 4, 5, 5, 3, 2, 2, 3, 3, 3, 3, 2, 3, 3, 1, 2, 3, 2, 4, 3, 3, 3, 4, 0, 4, 0, 2, 0, 4, 3, 2, 2, 1, 2, 0, 3, 0, 0, 4, 1),
)
+# fmt: on
-class JapaneseContextAnalysis(object):
+
+class JapaneseContextAnalysis:
NUM_OF_CATEGORY = 6
DONT_KNOW = -1
ENOUGH_REL_THRESHOLD = 100
@@ -153,7 +156,7 @@ class JapaneseContextAnalysis(object):
# this character will simply our logic and improve performance.
i = self._need_to_skip_char_num
while i < num_bytes:
- order, char_len = self.get_order(byte_str[i:i + 2])
+ order, char_len = self.get_order(byte_str[i : i + 2])
i += char_len
if i > num_bytes:
self._need_to_skip_char_num = i - num_bytes
@@ -164,7 +167,9 @@ class JapaneseContextAnalysis(object):
if self._total_rel > self.MAX_REL_THRESHOLD:
self._done = True
break
- self._rel_sample[jp2CharContext[self._last_char_order][order]] += 1
+ self._rel_sample[
+ jp2_char_context[self._last_char_order][order]
+ ] += 1
self._last_char_order = order
def got_enough_data(self):
@@ -174,15 +179,15 @@ class JapaneseContextAnalysis(object):
# This is just one way to calculate confidence. It works well for me.
if self._total_rel > self.MINIMUM_DATA_THRESHOLD:
return (self._total_rel - self._rel_sample[0]) / self._total_rel
- else:
- return self.DONT_KNOW
+ return self.DONT_KNOW
- def get_order(self, byte_str):
+ def get_order(self, _):
return -1, 1
+
class SJISContextAnalysis(JapaneseContextAnalysis):
def __init__(self):
- super(SJISContextAnalysis, self).__init__()
+ super().__init__()
self._charset_name = "SHIFT_JIS"
@property
@@ -209,6 +214,7 @@ class SJISContextAnalysis(JapaneseContextAnalysis):
return -1, char_len
+
class EUCJPContextAnalysis(JapaneseContextAnalysis):
def get_order(self, byte_str):
if not byte_str:
@@ -229,5 +235,3 @@ class EUCJPContextAnalysis(JapaneseContextAnalysis):
return second_char - 0xA1, char_len
return -1, char_len
-
-
diff --git a/src/pip/_vendor/chardet/langbulgarianmodel.py b/src/pip/_vendor/chardet/langbulgarianmodel.py
index e963a5097..994668219 100644
--- a/src/pip/_vendor/chardet/langbulgarianmodel.py
+++ b/src/pip/_vendor/chardet/langbulgarianmodel.py
@@ -1,9 +1,5 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel
-
# 3: Positive
# 2: Likely
# 1: Unlikely
@@ -4115,536 +4111,539 @@ BULGARIAN_LANG_MODEL = {
# Character Mapping Table(s):
ISO_8859_5_BULGARIAN_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 77, # 'A'
- 66: 90, # 'B'
- 67: 99, # 'C'
- 68: 100, # 'D'
- 69: 72, # 'E'
- 70: 109, # 'F'
- 71: 107, # 'G'
- 72: 101, # 'H'
- 73: 79, # 'I'
- 74: 185, # 'J'
- 75: 81, # 'K'
- 76: 102, # 'L'
- 77: 76, # 'M'
- 78: 94, # 'N'
- 79: 82, # 'O'
- 80: 110, # 'P'
- 81: 186, # 'Q'
- 82: 108, # 'R'
- 83: 91, # 'S'
- 84: 74, # 'T'
- 85: 119, # 'U'
- 86: 84, # 'V'
- 87: 96, # 'W'
- 88: 111, # 'X'
- 89: 187, # 'Y'
- 90: 115, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 65, # 'a'
- 98: 69, # 'b'
- 99: 70, # 'c'
- 100: 66, # 'd'
- 101: 63, # 'e'
- 102: 68, # 'f'
- 103: 112, # 'g'
- 104: 103, # 'h'
- 105: 92, # 'i'
- 106: 194, # 'j'
- 107: 104, # 'k'
- 108: 95, # 'l'
- 109: 86, # 'm'
- 110: 87, # 'n'
- 111: 71, # 'o'
- 112: 116, # 'p'
- 113: 195, # 'q'
- 114: 85, # 'r'
- 115: 93, # 's'
- 116: 97, # 't'
- 117: 113, # 'u'
- 118: 196, # 'v'
- 119: 197, # 'w'
- 120: 198, # 'x'
- 121: 199, # 'y'
- 122: 200, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 194, # '\x80'
- 129: 195, # '\x81'
- 130: 196, # '\x82'
- 131: 197, # '\x83'
- 132: 198, # '\x84'
- 133: 199, # '\x85'
- 134: 200, # '\x86'
- 135: 201, # '\x87'
- 136: 202, # '\x88'
- 137: 203, # '\x89'
- 138: 204, # '\x8a'
- 139: 205, # '\x8b'
- 140: 206, # '\x8c'
- 141: 207, # '\x8d'
- 142: 208, # '\x8e'
- 143: 209, # '\x8f'
- 144: 210, # '\x90'
- 145: 211, # '\x91'
- 146: 212, # '\x92'
- 147: 213, # '\x93'
- 148: 214, # '\x94'
- 149: 215, # '\x95'
- 150: 216, # '\x96'
- 151: 217, # '\x97'
- 152: 218, # '\x98'
- 153: 219, # '\x99'
- 154: 220, # '\x9a'
- 155: 221, # '\x9b'
- 156: 222, # '\x9c'
- 157: 223, # '\x9d'
- 158: 224, # '\x9e'
- 159: 225, # '\x9f'
- 160: 81, # '\xa0'
- 161: 226, # 'Ð'
- 162: 227, # 'Ђ'
- 163: 228, # 'Ѓ'
- 164: 229, # 'Є'
- 165: 230, # 'Ð…'
- 166: 105, # 'І'
- 167: 231, # 'Ї'
- 168: 232, # 'Ј'
- 169: 233, # 'Љ'
- 170: 234, # 'Њ'
- 171: 235, # 'Ћ'
- 172: 236, # 'Ќ'
- 173: 45, # '\xad'
- 174: 237, # 'ÐŽ'
- 175: 238, # 'Ð'
- 176: 31, # 'Ð'
- 177: 32, # 'Б'
- 178: 35, # 'Ð’'
- 179: 43, # 'Г'
- 180: 37, # 'Д'
- 181: 44, # 'Е'
- 182: 55, # 'Ж'
- 183: 47, # 'З'
- 184: 40, # 'И'
- 185: 59, # 'Й'
- 186: 33, # 'К'
- 187: 46, # 'Л'
- 188: 38, # 'М'
- 189: 36, # 'Ð'
- 190: 41, # 'О'
- 191: 30, # 'П'
- 192: 39, # 'Р'
- 193: 28, # 'С'
- 194: 34, # 'Т'
- 195: 51, # 'У'
- 196: 48, # 'Ф'
- 197: 49, # 'Ð¥'
- 198: 53, # 'Ц'
- 199: 50, # 'Ч'
- 200: 54, # 'Ш'
- 201: 57, # 'Щ'
- 202: 61, # 'Ъ'
- 203: 239, # 'Ы'
- 204: 67, # 'Ь'
- 205: 240, # 'Э'
- 206: 60, # 'Ю'
- 207: 56, # 'Я'
- 208: 1, # 'а'
- 209: 18, # 'б'
- 210: 9, # 'в'
- 211: 20, # 'г'
- 212: 11, # 'д'
- 213: 3, # 'е'
- 214: 23, # 'ж'
- 215: 15, # 'з'
- 216: 2, # 'и'
- 217: 26, # 'й'
- 218: 12, # 'к'
- 219: 10, # 'л'
- 220: 14, # 'м'
- 221: 6, # 'н'
- 222: 4, # 'о'
- 223: 13, # 'п'
- 224: 7, # 'Ñ€'
- 225: 8, # 'Ñ'
- 226: 5, # 'Ñ‚'
- 227: 19, # 'у'
- 228: 29, # 'Ñ„'
- 229: 25, # 'Ñ…'
- 230: 22, # 'ц'
- 231: 21, # 'ч'
- 232: 27, # 'ш'
- 233: 24, # 'щ'
- 234: 17, # 'ÑŠ'
- 235: 75, # 'Ñ‹'
- 236: 52, # 'ь'
- 237: 241, # 'Ñ'
- 238: 42, # 'ÑŽ'
- 239: 16, # 'Ñ'
- 240: 62, # 'â„–'
- 241: 242, # 'Ñ‘'
- 242: 243, # 'Ñ’'
- 243: 244, # 'Ñ“'
- 244: 58, # 'Ñ”'
- 245: 245, # 'Ñ•'
- 246: 98, # 'Ñ–'
- 247: 246, # 'Ñ—'
- 248: 247, # 'ј'
- 249: 248, # 'Ñ™'
- 250: 249, # 'Ñš'
- 251: 250, # 'Ñ›'
- 252: 251, # 'ќ'
- 253: 91, # '§'
- 254: 252, # 'Ñž'
- 255: 253, # 'ÑŸ'
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 77, # 'A'
+ 66: 90, # 'B'
+ 67: 99, # 'C'
+ 68: 100, # 'D'
+ 69: 72, # 'E'
+ 70: 109, # 'F'
+ 71: 107, # 'G'
+ 72: 101, # 'H'
+ 73: 79, # 'I'
+ 74: 185, # 'J'
+ 75: 81, # 'K'
+ 76: 102, # 'L'
+ 77: 76, # 'M'
+ 78: 94, # 'N'
+ 79: 82, # 'O'
+ 80: 110, # 'P'
+ 81: 186, # 'Q'
+ 82: 108, # 'R'
+ 83: 91, # 'S'
+ 84: 74, # 'T'
+ 85: 119, # 'U'
+ 86: 84, # 'V'
+ 87: 96, # 'W'
+ 88: 111, # 'X'
+ 89: 187, # 'Y'
+ 90: 115, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 65, # 'a'
+ 98: 69, # 'b'
+ 99: 70, # 'c'
+ 100: 66, # 'd'
+ 101: 63, # 'e'
+ 102: 68, # 'f'
+ 103: 112, # 'g'
+ 104: 103, # 'h'
+ 105: 92, # 'i'
+ 106: 194, # 'j'
+ 107: 104, # 'k'
+ 108: 95, # 'l'
+ 109: 86, # 'm'
+ 110: 87, # 'n'
+ 111: 71, # 'o'
+ 112: 116, # 'p'
+ 113: 195, # 'q'
+ 114: 85, # 'r'
+ 115: 93, # 's'
+ 116: 97, # 't'
+ 117: 113, # 'u'
+ 118: 196, # 'v'
+ 119: 197, # 'w'
+ 120: 198, # 'x'
+ 121: 199, # 'y'
+ 122: 200, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 194, # '\x80'
+ 129: 195, # '\x81'
+ 130: 196, # '\x82'
+ 131: 197, # '\x83'
+ 132: 198, # '\x84'
+ 133: 199, # '\x85'
+ 134: 200, # '\x86'
+ 135: 201, # '\x87'
+ 136: 202, # '\x88'
+ 137: 203, # '\x89'
+ 138: 204, # '\x8a'
+ 139: 205, # '\x8b'
+ 140: 206, # '\x8c'
+ 141: 207, # '\x8d'
+ 142: 208, # '\x8e'
+ 143: 209, # '\x8f'
+ 144: 210, # '\x90'
+ 145: 211, # '\x91'
+ 146: 212, # '\x92'
+ 147: 213, # '\x93'
+ 148: 214, # '\x94'
+ 149: 215, # '\x95'
+ 150: 216, # '\x96'
+ 151: 217, # '\x97'
+ 152: 218, # '\x98'
+ 153: 219, # '\x99'
+ 154: 220, # '\x9a'
+ 155: 221, # '\x9b'
+ 156: 222, # '\x9c'
+ 157: 223, # '\x9d'
+ 158: 224, # '\x9e'
+ 159: 225, # '\x9f'
+ 160: 81, # '\xa0'
+ 161: 226, # 'Ð'
+ 162: 227, # 'Ђ'
+ 163: 228, # 'Ѓ'
+ 164: 229, # 'Є'
+ 165: 230, # 'Ð…'
+ 166: 105, # 'І'
+ 167: 231, # 'Ї'
+ 168: 232, # 'Ј'
+ 169: 233, # 'Љ'
+ 170: 234, # 'Њ'
+ 171: 235, # 'Ћ'
+ 172: 236, # 'Ќ'
+ 173: 45, # '\xad'
+ 174: 237, # 'ÐŽ'
+ 175: 238, # 'Ð'
+ 176: 31, # 'Ð'
+ 177: 32, # 'Б'
+ 178: 35, # 'Ð’'
+ 179: 43, # 'Г'
+ 180: 37, # 'Д'
+ 181: 44, # 'Е'
+ 182: 55, # 'Ж'
+ 183: 47, # 'З'
+ 184: 40, # 'И'
+ 185: 59, # 'Й'
+ 186: 33, # 'К'
+ 187: 46, # 'Л'
+ 188: 38, # 'М'
+ 189: 36, # 'Ð'
+ 190: 41, # 'О'
+ 191: 30, # 'П'
+ 192: 39, # 'Р'
+ 193: 28, # 'С'
+ 194: 34, # 'Т'
+ 195: 51, # 'У'
+ 196: 48, # 'Ф'
+ 197: 49, # 'Ð¥'
+ 198: 53, # 'Ц'
+ 199: 50, # 'Ч'
+ 200: 54, # 'Ш'
+ 201: 57, # 'Щ'
+ 202: 61, # 'Ъ'
+ 203: 239, # 'Ы'
+ 204: 67, # 'Ь'
+ 205: 240, # 'Э'
+ 206: 60, # 'Ю'
+ 207: 56, # 'Я'
+ 208: 1, # 'а'
+ 209: 18, # 'б'
+ 210: 9, # 'в'
+ 211: 20, # 'г'
+ 212: 11, # 'д'
+ 213: 3, # 'е'
+ 214: 23, # 'ж'
+ 215: 15, # 'з'
+ 216: 2, # 'и'
+ 217: 26, # 'й'
+ 218: 12, # 'к'
+ 219: 10, # 'л'
+ 220: 14, # 'м'
+ 221: 6, # 'н'
+ 222: 4, # 'о'
+ 223: 13, # 'п'
+ 224: 7, # 'Ñ€'
+ 225: 8, # 'Ñ'
+ 226: 5, # 'Ñ‚'
+ 227: 19, # 'у'
+ 228: 29, # 'Ñ„'
+ 229: 25, # 'Ñ…'
+ 230: 22, # 'ц'
+ 231: 21, # 'ч'
+ 232: 27, # 'ш'
+ 233: 24, # 'щ'
+ 234: 17, # 'ÑŠ'
+ 235: 75, # 'Ñ‹'
+ 236: 52, # 'ь'
+ 237: 241, # 'Ñ'
+ 238: 42, # 'ÑŽ'
+ 239: 16, # 'Ñ'
+ 240: 62, # 'â„–'
+ 241: 242, # 'Ñ‘'
+ 242: 243, # 'Ñ’'
+ 243: 244, # 'Ñ“'
+ 244: 58, # 'Ñ”'
+ 245: 245, # 'Ñ•'
+ 246: 98, # 'Ñ–'
+ 247: 246, # 'Ñ—'
+ 248: 247, # 'ј'
+ 249: 248, # 'Ñ™'
+ 250: 249, # 'Ñš'
+ 251: 250, # 'Ñ›'
+ 252: 251, # 'ќ'
+ 253: 91, # '§'
+ 254: 252, # 'Ñž'
+ 255: 253, # 'ÑŸ'
}
-ISO_8859_5_BULGARIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-5',
- language='Bulgarian',
- char_to_order_map=ISO_8859_5_BULGARIAN_CHAR_TO_ORDER,
- language_model=BULGARIAN_LANG_MODEL,
- typical_positive_ratio=0.969392,
- keep_ascii_letters=False,
- alphabet='ÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрÑтуфхцчшщъьюÑ')
+ISO_8859_5_BULGARIAN_MODEL = SingleByteCharSetModel(
+ charset_name="ISO-8859-5",
+ language="Bulgarian",
+ char_to_order_map=ISO_8859_5_BULGARIAN_CHAR_TO_ORDER,
+ language_model=BULGARIAN_LANG_MODEL,
+ typical_positive_ratio=0.969392,
+ keep_ascii_letters=False,
+ alphabet="ÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрÑтуфхцчшщъьюÑ",
+)
WINDOWS_1251_BULGARIAN_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 77, # 'A'
- 66: 90, # 'B'
- 67: 99, # 'C'
- 68: 100, # 'D'
- 69: 72, # 'E'
- 70: 109, # 'F'
- 71: 107, # 'G'
- 72: 101, # 'H'
- 73: 79, # 'I'
- 74: 185, # 'J'
- 75: 81, # 'K'
- 76: 102, # 'L'
- 77: 76, # 'M'
- 78: 94, # 'N'
- 79: 82, # 'O'
- 80: 110, # 'P'
- 81: 186, # 'Q'
- 82: 108, # 'R'
- 83: 91, # 'S'
- 84: 74, # 'T'
- 85: 119, # 'U'
- 86: 84, # 'V'
- 87: 96, # 'W'
- 88: 111, # 'X'
- 89: 187, # 'Y'
- 90: 115, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 65, # 'a'
- 98: 69, # 'b'
- 99: 70, # 'c'
- 100: 66, # 'd'
- 101: 63, # 'e'
- 102: 68, # 'f'
- 103: 112, # 'g'
- 104: 103, # 'h'
- 105: 92, # 'i'
- 106: 194, # 'j'
- 107: 104, # 'k'
- 108: 95, # 'l'
- 109: 86, # 'm'
- 110: 87, # 'n'
- 111: 71, # 'o'
- 112: 116, # 'p'
- 113: 195, # 'q'
- 114: 85, # 'r'
- 115: 93, # 's'
- 116: 97, # 't'
- 117: 113, # 'u'
- 118: 196, # 'v'
- 119: 197, # 'w'
- 120: 198, # 'x'
- 121: 199, # 'y'
- 122: 200, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 206, # 'Ђ'
- 129: 207, # 'Ѓ'
- 130: 208, # '‚'
- 131: 209, # 'Ñ“'
- 132: 210, # '„'
- 133: 211, # '…'
- 134: 212, # '†'
- 135: 213, # '‡'
- 136: 120, # '€'
- 137: 214, # '‰'
- 138: 215, # 'Љ'
- 139: 216, # '‹'
- 140: 217, # 'Њ'
- 141: 218, # 'Ќ'
- 142: 219, # 'Ћ'
- 143: 220, # 'Ð'
- 144: 221, # 'Ñ’'
- 145: 78, # '‘'
- 146: 64, # '’'
- 147: 83, # '“'
- 148: 121, # 'â€'
- 149: 98, # '•'
- 150: 117, # '–'
- 151: 105, # '—'
- 152: 222, # None
- 153: 223, # 'â„¢'
- 154: 224, # 'Ñ™'
- 155: 225, # '›'
- 156: 226, # 'Ñš'
- 157: 227, # 'ќ'
- 158: 228, # 'Ñ›'
- 159: 229, # 'ÑŸ'
- 160: 88, # '\xa0'
- 161: 230, # 'ÐŽ'
- 162: 231, # 'Ñž'
- 163: 232, # 'Ј'
- 164: 233, # '¤'
- 165: 122, # 'Ò'
- 166: 89, # '¦'
- 167: 106, # '§'
- 168: 234, # 'Ð'
- 169: 235, # '©'
- 170: 236, # 'Є'
- 171: 237, # '«'
- 172: 238, # '¬'
- 173: 45, # '\xad'
- 174: 239, # '®'
- 175: 240, # 'Ї'
- 176: 73, # '°'
- 177: 80, # '±'
- 178: 118, # 'І'
- 179: 114, # 'Ñ–'
- 180: 241, # 'Ò‘'
- 181: 242, # 'µ'
- 182: 243, # '¶'
- 183: 244, # '·'
- 184: 245, # 'Ñ‘'
- 185: 62, # 'â„–'
- 186: 58, # 'Ñ”'
- 187: 246, # '»'
- 188: 247, # 'ј'
- 189: 248, # 'Ð…'
- 190: 249, # 'Ñ•'
- 191: 250, # 'Ñ—'
- 192: 31, # 'Ð'
- 193: 32, # 'Б'
- 194: 35, # 'Ð’'
- 195: 43, # 'Г'
- 196: 37, # 'Д'
- 197: 44, # 'Е'
- 198: 55, # 'Ж'
- 199: 47, # 'З'
- 200: 40, # 'И'
- 201: 59, # 'Й'
- 202: 33, # 'К'
- 203: 46, # 'Л'
- 204: 38, # 'М'
- 205: 36, # 'Ð'
- 206: 41, # 'О'
- 207: 30, # 'П'
- 208: 39, # 'Р'
- 209: 28, # 'С'
- 210: 34, # 'Т'
- 211: 51, # 'У'
- 212: 48, # 'Ф'
- 213: 49, # 'Ð¥'
- 214: 53, # 'Ц'
- 215: 50, # 'Ч'
- 216: 54, # 'Ш'
- 217: 57, # 'Щ'
- 218: 61, # 'Ъ'
- 219: 251, # 'Ы'
- 220: 67, # 'Ь'
- 221: 252, # 'Э'
- 222: 60, # 'Ю'
- 223: 56, # 'Я'
- 224: 1, # 'а'
- 225: 18, # 'б'
- 226: 9, # 'в'
- 227: 20, # 'г'
- 228: 11, # 'д'
- 229: 3, # 'е'
- 230: 23, # 'ж'
- 231: 15, # 'з'
- 232: 2, # 'и'
- 233: 26, # 'й'
- 234: 12, # 'к'
- 235: 10, # 'л'
- 236: 14, # 'м'
- 237: 6, # 'н'
- 238: 4, # 'о'
- 239: 13, # 'п'
- 240: 7, # 'Ñ€'
- 241: 8, # 'Ñ'
- 242: 5, # 'Ñ‚'
- 243: 19, # 'у'
- 244: 29, # 'Ñ„'
- 245: 25, # 'Ñ…'
- 246: 22, # 'ц'
- 247: 21, # 'ч'
- 248: 27, # 'ш'
- 249: 24, # 'щ'
- 250: 17, # 'ÑŠ'
- 251: 75, # 'Ñ‹'
- 252: 52, # 'ь'
- 253: 253, # 'Ñ'
- 254: 42, # 'ÑŽ'
- 255: 16, # 'Ñ'
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 77, # 'A'
+ 66: 90, # 'B'
+ 67: 99, # 'C'
+ 68: 100, # 'D'
+ 69: 72, # 'E'
+ 70: 109, # 'F'
+ 71: 107, # 'G'
+ 72: 101, # 'H'
+ 73: 79, # 'I'
+ 74: 185, # 'J'
+ 75: 81, # 'K'
+ 76: 102, # 'L'
+ 77: 76, # 'M'
+ 78: 94, # 'N'
+ 79: 82, # 'O'
+ 80: 110, # 'P'
+ 81: 186, # 'Q'
+ 82: 108, # 'R'
+ 83: 91, # 'S'
+ 84: 74, # 'T'
+ 85: 119, # 'U'
+ 86: 84, # 'V'
+ 87: 96, # 'W'
+ 88: 111, # 'X'
+ 89: 187, # 'Y'
+ 90: 115, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 65, # 'a'
+ 98: 69, # 'b'
+ 99: 70, # 'c'
+ 100: 66, # 'd'
+ 101: 63, # 'e'
+ 102: 68, # 'f'
+ 103: 112, # 'g'
+ 104: 103, # 'h'
+ 105: 92, # 'i'
+ 106: 194, # 'j'
+ 107: 104, # 'k'
+ 108: 95, # 'l'
+ 109: 86, # 'm'
+ 110: 87, # 'n'
+ 111: 71, # 'o'
+ 112: 116, # 'p'
+ 113: 195, # 'q'
+ 114: 85, # 'r'
+ 115: 93, # 's'
+ 116: 97, # 't'
+ 117: 113, # 'u'
+ 118: 196, # 'v'
+ 119: 197, # 'w'
+ 120: 198, # 'x'
+ 121: 199, # 'y'
+ 122: 200, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 206, # 'Ђ'
+ 129: 207, # 'Ѓ'
+ 130: 208, # '‚'
+ 131: 209, # 'Ñ“'
+ 132: 210, # '„'
+ 133: 211, # '…'
+ 134: 212, # '†'
+ 135: 213, # '‡'
+ 136: 120, # '€'
+ 137: 214, # '‰'
+ 138: 215, # 'Љ'
+ 139: 216, # '‹'
+ 140: 217, # 'Њ'
+ 141: 218, # 'Ќ'
+ 142: 219, # 'Ћ'
+ 143: 220, # 'Ð'
+ 144: 221, # 'Ñ’'
+ 145: 78, # '‘'
+ 146: 64, # '’'
+ 147: 83, # '“'
+ 148: 121, # 'â€'
+ 149: 98, # '•'
+ 150: 117, # '–'
+ 151: 105, # '—'
+ 152: 222, # None
+ 153: 223, # 'â„¢'
+ 154: 224, # 'Ñ™'
+ 155: 225, # '›'
+ 156: 226, # 'Ñš'
+ 157: 227, # 'ќ'
+ 158: 228, # 'Ñ›'
+ 159: 229, # 'ÑŸ'
+ 160: 88, # '\xa0'
+ 161: 230, # 'ÐŽ'
+ 162: 231, # 'Ñž'
+ 163: 232, # 'Ј'
+ 164: 233, # '¤'
+ 165: 122, # 'Ò'
+ 166: 89, # '¦'
+ 167: 106, # '§'
+ 168: 234, # 'Ð'
+ 169: 235, # '©'
+ 170: 236, # 'Є'
+ 171: 237, # '«'
+ 172: 238, # '¬'
+ 173: 45, # '\xad'
+ 174: 239, # '®'
+ 175: 240, # 'Ї'
+ 176: 73, # '°'
+ 177: 80, # '±'
+ 178: 118, # 'І'
+ 179: 114, # 'Ñ–'
+ 180: 241, # 'Ò‘'
+ 181: 242, # 'µ'
+ 182: 243, # '¶'
+ 183: 244, # '·'
+ 184: 245, # 'Ñ‘'
+ 185: 62, # 'â„–'
+ 186: 58, # 'Ñ”'
+ 187: 246, # '»'
+ 188: 247, # 'ј'
+ 189: 248, # 'Ð…'
+ 190: 249, # 'Ñ•'
+ 191: 250, # 'Ñ—'
+ 192: 31, # 'Ð'
+ 193: 32, # 'Б'
+ 194: 35, # 'Ð’'
+ 195: 43, # 'Г'
+ 196: 37, # 'Д'
+ 197: 44, # 'Е'
+ 198: 55, # 'Ж'
+ 199: 47, # 'З'
+ 200: 40, # 'И'
+ 201: 59, # 'Й'
+ 202: 33, # 'К'
+ 203: 46, # 'Л'
+ 204: 38, # 'М'
+ 205: 36, # 'Ð'
+ 206: 41, # 'О'
+ 207: 30, # 'П'
+ 208: 39, # 'Р'
+ 209: 28, # 'С'
+ 210: 34, # 'Т'
+ 211: 51, # 'У'
+ 212: 48, # 'Ф'
+ 213: 49, # 'Ð¥'
+ 214: 53, # 'Ц'
+ 215: 50, # 'Ч'
+ 216: 54, # 'Ш'
+ 217: 57, # 'Щ'
+ 218: 61, # 'Ъ'
+ 219: 251, # 'Ы'
+ 220: 67, # 'Ь'
+ 221: 252, # 'Э'
+ 222: 60, # 'Ю'
+ 223: 56, # 'Я'
+ 224: 1, # 'а'
+ 225: 18, # 'б'
+ 226: 9, # 'в'
+ 227: 20, # 'г'
+ 228: 11, # 'д'
+ 229: 3, # 'е'
+ 230: 23, # 'ж'
+ 231: 15, # 'з'
+ 232: 2, # 'и'
+ 233: 26, # 'й'
+ 234: 12, # 'к'
+ 235: 10, # 'л'
+ 236: 14, # 'м'
+ 237: 6, # 'н'
+ 238: 4, # 'о'
+ 239: 13, # 'п'
+ 240: 7, # 'Ñ€'
+ 241: 8, # 'Ñ'
+ 242: 5, # 'Ñ‚'
+ 243: 19, # 'у'
+ 244: 29, # 'Ñ„'
+ 245: 25, # 'Ñ…'
+ 246: 22, # 'ц'
+ 247: 21, # 'ч'
+ 248: 27, # 'ш'
+ 249: 24, # 'щ'
+ 250: 17, # 'ÑŠ'
+ 251: 75, # 'Ñ‹'
+ 252: 52, # 'ь'
+ 253: 253, # 'Ñ'
+ 254: 42, # 'ÑŽ'
+ 255: 16, # 'Ñ'
}
-WINDOWS_1251_BULGARIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1251',
- language='Bulgarian',
- char_to_order_map=WINDOWS_1251_BULGARIAN_CHAR_TO_ORDER,
- language_model=BULGARIAN_LANG_MODEL,
- typical_positive_ratio=0.969392,
- keep_ascii_letters=False,
- alphabet='ÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрÑтуфхцчшщъьюÑ')
-
+WINDOWS_1251_BULGARIAN_MODEL = SingleByteCharSetModel(
+ charset_name="windows-1251",
+ language="Bulgarian",
+ char_to_order_map=WINDOWS_1251_BULGARIAN_CHAR_TO_ORDER,
+ language_model=BULGARIAN_LANG_MODEL,
+ typical_positive_ratio=0.969392,
+ keep_ascii_letters=False,
+ alphabet="ÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрÑтуфхцчшщъьюÑ",
+)
diff --git a/src/pip/_vendor/chardet/langgreekmodel.py b/src/pip/_vendor/chardet/langgreekmodel.py
index d99528ede..cfb8639e5 100644
--- a/src/pip/_vendor/chardet/langgreekmodel.py
+++ b/src/pip/_vendor/chardet/langgreekmodel.py
@@ -1,9 +1,5 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel
-
# 3: Positive
# 2: Likely
# 1: Unlikely
@@ -3863,536 +3859,539 @@ GREEK_LANG_MODEL = {
# Character Mapping Table(s):
WINDOWS_1253_GREEK_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 82, # 'A'
- 66: 100, # 'B'
- 67: 104, # 'C'
- 68: 94, # 'D'
- 69: 98, # 'E'
- 70: 101, # 'F'
- 71: 116, # 'G'
- 72: 102, # 'H'
- 73: 111, # 'I'
- 74: 187, # 'J'
- 75: 117, # 'K'
- 76: 92, # 'L'
- 77: 88, # 'M'
- 78: 113, # 'N'
- 79: 85, # 'O'
- 80: 79, # 'P'
- 81: 118, # 'Q'
- 82: 105, # 'R'
- 83: 83, # 'S'
- 84: 67, # 'T'
- 85: 114, # 'U'
- 86: 119, # 'V'
- 87: 95, # 'W'
- 88: 99, # 'X'
- 89: 109, # 'Y'
- 90: 188, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 72, # 'a'
- 98: 70, # 'b'
- 99: 80, # 'c'
- 100: 81, # 'd'
- 101: 60, # 'e'
- 102: 96, # 'f'
- 103: 93, # 'g'
- 104: 89, # 'h'
- 105: 68, # 'i'
- 106: 120, # 'j'
- 107: 97, # 'k'
- 108: 77, # 'l'
- 109: 86, # 'm'
- 110: 69, # 'n'
- 111: 55, # 'o'
- 112: 78, # 'p'
- 113: 115, # 'q'
- 114: 65, # 'r'
- 115: 66, # 's'
- 116: 58, # 't'
- 117: 76, # 'u'
- 118: 106, # 'v'
- 119: 103, # 'w'
- 120: 87, # 'x'
- 121: 107, # 'y'
- 122: 112, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 255, # '€'
- 129: 255, # None
- 130: 255, # '‚'
- 131: 255, # 'Æ’'
- 132: 255, # '„'
- 133: 255, # '…'
- 134: 255, # '†'
- 135: 255, # '‡'
- 136: 255, # None
- 137: 255, # '‰'
- 138: 255, # None
- 139: 255, # '‹'
- 140: 255, # None
- 141: 255, # None
- 142: 255, # None
- 143: 255, # None
- 144: 255, # None
- 145: 255, # '‘'
- 146: 255, # '’'
- 147: 255, # '“'
- 148: 255, # 'â€'
- 149: 255, # '•'
- 150: 255, # '–'
- 151: 255, # '—'
- 152: 255, # None
- 153: 255, # 'â„¢'
- 154: 255, # None
- 155: 255, # '›'
- 156: 255, # None
- 157: 255, # None
- 158: 255, # None
- 159: 255, # None
- 160: 253, # '\xa0'
- 161: 233, # 'Î…'
- 162: 61, # 'Ά'
- 163: 253, # '£'
- 164: 253, # '¤'
- 165: 253, # 'Â¥'
- 166: 253, # '¦'
- 167: 253, # '§'
- 168: 253, # '¨'
- 169: 253, # '©'
- 170: 253, # None
- 171: 253, # '«'
- 172: 253, # '¬'
- 173: 74, # '\xad'
- 174: 253, # '®'
- 175: 253, # '―'
- 176: 253, # '°'
- 177: 253, # '±'
- 178: 253, # '²'
- 179: 253, # '³'
- 180: 247, # '΄'
- 181: 253, # 'µ'
- 182: 253, # '¶'
- 183: 36, # '·'
- 184: 46, # 'Έ'
- 185: 71, # 'Ή'
- 186: 73, # 'Ί'
- 187: 253, # '»'
- 188: 54, # 'Ό'
- 189: 253, # '½'
- 190: 108, # 'ÎŽ'
- 191: 123, # 'Î'
- 192: 110, # 'Î'
- 193: 31, # 'Α'
- 194: 51, # 'Î’'
- 195: 43, # 'Γ'
- 196: 41, # 'Δ'
- 197: 34, # 'Ε'
- 198: 91, # 'Ζ'
- 199: 40, # 'Η'
- 200: 52, # 'Θ'
- 201: 47, # 'Ι'
- 202: 44, # 'Κ'
- 203: 53, # 'Λ'
- 204: 38, # 'Μ'
- 205: 49, # 'Î'
- 206: 59, # 'Ξ'
- 207: 39, # 'Ο'
- 208: 35, # 'Π'
- 209: 48, # 'Ρ'
- 210: 250, # None
- 211: 37, # 'Σ'
- 212: 33, # 'Τ'
- 213: 45, # 'Î¥'
- 214: 56, # 'Φ'
- 215: 50, # 'Χ'
- 216: 84, # 'Ψ'
- 217: 57, # 'Ω'
- 218: 120, # 'Ϊ'
- 219: 121, # 'Ϋ'
- 220: 17, # 'ά'
- 221: 18, # 'έ'
- 222: 22, # 'ή'
- 223: 15, # 'ί'
- 224: 124, # 'ΰ'
- 225: 1, # 'α'
- 226: 29, # 'β'
- 227: 20, # 'γ'
- 228: 21, # 'δ'
- 229: 3, # 'ε'
- 230: 32, # 'ζ'
- 231: 13, # 'η'
- 232: 25, # 'θ'
- 233: 5, # 'ι'
- 234: 11, # 'κ'
- 235: 16, # 'λ'
- 236: 10, # 'μ'
- 237: 6, # 'ν'
- 238: 30, # 'ξ'
- 239: 4, # 'ο'
- 240: 9, # 'Ï€'
- 241: 8, # 'Ï'
- 242: 14, # 'Ï‚'
- 243: 7, # 'σ'
- 244: 2, # 'Ï„'
- 245: 12, # 'Ï…'
- 246: 28, # 'φ'
- 247: 23, # 'χ'
- 248: 42, # 'ψ'
- 249: 24, # 'ω'
- 250: 64, # 'ÏŠ'
- 251: 75, # 'Ï‹'
- 252: 19, # 'ό'
- 253: 26, # 'Ï'
- 254: 27, # 'ÏŽ'
- 255: 253, # None
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 82, # 'A'
+ 66: 100, # 'B'
+ 67: 104, # 'C'
+ 68: 94, # 'D'
+ 69: 98, # 'E'
+ 70: 101, # 'F'
+ 71: 116, # 'G'
+ 72: 102, # 'H'
+ 73: 111, # 'I'
+ 74: 187, # 'J'
+ 75: 117, # 'K'
+ 76: 92, # 'L'
+ 77: 88, # 'M'
+ 78: 113, # 'N'
+ 79: 85, # 'O'
+ 80: 79, # 'P'
+ 81: 118, # 'Q'
+ 82: 105, # 'R'
+ 83: 83, # 'S'
+ 84: 67, # 'T'
+ 85: 114, # 'U'
+ 86: 119, # 'V'
+ 87: 95, # 'W'
+ 88: 99, # 'X'
+ 89: 109, # 'Y'
+ 90: 188, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 72, # 'a'
+ 98: 70, # 'b'
+ 99: 80, # 'c'
+ 100: 81, # 'd'
+ 101: 60, # 'e'
+ 102: 96, # 'f'
+ 103: 93, # 'g'
+ 104: 89, # 'h'
+ 105: 68, # 'i'
+ 106: 120, # 'j'
+ 107: 97, # 'k'
+ 108: 77, # 'l'
+ 109: 86, # 'm'
+ 110: 69, # 'n'
+ 111: 55, # 'o'
+ 112: 78, # 'p'
+ 113: 115, # 'q'
+ 114: 65, # 'r'
+ 115: 66, # 's'
+ 116: 58, # 't'
+ 117: 76, # 'u'
+ 118: 106, # 'v'
+ 119: 103, # 'w'
+ 120: 87, # 'x'
+ 121: 107, # 'y'
+ 122: 112, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 255, # '€'
+ 129: 255, # None
+ 130: 255, # '‚'
+ 131: 255, # 'Æ’'
+ 132: 255, # '„'
+ 133: 255, # '…'
+ 134: 255, # '†'
+ 135: 255, # '‡'
+ 136: 255, # None
+ 137: 255, # '‰'
+ 138: 255, # None
+ 139: 255, # '‹'
+ 140: 255, # None
+ 141: 255, # None
+ 142: 255, # None
+ 143: 255, # None
+ 144: 255, # None
+ 145: 255, # '‘'
+ 146: 255, # '’'
+ 147: 255, # '“'
+ 148: 255, # 'â€'
+ 149: 255, # '•'
+ 150: 255, # '–'
+ 151: 255, # '—'
+ 152: 255, # None
+ 153: 255, # 'â„¢'
+ 154: 255, # None
+ 155: 255, # '›'
+ 156: 255, # None
+ 157: 255, # None
+ 158: 255, # None
+ 159: 255, # None
+ 160: 253, # '\xa0'
+ 161: 233, # 'Î…'
+ 162: 61, # 'Ά'
+ 163: 253, # '£'
+ 164: 253, # '¤'
+ 165: 253, # 'Â¥'
+ 166: 253, # '¦'
+ 167: 253, # '§'
+ 168: 253, # '¨'
+ 169: 253, # '©'
+ 170: 253, # None
+ 171: 253, # '«'
+ 172: 253, # '¬'
+ 173: 74, # '\xad'
+ 174: 253, # '®'
+ 175: 253, # '―'
+ 176: 253, # '°'
+ 177: 253, # '±'
+ 178: 253, # '²'
+ 179: 253, # '³'
+ 180: 247, # '΄'
+ 181: 253, # 'µ'
+ 182: 253, # '¶'
+ 183: 36, # '·'
+ 184: 46, # 'Έ'
+ 185: 71, # 'Ή'
+ 186: 73, # 'Ί'
+ 187: 253, # '»'
+ 188: 54, # 'Ό'
+ 189: 253, # '½'
+ 190: 108, # 'ÎŽ'
+ 191: 123, # 'Î'
+ 192: 110, # 'Î'
+ 193: 31, # 'Α'
+ 194: 51, # 'Î’'
+ 195: 43, # 'Γ'
+ 196: 41, # 'Δ'
+ 197: 34, # 'Ε'
+ 198: 91, # 'Ζ'
+ 199: 40, # 'Η'
+ 200: 52, # 'Θ'
+ 201: 47, # 'Ι'
+ 202: 44, # 'Κ'
+ 203: 53, # 'Λ'
+ 204: 38, # 'Μ'
+ 205: 49, # 'Î'
+ 206: 59, # 'Ξ'
+ 207: 39, # 'Ο'
+ 208: 35, # 'Π'
+ 209: 48, # 'Ρ'
+ 210: 250, # None
+ 211: 37, # 'Σ'
+ 212: 33, # 'Τ'
+ 213: 45, # 'Î¥'
+ 214: 56, # 'Φ'
+ 215: 50, # 'Χ'
+ 216: 84, # 'Ψ'
+ 217: 57, # 'Ω'
+ 218: 120, # 'Ϊ'
+ 219: 121, # 'Ϋ'
+ 220: 17, # 'ά'
+ 221: 18, # 'έ'
+ 222: 22, # 'ή'
+ 223: 15, # 'ί'
+ 224: 124, # 'ΰ'
+ 225: 1, # 'α'
+ 226: 29, # 'β'
+ 227: 20, # 'γ'
+ 228: 21, # 'δ'
+ 229: 3, # 'ε'
+ 230: 32, # 'ζ'
+ 231: 13, # 'η'
+ 232: 25, # 'θ'
+ 233: 5, # 'ι'
+ 234: 11, # 'κ'
+ 235: 16, # 'λ'
+ 236: 10, # 'μ'
+ 237: 6, # 'ν'
+ 238: 30, # 'ξ'
+ 239: 4, # 'ο'
+ 240: 9, # 'Ï€'
+ 241: 8, # 'Ï'
+ 242: 14, # 'Ï‚'
+ 243: 7, # 'σ'
+ 244: 2, # 'Ï„'
+ 245: 12, # 'Ï…'
+ 246: 28, # 'φ'
+ 247: 23, # 'χ'
+ 248: 42, # 'ψ'
+ 249: 24, # 'ω'
+ 250: 64, # 'ÏŠ'
+ 251: 75, # 'Ï‹'
+ 252: 19, # 'ό'
+ 253: 26, # 'Ï'
+ 254: 27, # 'ÏŽ'
+ 255: 253, # None
}
-WINDOWS_1253_GREEK_MODEL = SingleByteCharSetModel(charset_name='windows-1253',
- language='Greek',
- char_to_order_map=WINDOWS_1253_GREEK_CHAR_TO_ORDER,
- language_model=GREEK_LANG_MODEL,
- typical_positive_ratio=0.982851,
- keep_ascii_letters=False,
- alphabet='ΆΈΉΊΌΎÎΑΒΓΔΕΖΗΘΙΚΛΜÎΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπÏςστυφχψωόÏÏŽ')
+WINDOWS_1253_GREEK_MODEL = SingleByteCharSetModel(
+ charset_name="windows-1253",
+ language="Greek",
+ char_to_order_map=WINDOWS_1253_GREEK_CHAR_TO_ORDER,
+ language_model=GREEK_LANG_MODEL,
+ typical_positive_ratio=0.982851,
+ keep_ascii_letters=False,
+ alphabet="ΆΈΉΊΌΎÎΑΒΓΔΕΖΗΘΙΚΛΜÎΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπÏςστυφχψωόÏÏŽ",
+)
ISO_8859_7_GREEK_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 82, # 'A'
- 66: 100, # 'B'
- 67: 104, # 'C'
- 68: 94, # 'D'
- 69: 98, # 'E'
- 70: 101, # 'F'
- 71: 116, # 'G'
- 72: 102, # 'H'
- 73: 111, # 'I'
- 74: 187, # 'J'
- 75: 117, # 'K'
- 76: 92, # 'L'
- 77: 88, # 'M'
- 78: 113, # 'N'
- 79: 85, # 'O'
- 80: 79, # 'P'
- 81: 118, # 'Q'
- 82: 105, # 'R'
- 83: 83, # 'S'
- 84: 67, # 'T'
- 85: 114, # 'U'
- 86: 119, # 'V'
- 87: 95, # 'W'
- 88: 99, # 'X'
- 89: 109, # 'Y'
- 90: 188, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 72, # 'a'
- 98: 70, # 'b'
- 99: 80, # 'c'
- 100: 81, # 'd'
- 101: 60, # 'e'
- 102: 96, # 'f'
- 103: 93, # 'g'
- 104: 89, # 'h'
- 105: 68, # 'i'
- 106: 120, # 'j'
- 107: 97, # 'k'
- 108: 77, # 'l'
- 109: 86, # 'm'
- 110: 69, # 'n'
- 111: 55, # 'o'
- 112: 78, # 'p'
- 113: 115, # 'q'
- 114: 65, # 'r'
- 115: 66, # 's'
- 116: 58, # 't'
- 117: 76, # 'u'
- 118: 106, # 'v'
- 119: 103, # 'w'
- 120: 87, # 'x'
- 121: 107, # 'y'
- 122: 112, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 255, # '\x80'
- 129: 255, # '\x81'
- 130: 255, # '\x82'
- 131: 255, # '\x83'
- 132: 255, # '\x84'
- 133: 255, # '\x85'
- 134: 255, # '\x86'
- 135: 255, # '\x87'
- 136: 255, # '\x88'
- 137: 255, # '\x89'
- 138: 255, # '\x8a'
- 139: 255, # '\x8b'
- 140: 255, # '\x8c'
- 141: 255, # '\x8d'
- 142: 255, # '\x8e'
- 143: 255, # '\x8f'
- 144: 255, # '\x90'
- 145: 255, # '\x91'
- 146: 255, # '\x92'
- 147: 255, # '\x93'
- 148: 255, # '\x94'
- 149: 255, # '\x95'
- 150: 255, # '\x96'
- 151: 255, # '\x97'
- 152: 255, # '\x98'
- 153: 255, # '\x99'
- 154: 255, # '\x9a'
- 155: 255, # '\x9b'
- 156: 255, # '\x9c'
- 157: 255, # '\x9d'
- 158: 255, # '\x9e'
- 159: 255, # '\x9f'
- 160: 253, # '\xa0'
- 161: 233, # '‘'
- 162: 90, # '’'
- 163: 253, # '£'
- 164: 253, # '€'
- 165: 253, # '₯'
- 166: 253, # '¦'
- 167: 253, # '§'
- 168: 253, # '¨'
- 169: 253, # '©'
- 170: 253, # 'ͺ'
- 171: 253, # '«'
- 172: 253, # '¬'
- 173: 74, # '\xad'
- 174: 253, # None
- 175: 253, # '―'
- 176: 253, # '°'
- 177: 253, # '±'
- 178: 253, # '²'
- 179: 253, # '³'
- 180: 247, # '΄'
- 181: 248, # 'Î…'
- 182: 61, # 'Ά'
- 183: 36, # '·'
- 184: 46, # 'Έ'
- 185: 71, # 'Ή'
- 186: 73, # 'Ί'
- 187: 253, # '»'
- 188: 54, # 'Ό'
- 189: 253, # '½'
- 190: 108, # 'ÎŽ'
- 191: 123, # 'Î'
- 192: 110, # 'Î'
- 193: 31, # 'Α'
- 194: 51, # 'Î’'
- 195: 43, # 'Γ'
- 196: 41, # 'Δ'
- 197: 34, # 'Ε'
- 198: 91, # 'Ζ'
- 199: 40, # 'Η'
- 200: 52, # 'Θ'
- 201: 47, # 'Ι'
- 202: 44, # 'Κ'
- 203: 53, # 'Λ'
- 204: 38, # 'Μ'
- 205: 49, # 'Î'
- 206: 59, # 'Ξ'
- 207: 39, # 'Ο'
- 208: 35, # 'Π'
- 209: 48, # 'Ρ'
- 210: 250, # None
- 211: 37, # 'Σ'
- 212: 33, # 'Τ'
- 213: 45, # 'Î¥'
- 214: 56, # 'Φ'
- 215: 50, # 'Χ'
- 216: 84, # 'Ψ'
- 217: 57, # 'Ω'
- 218: 120, # 'Ϊ'
- 219: 121, # 'Ϋ'
- 220: 17, # 'ά'
- 221: 18, # 'έ'
- 222: 22, # 'ή'
- 223: 15, # 'ί'
- 224: 124, # 'ΰ'
- 225: 1, # 'α'
- 226: 29, # 'β'
- 227: 20, # 'γ'
- 228: 21, # 'δ'
- 229: 3, # 'ε'
- 230: 32, # 'ζ'
- 231: 13, # 'η'
- 232: 25, # 'θ'
- 233: 5, # 'ι'
- 234: 11, # 'κ'
- 235: 16, # 'λ'
- 236: 10, # 'μ'
- 237: 6, # 'ν'
- 238: 30, # 'ξ'
- 239: 4, # 'ο'
- 240: 9, # 'Ï€'
- 241: 8, # 'Ï'
- 242: 14, # 'Ï‚'
- 243: 7, # 'σ'
- 244: 2, # 'Ï„'
- 245: 12, # 'Ï…'
- 246: 28, # 'φ'
- 247: 23, # 'χ'
- 248: 42, # 'ψ'
- 249: 24, # 'ω'
- 250: 64, # 'ÏŠ'
- 251: 75, # 'Ï‹'
- 252: 19, # 'ό'
- 253: 26, # 'Ï'
- 254: 27, # 'ÏŽ'
- 255: 253, # None
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 82, # 'A'
+ 66: 100, # 'B'
+ 67: 104, # 'C'
+ 68: 94, # 'D'
+ 69: 98, # 'E'
+ 70: 101, # 'F'
+ 71: 116, # 'G'
+ 72: 102, # 'H'
+ 73: 111, # 'I'
+ 74: 187, # 'J'
+ 75: 117, # 'K'
+ 76: 92, # 'L'
+ 77: 88, # 'M'
+ 78: 113, # 'N'
+ 79: 85, # 'O'
+ 80: 79, # 'P'
+ 81: 118, # 'Q'
+ 82: 105, # 'R'
+ 83: 83, # 'S'
+ 84: 67, # 'T'
+ 85: 114, # 'U'
+ 86: 119, # 'V'
+ 87: 95, # 'W'
+ 88: 99, # 'X'
+ 89: 109, # 'Y'
+ 90: 188, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 72, # 'a'
+ 98: 70, # 'b'
+ 99: 80, # 'c'
+ 100: 81, # 'd'
+ 101: 60, # 'e'
+ 102: 96, # 'f'
+ 103: 93, # 'g'
+ 104: 89, # 'h'
+ 105: 68, # 'i'
+ 106: 120, # 'j'
+ 107: 97, # 'k'
+ 108: 77, # 'l'
+ 109: 86, # 'm'
+ 110: 69, # 'n'
+ 111: 55, # 'o'
+ 112: 78, # 'p'
+ 113: 115, # 'q'
+ 114: 65, # 'r'
+ 115: 66, # 's'
+ 116: 58, # 't'
+ 117: 76, # 'u'
+ 118: 106, # 'v'
+ 119: 103, # 'w'
+ 120: 87, # 'x'
+ 121: 107, # 'y'
+ 122: 112, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 255, # '\x80'
+ 129: 255, # '\x81'
+ 130: 255, # '\x82'
+ 131: 255, # '\x83'
+ 132: 255, # '\x84'
+ 133: 255, # '\x85'
+ 134: 255, # '\x86'
+ 135: 255, # '\x87'
+ 136: 255, # '\x88'
+ 137: 255, # '\x89'
+ 138: 255, # '\x8a'
+ 139: 255, # '\x8b'
+ 140: 255, # '\x8c'
+ 141: 255, # '\x8d'
+ 142: 255, # '\x8e'
+ 143: 255, # '\x8f'
+ 144: 255, # '\x90'
+ 145: 255, # '\x91'
+ 146: 255, # '\x92'
+ 147: 255, # '\x93'
+ 148: 255, # '\x94'
+ 149: 255, # '\x95'
+ 150: 255, # '\x96'
+ 151: 255, # '\x97'
+ 152: 255, # '\x98'
+ 153: 255, # '\x99'
+ 154: 255, # '\x9a'
+ 155: 255, # '\x9b'
+ 156: 255, # '\x9c'
+ 157: 255, # '\x9d'
+ 158: 255, # '\x9e'
+ 159: 255, # '\x9f'
+ 160: 253, # '\xa0'
+ 161: 233, # '‘'
+ 162: 90, # '’'
+ 163: 253, # '£'
+ 164: 253, # '€'
+ 165: 253, # '₯'
+ 166: 253, # '¦'
+ 167: 253, # '§'
+ 168: 253, # '¨'
+ 169: 253, # '©'
+ 170: 253, # 'ͺ'
+ 171: 253, # '«'
+ 172: 253, # '¬'
+ 173: 74, # '\xad'
+ 174: 253, # None
+ 175: 253, # '―'
+ 176: 253, # '°'
+ 177: 253, # '±'
+ 178: 253, # '²'
+ 179: 253, # '³'
+ 180: 247, # '΄'
+ 181: 248, # 'Î…'
+ 182: 61, # 'Ά'
+ 183: 36, # '·'
+ 184: 46, # 'Έ'
+ 185: 71, # 'Ή'
+ 186: 73, # 'Ί'
+ 187: 253, # '»'
+ 188: 54, # 'Ό'
+ 189: 253, # '½'
+ 190: 108, # 'ÎŽ'
+ 191: 123, # 'Î'
+ 192: 110, # 'Î'
+ 193: 31, # 'Α'
+ 194: 51, # 'Î’'
+ 195: 43, # 'Γ'
+ 196: 41, # 'Δ'
+ 197: 34, # 'Ε'
+ 198: 91, # 'Ζ'
+ 199: 40, # 'Η'
+ 200: 52, # 'Θ'
+ 201: 47, # 'Ι'
+ 202: 44, # 'Κ'
+ 203: 53, # 'Λ'
+ 204: 38, # 'Μ'
+ 205: 49, # 'Î'
+ 206: 59, # 'Ξ'
+ 207: 39, # 'Ο'
+ 208: 35, # 'Π'
+ 209: 48, # 'Ρ'
+ 210: 250, # None
+ 211: 37, # 'Σ'
+ 212: 33, # 'Τ'
+ 213: 45, # 'Î¥'
+ 214: 56, # 'Φ'
+ 215: 50, # 'Χ'
+ 216: 84, # 'Ψ'
+ 217: 57, # 'Ω'
+ 218: 120, # 'Ϊ'
+ 219: 121, # 'Ϋ'
+ 220: 17, # 'ά'
+ 221: 18, # 'έ'
+ 222: 22, # 'ή'
+ 223: 15, # 'ί'
+ 224: 124, # 'ΰ'
+ 225: 1, # 'α'
+ 226: 29, # 'β'
+ 227: 20, # 'γ'
+ 228: 21, # 'δ'
+ 229: 3, # 'ε'
+ 230: 32, # 'ζ'
+ 231: 13, # 'η'
+ 232: 25, # 'θ'
+ 233: 5, # 'ι'
+ 234: 11, # 'κ'
+ 235: 16, # 'λ'
+ 236: 10, # 'μ'
+ 237: 6, # 'ν'
+ 238: 30, # 'ξ'
+ 239: 4, # 'ο'
+ 240: 9, # 'Ï€'
+ 241: 8, # 'Ï'
+ 242: 14, # 'Ï‚'
+ 243: 7, # 'σ'
+ 244: 2, # 'Ï„'
+ 245: 12, # 'Ï…'
+ 246: 28, # 'φ'
+ 247: 23, # 'χ'
+ 248: 42, # 'ψ'
+ 249: 24, # 'ω'
+ 250: 64, # 'ÏŠ'
+ 251: 75, # 'Ï‹'
+ 252: 19, # 'ό'
+ 253: 26, # 'Ï'
+ 254: 27, # 'ÏŽ'
+ 255: 253, # None
}
-ISO_8859_7_GREEK_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-7',
- language='Greek',
- char_to_order_map=ISO_8859_7_GREEK_CHAR_TO_ORDER,
- language_model=GREEK_LANG_MODEL,
- typical_positive_ratio=0.982851,
- keep_ascii_letters=False,
- alphabet='ΆΈΉΊΌΎÎΑΒΓΔΕΖΗΘΙΚΛΜÎΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπÏςστυφχψωόÏÏŽ')
-
+ISO_8859_7_GREEK_MODEL = SingleByteCharSetModel(
+ charset_name="ISO-8859-7",
+ language="Greek",
+ char_to_order_map=ISO_8859_7_GREEK_CHAR_TO_ORDER,
+ language_model=GREEK_LANG_MODEL,
+ typical_positive_ratio=0.982851,
+ keep_ascii_letters=False,
+ alphabet="ΆΈΉΊΌΎÎΑΒΓΔΕΖΗΘΙΚΛΜÎΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπÏςστυφχψωόÏÏŽ",
+)
diff --git a/src/pip/_vendor/chardet/langhebrewmodel.py b/src/pip/_vendor/chardet/langhebrewmodel.py
index 484c652a4..56d297587 100644
--- a/src/pip/_vendor/chardet/langhebrewmodel.py
+++ b/src/pip/_vendor/chardet/langhebrewmodel.py
@@ -1,9 +1,5 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel
-
# 3: Positive
# 2: Likely
# 1: Unlikely
@@ -4115,269 +4111,270 @@ HEBREW_LANG_MODEL = {
# Character Mapping Table(s):
WINDOWS_1255_HEBREW_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 69, # 'A'
- 66: 91, # 'B'
- 67: 79, # 'C'
- 68: 80, # 'D'
- 69: 92, # 'E'
- 70: 89, # 'F'
- 71: 97, # 'G'
- 72: 90, # 'H'
- 73: 68, # 'I'
- 74: 111, # 'J'
- 75: 112, # 'K'
- 76: 82, # 'L'
- 77: 73, # 'M'
- 78: 95, # 'N'
- 79: 85, # 'O'
- 80: 78, # 'P'
- 81: 121, # 'Q'
- 82: 86, # 'R'
- 83: 71, # 'S'
- 84: 67, # 'T'
- 85: 102, # 'U'
- 86: 107, # 'V'
- 87: 84, # 'W'
- 88: 114, # 'X'
- 89: 103, # 'Y'
- 90: 115, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 50, # 'a'
- 98: 74, # 'b'
- 99: 60, # 'c'
- 100: 61, # 'd'
- 101: 42, # 'e'
- 102: 76, # 'f'
- 103: 70, # 'g'
- 104: 64, # 'h'
- 105: 53, # 'i'
- 106: 105, # 'j'
- 107: 93, # 'k'
- 108: 56, # 'l'
- 109: 65, # 'm'
- 110: 54, # 'n'
- 111: 49, # 'o'
- 112: 66, # 'p'
- 113: 110, # 'q'
- 114: 51, # 'r'
- 115: 43, # 's'
- 116: 44, # 't'
- 117: 63, # 'u'
- 118: 81, # 'v'
- 119: 77, # 'w'
- 120: 98, # 'x'
- 121: 75, # 'y'
- 122: 108, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 124, # '€'
- 129: 202, # None
- 130: 203, # '‚'
- 131: 204, # 'Æ’'
- 132: 205, # '„'
- 133: 40, # '…'
- 134: 58, # '†'
- 135: 206, # '‡'
- 136: 207, # 'ˆ'
- 137: 208, # '‰'
- 138: 209, # None
- 139: 210, # '‹'
- 140: 211, # None
- 141: 212, # None
- 142: 213, # None
- 143: 214, # None
- 144: 215, # None
- 145: 83, # '‘'
- 146: 52, # '’'
- 147: 47, # '“'
- 148: 46, # 'â€'
- 149: 72, # '•'
- 150: 32, # '–'
- 151: 94, # '—'
- 152: 216, # '˜'
- 153: 113, # 'â„¢'
- 154: 217, # None
- 155: 109, # '›'
- 156: 218, # None
- 157: 219, # None
- 158: 220, # None
- 159: 221, # None
- 160: 34, # '\xa0'
- 161: 116, # '¡'
- 162: 222, # '¢'
- 163: 118, # '£'
- 164: 100, # '₪'
- 165: 223, # 'Â¥'
- 166: 224, # '¦'
- 167: 117, # '§'
- 168: 119, # '¨'
- 169: 104, # '©'
- 170: 125, # '×'
- 171: 225, # '«'
- 172: 226, # '¬'
- 173: 87, # '\xad'
- 174: 99, # '®'
- 175: 227, # '¯'
- 176: 106, # '°'
- 177: 122, # '±'
- 178: 123, # '²'
- 179: 228, # '³'
- 180: 55, # '´'
- 181: 229, # 'µ'
- 182: 230, # '¶'
- 183: 101, # '·'
- 184: 231, # '¸'
- 185: 232, # '¹'
- 186: 120, # '÷'
- 187: 233, # '»'
- 188: 48, # '¼'
- 189: 39, # '½'
- 190: 57, # '¾'
- 191: 234, # '¿'
- 192: 30, # 'Ö°'
- 193: 59, # 'Ö±'
- 194: 41, # 'Ö²'
- 195: 88, # 'Ö³'
- 196: 33, # 'Ö´'
- 197: 37, # 'Öµ'
- 198: 36, # 'Ö¶'
- 199: 31, # 'Ö·'
- 200: 29, # 'Ö¸'
- 201: 35, # 'Ö¹'
- 202: 235, # None
- 203: 62, # 'Ö»'
- 204: 28, # 'Ö¼'
- 205: 236, # 'Ö½'
- 206: 126, # 'Ö¾'
- 207: 237, # 'Ö¿'
- 208: 238, # '×€'
- 209: 38, # '×'
- 210: 45, # 'ׂ'
- 211: 239, # '׃'
- 212: 240, # '×°'
- 213: 241, # '×±'
- 214: 242, # 'ײ'
- 215: 243, # '׳'
- 216: 127, # '×´'
- 217: 244, # None
- 218: 245, # None
- 219: 246, # None
- 220: 247, # None
- 221: 248, # None
- 222: 249, # None
- 223: 250, # None
- 224: 9, # '×'
- 225: 8, # 'ב'
- 226: 20, # '×’'
- 227: 16, # 'ד'
- 228: 3, # '×”'
- 229: 2, # 'ו'
- 230: 24, # '×–'
- 231: 14, # '×—'
- 232: 22, # 'ט'
- 233: 1, # '×™'
- 234: 25, # 'ך'
- 235: 15, # '×›'
- 236: 4, # 'ל'
- 237: 11, # '×'
- 238: 6, # 'מ'
- 239: 23, # 'ן'
- 240: 12, # '× '
- 241: 19, # 'ס'
- 242: 13, # '×¢'
- 243: 26, # '×£'
- 244: 18, # 'פ'
- 245: 27, # '×¥'
- 246: 21, # 'צ'
- 247: 17, # 'ק'
- 248: 7, # 'ר'
- 249: 10, # 'ש'
- 250: 5, # 'ת'
- 251: 251, # None
- 252: 252, # None
- 253: 128, # '\u200e'
- 254: 96, # '\u200f'
- 255: 253, # None
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 69, # 'A'
+ 66: 91, # 'B'
+ 67: 79, # 'C'
+ 68: 80, # 'D'
+ 69: 92, # 'E'
+ 70: 89, # 'F'
+ 71: 97, # 'G'
+ 72: 90, # 'H'
+ 73: 68, # 'I'
+ 74: 111, # 'J'
+ 75: 112, # 'K'
+ 76: 82, # 'L'
+ 77: 73, # 'M'
+ 78: 95, # 'N'
+ 79: 85, # 'O'
+ 80: 78, # 'P'
+ 81: 121, # 'Q'
+ 82: 86, # 'R'
+ 83: 71, # 'S'
+ 84: 67, # 'T'
+ 85: 102, # 'U'
+ 86: 107, # 'V'
+ 87: 84, # 'W'
+ 88: 114, # 'X'
+ 89: 103, # 'Y'
+ 90: 115, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 50, # 'a'
+ 98: 74, # 'b'
+ 99: 60, # 'c'
+ 100: 61, # 'd'
+ 101: 42, # 'e'
+ 102: 76, # 'f'
+ 103: 70, # 'g'
+ 104: 64, # 'h'
+ 105: 53, # 'i'
+ 106: 105, # 'j'
+ 107: 93, # 'k'
+ 108: 56, # 'l'
+ 109: 65, # 'm'
+ 110: 54, # 'n'
+ 111: 49, # 'o'
+ 112: 66, # 'p'
+ 113: 110, # 'q'
+ 114: 51, # 'r'
+ 115: 43, # 's'
+ 116: 44, # 't'
+ 117: 63, # 'u'
+ 118: 81, # 'v'
+ 119: 77, # 'w'
+ 120: 98, # 'x'
+ 121: 75, # 'y'
+ 122: 108, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 124, # '€'
+ 129: 202, # None
+ 130: 203, # '‚'
+ 131: 204, # 'Æ’'
+ 132: 205, # '„'
+ 133: 40, # '…'
+ 134: 58, # '†'
+ 135: 206, # '‡'
+ 136: 207, # 'ˆ'
+ 137: 208, # '‰'
+ 138: 209, # None
+ 139: 210, # '‹'
+ 140: 211, # None
+ 141: 212, # None
+ 142: 213, # None
+ 143: 214, # None
+ 144: 215, # None
+ 145: 83, # '‘'
+ 146: 52, # '’'
+ 147: 47, # '“'
+ 148: 46, # 'â€'
+ 149: 72, # '•'
+ 150: 32, # '–'
+ 151: 94, # '—'
+ 152: 216, # '˜'
+ 153: 113, # 'â„¢'
+ 154: 217, # None
+ 155: 109, # '›'
+ 156: 218, # None
+ 157: 219, # None
+ 158: 220, # None
+ 159: 221, # None
+ 160: 34, # '\xa0'
+ 161: 116, # '¡'
+ 162: 222, # '¢'
+ 163: 118, # '£'
+ 164: 100, # '₪'
+ 165: 223, # 'Â¥'
+ 166: 224, # '¦'
+ 167: 117, # '§'
+ 168: 119, # '¨'
+ 169: 104, # '©'
+ 170: 125, # '×'
+ 171: 225, # '«'
+ 172: 226, # '¬'
+ 173: 87, # '\xad'
+ 174: 99, # '®'
+ 175: 227, # '¯'
+ 176: 106, # '°'
+ 177: 122, # '±'
+ 178: 123, # '²'
+ 179: 228, # '³'
+ 180: 55, # '´'
+ 181: 229, # 'µ'
+ 182: 230, # '¶'
+ 183: 101, # '·'
+ 184: 231, # '¸'
+ 185: 232, # '¹'
+ 186: 120, # '÷'
+ 187: 233, # '»'
+ 188: 48, # '¼'
+ 189: 39, # '½'
+ 190: 57, # '¾'
+ 191: 234, # '¿'
+ 192: 30, # 'Ö°'
+ 193: 59, # 'Ö±'
+ 194: 41, # 'Ö²'
+ 195: 88, # 'Ö³'
+ 196: 33, # 'Ö´'
+ 197: 37, # 'Öµ'
+ 198: 36, # 'Ö¶'
+ 199: 31, # 'Ö·'
+ 200: 29, # 'Ö¸'
+ 201: 35, # 'Ö¹'
+ 202: 235, # None
+ 203: 62, # 'Ö»'
+ 204: 28, # 'Ö¼'
+ 205: 236, # 'Ö½'
+ 206: 126, # 'Ö¾'
+ 207: 237, # 'Ö¿'
+ 208: 238, # '×€'
+ 209: 38, # '×'
+ 210: 45, # 'ׂ'
+ 211: 239, # '׃'
+ 212: 240, # '×°'
+ 213: 241, # '×±'
+ 214: 242, # 'ײ'
+ 215: 243, # '׳'
+ 216: 127, # '×´'
+ 217: 244, # None
+ 218: 245, # None
+ 219: 246, # None
+ 220: 247, # None
+ 221: 248, # None
+ 222: 249, # None
+ 223: 250, # None
+ 224: 9, # '×'
+ 225: 8, # 'ב'
+ 226: 20, # '×’'
+ 227: 16, # 'ד'
+ 228: 3, # '×”'
+ 229: 2, # 'ו'
+ 230: 24, # '×–'
+ 231: 14, # '×—'
+ 232: 22, # 'ט'
+ 233: 1, # '×™'
+ 234: 25, # 'ך'
+ 235: 15, # '×›'
+ 236: 4, # 'ל'
+ 237: 11, # '×'
+ 238: 6, # 'מ'
+ 239: 23, # 'ן'
+ 240: 12, # '× '
+ 241: 19, # 'ס'
+ 242: 13, # '×¢'
+ 243: 26, # '×£'
+ 244: 18, # 'פ'
+ 245: 27, # '×¥'
+ 246: 21, # 'צ'
+ 247: 17, # 'ק'
+ 248: 7, # 'ר'
+ 249: 10, # 'ש'
+ 250: 5, # 'ת'
+ 251: 251, # None
+ 252: 252, # None
+ 253: 128, # '\u200e'
+ 254: 96, # '\u200f'
+ 255: 253, # None
}
-WINDOWS_1255_HEBREW_MODEL = SingleByteCharSetModel(charset_name='windows-1255',
- language='Hebrew',
- char_to_order_map=WINDOWS_1255_HEBREW_CHAR_TO_ORDER,
- language_model=HEBREW_LANG_MODEL,
- typical_positive_ratio=0.984004,
- keep_ascii_letters=False,
- alphabet='×בגדהוזחטיךכל×מןנסעףפץצקרשתװױײ')
-
+WINDOWS_1255_HEBREW_MODEL = SingleByteCharSetModel(
+ charset_name="windows-1255",
+ language="Hebrew",
+ char_to_order_map=WINDOWS_1255_HEBREW_CHAR_TO_ORDER,
+ language_model=HEBREW_LANG_MODEL,
+ typical_positive_ratio=0.984004,
+ keep_ascii_letters=False,
+ alphabet="×בגדהוזחטיךכל×מןנסעףפץצקרשתװױײ",
+)
diff --git a/src/pip/_vendor/chardet/langhungarianmodel.py b/src/pip/_vendor/chardet/langhungarianmodel.py
index bbc5cda64..09a0d326b 100644
--- a/src/pip/_vendor/chardet/langhungarianmodel.py
+++ b/src/pip/_vendor/chardet/langhungarianmodel.py
@@ -1,9 +1,5 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel
-
# 3: Positive
# 2: Likely
# 1: Unlikely
@@ -4115,536 +4111,539 @@ HUNGARIAN_LANG_MODEL = {
# Character Mapping Table(s):
WINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 28, # 'A'
- 66: 40, # 'B'
- 67: 54, # 'C'
- 68: 45, # 'D'
- 69: 32, # 'E'
- 70: 50, # 'F'
- 71: 49, # 'G'
- 72: 38, # 'H'
- 73: 39, # 'I'
- 74: 53, # 'J'
- 75: 36, # 'K'
- 76: 41, # 'L'
- 77: 34, # 'M'
- 78: 35, # 'N'
- 79: 47, # 'O'
- 80: 46, # 'P'
- 81: 72, # 'Q'
- 82: 43, # 'R'
- 83: 33, # 'S'
- 84: 37, # 'T'
- 85: 57, # 'U'
- 86: 48, # 'V'
- 87: 64, # 'W'
- 88: 68, # 'X'
- 89: 55, # 'Y'
- 90: 52, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 2, # 'a'
- 98: 18, # 'b'
- 99: 26, # 'c'
- 100: 17, # 'd'
- 101: 1, # 'e'
- 102: 27, # 'f'
- 103: 12, # 'g'
- 104: 20, # 'h'
- 105: 9, # 'i'
- 106: 22, # 'j'
- 107: 7, # 'k'
- 108: 6, # 'l'
- 109: 13, # 'm'
- 110: 4, # 'n'
- 111: 8, # 'o'
- 112: 23, # 'p'
- 113: 67, # 'q'
- 114: 10, # 'r'
- 115: 5, # 's'
- 116: 3, # 't'
- 117: 21, # 'u'
- 118: 19, # 'v'
- 119: 65, # 'w'
- 120: 62, # 'x'
- 121: 16, # 'y'
- 122: 11, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 161, # '€'
- 129: 162, # None
- 130: 163, # '‚'
- 131: 164, # None
- 132: 165, # '„'
- 133: 166, # '…'
- 134: 167, # '†'
- 135: 168, # '‡'
- 136: 169, # None
- 137: 170, # '‰'
- 138: 171, # 'Å '
- 139: 172, # '‹'
- 140: 173, # 'Åš'
- 141: 174, # 'Ť'
- 142: 175, # 'Ž'
- 143: 176, # 'Ź'
- 144: 177, # None
- 145: 178, # '‘'
- 146: 179, # '’'
- 147: 180, # '“'
- 148: 78, # 'â€'
- 149: 181, # '•'
- 150: 69, # '–'
- 151: 182, # '—'
- 152: 183, # None
- 153: 184, # 'â„¢'
- 154: 185, # 'Å¡'
- 155: 186, # '›'
- 156: 187, # 'Å›'
- 157: 188, # 'Å¥'
- 158: 189, # 'ž'
- 159: 190, # 'ź'
- 160: 191, # '\xa0'
- 161: 192, # 'ˇ'
- 162: 193, # '˘'
- 163: 194, # 'Å'
- 164: 195, # '¤'
- 165: 196, # 'Ä„'
- 166: 197, # '¦'
- 167: 76, # '§'
- 168: 198, # '¨'
- 169: 199, # '©'
- 170: 200, # 'Åž'
- 171: 201, # '«'
- 172: 202, # '¬'
- 173: 203, # '\xad'
- 174: 204, # '®'
- 175: 205, # 'Å»'
- 176: 81, # '°'
- 177: 206, # '±'
- 178: 207, # 'Ë›'
- 179: 208, # 'Å‚'
- 180: 209, # '´'
- 181: 210, # 'µ'
- 182: 211, # '¶'
- 183: 212, # '·'
- 184: 213, # '¸'
- 185: 214, # 'Ä…'
- 186: 215, # 'ÅŸ'
- 187: 216, # '»'
- 188: 217, # 'Ľ'
- 189: 218, # 'Ë'
- 190: 219, # 'ľ'
- 191: 220, # 'ż'
- 192: 221, # 'Å”'
- 193: 51, # 'Ã'
- 194: 83, # 'Â'
- 195: 222, # 'Ä‚'
- 196: 80, # 'Ä'
- 197: 223, # 'Ĺ'
- 198: 224, # 'Ć'
- 199: 225, # 'Ç'
- 200: 226, # 'Č'
- 201: 44, # 'É'
- 202: 227, # 'Ę'
- 203: 228, # 'Ë'
- 204: 229, # 'Äš'
- 205: 61, # 'Ã'
- 206: 230, # 'ÃŽ'
- 207: 231, # 'ÄŽ'
- 208: 232, # 'Ä'
- 209: 233, # 'Ń'
- 210: 234, # 'Ň'
- 211: 58, # 'Ó'
- 212: 235, # 'Ô'
- 213: 66, # 'Å'
- 214: 59, # 'Ö'
- 215: 236, # '×'
- 216: 237, # 'Ř'
- 217: 238, # 'Å®'
- 218: 60, # 'Ú'
- 219: 70, # 'Å°'
- 220: 63, # 'Ü'
- 221: 239, # 'Ã'
- 222: 240, # 'Å¢'
- 223: 241, # 'ß'
- 224: 84, # 'Å•'
- 225: 14, # 'á'
- 226: 75, # 'â'
- 227: 242, # 'ă'
- 228: 71, # 'ä'
- 229: 82, # 'ĺ'
- 230: 243, # 'ć'
- 231: 73, # 'ç'
- 232: 244, # 'Ä'
- 233: 15, # 'é'
- 234: 85, # 'Ä™'
- 235: 79, # 'ë'
- 236: 86, # 'Ä›'
- 237: 30, # 'í'
- 238: 77, # 'î'
- 239: 87, # 'Ä'
- 240: 245, # 'Ä‘'
- 241: 246, # 'Å„'
- 242: 247, # 'ň'
- 243: 25, # 'ó'
- 244: 74, # 'ô'
- 245: 42, # 'Å‘'
- 246: 24, # 'ö'
- 247: 248, # '÷'
- 248: 249, # 'Å™'
- 249: 250, # 'ů'
- 250: 31, # 'ú'
- 251: 56, # 'ű'
- 252: 29, # 'ü'
- 253: 251, # 'ý'
- 254: 252, # 'Å£'
- 255: 253, # 'Ë™'
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 28, # 'A'
+ 66: 40, # 'B'
+ 67: 54, # 'C'
+ 68: 45, # 'D'
+ 69: 32, # 'E'
+ 70: 50, # 'F'
+ 71: 49, # 'G'
+ 72: 38, # 'H'
+ 73: 39, # 'I'
+ 74: 53, # 'J'
+ 75: 36, # 'K'
+ 76: 41, # 'L'
+ 77: 34, # 'M'
+ 78: 35, # 'N'
+ 79: 47, # 'O'
+ 80: 46, # 'P'
+ 81: 72, # 'Q'
+ 82: 43, # 'R'
+ 83: 33, # 'S'
+ 84: 37, # 'T'
+ 85: 57, # 'U'
+ 86: 48, # 'V'
+ 87: 64, # 'W'
+ 88: 68, # 'X'
+ 89: 55, # 'Y'
+ 90: 52, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 2, # 'a'
+ 98: 18, # 'b'
+ 99: 26, # 'c'
+ 100: 17, # 'd'
+ 101: 1, # 'e'
+ 102: 27, # 'f'
+ 103: 12, # 'g'
+ 104: 20, # 'h'
+ 105: 9, # 'i'
+ 106: 22, # 'j'
+ 107: 7, # 'k'
+ 108: 6, # 'l'
+ 109: 13, # 'm'
+ 110: 4, # 'n'
+ 111: 8, # 'o'
+ 112: 23, # 'p'
+ 113: 67, # 'q'
+ 114: 10, # 'r'
+ 115: 5, # 's'
+ 116: 3, # 't'
+ 117: 21, # 'u'
+ 118: 19, # 'v'
+ 119: 65, # 'w'
+ 120: 62, # 'x'
+ 121: 16, # 'y'
+ 122: 11, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 161, # '€'
+ 129: 162, # None
+ 130: 163, # '‚'
+ 131: 164, # None
+ 132: 165, # '„'
+ 133: 166, # '…'
+ 134: 167, # '†'
+ 135: 168, # '‡'
+ 136: 169, # None
+ 137: 170, # '‰'
+ 138: 171, # 'Å '
+ 139: 172, # '‹'
+ 140: 173, # 'Åš'
+ 141: 174, # 'Ť'
+ 142: 175, # 'Ž'
+ 143: 176, # 'Ź'
+ 144: 177, # None
+ 145: 178, # '‘'
+ 146: 179, # '’'
+ 147: 180, # '“'
+ 148: 78, # 'â€'
+ 149: 181, # '•'
+ 150: 69, # '–'
+ 151: 182, # '—'
+ 152: 183, # None
+ 153: 184, # 'â„¢'
+ 154: 185, # 'Å¡'
+ 155: 186, # '›'
+ 156: 187, # 'Å›'
+ 157: 188, # 'Å¥'
+ 158: 189, # 'ž'
+ 159: 190, # 'ź'
+ 160: 191, # '\xa0'
+ 161: 192, # 'ˇ'
+ 162: 193, # '˘'
+ 163: 194, # 'Å'
+ 164: 195, # '¤'
+ 165: 196, # 'Ä„'
+ 166: 197, # '¦'
+ 167: 76, # '§'
+ 168: 198, # '¨'
+ 169: 199, # '©'
+ 170: 200, # 'Åž'
+ 171: 201, # '«'
+ 172: 202, # '¬'
+ 173: 203, # '\xad'
+ 174: 204, # '®'
+ 175: 205, # 'Å»'
+ 176: 81, # '°'
+ 177: 206, # '±'
+ 178: 207, # 'Ë›'
+ 179: 208, # 'Å‚'
+ 180: 209, # '´'
+ 181: 210, # 'µ'
+ 182: 211, # '¶'
+ 183: 212, # '·'
+ 184: 213, # '¸'
+ 185: 214, # 'Ä…'
+ 186: 215, # 'ÅŸ'
+ 187: 216, # '»'
+ 188: 217, # 'Ľ'
+ 189: 218, # 'Ë'
+ 190: 219, # 'ľ'
+ 191: 220, # 'ż'
+ 192: 221, # 'Å”'
+ 193: 51, # 'Ã'
+ 194: 83, # 'Â'
+ 195: 222, # 'Ä‚'
+ 196: 80, # 'Ä'
+ 197: 223, # 'Ĺ'
+ 198: 224, # 'Ć'
+ 199: 225, # 'Ç'
+ 200: 226, # 'Č'
+ 201: 44, # 'É'
+ 202: 227, # 'Ę'
+ 203: 228, # 'Ë'
+ 204: 229, # 'Äš'
+ 205: 61, # 'Ã'
+ 206: 230, # 'ÃŽ'
+ 207: 231, # 'ÄŽ'
+ 208: 232, # 'Ä'
+ 209: 233, # 'Ń'
+ 210: 234, # 'Ň'
+ 211: 58, # 'Ó'
+ 212: 235, # 'Ô'
+ 213: 66, # 'Å'
+ 214: 59, # 'Ö'
+ 215: 236, # '×'
+ 216: 237, # 'Ř'
+ 217: 238, # 'Å®'
+ 218: 60, # 'Ú'
+ 219: 70, # 'Å°'
+ 220: 63, # 'Ü'
+ 221: 239, # 'Ã'
+ 222: 240, # 'Å¢'
+ 223: 241, # 'ß'
+ 224: 84, # 'Å•'
+ 225: 14, # 'á'
+ 226: 75, # 'â'
+ 227: 242, # 'ă'
+ 228: 71, # 'ä'
+ 229: 82, # 'ĺ'
+ 230: 243, # 'ć'
+ 231: 73, # 'ç'
+ 232: 244, # 'Ä'
+ 233: 15, # 'é'
+ 234: 85, # 'Ä™'
+ 235: 79, # 'ë'
+ 236: 86, # 'Ä›'
+ 237: 30, # 'í'
+ 238: 77, # 'î'
+ 239: 87, # 'Ä'
+ 240: 245, # 'Ä‘'
+ 241: 246, # 'Å„'
+ 242: 247, # 'ň'
+ 243: 25, # 'ó'
+ 244: 74, # 'ô'
+ 245: 42, # 'Å‘'
+ 246: 24, # 'ö'
+ 247: 248, # '÷'
+ 248: 249, # 'Å™'
+ 249: 250, # 'ů'
+ 250: 31, # 'ú'
+ 251: 56, # 'ű'
+ 252: 29, # 'ü'
+ 253: 251, # 'ý'
+ 254: 252, # 'Å£'
+ 255: 253, # 'Ë™'
}
-WINDOWS_1250_HUNGARIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1250',
- language='Hungarian',
- char_to_order_map=WINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER,
- language_model=HUNGARIAN_LANG_MODEL,
- typical_positive_ratio=0.947368,
- keep_ascii_letters=True,
- alphabet='ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÃÉÃÓÖÚÜáéíóöúüÅőŰű')
+WINDOWS_1250_HUNGARIAN_MODEL = SingleByteCharSetModel(
+ charset_name="windows-1250",
+ language="Hungarian",
+ char_to_order_map=WINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER,
+ language_model=HUNGARIAN_LANG_MODEL,
+ typical_positive_ratio=0.947368,
+ keep_ascii_letters=True,
+ alphabet="ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÃÉÃÓÖÚÜáéíóöúüÅőŰű",
+)
ISO_8859_2_HUNGARIAN_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 28, # 'A'
- 66: 40, # 'B'
- 67: 54, # 'C'
- 68: 45, # 'D'
- 69: 32, # 'E'
- 70: 50, # 'F'
- 71: 49, # 'G'
- 72: 38, # 'H'
- 73: 39, # 'I'
- 74: 53, # 'J'
- 75: 36, # 'K'
- 76: 41, # 'L'
- 77: 34, # 'M'
- 78: 35, # 'N'
- 79: 47, # 'O'
- 80: 46, # 'P'
- 81: 71, # 'Q'
- 82: 43, # 'R'
- 83: 33, # 'S'
- 84: 37, # 'T'
- 85: 57, # 'U'
- 86: 48, # 'V'
- 87: 64, # 'W'
- 88: 68, # 'X'
- 89: 55, # 'Y'
- 90: 52, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 2, # 'a'
- 98: 18, # 'b'
- 99: 26, # 'c'
- 100: 17, # 'd'
- 101: 1, # 'e'
- 102: 27, # 'f'
- 103: 12, # 'g'
- 104: 20, # 'h'
- 105: 9, # 'i'
- 106: 22, # 'j'
- 107: 7, # 'k'
- 108: 6, # 'l'
- 109: 13, # 'm'
- 110: 4, # 'n'
- 111: 8, # 'o'
- 112: 23, # 'p'
- 113: 67, # 'q'
- 114: 10, # 'r'
- 115: 5, # 's'
- 116: 3, # 't'
- 117: 21, # 'u'
- 118: 19, # 'v'
- 119: 65, # 'w'
- 120: 62, # 'x'
- 121: 16, # 'y'
- 122: 11, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 159, # '\x80'
- 129: 160, # '\x81'
- 130: 161, # '\x82'
- 131: 162, # '\x83'
- 132: 163, # '\x84'
- 133: 164, # '\x85'
- 134: 165, # '\x86'
- 135: 166, # '\x87'
- 136: 167, # '\x88'
- 137: 168, # '\x89'
- 138: 169, # '\x8a'
- 139: 170, # '\x8b'
- 140: 171, # '\x8c'
- 141: 172, # '\x8d'
- 142: 173, # '\x8e'
- 143: 174, # '\x8f'
- 144: 175, # '\x90'
- 145: 176, # '\x91'
- 146: 177, # '\x92'
- 147: 178, # '\x93'
- 148: 179, # '\x94'
- 149: 180, # '\x95'
- 150: 181, # '\x96'
- 151: 182, # '\x97'
- 152: 183, # '\x98'
- 153: 184, # '\x99'
- 154: 185, # '\x9a'
- 155: 186, # '\x9b'
- 156: 187, # '\x9c'
- 157: 188, # '\x9d'
- 158: 189, # '\x9e'
- 159: 190, # '\x9f'
- 160: 191, # '\xa0'
- 161: 192, # 'Ä„'
- 162: 193, # '˘'
- 163: 194, # 'Å'
- 164: 195, # '¤'
- 165: 196, # 'Ľ'
- 166: 197, # 'Åš'
- 167: 75, # '§'
- 168: 198, # '¨'
- 169: 199, # 'Å '
- 170: 200, # 'Åž'
- 171: 201, # 'Ť'
- 172: 202, # 'Ź'
- 173: 203, # '\xad'
- 174: 204, # 'Ž'
- 175: 205, # 'Å»'
- 176: 79, # '°'
- 177: 206, # 'Ä…'
- 178: 207, # 'Ë›'
- 179: 208, # 'Å‚'
- 180: 209, # '´'
- 181: 210, # 'ľ'
- 182: 211, # 'Å›'
- 183: 212, # 'ˇ'
- 184: 213, # '¸'
- 185: 214, # 'Å¡'
- 186: 215, # 'ÅŸ'
- 187: 216, # 'Å¥'
- 188: 217, # 'ź'
- 189: 218, # 'Ë'
- 190: 219, # 'ž'
- 191: 220, # 'ż'
- 192: 221, # 'Å”'
- 193: 51, # 'Ã'
- 194: 81, # 'Â'
- 195: 222, # 'Ä‚'
- 196: 78, # 'Ä'
- 197: 223, # 'Ĺ'
- 198: 224, # 'Ć'
- 199: 225, # 'Ç'
- 200: 226, # 'Č'
- 201: 44, # 'É'
- 202: 227, # 'Ę'
- 203: 228, # 'Ë'
- 204: 229, # 'Äš'
- 205: 61, # 'Ã'
- 206: 230, # 'ÃŽ'
- 207: 231, # 'ÄŽ'
- 208: 232, # 'Ä'
- 209: 233, # 'Ń'
- 210: 234, # 'Ň'
- 211: 58, # 'Ó'
- 212: 235, # 'Ô'
- 213: 66, # 'Å'
- 214: 59, # 'Ö'
- 215: 236, # '×'
- 216: 237, # 'Ř'
- 217: 238, # 'Å®'
- 218: 60, # 'Ú'
- 219: 69, # 'Å°'
- 220: 63, # 'Ü'
- 221: 239, # 'Ã'
- 222: 240, # 'Å¢'
- 223: 241, # 'ß'
- 224: 82, # 'Å•'
- 225: 14, # 'á'
- 226: 74, # 'â'
- 227: 242, # 'ă'
- 228: 70, # 'ä'
- 229: 80, # 'ĺ'
- 230: 243, # 'ć'
- 231: 72, # 'ç'
- 232: 244, # 'Ä'
- 233: 15, # 'é'
- 234: 83, # 'Ä™'
- 235: 77, # 'ë'
- 236: 84, # 'Ä›'
- 237: 30, # 'í'
- 238: 76, # 'î'
- 239: 85, # 'Ä'
- 240: 245, # 'Ä‘'
- 241: 246, # 'Å„'
- 242: 247, # 'ň'
- 243: 25, # 'ó'
- 244: 73, # 'ô'
- 245: 42, # 'Å‘'
- 246: 24, # 'ö'
- 247: 248, # '÷'
- 248: 249, # 'Å™'
- 249: 250, # 'ů'
- 250: 31, # 'ú'
- 251: 56, # 'ű'
- 252: 29, # 'ü'
- 253: 251, # 'ý'
- 254: 252, # 'Å£'
- 255: 253, # 'Ë™'
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 28, # 'A'
+ 66: 40, # 'B'
+ 67: 54, # 'C'
+ 68: 45, # 'D'
+ 69: 32, # 'E'
+ 70: 50, # 'F'
+ 71: 49, # 'G'
+ 72: 38, # 'H'
+ 73: 39, # 'I'
+ 74: 53, # 'J'
+ 75: 36, # 'K'
+ 76: 41, # 'L'
+ 77: 34, # 'M'
+ 78: 35, # 'N'
+ 79: 47, # 'O'
+ 80: 46, # 'P'
+ 81: 71, # 'Q'
+ 82: 43, # 'R'
+ 83: 33, # 'S'
+ 84: 37, # 'T'
+ 85: 57, # 'U'
+ 86: 48, # 'V'
+ 87: 64, # 'W'
+ 88: 68, # 'X'
+ 89: 55, # 'Y'
+ 90: 52, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 2, # 'a'
+ 98: 18, # 'b'
+ 99: 26, # 'c'
+ 100: 17, # 'd'
+ 101: 1, # 'e'
+ 102: 27, # 'f'
+ 103: 12, # 'g'
+ 104: 20, # 'h'
+ 105: 9, # 'i'
+ 106: 22, # 'j'
+ 107: 7, # 'k'
+ 108: 6, # 'l'
+ 109: 13, # 'm'
+ 110: 4, # 'n'
+ 111: 8, # 'o'
+ 112: 23, # 'p'
+ 113: 67, # 'q'
+ 114: 10, # 'r'
+ 115: 5, # 's'
+ 116: 3, # 't'
+ 117: 21, # 'u'
+ 118: 19, # 'v'
+ 119: 65, # 'w'
+ 120: 62, # 'x'
+ 121: 16, # 'y'
+ 122: 11, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 159, # '\x80'
+ 129: 160, # '\x81'
+ 130: 161, # '\x82'
+ 131: 162, # '\x83'
+ 132: 163, # '\x84'
+ 133: 164, # '\x85'
+ 134: 165, # '\x86'
+ 135: 166, # '\x87'
+ 136: 167, # '\x88'
+ 137: 168, # '\x89'
+ 138: 169, # '\x8a'
+ 139: 170, # '\x8b'
+ 140: 171, # '\x8c'
+ 141: 172, # '\x8d'
+ 142: 173, # '\x8e'
+ 143: 174, # '\x8f'
+ 144: 175, # '\x90'
+ 145: 176, # '\x91'
+ 146: 177, # '\x92'
+ 147: 178, # '\x93'
+ 148: 179, # '\x94'
+ 149: 180, # '\x95'
+ 150: 181, # '\x96'
+ 151: 182, # '\x97'
+ 152: 183, # '\x98'
+ 153: 184, # '\x99'
+ 154: 185, # '\x9a'
+ 155: 186, # '\x9b'
+ 156: 187, # '\x9c'
+ 157: 188, # '\x9d'
+ 158: 189, # '\x9e'
+ 159: 190, # '\x9f'
+ 160: 191, # '\xa0'
+ 161: 192, # 'Ä„'
+ 162: 193, # '˘'
+ 163: 194, # 'Å'
+ 164: 195, # '¤'
+ 165: 196, # 'Ľ'
+ 166: 197, # 'Åš'
+ 167: 75, # '§'
+ 168: 198, # '¨'
+ 169: 199, # 'Å '
+ 170: 200, # 'Åž'
+ 171: 201, # 'Ť'
+ 172: 202, # 'Ź'
+ 173: 203, # '\xad'
+ 174: 204, # 'Ž'
+ 175: 205, # 'Å»'
+ 176: 79, # '°'
+ 177: 206, # 'Ä…'
+ 178: 207, # 'Ë›'
+ 179: 208, # 'Å‚'
+ 180: 209, # '´'
+ 181: 210, # 'ľ'
+ 182: 211, # 'Å›'
+ 183: 212, # 'ˇ'
+ 184: 213, # '¸'
+ 185: 214, # 'Å¡'
+ 186: 215, # 'ÅŸ'
+ 187: 216, # 'Å¥'
+ 188: 217, # 'ź'
+ 189: 218, # 'Ë'
+ 190: 219, # 'ž'
+ 191: 220, # 'ż'
+ 192: 221, # 'Å”'
+ 193: 51, # 'Ã'
+ 194: 81, # 'Â'
+ 195: 222, # 'Ä‚'
+ 196: 78, # 'Ä'
+ 197: 223, # 'Ĺ'
+ 198: 224, # 'Ć'
+ 199: 225, # 'Ç'
+ 200: 226, # 'Č'
+ 201: 44, # 'É'
+ 202: 227, # 'Ę'
+ 203: 228, # 'Ë'
+ 204: 229, # 'Äš'
+ 205: 61, # 'Ã'
+ 206: 230, # 'ÃŽ'
+ 207: 231, # 'ÄŽ'
+ 208: 232, # 'Ä'
+ 209: 233, # 'Ń'
+ 210: 234, # 'Ň'
+ 211: 58, # 'Ó'
+ 212: 235, # 'Ô'
+ 213: 66, # 'Å'
+ 214: 59, # 'Ö'
+ 215: 236, # '×'
+ 216: 237, # 'Ř'
+ 217: 238, # 'Å®'
+ 218: 60, # 'Ú'
+ 219: 69, # 'Å°'
+ 220: 63, # 'Ü'
+ 221: 239, # 'Ã'
+ 222: 240, # 'Å¢'
+ 223: 241, # 'ß'
+ 224: 82, # 'Å•'
+ 225: 14, # 'á'
+ 226: 74, # 'â'
+ 227: 242, # 'ă'
+ 228: 70, # 'ä'
+ 229: 80, # 'ĺ'
+ 230: 243, # 'ć'
+ 231: 72, # 'ç'
+ 232: 244, # 'Ä'
+ 233: 15, # 'é'
+ 234: 83, # 'Ä™'
+ 235: 77, # 'ë'
+ 236: 84, # 'Ä›'
+ 237: 30, # 'í'
+ 238: 76, # 'î'
+ 239: 85, # 'Ä'
+ 240: 245, # 'Ä‘'
+ 241: 246, # 'Å„'
+ 242: 247, # 'ň'
+ 243: 25, # 'ó'
+ 244: 73, # 'ô'
+ 245: 42, # 'Å‘'
+ 246: 24, # 'ö'
+ 247: 248, # '÷'
+ 248: 249, # 'Å™'
+ 249: 250, # 'ů'
+ 250: 31, # 'ú'
+ 251: 56, # 'ű'
+ 252: 29, # 'ü'
+ 253: 251, # 'ý'
+ 254: 252, # 'Å£'
+ 255: 253, # 'Ë™'
}
-ISO_8859_2_HUNGARIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-2',
- language='Hungarian',
- char_to_order_map=ISO_8859_2_HUNGARIAN_CHAR_TO_ORDER,
- language_model=HUNGARIAN_LANG_MODEL,
- typical_positive_ratio=0.947368,
- keep_ascii_letters=True,
- alphabet='ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÃÉÃÓÖÚÜáéíóöúüÅőŰű')
-
+ISO_8859_2_HUNGARIAN_MODEL = SingleByteCharSetModel(
+ charset_name="ISO-8859-2",
+ language="Hungarian",
+ char_to_order_map=ISO_8859_2_HUNGARIAN_CHAR_TO_ORDER,
+ language_model=HUNGARIAN_LANG_MODEL,
+ typical_positive_ratio=0.947368,
+ keep_ascii_letters=True,
+ alphabet="ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÃÉÃÓÖÚÜáéíóöúüÅőŰű",
+)
diff --git a/src/pip/_vendor/chardet/langrussianmodel.py b/src/pip/_vendor/chardet/langrussianmodel.py
index 5594452b5..39a538894 100644
--- a/src/pip/_vendor/chardet/langrussianmodel.py
+++ b/src/pip/_vendor/chardet/langrussianmodel.py
@@ -1,9 +1,5 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel
-
# 3: Positive
# 2: Likely
# 1: Unlikely
@@ -4115,1604 +4111,1615 @@ RUSSIAN_LANG_MODEL = {
# Character Mapping Table(s):
IBM866_RUSSIAN_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 142, # 'A'
- 66: 143, # 'B'
- 67: 144, # 'C'
- 68: 145, # 'D'
- 69: 146, # 'E'
- 70: 147, # 'F'
- 71: 148, # 'G'
- 72: 149, # 'H'
- 73: 150, # 'I'
- 74: 151, # 'J'
- 75: 152, # 'K'
- 76: 74, # 'L'
- 77: 153, # 'M'
- 78: 75, # 'N'
- 79: 154, # 'O'
- 80: 155, # 'P'
- 81: 156, # 'Q'
- 82: 157, # 'R'
- 83: 158, # 'S'
- 84: 159, # 'T'
- 85: 160, # 'U'
- 86: 161, # 'V'
- 87: 162, # 'W'
- 88: 163, # 'X'
- 89: 164, # 'Y'
- 90: 165, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 71, # 'a'
- 98: 172, # 'b'
- 99: 66, # 'c'
- 100: 173, # 'd'
- 101: 65, # 'e'
- 102: 174, # 'f'
- 103: 76, # 'g'
- 104: 175, # 'h'
- 105: 64, # 'i'
- 106: 176, # 'j'
- 107: 177, # 'k'
- 108: 77, # 'l'
- 109: 72, # 'm'
- 110: 178, # 'n'
- 111: 69, # 'o'
- 112: 67, # 'p'
- 113: 179, # 'q'
- 114: 78, # 'r'
- 115: 73, # 's'
- 116: 180, # 't'
- 117: 181, # 'u'
- 118: 79, # 'v'
- 119: 182, # 'w'
- 120: 183, # 'x'
- 121: 184, # 'y'
- 122: 185, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 37, # 'Ð'
- 129: 44, # 'Б'
- 130: 33, # 'Ð’'
- 131: 46, # 'Г'
- 132: 41, # 'Д'
- 133: 48, # 'Е'
- 134: 56, # 'Ж'
- 135: 51, # 'З'
- 136: 42, # 'И'
- 137: 60, # 'Й'
- 138: 36, # 'К'
- 139: 49, # 'Л'
- 140: 38, # 'М'
- 141: 31, # 'Ð'
- 142: 34, # 'О'
- 143: 35, # 'П'
- 144: 45, # 'Р'
- 145: 32, # 'С'
- 146: 40, # 'Т'
- 147: 52, # 'У'
- 148: 53, # 'Ф'
- 149: 55, # 'Ð¥'
- 150: 58, # 'Ц'
- 151: 50, # 'Ч'
- 152: 57, # 'Ш'
- 153: 63, # 'Щ'
- 154: 70, # 'Ъ'
- 155: 62, # 'Ы'
- 156: 61, # 'Ь'
- 157: 47, # 'Э'
- 158: 59, # 'Ю'
- 159: 43, # 'Я'
- 160: 3, # 'а'
- 161: 21, # 'б'
- 162: 10, # 'в'
- 163: 19, # 'г'
- 164: 13, # 'д'
- 165: 2, # 'е'
- 166: 24, # 'ж'
- 167: 20, # 'з'
- 168: 4, # 'и'
- 169: 23, # 'й'
- 170: 11, # 'к'
- 171: 8, # 'л'
- 172: 12, # 'м'
- 173: 5, # 'н'
- 174: 1, # 'о'
- 175: 15, # 'п'
- 176: 191, # 'â–‘'
- 177: 192, # 'â–’'
- 178: 193, # 'â–“'
- 179: 194, # '│'
- 180: 195, # '┤'
- 181: 196, # 'â•¡'
- 182: 197, # 'â•¢'
- 183: 198, # 'â•–'
- 184: 199, # 'â••'
- 185: 200, # 'â•£'
- 186: 201, # 'â•‘'
- 187: 202, # 'â•—'
- 188: 203, # 'â•'
- 189: 204, # '╜'
- 190: 205, # 'â•›'
- 191: 206, # 'â”'
- 192: 207, # 'â””'
- 193: 208, # 'â”´'
- 194: 209, # '┬'
- 195: 210, # '├'
- 196: 211, # '─'
- 197: 212, # '┼'
- 198: 213, # 'â•ž'
- 199: 214, # 'â•Ÿ'
- 200: 215, # 'â•š'
- 201: 216, # 'â•”'
- 202: 217, # 'â•©'
- 203: 218, # '╦'
- 204: 219, # 'â• '
- 205: 220, # 'â•'
- 206: 221, # '╬'
- 207: 222, # '╧'
- 208: 223, # '╨'
- 209: 224, # '╤'
- 210: 225, # 'â•¥'
- 211: 226, # 'â•™'
- 212: 227, # '╘'
- 213: 228, # 'â•’'
- 214: 229, # 'â•“'
- 215: 230, # 'â•«'
- 216: 231, # '╪'
- 217: 232, # '┘'
- 218: 233, # '┌'
- 219: 234, # 'â–ˆ'
- 220: 235, # 'â–„'
- 221: 236, # '▌'
- 222: 237, # 'â–'
- 223: 238, # 'â–€'
- 224: 9, # 'Ñ€'
- 225: 7, # 'Ñ'
- 226: 6, # 'Ñ‚'
- 227: 14, # 'у'
- 228: 39, # 'Ñ„'
- 229: 26, # 'Ñ…'
- 230: 28, # 'ц'
- 231: 22, # 'ч'
- 232: 25, # 'ш'
- 233: 29, # 'щ'
- 234: 54, # 'ÑŠ'
- 235: 18, # 'Ñ‹'
- 236: 17, # 'ь'
- 237: 30, # 'Ñ'
- 238: 27, # 'ÑŽ'
- 239: 16, # 'Ñ'
- 240: 239, # 'Ð'
- 241: 68, # 'Ñ‘'
- 242: 240, # 'Є'
- 243: 241, # 'Ñ”'
- 244: 242, # 'Ї'
- 245: 243, # 'Ñ—'
- 246: 244, # 'ÐŽ'
- 247: 245, # 'Ñž'
- 248: 246, # '°'
- 249: 247, # '∙'
- 250: 248, # '·'
- 251: 249, # '√'
- 252: 250, # 'â„–'
- 253: 251, # '¤'
- 254: 252, # 'â– '
- 255: 255, # '\xa0'
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 142, # 'A'
+ 66: 143, # 'B'
+ 67: 144, # 'C'
+ 68: 145, # 'D'
+ 69: 146, # 'E'
+ 70: 147, # 'F'
+ 71: 148, # 'G'
+ 72: 149, # 'H'
+ 73: 150, # 'I'
+ 74: 151, # 'J'
+ 75: 152, # 'K'
+ 76: 74, # 'L'
+ 77: 153, # 'M'
+ 78: 75, # 'N'
+ 79: 154, # 'O'
+ 80: 155, # 'P'
+ 81: 156, # 'Q'
+ 82: 157, # 'R'
+ 83: 158, # 'S'
+ 84: 159, # 'T'
+ 85: 160, # 'U'
+ 86: 161, # 'V'
+ 87: 162, # 'W'
+ 88: 163, # 'X'
+ 89: 164, # 'Y'
+ 90: 165, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 71, # 'a'
+ 98: 172, # 'b'
+ 99: 66, # 'c'
+ 100: 173, # 'd'
+ 101: 65, # 'e'
+ 102: 174, # 'f'
+ 103: 76, # 'g'
+ 104: 175, # 'h'
+ 105: 64, # 'i'
+ 106: 176, # 'j'
+ 107: 177, # 'k'
+ 108: 77, # 'l'
+ 109: 72, # 'm'
+ 110: 178, # 'n'
+ 111: 69, # 'o'
+ 112: 67, # 'p'
+ 113: 179, # 'q'
+ 114: 78, # 'r'
+ 115: 73, # 's'
+ 116: 180, # 't'
+ 117: 181, # 'u'
+ 118: 79, # 'v'
+ 119: 182, # 'w'
+ 120: 183, # 'x'
+ 121: 184, # 'y'
+ 122: 185, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 37, # 'Ð'
+ 129: 44, # 'Б'
+ 130: 33, # 'Ð’'
+ 131: 46, # 'Г'
+ 132: 41, # 'Д'
+ 133: 48, # 'Е'
+ 134: 56, # 'Ж'
+ 135: 51, # 'З'
+ 136: 42, # 'И'
+ 137: 60, # 'Й'
+ 138: 36, # 'К'
+ 139: 49, # 'Л'
+ 140: 38, # 'М'
+ 141: 31, # 'Ð'
+ 142: 34, # 'О'
+ 143: 35, # 'П'
+ 144: 45, # 'Р'
+ 145: 32, # 'С'
+ 146: 40, # 'Т'
+ 147: 52, # 'У'
+ 148: 53, # 'Ф'
+ 149: 55, # 'Ð¥'
+ 150: 58, # 'Ц'
+ 151: 50, # 'Ч'
+ 152: 57, # 'Ш'
+ 153: 63, # 'Щ'
+ 154: 70, # 'Ъ'
+ 155: 62, # 'Ы'
+ 156: 61, # 'Ь'
+ 157: 47, # 'Э'
+ 158: 59, # 'Ю'
+ 159: 43, # 'Я'
+ 160: 3, # 'а'
+ 161: 21, # 'б'
+ 162: 10, # 'в'
+ 163: 19, # 'г'
+ 164: 13, # 'д'
+ 165: 2, # 'е'
+ 166: 24, # 'ж'
+ 167: 20, # 'з'
+ 168: 4, # 'и'
+ 169: 23, # 'й'
+ 170: 11, # 'к'
+ 171: 8, # 'л'
+ 172: 12, # 'м'
+ 173: 5, # 'н'
+ 174: 1, # 'о'
+ 175: 15, # 'п'
+ 176: 191, # 'â–‘'
+ 177: 192, # 'â–’'
+ 178: 193, # 'â–“'
+ 179: 194, # '│'
+ 180: 195, # '┤'
+ 181: 196, # 'â•¡'
+ 182: 197, # 'â•¢'
+ 183: 198, # 'â•–'
+ 184: 199, # 'â••'
+ 185: 200, # 'â•£'
+ 186: 201, # 'â•‘'
+ 187: 202, # 'â•—'
+ 188: 203, # 'â•'
+ 189: 204, # '╜'
+ 190: 205, # 'â•›'
+ 191: 206, # 'â”'
+ 192: 207, # 'â””'
+ 193: 208, # 'â”´'
+ 194: 209, # '┬'
+ 195: 210, # '├'
+ 196: 211, # '─'
+ 197: 212, # '┼'
+ 198: 213, # 'â•ž'
+ 199: 214, # 'â•Ÿ'
+ 200: 215, # 'â•š'
+ 201: 216, # 'â•”'
+ 202: 217, # 'â•©'
+ 203: 218, # '╦'
+ 204: 219, # 'â• '
+ 205: 220, # 'â•'
+ 206: 221, # '╬'
+ 207: 222, # '╧'
+ 208: 223, # '╨'
+ 209: 224, # '╤'
+ 210: 225, # 'â•¥'
+ 211: 226, # 'â•™'
+ 212: 227, # '╘'
+ 213: 228, # 'â•’'
+ 214: 229, # 'â•“'
+ 215: 230, # 'â•«'
+ 216: 231, # '╪'
+ 217: 232, # '┘'
+ 218: 233, # '┌'
+ 219: 234, # 'â–ˆ'
+ 220: 235, # 'â–„'
+ 221: 236, # '▌'
+ 222: 237, # 'â–'
+ 223: 238, # 'â–€'
+ 224: 9, # 'Ñ€'
+ 225: 7, # 'Ñ'
+ 226: 6, # 'Ñ‚'
+ 227: 14, # 'у'
+ 228: 39, # 'Ñ„'
+ 229: 26, # 'Ñ…'
+ 230: 28, # 'ц'
+ 231: 22, # 'ч'
+ 232: 25, # 'ш'
+ 233: 29, # 'щ'
+ 234: 54, # 'ÑŠ'
+ 235: 18, # 'Ñ‹'
+ 236: 17, # 'ь'
+ 237: 30, # 'Ñ'
+ 238: 27, # 'ÑŽ'
+ 239: 16, # 'Ñ'
+ 240: 239, # 'Ð'
+ 241: 68, # 'Ñ‘'
+ 242: 240, # 'Є'
+ 243: 241, # 'Ñ”'
+ 244: 242, # 'Ї'
+ 245: 243, # 'Ñ—'
+ 246: 244, # 'ÐŽ'
+ 247: 245, # 'Ñž'
+ 248: 246, # '°'
+ 249: 247, # '∙'
+ 250: 248, # '·'
+ 251: 249, # '√'
+ 252: 250, # 'â„–'
+ 253: 251, # '¤'
+ 254: 252, # 'â– '
+ 255: 255, # '\xa0'
}
-IBM866_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='IBM866',
- language='Russian',
- char_to_order_map=IBM866_RUSSIAN_CHAR_TO_ORDER,
- language_model=RUSSIAN_LANG_MODEL,
- typical_positive_ratio=0.976601,
- keep_ascii_letters=False,
- alphabet='ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘')
+IBM866_RUSSIAN_MODEL = SingleByteCharSetModel(
+ charset_name="IBM866",
+ language="Russian",
+ char_to_order_map=IBM866_RUSSIAN_CHAR_TO_ORDER,
+ language_model=RUSSIAN_LANG_MODEL,
+ typical_positive_ratio=0.976601,
+ keep_ascii_letters=False,
+ alphabet="ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘",
+)
WINDOWS_1251_RUSSIAN_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 142, # 'A'
- 66: 143, # 'B'
- 67: 144, # 'C'
- 68: 145, # 'D'
- 69: 146, # 'E'
- 70: 147, # 'F'
- 71: 148, # 'G'
- 72: 149, # 'H'
- 73: 150, # 'I'
- 74: 151, # 'J'
- 75: 152, # 'K'
- 76: 74, # 'L'
- 77: 153, # 'M'
- 78: 75, # 'N'
- 79: 154, # 'O'
- 80: 155, # 'P'
- 81: 156, # 'Q'
- 82: 157, # 'R'
- 83: 158, # 'S'
- 84: 159, # 'T'
- 85: 160, # 'U'
- 86: 161, # 'V'
- 87: 162, # 'W'
- 88: 163, # 'X'
- 89: 164, # 'Y'
- 90: 165, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 71, # 'a'
- 98: 172, # 'b'
- 99: 66, # 'c'
- 100: 173, # 'd'
- 101: 65, # 'e'
- 102: 174, # 'f'
- 103: 76, # 'g'
- 104: 175, # 'h'
- 105: 64, # 'i'
- 106: 176, # 'j'
- 107: 177, # 'k'
- 108: 77, # 'l'
- 109: 72, # 'm'
- 110: 178, # 'n'
- 111: 69, # 'o'
- 112: 67, # 'p'
- 113: 179, # 'q'
- 114: 78, # 'r'
- 115: 73, # 's'
- 116: 180, # 't'
- 117: 181, # 'u'
- 118: 79, # 'v'
- 119: 182, # 'w'
- 120: 183, # 'x'
- 121: 184, # 'y'
- 122: 185, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 191, # 'Ђ'
- 129: 192, # 'Ѓ'
- 130: 193, # '‚'
- 131: 194, # 'Ñ“'
- 132: 195, # '„'
- 133: 196, # '…'
- 134: 197, # '†'
- 135: 198, # '‡'
- 136: 199, # '€'
- 137: 200, # '‰'
- 138: 201, # 'Љ'
- 139: 202, # '‹'
- 140: 203, # 'Њ'
- 141: 204, # 'Ќ'
- 142: 205, # 'Ћ'
- 143: 206, # 'Ð'
- 144: 207, # 'Ñ’'
- 145: 208, # '‘'
- 146: 209, # '’'
- 147: 210, # '“'
- 148: 211, # 'â€'
- 149: 212, # '•'
- 150: 213, # '–'
- 151: 214, # '—'
- 152: 215, # None
- 153: 216, # 'â„¢'
- 154: 217, # 'Ñ™'
- 155: 218, # '›'
- 156: 219, # 'Ñš'
- 157: 220, # 'ќ'
- 158: 221, # 'Ñ›'
- 159: 222, # 'ÑŸ'
- 160: 223, # '\xa0'
- 161: 224, # 'ÐŽ'
- 162: 225, # 'Ñž'
- 163: 226, # 'Ј'
- 164: 227, # '¤'
- 165: 228, # 'Ò'
- 166: 229, # '¦'
- 167: 230, # '§'
- 168: 231, # 'Ð'
- 169: 232, # '©'
- 170: 233, # 'Є'
- 171: 234, # '«'
- 172: 235, # '¬'
- 173: 236, # '\xad'
- 174: 237, # '®'
- 175: 238, # 'Ї'
- 176: 239, # '°'
- 177: 240, # '±'
- 178: 241, # 'І'
- 179: 242, # 'Ñ–'
- 180: 243, # 'Ò‘'
- 181: 244, # 'µ'
- 182: 245, # '¶'
- 183: 246, # '·'
- 184: 68, # 'Ñ‘'
- 185: 247, # 'â„–'
- 186: 248, # 'Ñ”'
- 187: 249, # '»'
- 188: 250, # 'ј'
- 189: 251, # 'Ð…'
- 190: 252, # 'Ñ•'
- 191: 253, # 'Ñ—'
- 192: 37, # 'Ð'
- 193: 44, # 'Б'
- 194: 33, # 'Ð’'
- 195: 46, # 'Г'
- 196: 41, # 'Д'
- 197: 48, # 'Е'
- 198: 56, # 'Ж'
- 199: 51, # 'З'
- 200: 42, # 'И'
- 201: 60, # 'Й'
- 202: 36, # 'К'
- 203: 49, # 'Л'
- 204: 38, # 'М'
- 205: 31, # 'Ð'
- 206: 34, # 'О'
- 207: 35, # 'П'
- 208: 45, # 'Р'
- 209: 32, # 'С'
- 210: 40, # 'Т'
- 211: 52, # 'У'
- 212: 53, # 'Ф'
- 213: 55, # 'Ð¥'
- 214: 58, # 'Ц'
- 215: 50, # 'Ч'
- 216: 57, # 'Ш'
- 217: 63, # 'Щ'
- 218: 70, # 'Ъ'
- 219: 62, # 'Ы'
- 220: 61, # 'Ь'
- 221: 47, # 'Э'
- 222: 59, # 'Ю'
- 223: 43, # 'Я'
- 224: 3, # 'а'
- 225: 21, # 'б'
- 226: 10, # 'в'
- 227: 19, # 'г'
- 228: 13, # 'д'
- 229: 2, # 'е'
- 230: 24, # 'ж'
- 231: 20, # 'з'
- 232: 4, # 'и'
- 233: 23, # 'й'
- 234: 11, # 'к'
- 235: 8, # 'л'
- 236: 12, # 'м'
- 237: 5, # 'н'
- 238: 1, # 'о'
- 239: 15, # 'п'
- 240: 9, # 'Ñ€'
- 241: 7, # 'Ñ'
- 242: 6, # 'Ñ‚'
- 243: 14, # 'у'
- 244: 39, # 'Ñ„'
- 245: 26, # 'Ñ…'
- 246: 28, # 'ц'
- 247: 22, # 'ч'
- 248: 25, # 'ш'
- 249: 29, # 'щ'
- 250: 54, # 'ÑŠ'
- 251: 18, # 'Ñ‹'
- 252: 17, # 'ь'
- 253: 30, # 'Ñ'
- 254: 27, # 'ÑŽ'
- 255: 16, # 'Ñ'
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 142, # 'A'
+ 66: 143, # 'B'
+ 67: 144, # 'C'
+ 68: 145, # 'D'
+ 69: 146, # 'E'
+ 70: 147, # 'F'
+ 71: 148, # 'G'
+ 72: 149, # 'H'
+ 73: 150, # 'I'
+ 74: 151, # 'J'
+ 75: 152, # 'K'
+ 76: 74, # 'L'
+ 77: 153, # 'M'
+ 78: 75, # 'N'
+ 79: 154, # 'O'
+ 80: 155, # 'P'
+ 81: 156, # 'Q'
+ 82: 157, # 'R'
+ 83: 158, # 'S'
+ 84: 159, # 'T'
+ 85: 160, # 'U'
+ 86: 161, # 'V'
+ 87: 162, # 'W'
+ 88: 163, # 'X'
+ 89: 164, # 'Y'
+ 90: 165, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 71, # 'a'
+ 98: 172, # 'b'
+ 99: 66, # 'c'
+ 100: 173, # 'd'
+ 101: 65, # 'e'
+ 102: 174, # 'f'
+ 103: 76, # 'g'
+ 104: 175, # 'h'
+ 105: 64, # 'i'
+ 106: 176, # 'j'
+ 107: 177, # 'k'
+ 108: 77, # 'l'
+ 109: 72, # 'm'
+ 110: 178, # 'n'
+ 111: 69, # 'o'
+ 112: 67, # 'p'
+ 113: 179, # 'q'
+ 114: 78, # 'r'
+ 115: 73, # 's'
+ 116: 180, # 't'
+ 117: 181, # 'u'
+ 118: 79, # 'v'
+ 119: 182, # 'w'
+ 120: 183, # 'x'
+ 121: 184, # 'y'
+ 122: 185, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 191, # 'Ђ'
+ 129: 192, # 'Ѓ'
+ 130: 193, # '‚'
+ 131: 194, # 'Ñ“'
+ 132: 195, # '„'
+ 133: 196, # '…'
+ 134: 197, # '†'
+ 135: 198, # '‡'
+ 136: 199, # '€'
+ 137: 200, # '‰'
+ 138: 201, # 'Љ'
+ 139: 202, # '‹'
+ 140: 203, # 'Њ'
+ 141: 204, # 'Ќ'
+ 142: 205, # 'Ћ'
+ 143: 206, # 'Ð'
+ 144: 207, # 'Ñ’'
+ 145: 208, # '‘'
+ 146: 209, # '’'
+ 147: 210, # '“'
+ 148: 211, # 'â€'
+ 149: 212, # '•'
+ 150: 213, # '–'
+ 151: 214, # '—'
+ 152: 215, # None
+ 153: 216, # 'â„¢'
+ 154: 217, # 'Ñ™'
+ 155: 218, # '›'
+ 156: 219, # 'Ñš'
+ 157: 220, # 'ќ'
+ 158: 221, # 'Ñ›'
+ 159: 222, # 'ÑŸ'
+ 160: 223, # '\xa0'
+ 161: 224, # 'ÐŽ'
+ 162: 225, # 'Ñž'
+ 163: 226, # 'Ј'
+ 164: 227, # '¤'
+ 165: 228, # 'Ò'
+ 166: 229, # '¦'
+ 167: 230, # '§'
+ 168: 231, # 'Ð'
+ 169: 232, # '©'
+ 170: 233, # 'Є'
+ 171: 234, # '«'
+ 172: 235, # '¬'
+ 173: 236, # '\xad'
+ 174: 237, # '®'
+ 175: 238, # 'Ї'
+ 176: 239, # '°'
+ 177: 240, # '±'
+ 178: 241, # 'І'
+ 179: 242, # 'Ñ–'
+ 180: 243, # 'Ò‘'
+ 181: 244, # 'µ'
+ 182: 245, # '¶'
+ 183: 246, # '·'
+ 184: 68, # 'Ñ‘'
+ 185: 247, # 'â„–'
+ 186: 248, # 'Ñ”'
+ 187: 249, # '»'
+ 188: 250, # 'ј'
+ 189: 251, # 'Ð…'
+ 190: 252, # 'Ñ•'
+ 191: 253, # 'Ñ—'
+ 192: 37, # 'Ð'
+ 193: 44, # 'Б'
+ 194: 33, # 'Ð’'
+ 195: 46, # 'Г'
+ 196: 41, # 'Д'
+ 197: 48, # 'Е'
+ 198: 56, # 'Ж'
+ 199: 51, # 'З'
+ 200: 42, # 'И'
+ 201: 60, # 'Й'
+ 202: 36, # 'К'
+ 203: 49, # 'Л'
+ 204: 38, # 'М'
+ 205: 31, # 'Ð'
+ 206: 34, # 'О'
+ 207: 35, # 'П'
+ 208: 45, # 'Р'
+ 209: 32, # 'С'
+ 210: 40, # 'Т'
+ 211: 52, # 'У'
+ 212: 53, # 'Ф'
+ 213: 55, # 'Ð¥'
+ 214: 58, # 'Ц'
+ 215: 50, # 'Ч'
+ 216: 57, # 'Ш'
+ 217: 63, # 'Щ'
+ 218: 70, # 'Ъ'
+ 219: 62, # 'Ы'
+ 220: 61, # 'Ь'
+ 221: 47, # 'Э'
+ 222: 59, # 'Ю'
+ 223: 43, # 'Я'
+ 224: 3, # 'а'
+ 225: 21, # 'б'
+ 226: 10, # 'в'
+ 227: 19, # 'г'
+ 228: 13, # 'д'
+ 229: 2, # 'е'
+ 230: 24, # 'ж'
+ 231: 20, # 'з'
+ 232: 4, # 'и'
+ 233: 23, # 'й'
+ 234: 11, # 'к'
+ 235: 8, # 'л'
+ 236: 12, # 'м'
+ 237: 5, # 'н'
+ 238: 1, # 'о'
+ 239: 15, # 'п'
+ 240: 9, # 'Ñ€'
+ 241: 7, # 'Ñ'
+ 242: 6, # 'Ñ‚'
+ 243: 14, # 'у'
+ 244: 39, # 'Ñ„'
+ 245: 26, # 'Ñ…'
+ 246: 28, # 'ц'
+ 247: 22, # 'ч'
+ 248: 25, # 'ш'
+ 249: 29, # 'щ'
+ 250: 54, # 'ÑŠ'
+ 251: 18, # 'Ñ‹'
+ 252: 17, # 'ь'
+ 253: 30, # 'Ñ'
+ 254: 27, # 'ÑŽ'
+ 255: 16, # 'Ñ'
}
-WINDOWS_1251_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1251',
- language='Russian',
- char_to_order_map=WINDOWS_1251_RUSSIAN_CHAR_TO_ORDER,
- language_model=RUSSIAN_LANG_MODEL,
- typical_positive_ratio=0.976601,
- keep_ascii_letters=False,
- alphabet='ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘')
+WINDOWS_1251_RUSSIAN_MODEL = SingleByteCharSetModel(
+ charset_name="windows-1251",
+ language="Russian",
+ char_to_order_map=WINDOWS_1251_RUSSIAN_CHAR_TO_ORDER,
+ language_model=RUSSIAN_LANG_MODEL,
+ typical_positive_ratio=0.976601,
+ keep_ascii_letters=False,
+ alphabet="ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘",
+)
IBM855_RUSSIAN_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 142, # 'A'
- 66: 143, # 'B'
- 67: 144, # 'C'
- 68: 145, # 'D'
- 69: 146, # 'E'
- 70: 147, # 'F'
- 71: 148, # 'G'
- 72: 149, # 'H'
- 73: 150, # 'I'
- 74: 151, # 'J'
- 75: 152, # 'K'
- 76: 74, # 'L'
- 77: 153, # 'M'
- 78: 75, # 'N'
- 79: 154, # 'O'
- 80: 155, # 'P'
- 81: 156, # 'Q'
- 82: 157, # 'R'
- 83: 158, # 'S'
- 84: 159, # 'T'
- 85: 160, # 'U'
- 86: 161, # 'V'
- 87: 162, # 'W'
- 88: 163, # 'X'
- 89: 164, # 'Y'
- 90: 165, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 71, # 'a'
- 98: 172, # 'b'
- 99: 66, # 'c'
- 100: 173, # 'd'
- 101: 65, # 'e'
- 102: 174, # 'f'
- 103: 76, # 'g'
- 104: 175, # 'h'
- 105: 64, # 'i'
- 106: 176, # 'j'
- 107: 177, # 'k'
- 108: 77, # 'l'
- 109: 72, # 'm'
- 110: 178, # 'n'
- 111: 69, # 'o'
- 112: 67, # 'p'
- 113: 179, # 'q'
- 114: 78, # 'r'
- 115: 73, # 's'
- 116: 180, # 't'
- 117: 181, # 'u'
- 118: 79, # 'v'
- 119: 182, # 'w'
- 120: 183, # 'x'
- 121: 184, # 'y'
- 122: 185, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 191, # 'Ñ’'
- 129: 192, # 'Ђ'
- 130: 193, # 'Ñ“'
- 131: 194, # 'Ѓ'
- 132: 68, # 'Ñ‘'
- 133: 195, # 'Ð'
- 134: 196, # 'Ñ”'
- 135: 197, # 'Є'
- 136: 198, # 'Ñ•'
- 137: 199, # 'Ð…'
- 138: 200, # 'Ñ–'
- 139: 201, # 'І'
- 140: 202, # 'Ñ—'
- 141: 203, # 'Ї'
- 142: 204, # 'ј'
- 143: 205, # 'Ј'
- 144: 206, # 'Ñ™'
- 145: 207, # 'Љ'
- 146: 208, # 'Ñš'
- 147: 209, # 'Њ'
- 148: 210, # 'Ñ›'
- 149: 211, # 'Ћ'
- 150: 212, # 'ќ'
- 151: 213, # 'Ќ'
- 152: 214, # 'Ñž'
- 153: 215, # 'ÐŽ'
- 154: 216, # 'ÑŸ'
- 155: 217, # 'Ð'
- 156: 27, # 'ÑŽ'
- 157: 59, # 'Ю'
- 158: 54, # 'ÑŠ'
- 159: 70, # 'Ъ'
- 160: 3, # 'а'
- 161: 37, # 'Ð'
- 162: 21, # 'б'
- 163: 44, # 'Б'
- 164: 28, # 'ц'
- 165: 58, # 'Ц'
- 166: 13, # 'д'
- 167: 41, # 'Д'
- 168: 2, # 'е'
- 169: 48, # 'Е'
- 170: 39, # 'Ñ„'
- 171: 53, # 'Ф'
- 172: 19, # 'г'
- 173: 46, # 'Г'
- 174: 218, # '«'
- 175: 219, # '»'
- 176: 220, # 'â–‘'
- 177: 221, # 'â–’'
- 178: 222, # 'â–“'
- 179: 223, # '│'
- 180: 224, # '┤'
- 181: 26, # 'Ñ…'
- 182: 55, # 'Ð¥'
- 183: 4, # 'и'
- 184: 42, # 'И'
- 185: 225, # 'â•£'
- 186: 226, # 'â•‘'
- 187: 227, # 'â•—'
- 188: 228, # 'â•'
- 189: 23, # 'й'
- 190: 60, # 'Й'
- 191: 229, # 'â”'
- 192: 230, # 'â””'
- 193: 231, # 'â”´'
- 194: 232, # '┬'
- 195: 233, # '├'
- 196: 234, # '─'
- 197: 235, # '┼'
- 198: 11, # 'к'
- 199: 36, # 'К'
- 200: 236, # 'â•š'
- 201: 237, # 'â•”'
- 202: 238, # 'â•©'
- 203: 239, # '╦'
- 204: 240, # 'â• '
- 205: 241, # 'â•'
- 206: 242, # '╬'
- 207: 243, # '¤'
- 208: 8, # 'л'
- 209: 49, # 'Л'
- 210: 12, # 'м'
- 211: 38, # 'М'
- 212: 5, # 'н'
- 213: 31, # 'Ð'
- 214: 1, # 'о'
- 215: 34, # 'О'
- 216: 15, # 'п'
- 217: 244, # '┘'
- 218: 245, # '┌'
- 219: 246, # 'â–ˆ'
- 220: 247, # 'â–„'
- 221: 35, # 'П'
- 222: 16, # 'Ñ'
- 223: 248, # 'â–€'
- 224: 43, # 'Я'
- 225: 9, # 'Ñ€'
- 226: 45, # 'Р'
- 227: 7, # 'Ñ'
- 228: 32, # 'С'
- 229: 6, # 'Ñ‚'
- 230: 40, # 'Т'
- 231: 14, # 'у'
- 232: 52, # 'У'
- 233: 24, # 'ж'
- 234: 56, # 'Ж'
- 235: 10, # 'в'
- 236: 33, # 'Ð’'
- 237: 17, # 'ь'
- 238: 61, # 'Ь'
- 239: 249, # 'â„–'
- 240: 250, # '\xad'
- 241: 18, # 'Ñ‹'
- 242: 62, # 'Ы'
- 243: 20, # 'з'
- 244: 51, # 'З'
- 245: 25, # 'ш'
- 246: 57, # 'Ш'
- 247: 30, # 'Ñ'
- 248: 47, # 'Э'
- 249: 29, # 'щ'
- 250: 63, # 'Щ'
- 251: 22, # 'ч'
- 252: 50, # 'Ч'
- 253: 251, # '§'
- 254: 252, # 'â– '
- 255: 255, # '\xa0'
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 142, # 'A'
+ 66: 143, # 'B'
+ 67: 144, # 'C'
+ 68: 145, # 'D'
+ 69: 146, # 'E'
+ 70: 147, # 'F'
+ 71: 148, # 'G'
+ 72: 149, # 'H'
+ 73: 150, # 'I'
+ 74: 151, # 'J'
+ 75: 152, # 'K'
+ 76: 74, # 'L'
+ 77: 153, # 'M'
+ 78: 75, # 'N'
+ 79: 154, # 'O'
+ 80: 155, # 'P'
+ 81: 156, # 'Q'
+ 82: 157, # 'R'
+ 83: 158, # 'S'
+ 84: 159, # 'T'
+ 85: 160, # 'U'
+ 86: 161, # 'V'
+ 87: 162, # 'W'
+ 88: 163, # 'X'
+ 89: 164, # 'Y'
+ 90: 165, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 71, # 'a'
+ 98: 172, # 'b'
+ 99: 66, # 'c'
+ 100: 173, # 'd'
+ 101: 65, # 'e'
+ 102: 174, # 'f'
+ 103: 76, # 'g'
+ 104: 175, # 'h'
+ 105: 64, # 'i'
+ 106: 176, # 'j'
+ 107: 177, # 'k'
+ 108: 77, # 'l'
+ 109: 72, # 'm'
+ 110: 178, # 'n'
+ 111: 69, # 'o'
+ 112: 67, # 'p'
+ 113: 179, # 'q'
+ 114: 78, # 'r'
+ 115: 73, # 's'
+ 116: 180, # 't'
+ 117: 181, # 'u'
+ 118: 79, # 'v'
+ 119: 182, # 'w'
+ 120: 183, # 'x'
+ 121: 184, # 'y'
+ 122: 185, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 191, # 'Ñ’'
+ 129: 192, # 'Ђ'
+ 130: 193, # 'Ñ“'
+ 131: 194, # 'Ѓ'
+ 132: 68, # 'Ñ‘'
+ 133: 195, # 'Ð'
+ 134: 196, # 'Ñ”'
+ 135: 197, # 'Є'
+ 136: 198, # 'Ñ•'
+ 137: 199, # 'Ð…'
+ 138: 200, # 'Ñ–'
+ 139: 201, # 'І'
+ 140: 202, # 'Ñ—'
+ 141: 203, # 'Ї'
+ 142: 204, # 'ј'
+ 143: 205, # 'Ј'
+ 144: 206, # 'Ñ™'
+ 145: 207, # 'Љ'
+ 146: 208, # 'Ñš'
+ 147: 209, # 'Њ'
+ 148: 210, # 'Ñ›'
+ 149: 211, # 'Ћ'
+ 150: 212, # 'ќ'
+ 151: 213, # 'Ќ'
+ 152: 214, # 'Ñž'
+ 153: 215, # 'ÐŽ'
+ 154: 216, # 'ÑŸ'
+ 155: 217, # 'Ð'
+ 156: 27, # 'ÑŽ'
+ 157: 59, # 'Ю'
+ 158: 54, # 'ÑŠ'
+ 159: 70, # 'Ъ'
+ 160: 3, # 'а'
+ 161: 37, # 'Ð'
+ 162: 21, # 'б'
+ 163: 44, # 'Б'
+ 164: 28, # 'ц'
+ 165: 58, # 'Ц'
+ 166: 13, # 'д'
+ 167: 41, # 'Д'
+ 168: 2, # 'е'
+ 169: 48, # 'Е'
+ 170: 39, # 'Ñ„'
+ 171: 53, # 'Ф'
+ 172: 19, # 'г'
+ 173: 46, # 'Г'
+ 174: 218, # '«'
+ 175: 219, # '»'
+ 176: 220, # 'â–‘'
+ 177: 221, # 'â–’'
+ 178: 222, # 'â–“'
+ 179: 223, # '│'
+ 180: 224, # '┤'
+ 181: 26, # 'Ñ…'
+ 182: 55, # 'Ð¥'
+ 183: 4, # 'и'
+ 184: 42, # 'И'
+ 185: 225, # 'â•£'
+ 186: 226, # 'â•‘'
+ 187: 227, # 'â•—'
+ 188: 228, # 'â•'
+ 189: 23, # 'й'
+ 190: 60, # 'Й'
+ 191: 229, # 'â”'
+ 192: 230, # 'â””'
+ 193: 231, # 'â”´'
+ 194: 232, # '┬'
+ 195: 233, # '├'
+ 196: 234, # '─'
+ 197: 235, # '┼'
+ 198: 11, # 'к'
+ 199: 36, # 'К'
+ 200: 236, # 'â•š'
+ 201: 237, # 'â•”'
+ 202: 238, # 'â•©'
+ 203: 239, # '╦'
+ 204: 240, # 'â• '
+ 205: 241, # 'â•'
+ 206: 242, # '╬'
+ 207: 243, # '¤'
+ 208: 8, # 'л'
+ 209: 49, # 'Л'
+ 210: 12, # 'м'
+ 211: 38, # 'М'
+ 212: 5, # 'н'
+ 213: 31, # 'Ð'
+ 214: 1, # 'о'
+ 215: 34, # 'О'
+ 216: 15, # 'п'
+ 217: 244, # '┘'
+ 218: 245, # '┌'
+ 219: 246, # 'â–ˆ'
+ 220: 247, # 'â–„'
+ 221: 35, # 'П'
+ 222: 16, # 'Ñ'
+ 223: 248, # 'â–€'
+ 224: 43, # 'Я'
+ 225: 9, # 'Ñ€'
+ 226: 45, # 'Р'
+ 227: 7, # 'Ñ'
+ 228: 32, # 'С'
+ 229: 6, # 'Ñ‚'
+ 230: 40, # 'Т'
+ 231: 14, # 'у'
+ 232: 52, # 'У'
+ 233: 24, # 'ж'
+ 234: 56, # 'Ж'
+ 235: 10, # 'в'
+ 236: 33, # 'Ð’'
+ 237: 17, # 'ь'
+ 238: 61, # 'Ь'
+ 239: 249, # 'â„–'
+ 240: 250, # '\xad'
+ 241: 18, # 'Ñ‹'
+ 242: 62, # 'Ы'
+ 243: 20, # 'з'
+ 244: 51, # 'З'
+ 245: 25, # 'ш'
+ 246: 57, # 'Ш'
+ 247: 30, # 'Ñ'
+ 248: 47, # 'Э'
+ 249: 29, # 'щ'
+ 250: 63, # 'Щ'
+ 251: 22, # 'ч'
+ 252: 50, # 'Ч'
+ 253: 251, # '§'
+ 254: 252, # 'â– '
+ 255: 255, # '\xa0'
}
-IBM855_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='IBM855',
- language='Russian',
- char_to_order_map=IBM855_RUSSIAN_CHAR_TO_ORDER,
- language_model=RUSSIAN_LANG_MODEL,
- typical_positive_ratio=0.976601,
- keep_ascii_letters=False,
- alphabet='ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘')
+IBM855_RUSSIAN_MODEL = SingleByteCharSetModel(
+ charset_name="IBM855",
+ language="Russian",
+ char_to_order_map=IBM855_RUSSIAN_CHAR_TO_ORDER,
+ language_model=RUSSIAN_LANG_MODEL,
+ typical_positive_ratio=0.976601,
+ keep_ascii_letters=False,
+ alphabet="ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘",
+)
KOI8_R_RUSSIAN_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 142, # 'A'
- 66: 143, # 'B'
- 67: 144, # 'C'
- 68: 145, # 'D'
- 69: 146, # 'E'
- 70: 147, # 'F'
- 71: 148, # 'G'
- 72: 149, # 'H'
- 73: 150, # 'I'
- 74: 151, # 'J'
- 75: 152, # 'K'
- 76: 74, # 'L'
- 77: 153, # 'M'
- 78: 75, # 'N'
- 79: 154, # 'O'
- 80: 155, # 'P'
- 81: 156, # 'Q'
- 82: 157, # 'R'
- 83: 158, # 'S'
- 84: 159, # 'T'
- 85: 160, # 'U'
- 86: 161, # 'V'
- 87: 162, # 'W'
- 88: 163, # 'X'
- 89: 164, # 'Y'
- 90: 165, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 71, # 'a'
- 98: 172, # 'b'
- 99: 66, # 'c'
- 100: 173, # 'd'
- 101: 65, # 'e'
- 102: 174, # 'f'
- 103: 76, # 'g'
- 104: 175, # 'h'
- 105: 64, # 'i'
- 106: 176, # 'j'
- 107: 177, # 'k'
- 108: 77, # 'l'
- 109: 72, # 'm'
- 110: 178, # 'n'
- 111: 69, # 'o'
- 112: 67, # 'p'
- 113: 179, # 'q'
- 114: 78, # 'r'
- 115: 73, # 's'
- 116: 180, # 't'
- 117: 181, # 'u'
- 118: 79, # 'v'
- 119: 182, # 'w'
- 120: 183, # 'x'
- 121: 184, # 'y'
- 122: 185, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 191, # '─'
- 129: 192, # '│'
- 130: 193, # '┌'
- 131: 194, # 'â”'
- 132: 195, # 'â””'
- 133: 196, # '┘'
- 134: 197, # '├'
- 135: 198, # '┤'
- 136: 199, # '┬'
- 137: 200, # 'â”´'
- 138: 201, # '┼'
- 139: 202, # 'â–€'
- 140: 203, # 'â–„'
- 141: 204, # 'â–ˆ'
- 142: 205, # '▌'
- 143: 206, # 'â–'
- 144: 207, # 'â–‘'
- 145: 208, # 'â–’'
- 146: 209, # 'â–“'
- 147: 210, # '⌠'
- 148: 211, # 'â– '
- 149: 212, # '∙'
- 150: 213, # '√'
- 151: 214, # '≈'
- 152: 215, # '≤'
- 153: 216, # '≥'
- 154: 217, # '\xa0'
- 155: 218, # '⌡'
- 156: 219, # '°'
- 157: 220, # '²'
- 158: 221, # '·'
- 159: 222, # '÷'
- 160: 223, # 'â•'
- 161: 224, # 'â•‘'
- 162: 225, # 'â•’'
- 163: 68, # 'Ñ‘'
- 164: 226, # 'â•“'
- 165: 227, # 'â•”'
- 166: 228, # 'â••'
- 167: 229, # 'â•–'
- 168: 230, # 'â•—'
- 169: 231, # '╘'
- 170: 232, # 'â•™'
- 171: 233, # 'â•š'
- 172: 234, # 'â•›'
- 173: 235, # '╜'
- 174: 236, # 'â•'
- 175: 237, # 'â•ž'
- 176: 238, # 'â•Ÿ'
- 177: 239, # 'â• '
- 178: 240, # 'â•¡'
- 179: 241, # 'Ð'
- 180: 242, # 'â•¢'
- 181: 243, # 'â•£'
- 182: 244, # '╤'
- 183: 245, # 'â•¥'
- 184: 246, # '╦'
- 185: 247, # '╧'
- 186: 248, # '╨'
- 187: 249, # 'â•©'
- 188: 250, # '╪'
- 189: 251, # 'â•«'
- 190: 252, # '╬'
- 191: 253, # '©'
- 192: 27, # 'ÑŽ'
- 193: 3, # 'а'
- 194: 21, # 'б'
- 195: 28, # 'ц'
- 196: 13, # 'д'
- 197: 2, # 'е'
- 198: 39, # 'Ñ„'
- 199: 19, # 'г'
- 200: 26, # 'Ñ…'
- 201: 4, # 'и'
- 202: 23, # 'й'
- 203: 11, # 'к'
- 204: 8, # 'л'
- 205: 12, # 'м'
- 206: 5, # 'н'
- 207: 1, # 'о'
- 208: 15, # 'п'
- 209: 16, # 'Ñ'
- 210: 9, # 'Ñ€'
- 211: 7, # 'Ñ'
- 212: 6, # 'Ñ‚'
- 213: 14, # 'у'
- 214: 24, # 'ж'
- 215: 10, # 'в'
- 216: 17, # 'ь'
- 217: 18, # 'Ñ‹'
- 218: 20, # 'з'
- 219: 25, # 'ш'
- 220: 30, # 'Ñ'
- 221: 29, # 'щ'
- 222: 22, # 'ч'
- 223: 54, # 'ÑŠ'
- 224: 59, # 'Ю'
- 225: 37, # 'Ð'
- 226: 44, # 'Б'
- 227: 58, # 'Ц'
- 228: 41, # 'Д'
- 229: 48, # 'Е'
- 230: 53, # 'Ф'
- 231: 46, # 'Г'
- 232: 55, # 'Ð¥'
- 233: 42, # 'И'
- 234: 60, # 'Й'
- 235: 36, # 'К'
- 236: 49, # 'Л'
- 237: 38, # 'М'
- 238: 31, # 'Ð'
- 239: 34, # 'О'
- 240: 35, # 'П'
- 241: 43, # 'Я'
- 242: 45, # 'Р'
- 243: 32, # 'С'
- 244: 40, # 'Т'
- 245: 52, # 'У'
- 246: 56, # 'Ж'
- 247: 33, # 'Ð’'
- 248: 61, # 'Ь'
- 249: 62, # 'Ы'
- 250: 51, # 'З'
- 251: 57, # 'Ш'
- 252: 47, # 'Э'
- 253: 63, # 'Щ'
- 254: 50, # 'Ч'
- 255: 70, # 'Ъ'
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 142, # 'A'
+ 66: 143, # 'B'
+ 67: 144, # 'C'
+ 68: 145, # 'D'
+ 69: 146, # 'E'
+ 70: 147, # 'F'
+ 71: 148, # 'G'
+ 72: 149, # 'H'
+ 73: 150, # 'I'
+ 74: 151, # 'J'
+ 75: 152, # 'K'
+ 76: 74, # 'L'
+ 77: 153, # 'M'
+ 78: 75, # 'N'
+ 79: 154, # 'O'
+ 80: 155, # 'P'
+ 81: 156, # 'Q'
+ 82: 157, # 'R'
+ 83: 158, # 'S'
+ 84: 159, # 'T'
+ 85: 160, # 'U'
+ 86: 161, # 'V'
+ 87: 162, # 'W'
+ 88: 163, # 'X'
+ 89: 164, # 'Y'
+ 90: 165, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 71, # 'a'
+ 98: 172, # 'b'
+ 99: 66, # 'c'
+ 100: 173, # 'd'
+ 101: 65, # 'e'
+ 102: 174, # 'f'
+ 103: 76, # 'g'
+ 104: 175, # 'h'
+ 105: 64, # 'i'
+ 106: 176, # 'j'
+ 107: 177, # 'k'
+ 108: 77, # 'l'
+ 109: 72, # 'm'
+ 110: 178, # 'n'
+ 111: 69, # 'o'
+ 112: 67, # 'p'
+ 113: 179, # 'q'
+ 114: 78, # 'r'
+ 115: 73, # 's'
+ 116: 180, # 't'
+ 117: 181, # 'u'
+ 118: 79, # 'v'
+ 119: 182, # 'w'
+ 120: 183, # 'x'
+ 121: 184, # 'y'
+ 122: 185, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 191, # '─'
+ 129: 192, # '│'
+ 130: 193, # '┌'
+ 131: 194, # 'â”'
+ 132: 195, # 'â””'
+ 133: 196, # '┘'
+ 134: 197, # '├'
+ 135: 198, # '┤'
+ 136: 199, # '┬'
+ 137: 200, # 'â”´'
+ 138: 201, # '┼'
+ 139: 202, # 'â–€'
+ 140: 203, # 'â–„'
+ 141: 204, # 'â–ˆ'
+ 142: 205, # '▌'
+ 143: 206, # 'â–'
+ 144: 207, # 'â–‘'
+ 145: 208, # 'â–’'
+ 146: 209, # 'â–“'
+ 147: 210, # '⌠'
+ 148: 211, # 'â– '
+ 149: 212, # '∙'
+ 150: 213, # '√'
+ 151: 214, # '≈'
+ 152: 215, # '≤'
+ 153: 216, # '≥'
+ 154: 217, # '\xa0'
+ 155: 218, # '⌡'
+ 156: 219, # '°'
+ 157: 220, # '²'
+ 158: 221, # '·'
+ 159: 222, # '÷'
+ 160: 223, # 'â•'
+ 161: 224, # 'â•‘'
+ 162: 225, # 'â•’'
+ 163: 68, # 'Ñ‘'
+ 164: 226, # 'â•“'
+ 165: 227, # 'â•”'
+ 166: 228, # 'â••'
+ 167: 229, # 'â•–'
+ 168: 230, # 'â•—'
+ 169: 231, # '╘'
+ 170: 232, # 'â•™'
+ 171: 233, # 'â•š'
+ 172: 234, # 'â•›'
+ 173: 235, # '╜'
+ 174: 236, # 'â•'
+ 175: 237, # 'â•ž'
+ 176: 238, # 'â•Ÿ'
+ 177: 239, # 'â• '
+ 178: 240, # 'â•¡'
+ 179: 241, # 'Ð'
+ 180: 242, # 'â•¢'
+ 181: 243, # 'â•£'
+ 182: 244, # '╤'
+ 183: 245, # 'â•¥'
+ 184: 246, # '╦'
+ 185: 247, # '╧'
+ 186: 248, # '╨'
+ 187: 249, # 'â•©'
+ 188: 250, # '╪'
+ 189: 251, # 'â•«'
+ 190: 252, # '╬'
+ 191: 253, # '©'
+ 192: 27, # 'ÑŽ'
+ 193: 3, # 'а'
+ 194: 21, # 'б'
+ 195: 28, # 'ц'
+ 196: 13, # 'д'
+ 197: 2, # 'е'
+ 198: 39, # 'Ñ„'
+ 199: 19, # 'г'
+ 200: 26, # 'Ñ…'
+ 201: 4, # 'и'
+ 202: 23, # 'й'
+ 203: 11, # 'к'
+ 204: 8, # 'л'
+ 205: 12, # 'м'
+ 206: 5, # 'н'
+ 207: 1, # 'о'
+ 208: 15, # 'п'
+ 209: 16, # 'Ñ'
+ 210: 9, # 'Ñ€'
+ 211: 7, # 'Ñ'
+ 212: 6, # 'Ñ‚'
+ 213: 14, # 'у'
+ 214: 24, # 'ж'
+ 215: 10, # 'в'
+ 216: 17, # 'ь'
+ 217: 18, # 'Ñ‹'
+ 218: 20, # 'з'
+ 219: 25, # 'ш'
+ 220: 30, # 'Ñ'
+ 221: 29, # 'щ'
+ 222: 22, # 'ч'
+ 223: 54, # 'ÑŠ'
+ 224: 59, # 'Ю'
+ 225: 37, # 'Ð'
+ 226: 44, # 'Б'
+ 227: 58, # 'Ц'
+ 228: 41, # 'Д'
+ 229: 48, # 'Е'
+ 230: 53, # 'Ф'
+ 231: 46, # 'Г'
+ 232: 55, # 'Ð¥'
+ 233: 42, # 'И'
+ 234: 60, # 'Й'
+ 235: 36, # 'К'
+ 236: 49, # 'Л'
+ 237: 38, # 'М'
+ 238: 31, # 'Ð'
+ 239: 34, # 'О'
+ 240: 35, # 'П'
+ 241: 43, # 'Я'
+ 242: 45, # 'Р'
+ 243: 32, # 'С'
+ 244: 40, # 'Т'
+ 245: 52, # 'У'
+ 246: 56, # 'Ж'
+ 247: 33, # 'Ð’'
+ 248: 61, # 'Ь'
+ 249: 62, # 'Ы'
+ 250: 51, # 'З'
+ 251: 57, # 'Ш'
+ 252: 47, # 'Э'
+ 253: 63, # 'Щ'
+ 254: 50, # 'Ч'
+ 255: 70, # 'Ъ'
}
-KOI8_R_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='KOI8-R',
- language='Russian',
- char_to_order_map=KOI8_R_RUSSIAN_CHAR_TO_ORDER,
- language_model=RUSSIAN_LANG_MODEL,
- typical_positive_ratio=0.976601,
- keep_ascii_letters=False,
- alphabet='ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘')
+KOI8_R_RUSSIAN_MODEL = SingleByteCharSetModel(
+ charset_name="KOI8-R",
+ language="Russian",
+ char_to_order_map=KOI8_R_RUSSIAN_CHAR_TO_ORDER,
+ language_model=RUSSIAN_LANG_MODEL,
+ typical_positive_ratio=0.976601,
+ keep_ascii_letters=False,
+ alphabet="ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘",
+)
MACCYRILLIC_RUSSIAN_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 142, # 'A'
- 66: 143, # 'B'
- 67: 144, # 'C'
- 68: 145, # 'D'
- 69: 146, # 'E'
- 70: 147, # 'F'
- 71: 148, # 'G'
- 72: 149, # 'H'
- 73: 150, # 'I'
- 74: 151, # 'J'
- 75: 152, # 'K'
- 76: 74, # 'L'
- 77: 153, # 'M'
- 78: 75, # 'N'
- 79: 154, # 'O'
- 80: 155, # 'P'
- 81: 156, # 'Q'
- 82: 157, # 'R'
- 83: 158, # 'S'
- 84: 159, # 'T'
- 85: 160, # 'U'
- 86: 161, # 'V'
- 87: 162, # 'W'
- 88: 163, # 'X'
- 89: 164, # 'Y'
- 90: 165, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 71, # 'a'
- 98: 172, # 'b'
- 99: 66, # 'c'
- 100: 173, # 'd'
- 101: 65, # 'e'
- 102: 174, # 'f'
- 103: 76, # 'g'
- 104: 175, # 'h'
- 105: 64, # 'i'
- 106: 176, # 'j'
- 107: 177, # 'k'
- 108: 77, # 'l'
- 109: 72, # 'm'
- 110: 178, # 'n'
- 111: 69, # 'o'
- 112: 67, # 'p'
- 113: 179, # 'q'
- 114: 78, # 'r'
- 115: 73, # 's'
- 116: 180, # 't'
- 117: 181, # 'u'
- 118: 79, # 'v'
- 119: 182, # 'w'
- 120: 183, # 'x'
- 121: 184, # 'y'
- 122: 185, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 37, # 'Ð'
- 129: 44, # 'Б'
- 130: 33, # 'Ð’'
- 131: 46, # 'Г'
- 132: 41, # 'Д'
- 133: 48, # 'Е'
- 134: 56, # 'Ж'
- 135: 51, # 'З'
- 136: 42, # 'И'
- 137: 60, # 'Й'
- 138: 36, # 'К'
- 139: 49, # 'Л'
- 140: 38, # 'М'
- 141: 31, # 'Ð'
- 142: 34, # 'О'
- 143: 35, # 'П'
- 144: 45, # 'Р'
- 145: 32, # 'С'
- 146: 40, # 'Т'
- 147: 52, # 'У'
- 148: 53, # 'Ф'
- 149: 55, # 'Ð¥'
- 150: 58, # 'Ц'
- 151: 50, # 'Ч'
- 152: 57, # 'Ш'
- 153: 63, # 'Щ'
- 154: 70, # 'Ъ'
- 155: 62, # 'Ы'
- 156: 61, # 'Ь'
- 157: 47, # 'Э'
- 158: 59, # 'Ю'
- 159: 43, # 'Я'
- 160: 191, # '†'
- 161: 192, # '°'
- 162: 193, # 'Ò'
- 163: 194, # '£'
- 164: 195, # '§'
- 165: 196, # '•'
- 166: 197, # '¶'
- 167: 198, # 'І'
- 168: 199, # '®'
- 169: 200, # '©'
- 170: 201, # 'â„¢'
- 171: 202, # 'Ђ'
- 172: 203, # 'Ñ’'
- 173: 204, # '≠'
- 174: 205, # 'Ѓ'
- 175: 206, # 'Ñ“'
- 176: 207, # '∞'
- 177: 208, # '±'
- 178: 209, # '≤'
- 179: 210, # '≥'
- 180: 211, # 'Ñ–'
- 181: 212, # 'µ'
- 182: 213, # 'Ò‘'
- 183: 214, # 'Ј'
- 184: 215, # 'Є'
- 185: 216, # 'Ñ”'
- 186: 217, # 'Ї'
- 187: 218, # 'Ñ—'
- 188: 219, # 'Љ'
- 189: 220, # 'Ñ™'
- 190: 221, # 'Њ'
- 191: 222, # 'Ñš'
- 192: 223, # 'ј'
- 193: 224, # 'Ð…'
- 194: 225, # '¬'
- 195: 226, # '√'
- 196: 227, # 'Æ’'
- 197: 228, # '≈'
- 198: 229, # '∆'
- 199: 230, # '«'
- 200: 231, # '»'
- 201: 232, # '…'
- 202: 233, # '\xa0'
- 203: 234, # 'Ћ'
- 204: 235, # 'Ñ›'
- 205: 236, # 'Ќ'
- 206: 237, # 'ќ'
- 207: 238, # 'Ñ•'
- 208: 239, # '–'
- 209: 240, # '—'
- 210: 241, # '“'
- 211: 242, # 'â€'
- 212: 243, # '‘'
- 213: 244, # '’'
- 214: 245, # '÷'
- 215: 246, # '„'
- 216: 247, # 'ÐŽ'
- 217: 248, # 'Ñž'
- 218: 249, # 'Ð'
- 219: 250, # 'ÑŸ'
- 220: 251, # 'â„–'
- 221: 252, # 'Ð'
- 222: 68, # 'Ñ‘'
- 223: 16, # 'Ñ'
- 224: 3, # 'а'
- 225: 21, # 'б'
- 226: 10, # 'в'
- 227: 19, # 'г'
- 228: 13, # 'д'
- 229: 2, # 'е'
- 230: 24, # 'ж'
- 231: 20, # 'з'
- 232: 4, # 'и'
- 233: 23, # 'й'
- 234: 11, # 'к'
- 235: 8, # 'л'
- 236: 12, # 'м'
- 237: 5, # 'н'
- 238: 1, # 'о'
- 239: 15, # 'п'
- 240: 9, # 'Ñ€'
- 241: 7, # 'Ñ'
- 242: 6, # 'Ñ‚'
- 243: 14, # 'у'
- 244: 39, # 'Ñ„'
- 245: 26, # 'Ñ…'
- 246: 28, # 'ц'
- 247: 22, # 'ч'
- 248: 25, # 'ш'
- 249: 29, # 'щ'
- 250: 54, # 'ÑŠ'
- 251: 18, # 'Ñ‹'
- 252: 17, # 'ь'
- 253: 30, # 'Ñ'
- 254: 27, # 'ÑŽ'
- 255: 255, # '€'
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 142, # 'A'
+ 66: 143, # 'B'
+ 67: 144, # 'C'
+ 68: 145, # 'D'
+ 69: 146, # 'E'
+ 70: 147, # 'F'
+ 71: 148, # 'G'
+ 72: 149, # 'H'
+ 73: 150, # 'I'
+ 74: 151, # 'J'
+ 75: 152, # 'K'
+ 76: 74, # 'L'
+ 77: 153, # 'M'
+ 78: 75, # 'N'
+ 79: 154, # 'O'
+ 80: 155, # 'P'
+ 81: 156, # 'Q'
+ 82: 157, # 'R'
+ 83: 158, # 'S'
+ 84: 159, # 'T'
+ 85: 160, # 'U'
+ 86: 161, # 'V'
+ 87: 162, # 'W'
+ 88: 163, # 'X'
+ 89: 164, # 'Y'
+ 90: 165, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 71, # 'a'
+ 98: 172, # 'b'
+ 99: 66, # 'c'
+ 100: 173, # 'd'
+ 101: 65, # 'e'
+ 102: 174, # 'f'
+ 103: 76, # 'g'
+ 104: 175, # 'h'
+ 105: 64, # 'i'
+ 106: 176, # 'j'
+ 107: 177, # 'k'
+ 108: 77, # 'l'
+ 109: 72, # 'm'
+ 110: 178, # 'n'
+ 111: 69, # 'o'
+ 112: 67, # 'p'
+ 113: 179, # 'q'
+ 114: 78, # 'r'
+ 115: 73, # 's'
+ 116: 180, # 't'
+ 117: 181, # 'u'
+ 118: 79, # 'v'
+ 119: 182, # 'w'
+ 120: 183, # 'x'
+ 121: 184, # 'y'
+ 122: 185, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 37, # 'Ð'
+ 129: 44, # 'Б'
+ 130: 33, # 'Ð’'
+ 131: 46, # 'Г'
+ 132: 41, # 'Д'
+ 133: 48, # 'Е'
+ 134: 56, # 'Ж'
+ 135: 51, # 'З'
+ 136: 42, # 'И'
+ 137: 60, # 'Й'
+ 138: 36, # 'К'
+ 139: 49, # 'Л'
+ 140: 38, # 'М'
+ 141: 31, # 'Ð'
+ 142: 34, # 'О'
+ 143: 35, # 'П'
+ 144: 45, # 'Р'
+ 145: 32, # 'С'
+ 146: 40, # 'Т'
+ 147: 52, # 'У'
+ 148: 53, # 'Ф'
+ 149: 55, # 'Ð¥'
+ 150: 58, # 'Ц'
+ 151: 50, # 'Ч'
+ 152: 57, # 'Ш'
+ 153: 63, # 'Щ'
+ 154: 70, # 'Ъ'
+ 155: 62, # 'Ы'
+ 156: 61, # 'Ь'
+ 157: 47, # 'Э'
+ 158: 59, # 'Ю'
+ 159: 43, # 'Я'
+ 160: 191, # '†'
+ 161: 192, # '°'
+ 162: 193, # 'Ò'
+ 163: 194, # '£'
+ 164: 195, # '§'
+ 165: 196, # '•'
+ 166: 197, # '¶'
+ 167: 198, # 'І'
+ 168: 199, # '®'
+ 169: 200, # '©'
+ 170: 201, # 'â„¢'
+ 171: 202, # 'Ђ'
+ 172: 203, # 'Ñ’'
+ 173: 204, # '≠'
+ 174: 205, # 'Ѓ'
+ 175: 206, # 'Ñ“'
+ 176: 207, # '∞'
+ 177: 208, # '±'
+ 178: 209, # '≤'
+ 179: 210, # '≥'
+ 180: 211, # 'Ñ–'
+ 181: 212, # 'µ'
+ 182: 213, # 'Ò‘'
+ 183: 214, # 'Ј'
+ 184: 215, # 'Є'
+ 185: 216, # 'Ñ”'
+ 186: 217, # 'Ї'
+ 187: 218, # 'Ñ—'
+ 188: 219, # 'Љ'
+ 189: 220, # 'Ñ™'
+ 190: 221, # 'Њ'
+ 191: 222, # 'Ñš'
+ 192: 223, # 'ј'
+ 193: 224, # 'Ð…'
+ 194: 225, # '¬'
+ 195: 226, # '√'
+ 196: 227, # 'Æ’'
+ 197: 228, # '≈'
+ 198: 229, # '∆'
+ 199: 230, # '«'
+ 200: 231, # '»'
+ 201: 232, # '…'
+ 202: 233, # '\xa0'
+ 203: 234, # 'Ћ'
+ 204: 235, # 'Ñ›'
+ 205: 236, # 'Ќ'
+ 206: 237, # 'ќ'
+ 207: 238, # 'Ñ•'
+ 208: 239, # '–'
+ 209: 240, # '—'
+ 210: 241, # '“'
+ 211: 242, # 'â€'
+ 212: 243, # '‘'
+ 213: 244, # '’'
+ 214: 245, # '÷'
+ 215: 246, # '„'
+ 216: 247, # 'ÐŽ'
+ 217: 248, # 'Ñž'
+ 218: 249, # 'Ð'
+ 219: 250, # 'ÑŸ'
+ 220: 251, # 'â„–'
+ 221: 252, # 'Ð'
+ 222: 68, # 'Ñ‘'
+ 223: 16, # 'Ñ'
+ 224: 3, # 'а'
+ 225: 21, # 'б'
+ 226: 10, # 'в'
+ 227: 19, # 'г'
+ 228: 13, # 'д'
+ 229: 2, # 'е'
+ 230: 24, # 'ж'
+ 231: 20, # 'з'
+ 232: 4, # 'и'
+ 233: 23, # 'й'
+ 234: 11, # 'к'
+ 235: 8, # 'л'
+ 236: 12, # 'м'
+ 237: 5, # 'н'
+ 238: 1, # 'о'
+ 239: 15, # 'п'
+ 240: 9, # 'Ñ€'
+ 241: 7, # 'Ñ'
+ 242: 6, # 'Ñ‚'
+ 243: 14, # 'у'
+ 244: 39, # 'Ñ„'
+ 245: 26, # 'Ñ…'
+ 246: 28, # 'ц'
+ 247: 22, # 'ч'
+ 248: 25, # 'ш'
+ 249: 29, # 'щ'
+ 250: 54, # 'ÑŠ'
+ 251: 18, # 'Ñ‹'
+ 252: 17, # 'ь'
+ 253: 30, # 'Ñ'
+ 254: 27, # 'ÑŽ'
+ 255: 255, # '€'
}
-MACCYRILLIC_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='MacCyrillic',
- language='Russian',
- char_to_order_map=MACCYRILLIC_RUSSIAN_CHAR_TO_ORDER,
- language_model=RUSSIAN_LANG_MODEL,
- typical_positive_ratio=0.976601,
- keep_ascii_letters=False,
- alphabet='ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘')
+MACCYRILLIC_RUSSIAN_MODEL = SingleByteCharSetModel(
+ charset_name="MacCyrillic",
+ language="Russian",
+ char_to_order_map=MACCYRILLIC_RUSSIAN_CHAR_TO_ORDER,
+ language_model=RUSSIAN_LANG_MODEL,
+ typical_positive_ratio=0.976601,
+ keep_ascii_letters=False,
+ alphabet="ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘",
+)
ISO_8859_5_RUSSIAN_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 142, # 'A'
- 66: 143, # 'B'
- 67: 144, # 'C'
- 68: 145, # 'D'
- 69: 146, # 'E'
- 70: 147, # 'F'
- 71: 148, # 'G'
- 72: 149, # 'H'
- 73: 150, # 'I'
- 74: 151, # 'J'
- 75: 152, # 'K'
- 76: 74, # 'L'
- 77: 153, # 'M'
- 78: 75, # 'N'
- 79: 154, # 'O'
- 80: 155, # 'P'
- 81: 156, # 'Q'
- 82: 157, # 'R'
- 83: 158, # 'S'
- 84: 159, # 'T'
- 85: 160, # 'U'
- 86: 161, # 'V'
- 87: 162, # 'W'
- 88: 163, # 'X'
- 89: 164, # 'Y'
- 90: 165, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 71, # 'a'
- 98: 172, # 'b'
- 99: 66, # 'c'
- 100: 173, # 'd'
- 101: 65, # 'e'
- 102: 174, # 'f'
- 103: 76, # 'g'
- 104: 175, # 'h'
- 105: 64, # 'i'
- 106: 176, # 'j'
- 107: 177, # 'k'
- 108: 77, # 'l'
- 109: 72, # 'm'
- 110: 178, # 'n'
- 111: 69, # 'o'
- 112: 67, # 'p'
- 113: 179, # 'q'
- 114: 78, # 'r'
- 115: 73, # 's'
- 116: 180, # 't'
- 117: 181, # 'u'
- 118: 79, # 'v'
- 119: 182, # 'w'
- 120: 183, # 'x'
- 121: 184, # 'y'
- 122: 185, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 191, # '\x80'
- 129: 192, # '\x81'
- 130: 193, # '\x82'
- 131: 194, # '\x83'
- 132: 195, # '\x84'
- 133: 196, # '\x85'
- 134: 197, # '\x86'
- 135: 198, # '\x87'
- 136: 199, # '\x88'
- 137: 200, # '\x89'
- 138: 201, # '\x8a'
- 139: 202, # '\x8b'
- 140: 203, # '\x8c'
- 141: 204, # '\x8d'
- 142: 205, # '\x8e'
- 143: 206, # '\x8f'
- 144: 207, # '\x90'
- 145: 208, # '\x91'
- 146: 209, # '\x92'
- 147: 210, # '\x93'
- 148: 211, # '\x94'
- 149: 212, # '\x95'
- 150: 213, # '\x96'
- 151: 214, # '\x97'
- 152: 215, # '\x98'
- 153: 216, # '\x99'
- 154: 217, # '\x9a'
- 155: 218, # '\x9b'
- 156: 219, # '\x9c'
- 157: 220, # '\x9d'
- 158: 221, # '\x9e'
- 159: 222, # '\x9f'
- 160: 223, # '\xa0'
- 161: 224, # 'Ð'
- 162: 225, # 'Ђ'
- 163: 226, # 'Ѓ'
- 164: 227, # 'Є'
- 165: 228, # 'Ð…'
- 166: 229, # 'І'
- 167: 230, # 'Ї'
- 168: 231, # 'Ј'
- 169: 232, # 'Љ'
- 170: 233, # 'Њ'
- 171: 234, # 'Ћ'
- 172: 235, # 'Ќ'
- 173: 236, # '\xad'
- 174: 237, # 'ÐŽ'
- 175: 238, # 'Ð'
- 176: 37, # 'Ð'
- 177: 44, # 'Б'
- 178: 33, # 'Ð’'
- 179: 46, # 'Г'
- 180: 41, # 'Д'
- 181: 48, # 'Е'
- 182: 56, # 'Ж'
- 183: 51, # 'З'
- 184: 42, # 'И'
- 185: 60, # 'Й'
- 186: 36, # 'К'
- 187: 49, # 'Л'
- 188: 38, # 'М'
- 189: 31, # 'Ð'
- 190: 34, # 'О'
- 191: 35, # 'П'
- 192: 45, # 'Р'
- 193: 32, # 'С'
- 194: 40, # 'Т'
- 195: 52, # 'У'
- 196: 53, # 'Ф'
- 197: 55, # 'Ð¥'
- 198: 58, # 'Ц'
- 199: 50, # 'Ч'
- 200: 57, # 'Ш'
- 201: 63, # 'Щ'
- 202: 70, # 'Ъ'
- 203: 62, # 'Ы'
- 204: 61, # 'Ь'
- 205: 47, # 'Э'
- 206: 59, # 'Ю'
- 207: 43, # 'Я'
- 208: 3, # 'а'
- 209: 21, # 'б'
- 210: 10, # 'в'
- 211: 19, # 'г'
- 212: 13, # 'д'
- 213: 2, # 'е'
- 214: 24, # 'ж'
- 215: 20, # 'з'
- 216: 4, # 'и'
- 217: 23, # 'й'
- 218: 11, # 'к'
- 219: 8, # 'л'
- 220: 12, # 'м'
- 221: 5, # 'н'
- 222: 1, # 'о'
- 223: 15, # 'п'
- 224: 9, # 'Ñ€'
- 225: 7, # 'Ñ'
- 226: 6, # 'Ñ‚'
- 227: 14, # 'у'
- 228: 39, # 'Ñ„'
- 229: 26, # 'Ñ…'
- 230: 28, # 'ц'
- 231: 22, # 'ч'
- 232: 25, # 'ш'
- 233: 29, # 'щ'
- 234: 54, # 'ÑŠ'
- 235: 18, # 'Ñ‹'
- 236: 17, # 'ь'
- 237: 30, # 'Ñ'
- 238: 27, # 'ÑŽ'
- 239: 16, # 'Ñ'
- 240: 239, # 'â„–'
- 241: 68, # 'Ñ‘'
- 242: 240, # 'Ñ’'
- 243: 241, # 'Ñ“'
- 244: 242, # 'Ñ”'
- 245: 243, # 'Ñ•'
- 246: 244, # 'Ñ–'
- 247: 245, # 'Ñ—'
- 248: 246, # 'ј'
- 249: 247, # 'Ñ™'
- 250: 248, # 'Ñš'
- 251: 249, # 'Ñ›'
- 252: 250, # 'ќ'
- 253: 251, # '§'
- 254: 252, # 'Ñž'
- 255: 255, # 'ÑŸ'
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 142, # 'A'
+ 66: 143, # 'B'
+ 67: 144, # 'C'
+ 68: 145, # 'D'
+ 69: 146, # 'E'
+ 70: 147, # 'F'
+ 71: 148, # 'G'
+ 72: 149, # 'H'
+ 73: 150, # 'I'
+ 74: 151, # 'J'
+ 75: 152, # 'K'
+ 76: 74, # 'L'
+ 77: 153, # 'M'
+ 78: 75, # 'N'
+ 79: 154, # 'O'
+ 80: 155, # 'P'
+ 81: 156, # 'Q'
+ 82: 157, # 'R'
+ 83: 158, # 'S'
+ 84: 159, # 'T'
+ 85: 160, # 'U'
+ 86: 161, # 'V'
+ 87: 162, # 'W'
+ 88: 163, # 'X'
+ 89: 164, # 'Y'
+ 90: 165, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 71, # 'a'
+ 98: 172, # 'b'
+ 99: 66, # 'c'
+ 100: 173, # 'd'
+ 101: 65, # 'e'
+ 102: 174, # 'f'
+ 103: 76, # 'g'
+ 104: 175, # 'h'
+ 105: 64, # 'i'
+ 106: 176, # 'j'
+ 107: 177, # 'k'
+ 108: 77, # 'l'
+ 109: 72, # 'm'
+ 110: 178, # 'n'
+ 111: 69, # 'o'
+ 112: 67, # 'p'
+ 113: 179, # 'q'
+ 114: 78, # 'r'
+ 115: 73, # 's'
+ 116: 180, # 't'
+ 117: 181, # 'u'
+ 118: 79, # 'v'
+ 119: 182, # 'w'
+ 120: 183, # 'x'
+ 121: 184, # 'y'
+ 122: 185, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 191, # '\x80'
+ 129: 192, # '\x81'
+ 130: 193, # '\x82'
+ 131: 194, # '\x83'
+ 132: 195, # '\x84'
+ 133: 196, # '\x85'
+ 134: 197, # '\x86'
+ 135: 198, # '\x87'
+ 136: 199, # '\x88'
+ 137: 200, # '\x89'
+ 138: 201, # '\x8a'
+ 139: 202, # '\x8b'
+ 140: 203, # '\x8c'
+ 141: 204, # '\x8d'
+ 142: 205, # '\x8e'
+ 143: 206, # '\x8f'
+ 144: 207, # '\x90'
+ 145: 208, # '\x91'
+ 146: 209, # '\x92'
+ 147: 210, # '\x93'
+ 148: 211, # '\x94'
+ 149: 212, # '\x95'
+ 150: 213, # '\x96'
+ 151: 214, # '\x97'
+ 152: 215, # '\x98'
+ 153: 216, # '\x99'
+ 154: 217, # '\x9a'
+ 155: 218, # '\x9b'
+ 156: 219, # '\x9c'
+ 157: 220, # '\x9d'
+ 158: 221, # '\x9e'
+ 159: 222, # '\x9f'
+ 160: 223, # '\xa0'
+ 161: 224, # 'Ð'
+ 162: 225, # 'Ђ'
+ 163: 226, # 'Ѓ'
+ 164: 227, # 'Є'
+ 165: 228, # 'Ð…'
+ 166: 229, # 'І'
+ 167: 230, # 'Ї'
+ 168: 231, # 'Ј'
+ 169: 232, # 'Љ'
+ 170: 233, # 'Њ'
+ 171: 234, # 'Ћ'
+ 172: 235, # 'Ќ'
+ 173: 236, # '\xad'
+ 174: 237, # 'ÐŽ'
+ 175: 238, # 'Ð'
+ 176: 37, # 'Ð'
+ 177: 44, # 'Б'
+ 178: 33, # 'Ð’'
+ 179: 46, # 'Г'
+ 180: 41, # 'Д'
+ 181: 48, # 'Е'
+ 182: 56, # 'Ж'
+ 183: 51, # 'З'
+ 184: 42, # 'И'
+ 185: 60, # 'Й'
+ 186: 36, # 'К'
+ 187: 49, # 'Л'
+ 188: 38, # 'М'
+ 189: 31, # 'Ð'
+ 190: 34, # 'О'
+ 191: 35, # 'П'
+ 192: 45, # 'Р'
+ 193: 32, # 'С'
+ 194: 40, # 'Т'
+ 195: 52, # 'У'
+ 196: 53, # 'Ф'
+ 197: 55, # 'Ð¥'
+ 198: 58, # 'Ц'
+ 199: 50, # 'Ч'
+ 200: 57, # 'Ш'
+ 201: 63, # 'Щ'
+ 202: 70, # 'Ъ'
+ 203: 62, # 'Ы'
+ 204: 61, # 'Ь'
+ 205: 47, # 'Э'
+ 206: 59, # 'Ю'
+ 207: 43, # 'Я'
+ 208: 3, # 'а'
+ 209: 21, # 'б'
+ 210: 10, # 'в'
+ 211: 19, # 'г'
+ 212: 13, # 'д'
+ 213: 2, # 'е'
+ 214: 24, # 'ж'
+ 215: 20, # 'з'
+ 216: 4, # 'и'
+ 217: 23, # 'й'
+ 218: 11, # 'к'
+ 219: 8, # 'л'
+ 220: 12, # 'м'
+ 221: 5, # 'н'
+ 222: 1, # 'о'
+ 223: 15, # 'п'
+ 224: 9, # 'Ñ€'
+ 225: 7, # 'Ñ'
+ 226: 6, # 'Ñ‚'
+ 227: 14, # 'у'
+ 228: 39, # 'Ñ„'
+ 229: 26, # 'Ñ…'
+ 230: 28, # 'ц'
+ 231: 22, # 'ч'
+ 232: 25, # 'ш'
+ 233: 29, # 'щ'
+ 234: 54, # 'ÑŠ'
+ 235: 18, # 'Ñ‹'
+ 236: 17, # 'ь'
+ 237: 30, # 'Ñ'
+ 238: 27, # 'ÑŽ'
+ 239: 16, # 'Ñ'
+ 240: 239, # 'â„–'
+ 241: 68, # 'Ñ‘'
+ 242: 240, # 'Ñ’'
+ 243: 241, # 'Ñ“'
+ 244: 242, # 'Ñ”'
+ 245: 243, # 'Ñ•'
+ 246: 244, # 'Ñ–'
+ 247: 245, # 'Ñ—'
+ 248: 246, # 'ј'
+ 249: 247, # 'Ñ™'
+ 250: 248, # 'Ñš'
+ 251: 249, # 'Ñ›'
+ 252: 250, # 'ќ'
+ 253: 251, # '§'
+ 254: 252, # 'Ñž'
+ 255: 255, # 'ÑŸ'
}
-ISO_8859_5_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-5',
- language='Russian',
- char_to_order_map=ISO_8859_5_RUSSIAN_CHAR_TO_ORDER,
- language_model=RUSSIAN_LANG_MODEL,
- typical_positive_ratio=0.976601,
- keep_ascii_letters=False,
- alphabet='ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘')
-
+ISO_8859_5_RUSSIAN_MODEL = SingleByteCharSetModel(
+ charset_name="ISO-8859-5",
+ language="Russian",
+ char_to_order_map=ISO_8859_5_RUSSIAN_CHAR_TO_ORDER,
+ language_model=RUSSIAN_LANG_MODEL,
+ typical_positive_ratio=0.976601,
+ keep_ascii_letters=False,
+ alphabet="ÐÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑÑ‘",
+)
diff --git a/src/pip/_vendor/chardet/langthaimodel.py b/src/pip/_vendor/chardet/langthaimodel.py
index 9a37db573..489cad930 100644
--- a/src/pip/_vendor/chardet/langthaimodel.py
+++ b/src/pip/_vendor/chardet/langthaimodel.py
@@ -1,9 +1,5 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel
-
# 3: Positive
# 2: Likely
# 1: Unlikely
@@ -4115,269 +4111,270 @@ THAI_LANG_MODEL = {
# Character Mapping Table(s):
TIS_620_THAI_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 254, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 254, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 253, # ' '
- 33: 253, # '!'
- 34: 253, # '"'
- 35: 253, # '#'
- 36: 253, # '$'
- 37: 253, # '%'
- 38: 253, # '&'
- 39: 253, # "'"
- 40: 253, # '('
- 41: 253, # ')'
- 42: 253, # '*'
- 43: 253, # '+'
- 44: 253, # ','
- 45: 253, # '-'
- 46: 253, # '.'
- 47: 253, # '/'
- 48: 252, # '0'
- 49: 252, # '1'
- 50: 252, # '2'
- 51: 252, # '3'
- 52: 252, # '4'
- 53: 252, # '5'
- 54: 252, # '6'
- 55: 252, # '7'
- 56: 252, # '8'
- 57: 252, # '9'
- 58: 253, # ':'
- 59: 253, # ';'
- 60: 253, # '<'
- 61: 253, # '='
- 62: 253, # '>'
- 63: 253, # '?'
- 64: 253, # '@'
- 65: 182, # 'A'
- 66: 106, # 'B'
- 67: 107, # 'C'
- 68: 100, # 'D'
- 69: 183, # 'E'
- 70: 184, # 'F'
- 71: 185, # 'G'
- 72: 101, # 'H'
- 73: 94, # 'I'
- 74: 186, # 'J'
- 75: 187, # 'K'
- 76: 108, # 'L'
- 77: 109, # 'M'
- 78: 110, # 'N'
- 79: 111, # 'O'
- 80: 188, # 'P'
- 81: 189, # 'Q'
- 82: 190, # 'R'
- 83: 89, # 'S'
- 84: 95, # 'T'
- 85: 112, # 'U'
- 86: 113, # 'V'
- 87: 191, # 'W'
- 88: 192, # 'X'
- 89: 193, # 'Y'
- 90: 194, # 'Z'
- 91: 253, # '['
- 92: 253, # '\\'
- 93: 253, # ']'
- 94: 253, # '^'
- 95: 253, # '_'
- 96: 253, # '`'
- 97: 64, # 'a'
- 98: 72, # 'b'
- 99: 73, # 'c'
- 100: 114, # 'd'
- 101: 74, # 'e'
- 102: 115, # 'f'
- 103: 116, # 'g'
- 104: 102, # 'h'
- 105: 81, # 'i'
- 106: 201, # 'j'
- 107: 117, # 'k'
- 108: 90, # 'l'
- 109: 103, # 'm'
- 110: 78, # 'n'
- 111: 82, # 'o'
- 112: 96, # 'p'
- 113: 202, # 'q'
- 114: 91, # 'r'
- 115: 79, # 's'
- 116: 84, # 't'
- 117: 104, # 'u'
- 118: 105, # 'v'
- 119: 97, # 'w'
- 120: 98, # 'x'
- 121: 92, # 'y'
- 122: 203, # 'z'
- 123: 253, # '{'
- 124: 253, # '|'
- 125: 253, # '}'
- 126: 253, # '~'
- 127: 253, # '\x7f'
- 128: 209, # '\x80'
- 129: 210, # '\x81'
- 130: 211, # '\x82'
- 131: 212, # '\x83'
- 132: 213, # '\x84'
- 133: 88, # '\x85'
- 134: 214, # '\x86'
- 135: 215, # '\x87'
- 136: 216, # '\x88'
- 137: 217, # '\x89'
- 138: 218, # '\x8a'
- 139: 219, # '\x8b'
- 140: 220, # '\x8c'
- 141: 118, # '\x8d'
- 142: 221, # '\x8e'
- 143: 222, # '\x8f'
- 144: 223, # '\x90'
- 145: 224, # '\x91'
- 146: 99, # '\x92'
- 147: 85, # '\x93'
- 148: 83, # '\x94'
- 149: 225, # '\x95'
- 150: 226, # '\x96'
- 151: 227, # '\x97'
- 152: 228, # '\x98'
- 153: 229, # '\x99'
- 154: 230, # '\x9a'
- 155: 231, # '\x9b'
- 156: 232, # '\x9c'
- 157: 233, # '\x9d'
- 158: 234, # '\x9e'
- 159: 235, # '\x9f'
- 160: 236, # None
- 161: 5, # 'à¸'
- 162: 30, # 'ข'
- 163: 237, # 'ฃ'
- 164: 24, # 'ค'
- 165: 238, # 'ฅ'
- 166: 75, # 'ฆ'
- 167: 8, # 'ง'
- 168: 26, # 'จ'
- 169: 52, # 'ฉ'
- 170: 34, # 'ช'
- 171: 51, # 'ซ'
- 172: 119, # 'ฌ'
- 173: 47, # 'à¸'
- 174: 58, # 'ฎ'
- 175: 57, # 'à¸'
- 176: 49, # 'à¸'
- 177: 53, # 'ฑ'
- 178: 55, # 'ฒ'
- 179: 43, # 'ณ'
- 180: 20, # 'ด'
- 181: 19, # 'ต'
- 182: 44, # 'ถ'
- 183: 14, # 'ท'
- 184: 48, # 'ธ'
- 185: 3, # 'น'
- 186: 17, # 'บ'
- 187: 25, # 'ป'
- 188: 39, # 'ผ'
- 189: 62, # 'à¸'
- 190: 31, # 'พ'
- 191: 54, # 'ฟ'
- 192: 45, # 'ภ'
- 193: 9, # 'ม'
- 194: 16, # 'ย'
- 195: 2, # 'ร'
- 196: 61, # 'ฤ'
- 197: 15, # 'ล'
- 198: 239, # 'ฦ'
- 199: 12, # 'ว'
- 200: 42, # 'ศ'
- 201: 46, # 'ษ'
- 202: 18, # 'ส'
- 203: 21, # 'ห'
- 204: 76, # 'ฬ'
- 205: 4, # 'อ'
- 206: 66, # 'ฮ'
- 207: 63, # 'ฯ'
- 208: 22, # 'ะ'
- 209: 10, # 'ั'
- 210: 1, # 'า'
- 211: 36, # 'ำ'
- 212: 23, # 'ิ'
- 213: 13, # 'ี'
- 214: 40, # 'ึ'
- 215: 27, # 'ื'
- 216: 32, # 'ุ'
- 217: 35, # 'ู'
- 218: 86, # 'ฺ'
- 219: 240, # None
- 220: 241, # None
- 221: 242, # None
- 222: 243, # None
- 223: 244, # '฿'
- 224: 11, # 'เ'
- 225: 28, # 'à¹'
- 226: 41, # 'โ'
- 227: 29, # 'ใ'
- 228: 33, # 'ไ'
- 229: 245, # 'ๅ'
- 230: 50, # 'ๆ'
- 231: 37, # '็'
- 232: 6, # '่'
- 233: 7, # '้'
- 234: 67, # '๊'
- 235: 77, # '๋'
- 236: 38, # '์'
- 237: 93, # 'à¹'
- 238: 246, # '๎'
- 239: 247, # 'à¹'
- 240: 68, # 'à¹'
- 241: 56, # '๑'
- 242: 59, # '๒'
- 243: 65, # '๓'
- 244: 69, # '๔'
- 245: 60, # '๕'
- 246: 70, # '๖'
- 247: 80, # '๗'
- 248: 71, # '๘'
- 249: 87, # '๙'
- 250: 248, # '๚'
- 251: 249, # '๛'
- 252: 250, # None
- 253: 251, # None
- 254: 252, # None
- 255: 253, # None
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 254, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 254, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 253, # ' '
+ 33: 253, # '!'
+ 34: 253, # '"'
+ 35: 253, # '#'
+ 36: 253, # '$'
+ 37: 253, # '%'
+ 38: 253, # '&'
+ 39: 253, # "'"
+ 40: 253, # '('
+ 41: 253, # ')'
+ 42: 253, # '*'
+ 43: 253, # '+'
+ 44: 253, # ','
+ 45: 253, # '-'
+ 46: 253, # '.'
+ 47: 253, # '/'
+ 48: 252, # '0'
+ 49: 252, # '1'
+ 50: 252, # '2'
+ 51: 252, # '3'
+ 52: 252, # '4'
+ 53: 252, # '5'
+ 54: 252, # '6'
+ 55: 252, # '7'
+ 56: 252, # '8'
+ 57: 252, # '9'
+ 58: 253, # ':'
+ 59: 253, # ';'
+ 60: 253, # '<'
+ 61: 253, # '='
+ 62: 253, # '>'
+ 63: 253, # '?'
+ 64: 253, # '@'
+ 65: 182, # 'A'
+ 66: 106, # 'B'
+ 67: 107, # 'C'
+ 68: 100, # 'D'
+ 69: 183, # 'E'
+ 70: 184, # 'F'
+ 71: 185, # 'G'
+ 72: 101, # 'H'
+ 73: 94, # 'I'
+ 74: 186, # 'J'
+ 75: 187, # 'K'
+ 76: 108, # 'L'
+ 77: 109, # 'M'
+ 78: 110, # 'N'
+ 79: 111, # 'O'
+ 80: 188, # 'P'
+ 81: 189, # 'Q'
+ 82: 190, # 'R'
+ 83: 89, # 'S'
+ 84: 95, # 'T'
+ 85: 112, # 'U'
+ 86: 113, # 'V'
+ 87: 191, # 'W'
+ 88: 192, # 'X'
+ 89: 193, # 'Y'
+ 90: 194, # 'Z'
+ 91: 253, # '['
+ 92: 253, # '\\'
+ 93: 253, # ']'
+ 94: 253, # '^'
+ 95: 253, # '_'
+ 96: 253, # '`'
+ 97: 64, # 'a'
+ 98: 72, # 'b'
+ 99: 73, # 'c'
+ 100: 114, # 'd'
+ 101: 74, # 'e'
+ 102: 115, # 'f'
+ 103: 116, # 'g'
+ 104: 102, # 'h'
+ 105: 81, # 'i'
+ 106: 201, # 'j'
+ 107: 117, # 'k'
+ 108: 90, # 'l'
+ 109: 103, # 'm'
+ 110: 78, # 'n'
+ 111: 82, # 'o'
+ 112: 96, # 'p'
+ 113: 202, # 'q'
+ 114: 91, # 'r'
+ 115: 79, # 's'
+ 116: 84, # 't'
+ 117: 104, # 'u'
+ 118: 105, # 'v'
+ 119: 97, # 'w'
+ 120: 98, # 'x'
+ 121: 92, # 'y'
+ 122: 203, # 'z'
+ 123: 253, # '{'
+ 124: 253, # '|'
+ 125: 253, # '}'
+ 126: 253, # '~'
+ 127: 253, # '\x7f'
+ 128: 209, # '\x80'
+ 129: 210, # '\x81'
+ 130: 211, # '\x82'
+ 131: 212, # '\x83'
+ 132: 213, # '\x84'
+ 133: 88, # '\x85'
+ 134: 214, # '\x86'
+ 135: 215, # '\x87'
+ 136: 216, # '\x88'
+ 137: 217, # '\x89'
+ 138: 218, # '\x8a'
+ 139: 219, # '\x8b'
+ 140: 220, # '\x8c'
+ 141: 118, # '\x8d'
+ 142: 221, # '\x8e'
+ 143: 222, # '\x8f'
+ 144: 223, # '\x90'
+ 145: 224, # '\x91'
+ 146: 99, # '\x92'
+ 147: 85, # '\x93'
+ 148: 83, # '\x94'
+ 149: 225, # '\x95'
+ 150: 226, # '\x96'
+ 151: 227, # '\x97'
+ 152: 228, # '\x98'
+ 153: 229, # '\x99'
+ 154: 230, # '\x9a'
+ 155: 231, # '\x9b'
+ 156: 232, # '\x9c'
+ 157: 233, # '\x9d'
+ 158: 234, # '\x9e'
+ 159: 235, # '\x9f'
+ 160: 236, # None
+ 161: 5, # 'à¸'
+ 162: 30, # 'ข'
+ 163: 237, # 'ฃ'
+ 164: 24, # 'ค'
+ 165: 238, # 'ฅ'
+ 166: 75, # 'ฆ'
+ 167: 8, # 'ง'
+ 168: 26, # 'จ'
+ 169: 52, # 'ฉ'
+ 170: 34, # 'ช'
+ 171: 51, # 'ซ'
+ 172: 119, # 'ฌ'
+ 173: 47, # 'à¸'
+ 174: 58, # 'ฎ'
+ 175: 57, # 'à¸'
+ 176: 49, # 'à¸'
+ 177: 53, # 'ฑ'
+ 178: 55, # 'ฒ'
+ 179: 43, # 'ณ'
+ 180: 20, # 'ด'
+ 181: 19, # 'ต'
+ 182: 44, # 'ถ'
+ 183: 14, # 'ท'
+ 184: 48, # 'ธ'
+ 185: 3, # 'น'
+ 186: 17, # 'บ'
+ 187: 25, # 'ป'
+ 188: 39, # 'ผ'
+ 189: 62, # 'à¸'
+ 190: 31, # 'พ'
+ 191: 54, # 'ฟ'
+ 192: 45, # 'ภ'
+ 193: 9, # 'ม'
+ 194: 16, # 'ย'
+ 195: 2, # 'ร'
+ 196: 61, # 'ฤ'
+ 197: 15, # 'ล'
+ 198: 239, # 'ฦ'
+ 199: 12, # 'ว'
+ 200: 42, # 'ศ'
+ 201: 46, # 'ษ'
+ 202: 18, # 'ส'
+ 203: 21, # 'ห'
+ 204: 76, # 'ฬ'
+ 205: 4, # 'อ'
+ 206: 66, # 'ฮ'
+ 207: 63, # 'ฯ'
+ 208: 22, # 'ะ'
+ 209: 10, # 'ั'
+ 210: 1, # 'า'
+ 211: 36, # 'ำ'
+ 212: 23, # 'ิ'
+ 213: 13, # 'ี'
+ 214: 40, # 'ึ'
+ 215: 27, # 'ื'
+ 216: 32, # 'ุ'
+ 217: 35, # 'ู'
+ 218: 86, # 'ฺ'
+ 219: 240, # None
+ 220: 241, # None
+ 221: 242, # None
+ 222: 243, # None
+ 223: 244, # '฿'
+ 224: 11, # 'เ'
+ 225: 28, # 'à¹'
+ 226: 41, # 'โ'
+ 227: 29, # 'ใ'
+ 228: 33, # 'ไ'
+ 229: 245, # 'ๅ'
+ 230: 50, # 'ๆ'
+ 231: 37, # '็'
+ 232: 6, # '่'
+ 233: 7, # '้'
+ 234: 67, # '๊'
+ 235: 77, # '๋'
+ 236: 38, # '์'
+ 237: 93, # 'à¹'
+ 238: 246, # '๎'
+ 239: 247, # 'à¹'
+ 240: 68, # 'à¹'
+ 241: 56, # '๑'
+ 242: 59, # '๒'
+ 243: 65, # '๓'
+ 244: 69, # '๔'
+ 245: 60, # '๕'
+ 246: 70, # '๖'
+ 247: 80, # '๗'
+ 248: 71, # '๘'
+ 249: 87, # '๙'
+ 250: 248, # '๚'
+ 251: 249, # '๛'
+ 252: 250, # None
+ 253: 251, # None
+ 254: 252, # None
+ 255: 253, # None
}
-TIS_620_THAI_MODEL = SingleByteCharSetModel(charset_name='TIS-620',
- language='Thai',
- char_to_order_map=TIS_620_THAI_CHAR_TO_ORDER,
- language_model=THAI_LANG_MODEL,
- typical_positive_ratio=0.926386,
- keep_ascii_letters=False,
- alphabet='à¸à¸‚ฃคฅฆงจฉชซฌà¸à¸Žà¸à¸à¸‘ฒณดตถทธนบปผà¸à¸žà¸Ÿà¸ à¸¡à¸¢à¸£à¸¤à¸¥à¸¦à¸§à¸¨à¸©à¸ªà¸«à¸¬à¸­à¸®à¸¯à¸°à¸±à¸²à¸³à¸´à¸µà¸¶à¸·à¸¸à¸¹à¸ºà¸¿à¹€à¹à¹‚ใไๅๆ็่้๊๋์à¹à¹Žà¹à¹à¹‘๒๓๔๕๖๗๘๙๚๛')
-
+TIS_620_THAI_MODEL = SingleByteCharSetModel(
+ charset_name="TIS-620",
+ language="Thai",
+ char_to_order_map=TIS_620_THAI_CHAR_TO_ORDER,
+ language_model=THAI_LANG_MODEL,
+ typical_positive_ratio=0.926386,
+ keep_ascii_letters=False,
+ alphabet="à¸à¸‚ฃคฅฆงจฉชซฌà¸à¸Žà¸à¸à¸‘ฒณดตถทธนบปผà¸à¸žà¸Ÿà¸ à¸¡à¸¢à¸£à¸¤à¸¥à¸¦à¸§à¸¨à¸©à¸ªà¸«à¸¬à¸­à¸®à¸¯à¸°à¸±à¸²à¸³à¸´à¸µà¸¶à¸·à¸¸à¸¹à¸ºà¸¿à¹€à¹à¹‚ใไๅๆ็่้๊๋์à¹à¹Žà¹à¹à¹‘๒๓๔๕๖๗๘๙๚๛",
+)
diff --git a/src/pip/_vendor/chardet/langturkishmodel.py b/src/pip/_vendor/chardet/langturkishmodel.py
index 43f4230ae..291857c25 100644
--- a/src/pip/_vendor/chardet/langturkishmodel.py
+++ b/src/pip/_vendor/chardet/langturkishmodel.py
@@ -1,9 +1,5 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel
-
# 3: Positive
# 2: Likely
# 1: Unlikely
@@ -4115,269 +4111,270 @@ TURKISH_LANG_MODEL = {
# Character Mapping Table(s):
ISO_8859_9_TURKISH_CHAR_TO_ORDER = {
- 0: 255, # '\x00'
- 1: 255, # '\x01'
- 2: 255, # '\x02'
- 3: 255, # '\x03'
- 4: 255, # '\x04'
- 5: 255, # '\x05'
- 6: 255, # '\x06'
- 7: 255, # '\x07'
- 8: 255, # '\x08'
- 9: 255, # '\t'
- 10: 255, # '\n'
- 11: 255, # '\x0b'
- 12: 255, # '\x0c'
- 13: 255, # '\r'
- 14: 255, # '\x0e'
- 15: 255, # '\x0f'
- 16: 255, # '\x10'
- 17: 255, # '\x11'
- 18: 255, # '\x12'
- 19: 255, # '\x13'
- 20: 255, # '\x14'
- 21: 255, # '\x15'
- 22: 255, # '\x16'
- 23: 255, # '\x17'
- 24: 255, # '\x18'
- 25: 255, # '\x19'
- 26: 255, # '\x1a'
- 27: 255, # '\x1b'
- 28: 255, # '\x1c'
- 29: 255, # '\x1d'
- 30: 255, # '\x1e'
- 31: 255, # '\x1f'
- 32: 255, # ' '
- 33: 255, # '!'
- 34: 255, # '"'
- 35: 255, # '#'
- 36: 255, # '$'
- 37: 255, # '%'
- 38: 255, # '&'
- 39: 255, # "'"
- 40: 255, # '('
- 41: 255, # ')'
- 42: 255, # '*'
- 43: 255, # '+'
- 44: 255, # ','
- 45: 255, # '-'
- 46: 255, # '.'
- 47: 255, # '/'
- 48: 255, # '0'
- 49: 255, # '1'
- 50: 255, # '2'
- 51: 255, # '3'
- 52: 255, # '4'
- 53: 255, # '5'
- 54: 255, # '6'
- 55: 255, # '7'
- 56: 255, # '8'
- 57: 255, # '9'
- 58: 255, # ':'
- 59: 255, # ';'
- 60: 255, # '<'
- 61: 255, # '='
- 62: 255, # '>'
- 63: 255, # '?'
- 64: 255, # '@'
- 65: 23, # 'A'
- 66: 37, # 'B'
- 67: 47, # 'C'
- 68: 39, # 'D'
- 69: 29, # 'E'
- 70: 52, # 'F'
- 71: 36, # 'G'
- 72: 45, # 'H'
- 73: 53, # 'I'
- 74: 60, # 'J'
- 75: 16, # 'K'
- 76: 49, # 'L'
- 77: 20, # 'M'
- 78: 46, # 'N'
- 79: 42, # 'O'
- 80: 48, # 'P'
- 81: 69, # 'Q'
- 82: 44, # 'R'
- 83: 35, # 'S'
- 84: 31, # 'T'
- 85: 51, # 'U'
- 86: 38, # 'V'
- 87: 62, # 'W'
- 88: 65, # 'X'
- 89: 43, # 'Y'
- 90: 56, # 'Z'
- 91: 255, # '['
- 92: 255, # '\\'
- 93: 255, # ']'
- 94: 255, # '^'
- 95: 255, # '_'
- 96: 255, # '`'
- 97: 1, # 'a'
- 98: 21, # 'b'
- 99: 28, # 'c'
- 100: 12, # 'd'
- 101: 2, # 'e'
- 102: 18, # 'f'
- 103: 27, # 'g'
- 104: 25, # 'h'
- 105: 3, # 'i'
- 106: 24, # 'j'
- 107: 10, # 'k'
- 108: 5, # 'l'
- 109: 13, # 'm'
- 110: 4, # 'n'
- 111: 15, # 'o'
- 112: 26, # 'p'
- 113: 64, # 'q'
- 114: 7, # 'r'
- 115: 8, # 's'
- 116: 9, # 't'
- 117: 14, # 'u'
- 118: 32, # 'v'
- 119: 57, # 'w'
- 120: 58, # 'x'
- 121: 11, # 'y'
- 122: 22, # 'z'
- 123: 255, # '{'
- 124: 255, # '|'
- 125: 255, # '}'
- 126: 255, # '~'
- 127: 255, # '\x7f'
- 128: 180, # '\x80'
- 129: 179, # '\x81'
- 130: 178, # '\x82'
- 131: 177, # '\x83'
- 132: 176, # '\x84'
- 133: 175, # '\x85'
- 134: 174, # '\x86'
- 135: 173, # '\x87'
- 136: 172, # '\x88'
- 137: 171, # '\x89'
- 138: 170, # '\x8a'
- 139: 169, # '\x8b'
- 140: 168, # '\x8c'
- 141: 167, # '\x8d'
- 142: 166, # '\x8e'
- 143: 165, # '\x8f'
- 144: 164, # '\x90'
- 145: 163, # '\x91'
- 146: 162, # '\x92'
- 147: 161, # '\x93'
- 148: 160, # '\x94'
- 149: 159, # '\x95'
- 150: 101, # '\x96'
- 151: 158, # '\x97'
- 152: 157, # '\x98'
- 153: 156, # '\x99'
- 154: 155, # '\x9a'
- 155: 154, # '\x9b'
- 156: 153, # '\x9c'
- 157: 152, # '\x9d'
- 158: 151, # '\x9e'
- 159: 106, # '\x9f'
- 160: 150, # '\xa0'
- 161: 149, # '¡'
- 162: 148, # '¢'
- 163: 147, # '£'
- 164: 146, # '¤'
- 165: 145, # 'Â¥'
- 166: 144, # '¦'
- 167: 100, # '§'
- 168: 143, # '¨'
- 169: 142, # '©'
- 170: 141, # 'ª'
- 171: 140, # '«'
- 172: 139, # '¬'
- 173: 138, # '\xad'
- 174: 137, # '®'
- 175: 136, # '¯'
- 176: 94, # '°'
- 177: 80, # '±'
- 178: 93, # '²'
- 179: 135, # '³'
- 180: 105, # '´'
- 181: 134, # 'µ'
- 182: 133, # '¶'
- 183: 63, # '·'
- 184: 132, # '¸'
- 185: 131, # '¹'
- 186: 130, # 'º'
- 187: 129, # '»'
- 188: 128, # '¼'
- 189: 127, # '½'
- 190: 126, # '¾'
- 191: 125, # '¿'
- 192: 124, # 'À'
- 193: 104, # 'Ã'
- 194: 73, # 'Â'
- 195: 99, # 'Ã'
- 196: 79, # 'Ä'
- 197: 85, # 'Ã…'
- 198: 123, # 'Æ'
- 199: 54, # 'Ç'
- 200: 122, # 'È'
- 201: 98, # 'É'
- 202: 92, # 'Ê'
- 203: 121, # 'Ë'
- 204: 120, # 'Ì'
- 205: 91, # 'Ã'
- 206: 103, # 'ÃŽ'
- 207: 119, # 'Ã'
- 208: 68, # 'Äž'
- 209: 118, # 'Ñ'
- 210: 117, # 'Ã’'
- 211: 97, # 'Ó'
- 212: 116, # 'Ô'
- 213: 115, # 'Õ'
- 214: 50, # 'Ö'
- 215: 90, # '×'
- 216: 114, # 'Ø'
- 217: 113, # 'Ù'
- 218: 112, # 'Ú'
- 219: 111, # 'Û'
- 220: 55, # 'Ü'
- 221: 41, # 'Ä°'
- 222: 40, # 'Åž'
- 223: 86, # 'ß'
- 224: 89, # 'à'
- 225: 70, # 'á'
- 226: 59, # 'â'
- 227: 78, # 'ã'
- 228: 71, # 'ä'
- 229: 82, # 'Ã¥'
- 230: 88, # 'æ'
- 231: 33, # 'ç'
- 232: 77, # 'è'
- 233: 66, # 'é'
- 234: 84, # 'ê'
- 235: 83, # 'ë'
- 236: 110, # 'ì'
- 237: 75, # 'í'
- 238: 61, # 'î'
- 239: 96, # 'ï'
- 240: 30, # 'ÄŸ'
- 241: 67, # 'ñ'
- 242: 109, # 'ò'
- 243: 74, # 'ó'
- 244: 87, # 'ô'
- 245: 102, # 'õ'
- 246: 34, # 'ö'
- 247: 95, # '÷'
- 248: 81, # 'ø'
- 249: 108, # 'ù'
- 250: 76, # 'ú'
- 251: 72, # 'û'
- 252: 17, # 'ü'
- 253: 6, # 'ı'
- 254: 19, # 'ÅŸ'
- 255: 107, # 'ÿ'
+ 0: 255, # '\x00'
+ 1: 255, # '\x01'
+ 2: 255, # '\x02'
+ 3: 255, # '\x03'
+ 4: 255, # '\x04'
+ 5: 255, # '\x05'
+ 6: 255, # '\x06'
+ 7: 255, # '\x07'
+ 8: 255, # '\x08'
+ 9: 255, # '\t'
+ 10: 255, # '\n'
+ 11: 255, # '\x0b'
+ 12: 255, # '\x0c'
+ 13: 255, # '\r'
+ 14: 255, # '\x0e'
+ 15: 255, # '\x0f'
+ 16: 255, # '\x10'
+ 17: 255, # '\x11'
+ 18: 255, # '\x12'
+ 19: 255, # '\x13'
+ 20: 255, # '\x14'
+ 21: 255, # '\x15'
+ 22: 255, # '\x16'
+ 23: 255, # '\x17'
+ 24: 255, # '\x18'
+ 25: 255, # '\x19'
+ 26: 255, # '\x1a'
+ 27: 255, # '\x1b'
+ 28: 255, # '\x1c'
+ 29: 255, # '\x1d'
+ 30: 255, # '\x1e'
+ 31: 255, # '\x1f'
+ 32: 255, # ' '
+ 33: 255, # '!'
+ 34: 255, # '"'
+ 35: 255, # '#'
+ 36: 255, # '$'
+ 37: 255, # '%'
+ 38: 255, # '&'
+ 39: 255, # "'"
+ 40: 255, # '('
+ 41: 255, # ')'
+ 42: 255, # '*'
+ 43: 255, # '+'
+ 44: 255, # ','
+ 45: 255, # '-'
+ 46: 255, # '.'
+ 47: 255, # '/'
+ 48: 255, # '0'
+ 49: 255, # '1'
+ 50: 255, # '2'
+ 51: 255, # '3'
+ 52: 255, # '4'
+ 53: 255, # '5'
+ 54: 255, # '6'
+ 55: 255, # '7'
+ 56: 255, # '8'
+ 57: 255, # '9'
+ 58: 255, # ':'
+ 59: 255, # ';'
+ 60: 255, # '<'
+ 61: 255, # '='
+ 62: 255, # '>'
+ 63: 255, # '?'
+ 64: 255, # '@'
+ 65: 23, # 'A'
+ 66: 37, # 'B'
+ 67: 47, # 'C'
+ 68: 39, # 'D'
+ 69: 29, # 'E'
+ 70: 52, # 'F'
+ 71: 36, # 'G'
+ 72: 45, # 'H'
+ 73: 53, # 'I'
+ 74: 60, # 'J'
+ 75: 16, # 'K'
+ 76: 49, # 'L'
+ 77: 20, # 'M'
+ 78: 46, # 'N'
+ 79: 42, # 'O'
+ 80: 48, # 'P'
+ 81: 69, # 'Q'
+ 82: 44, # 'R'
+ 83: 35, # 'S'
+ 84: 31, # 'T'
+ 85: 51, # 'U'
+ 86: 38, # 'V'
+ 87: 62, # 'W'
+ 88: 65, # 'X'
+ 89: 43, # 'Y'
+ 90: 56, # 'Z'
+ 91: 255, # '['
+ 92: 255, # '\\'
+ 93: 255, # ']'
+ 94: 255, # '^'
+ 95: 255, # '_'
+ 96: 255, # '`'
+ 97: 1, # 'a'
+ 98: 21, # 'b'
+ 99: 28, # 'c'
+ 100: 12, # 'd'
+ 101: 2, # 'e'
+ 102: 18, # 'f'
+ 103: 27, # 'g'
+ 104: 25, # 'h'
+ 105: 3, # 'i'
+ 106: 24, # 'j'
+ 107: 10, # 'k'
+ 108: 5, # 'l'
+ 109: 13, # 'm'
+ 110: 4, # 'n'
+ 111: 15, # 'o'
+ 112: 26, # 'p'
+ 113: 64, # 'q'
+ 114: 7, # 'r'
+ 115: 8, # 's'
+ 116: 9, # 't'
+ 117: 14, # 'u'
+ 118: 32, # 'v'
+ 119: 57, # 'w'
+ 120: 58, # 'x'
+ 121: 11, # 'y'
+ 122: 22, # 'z'
+ 123: 255, # '{'
+ 124: 255, # '|'
+ 125: 255, # '}'
+ 126: 255, # '~'
+ 127: 255, # '\x7f'
+ 128: 180, # '\x80'
+ 129: 179, # '\x81'
+ 130: 178, # '\x82'
+ 131: 177, # '\x83'
+ 132: 176, # '\x84'
+ 133: 175, # '\x85'
+ 134: 174, # '\x86'
+ 135: 173, # '\x87'
+ 136: 172, # '\x88'
+ 137: 171, # '\x89'
+ 138: 170, # '\x8a'
+ 139: 169, # '\x8b'
+ 140: 168, # '\x8c'
+ 141: 167, # '\x8d'
+ 142: 166, # '\x8e'
+ 143: 165, # '\x8f'
+ 144: 164, # '\x90'
+ 145: 163, # '\x91'
+ 146: 162, # '\x92'
+ 147: 161, # '\x93'
+ 148: 160, # '\x94'
+ 149: 159, # '\x95'
+ 150: 101, # '\x96'
+ 151: 158, # '\x97'
+ 152: 157, # '\x98'
+ 153: 156, # '\x99'
+ 154: 155, # '\x9a'
+ 155: 154, # '\x9b'
+ 156: 153, # '\x9c'
+ 157: 152, # '\x9d'
+ 158: 151, # '\x9e'
+ 159: 106, # '\x9f'
+ 160: 150, # '\xa0'
+ 161: 149, # '¡'
+ 162: 148, # '¢'
+ 163: 147, # '£'
+ 164: 146, # '¤'
+ 165: 145, # 'Â¥'
+ 166: 144, # '¦'
+ 167: 100, # '§'
+ 168: 143, # '¨'
+ 169: 142, # '©'
+ 170: 141, # 'ª'
+ 171: 140, # '«'
+ 172: 139, # '¬'
+ 173: 138, # '\xad'
+ 174: 137, # '®'
+ 175: 136, # '¯'
+ 176: 94, # '°'
+ 177: 80, # '±'
+ 178: 93, # '²'
+ 179: 135, # '³'
+ 180: 105, # '´'
+ 181: 134, # 'µ'
+ 182: 133, # '¶'
+ 183: 63, # '·'
+ 184: 132, # '¸'
+ 185: 131, # '¹'
+ 186: 130, # 'º'
+ 187: 129, # '»'
+ 188: 128, # '¼'
+ 189: 127, # '½'
+ 190: 126, # '¾'
+ 191: 125, # '¿'
+ 192: 124, # 'À'
+ 193: 104, # 'Ã'
+ 194: 73, # 'Â'
+ 195: 99, # 'Ã'
+ 196: 79, # 'Ä'
+ 197: 85, # 'Ã…'
+ 198: 123, # 'Æ'
+ 199: 54, # 'Ç'
+ 200: 122, # 'È'
+ 201: 98, # 'É'
+ 202: 92, # 'Ê'
+ 203: 121, # 'Ë'
+ 204: 120, # 'Ì'
+ 205: 91, # 'Ã'
+ 206: 103, # 'ÃŽ'
+ 207: 119, # 'Ã'
+ 208: 68, # 'Äž'
+ 209: 118, # 'Ñ'
+ 210: 117, # 'Ã’'
+ 211: 97, # 'Ó'
+ 212: 116, # 'Ô'
+ 213: 115, # 'Õ'
+ 214: 50, # 'Ö'
+ 215: 90, # '×'
+ 216: 114, # 'Ø'
+ 217: 113, # 'Ù'
+ 218: 112, # 'Ú'
+ 219: 111, # 'Û'
+ 220: 55, # 'Ü'
+ 221: 41, # 'Ä°'
+ 222: 40, # 'Åž'
+ 223: 86, # 'ß'
+ 224: 89, # 'à'
+ 225: 70, # 'á'
+ 226: 59, # 'â'
+ 227: 78, # 'ã'
+ 228: 71, # 'ä'
+ 229: 82, # 'Ã¥'
+ 230: 88, # 'æ'
+ 231: 33, # 'ç'
+ 232: 77, # 'è'
+ 233: 66, # 'é'
+ 234: 84, # 'ê'
+ 235: 83, # 'ë'
+ 236: 110, # 'ì'
+ 237: 75, # 'í'
+ 238: 61, # 'î'
+ 239: 96, # 'ï'
+ 240: 30, # 'ÄŸ'
+ 241: 67, # 'ñ'
+ 242: 109, # 'ò'
+ 243: 74, # 'ó'
+ 244: 87, # 'ô'
+ 245: 102, # 'õ'
+ 246: 34, # 'ö'
+ 247: 95, # '÷'
+ 248: 81, # 'ø'
+ 249: 108, # 'ù'
+ 250: 76, # 'ú'
+ 251: 72, # 'û'
+ 252: 17, # 'ü'
+ 253: 6, # 'ı'
+ 254: 19, # 'ÅŸ'
+ 255: 107, # 'ÿ'
}
-ISO_8859_9_TURKISH_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-9',
- language='Turkish',
- char_to_order_map=ISO_8859_9_TURKISH_CHAR_TO_ORDER,
- language_model=TURKISH_LANG_MODEL,
- typical_positive_ratio=0.97029,
- keep_ascii_letters=True,
- alphabet='ABCDEFGHIJKLMNOPRSTUVYZabcdefghijklmnoprstuvyzÂÇÎÖÛÜâçîöûüĞğİıŞş')
-
+ISO_8859_9_TURKISH_MODEL = SingleByteCharSetModel(
+ charset_name="ISO-8859-9",
+ language="Turkish",
+ char_to_order_map=ISO_8859_9_TURKISH_CHAR_TO_ORDER,
+ language_model=TURKISH_LANG_MODEL,
+ typical_positive_ratio=0.97029,
+ keep_ascii_letters=True,
+ alphabet="ABCDEFGHIJKLMNOPRSTUVYZabcdefghijklmnoprstuvyzÂÇÎÖÛÜâçîöûüĞğİıŞş",
+)
diff --git a/src/pip/_vendor/chardet/latin1prober.py b/src/pip/_vendor/chardet/latin1prober.py
index 7d1e8c20f..241f14ab9 100644
--- a/src/pip/_vendor/chardet/latin1prober.py
+++ b/src/pip/_vendor/chardet/latin1prober.py
@@ -41,6 +41,7 @@ ASV = 6 # accent small vowel
ASO = 7 # accent small other
CLASS_NUM = 8 # total classes
+# fmt: off
Latin1_CharToClass = (
OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 00 - 07
OTH, OTH, OTH, OTH, OTH, OTH, OTH, OTH, # 08 - 0F
@@ -91,11 +92,12 @@ Latin1ClassModel = (
0, 3, 1, 3, 1, 1, 1, 3, # ASV
0, 3, 1, 3, 1, 1, 3, 3, # ASO
)
+# fmt: on
class Latin1Prober(CharSetProber):
def __init__(self):
- super(Latin1Prober, self).__init__()
+ super().__init__()
self._last_char_class = None
self._freq_counter = None
self.reset()
@@ -103,7 +105,7 @@ class Latin1Prober(CharSetProber):
def reset(self):
self._last_char_class = OTH
self._freq_counter = [0] * FREQ_CAT_NUM
- CharSetProber.reset(self)
+ super().reset()
@property
def charset_name(self):
@@ -114,11 +116,10 @@ class Latin1Prober(CharSetProber):
return ""
def feed(self, byte_str):
- byte_str = self.filter_with_english_letters(byte_str)
+ byte_str = self.remove_xml_tags(byte_str)
for c in byte_str:
char_class = Latin1_CharToClass[c]
- freq = Latin1ClassModel[(self._last_char_class * CLASS_NUM)
- + char_class]
+ freq = Latin1ClassModel[(self._last_char_class * CLASS_NUM) + char_class]
if freq == 0:
self._state = ProbingState.NOT_ME
break
@@ -132,14 +133,13 @@ class Latin1Prober(CharSetProber):
return 0.01
total = sum(self._freq_counter)
- if total < 0.01:
- confidence = 0.0
- else:
- confidence = ((self._freq_counter[3] - self._freq_counter[1] * 20.0)
- / total)
- if confidence < 0.0:
- confidence = 0.0
+ confidence = (
+ 0.0
+ if total < 0.01
+ else (self._freq_counter[3] - self._freq_counter[1] * 20.0) / total
+ )
+ confidence = max(confidence, 0.0)
# lower the confidence of latin1 so that other more accurate
# detector can take priority.
- confidence = confidence * 0.73
+ confidence *= 0.73
return confidence
diff --git a/src/pip/_vendor/chardet/mbcharsetprober.py b/src/pip/_vendor/chardet/mbcharsetprober.py
index 6256ecfd1..bf96ad5d4 100644
--- a/src/pip/_vendor/chardet/mbcharsetprober.py
+++ b/src/pip/_vendor/chardet/mbcharsetprober.py
@@ -28,7 +28,7 @@
######################### END LICENSE BLOCK #########################
from .charsetprober import CharSetProber
-from .enums import ProbingState, MachineState
+from .enums import MachineState, ProbingState
class MultiByteCharSetProber(CharSetProber):
@@ -37,13 +37,13 @@ class MultiByteCharSetProber(CharSetProber):
"""
def __init__(self, lang_filter=None):
- super(MultiByteCharSetProber, self).__init__(lang_filter=lang_filter)
+ super().__init__(lang_filter=lang_filter)
self.distribution_analyzer = None
self.coding_sm = None
self._last_char = [0, 0]
def reset(self):
- super(MultiByteCharSetProber, self).reset()
+ super().reset()
if self.coding_sm:
self.coding_sm.reset()
if self.distribution_analyzer:
@@ -59,30 +59,34 @@ class MultiByteCharSetProber(CharSetProber):
raise NotImplementedError
def feed(self, byte_str):
- for i in range(len(byte_str)):
- coding_state = self.coding_sm.next_state(byte_str[i])
+ for i, byte in enumerate(byte_str):
+ coding_state = self.coding_sm.next_state(byte)
if coding_state == MachineState.ERROR:
- self.logger.debug('%s %s prober hit error at byte %s',
- self.charset_name, self.language, i)
+ self.logger.debug(
+ "%s %s prober hit error at byte %s",
+ self.charset_name,
+ self.language,
+ i,
+ )
self._state = ProbingState.NOT_ME
break
- elif coding_state == MachineState.ITS_ME:
+ if coding_state == MachineState.ITS_ME:
self._state = ProbingState.FOUND_IT
break
- elif coding_state == MachineState.START:
+ if coding_state == MachineState.START:
char_len = self.coding_sm.get_current_charlen()
if i == 0:
- self._last_char[1] = byte_str[0]
+ self._last_char[1] = byte
self.distribution_analyzer.feed(self._last_char, char_len)
else:
- self.distribution_analyzer.feed(byte_str[i - 1:i + 1],
- char_len)
+ self.distribution_analyzer.feed(byte_str[i - 1 : i + 1], char_len)
self._last_char[0] = byte_str[-1]
if self.state == ProbingState.DETECTING:
- if (self.distribution_analyzer.got_enough_data() and
- (self.get_confidence() > self.SHORTCUT_THRESHOLD)):
+ if self.distribution_analyzer.got_enough_data() and (
+ self.get_confidence() > self.SHORTCUT_THRESHOLD
+ ):
self._state = ProbingState.FOUND_IT
return self.state
diff --git a/src/pip/_vendor/chardet/mbcsgroupprober.py b/src/pip/_vendor/chardet/mbcsgroupprober.py
index 530abe75e..94488360c 100644
--- a/src/pip/_vendor/chardet/mbcsgroupprober.py
+++ b/src/pip/_vendor/chardet/mbcsgroupprober.py
@@ -27,20 +27,21 @@
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
+from .big5prober import Big5Prober
from .charsetgroupprober import CharSetGroupProber
-from .utf8prober import UTF8Prober
-from .sjisprober import SJISProber
+from .cp949prober import CP949Prober
from .eucjpprober import EUCJPProber
-from .gb2312prober import GB2312Prober
from .euckrprober import EUCKRProber
-from .cp949prober import CP949Prober
-from .big5prober import Big5Prober
from .euctwprober import EUCTWProber
+from .gb2312prober import GB2312Prober
+from .johabprober import JOHABProber
+from .sjisprober import SJISProber
+from .utf8prober import UTF8Prober
class MBCSGroupProber(CharSetGroupProber):
def __init__(self, lang_filter=None):
- super(MBCSGroupProber, self).__init__(lang_filter=lang_filter)
+ super().__init__(lang_filter=lang_filter)
self.probers = [
UTF8Prober(),
SJISProber(),
@@ -49,6 +50,7 @@ class MBCSGroupProber(CharSetGroupProber):
EUCKRProber(),
CP949Prober(),
Big5Prober(),
- EUCTWProber()
+ EUCTWProber(),
+ JOHABProber(),
]
self.reset()
diff --git a/src/pip/_vendor/chardet/mbcssm.py b/src/pip/_vendor/chardet/mbcssm.py
index 8360d0f28..d3b9c4b75 100644
--- a/src/pip/_vendor/chardet/mbcssm.py
+++ b/src/pip/_vendor/chardet/mbcssm.py
@@ -29,39 +29,40 @@ from .enums import MachineState
# BIG5
+# fmt: off
BIG5_CLS = (
- 1,1,1,1,1,1,1,1, # 00 - 07 #allow 0x00 as legal value
- 1,1,1,1,1,1,0,0, # 08 - 0f
- 1,1,1,1,1,1,1,1, # 10 - 17
- 1,1,1,0,1,1,1,1, # 18 - 1f
- 1,1,1,1,1,1,1,1, # 20 - 27
- 1,1,1,1,1,1,1,1, # 28 - 2f
- 1,1,1,1,1,1,1,1, # 30 - 37
- 1,1,1,1,1,1,1,1, # 38 - 3f
- 2,2,2,2,2,2,2,2, # 40 - 47
- 2,2,2,2,2,2,2,2, # 48 - 4f
- 2,2,2,2,2,2,2,2, # 50 - 57
- 2,2,2,2,2,2,2,2, # 58 - 5f
- 2,2,2,2,2,2,2,2, # 60 - 67
- 2,2,2,2,2,2,2,2, # 68 - 6f
- 2,2,2,2,2,2,2,2, # 70 - 77
- 2,2,2,2,2,2,2,1, # 78 - 7f
- 4,4,4,4,4,4,4,4, # 80 - 87
- 4,4,4,4,4,4,4,4, # 88 - 8f
- 4,4,4,4,4,4,4,4, # 90 - 97
- 4,4,4,4,4,4,4,4, # 98 - 9f
- 4,3,3,3,3,3,3,3, # a0 - a7
- 3,3,3,3,3,3,3,3, # a8 - af
- 3,3,3,3,3,3,3,3, # b0 - b7
- 3,3,3,3,3,3,3,3, # b8 - bf
- 3,3,3,3,3,3,3,3, # c0 - c7
- 3,3,3,3,3,3,3,3, # c8 - cf
- 3,3,3,3,3,3,3,3, # d0 - d7
- 3,3,3,3,3,3,3,3, # d8 - df
- 3,3,3,3,3,3,3,3, # e0 - e7
- 3,3,3,3,3,3,3,3, # e8 - ef
- 3,3,3,3,3,3,3,3, # f0 - f7
- 3,3,3,3,3,3,3,0 # f8 - ff
+ 1, 1, 1, 1, 1, 1, 1, 1, # 00 - 07 #allow 0x00 as legal value
+ 1, 1, 1, 1, 1, 1, 0, 0, # 08 - 0f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 10 - 17
+ 1, 1, 1, 0, 1, 1, 1, 1, # 18 - 1f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 20 - 27
+ 1, 1, 1, 1, 1, 1, 1, 1, # 28 - 2f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 30 - 37
+ 1, 1, 1, 1, 1, 1, 1, 1, # 38 - 3f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 40 - 47
+ 2, 2, 2, 2, 2, 2, 2, 2, # 48 - 4f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 50 - 57
+ 2, 2, 2, 2, 2, 2, 2, 2, # 58 - 5f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 60 - 67
+ 2, 2, 2, 2, 2, 2, 2, 2, # 68 - 6f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 70 - 77
+ 2, 2, 2, 2, 2, 2, 2, 1, # 78 - 7f
+ 4, 4, 4, 4, 4, 4, 4, 4, # 80 - 87
+ 4, 4, 4, 4, 4, 4, 4, 4, # 88 - 8f
+ 4, 4, 4, 4, 4, 4, 4, 4, # 90 - 97
+ 4, 4, 4, 4, 4, 4, 4, 4, # 98 - 9f
+ 4, 3, 3, 3, 3, 3, 3, 3, # a0 - a7
+ 3, 3, 3, 3, 3, 3, 3, 3, # a8 - af
+ 3, 3, 3, 3, 3, 3, 3, 3, # b0 - b7
+ 3, 3, 3, 3, 3, 3, 3, 3, # b8 - bf
+ 3, 3, 3, 3, 3, 3, 3, 3, # c0 - c7
+ 3, 3, 3, 3, 3, 3, 3, 3, # c8 - cf
+ 3, 3, 3, 3, 3, 3, 3, 3, # d0 - d7
+ 3, 3, 3, 3, 3, 3, 3, 3, # d8 - df
+ 3, 3, 3, 3, 3, 3, 3, 3, # e0 - e7
+ 3, 3, 3, 3, 3, 3, 3, 3, # e8 - ef
+ 3, 3, 3, 3, 3, 3, 3, 3, # f0 - f7
+ 3, 3, 3, 3, 3, 3, 3, 0 # f8 - ff
)
BIG5_ST = (
@@ -69,34 +70,37 @@ BIG5_ST = (
MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,#08-0f
MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START#10-17
)
+# fmt: on
BIG5_CHAR_LEN_TABLE = (0, 1, 1, 2, 0)
-BIG5_SM_MODEL = {'class_table': BIG5_CLS,
- 'class_factor': 5,
- 'state_table': BIG5_ST,
- 'char_len_table': BIG5_CHAR_LEN_TABLE,
- 'name': 'Big5'}
+BIG5_SM_MODEL = {
+ "class_table": BIG5_CLS,
+ "class_factor": 5,
+ "state_table": BIG5_ST,
+ "char_len_table": BIG5_CHAR_LEN_TABLE,
+ "name": "Big5",
+}
# CP949
-
+# fmt: off
CP949_CLS = (
- 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,0,0, # 00 - 0f
- 1,1,1,1,1,1,1,1, 1,1,1,0,1,1,1,1, # 10 - 1f
- 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, # 20 - 2f
- 1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1, # 30 - 3f
- 1,4,4,4,4,4,4,4, 4,4,4,4,4,4,4,4, # 40 - 4f
- 4,4,5,5,5,5,5,5, 5,5,5,1,1,1,1,1, # 50 - 5f
- 1,5,5,5,5,5,5,5, 5,5,5,5,5,5,5,5, # 60 - 6f
- 5,5,5,5,5,5,5,5, 5,5,5,1,1,1,1,1, # 70 - 7f
- 0,6,6,6,6,6,6,6, 6,6,6,6,6,6,6,6, # 80 - 8f
- 6,6,6,6,6,6,6,6, 6,6,6,6,6,6,6,6, # 90 - 9f
- 6,7,7,7,7,7,7,7, 7,7,7,7,7,8,8,8, # a0 - af
- 7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7, # b0 - bf
- 7,7,7,7,7,7,9,2, 2,3,2,2,2,2,2,2, # c0 - cf
- 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, # d0 - df
- 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2, # e0 - ef
- 2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,0, # f0 - ff
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, # 00 - 0f
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, # 10 - 1f
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, # 20 - 2f
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, # 30 - 3f
+ 1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, # 40 - 4f
+ 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 1, 1, 1, # 50 - 5f
+ 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, # 60 - 6f
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 1, 1, 1, # 70 - 7f
+ 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, # 80 - 8f
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, # 90 - 9f
+ 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, # a0 - af
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, # b0 - bf
+ 7, 7, 7, 7, 7, 7, 9, 2, 2, 3, 2, 2, 2, 2, 2, 2, # c0 - cf
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, # d0 - df
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, # e0 - ef
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, # f0 - ff
)
CP949_ST = (
@@ -109,50 +113,53 @@ CP949_ST = (
MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START, # 5
MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START, # 6
)
+# fmt: on
CP949_CHAR_LEN_TABLE = (0, 1, 2, 0, 1, 1, 2, 2, 0, 2)
-CP949_SM_MODEL = {'class_table': CP949_CLS,
- 'class_factor': 10,
- 'state_table': CP949_ST,
- 'char_len_table': CP949_CHAR_LEN_TABLE,
- 'name': 'CP949'}
+CP949_SM_MODEL = {
+ "class_table": CP949_CLS,
+ "class_factor": 10,
+ "state_table": CP949_ST,
+ "char_len_table": CP949_CHAR_LEN_TABLE,
+ "name": "CP949",
+}
# EUC-JP
-
+# fmt: off
EUCJP_CLS = (
- 4,4,4,4,4,4,4,4, # 00 - 07
- 4,4,4,4,4,4,5,5, # 08 - 0f
- 4,4,4,4,4,4,4,4, # 10 - 17
- 4,4,4,5,4,4,4,4, # 18 - 1f
- 4,4,4,4,4,4,4,4, # 20 - 27
- 4,4,4,4,4,4,4,4, # 28 - 2f
- 4,4,4,4,4,4,4,4, # 30 - 37
- 4,4,4,4,4,4,4,4, # 38 - 3f
- 4,4,4,4,4,4,4,4, # 40 - 47
- 4,4,4,4,4,4,4,4, # 48 - 4f
- 4,4,4,4,4,4,4,4, # 50 - 57
- 4,4,4,4,4,4,4,4, # 58 - 5f
- 4,4,4,4,4,4,4,4, # 60 - 67
- 4,4,4,4,4,4,4,4, # 68 - 6f
- 4,4,4,4,4,4,4,4, # 70 - 77
- 4,4,4,4,4,4,4,4, # 78 - 7f
- 5,5,5,5,5,5,5,5, # 80 - 87
- 5,5,5,5,5,5,1,3, # 88 - 8f
- 5,5,5,5,5,5,5,5, # 90 - 97
- 5,5,5,5,5,5,5,5, # 98 - 9f
- 5,2,2,2,2,2,2,2, # a0 - a7
- 2,2,2,2,2,2,2,2, # a8 - af
- 2,2,2,2,2,2,2,2, # b0 - b7
- 2,2,2,2,2,2,2,2, # b8 - bf
- 2,2,2,2,2,2,2,2, # c0 - c7
- 2,2,2,2,2,2,2,2, # c8 - cf
- 2,2,2,2,2,2,2,2, # d0 - d7
- 2,2,2,2,2,2,2,2, # d8 - df
- 0,0,0,0,0,0,0,0, # e0 - e7
- 0,0,0,0,0,0,0,0, # e8 - ef
- 0,0,0,0,0,0,0,0, # f0 - f7
- 0,0,0,0,0,0,0,5 # f8 - ff
+ 4, 4, 4, 4, 4, 4, 4, 4, # 00 - 07
+ 4, 4, 4, 4, 4, 4, 5, 5, # 08 - 0f
+ 4, 4, 4, 4, 4, 4, 4, 4, # 10 - 17
+ 4, 4, 4, 5, 4, 4, 4, 4, # 18 - 1f
+ 4, 4, 4, 4, 4, 4, 4, 4, # 20 - 27
+ 4, 4, 4, 4, 4, 4, 4, 4, # 28 - 2f
+ 4, 4, 4, 4, 4, 4, 4, 4, # 30 - 37
+ 4, 4, 4, 4, 4, 4, 4, 4, # 38 - 3f
+ 4, 4, 4, 4, 4, 4, 4, 4, # 40 - 47
+ 4, 4, 4, 4, 4, 4, 4, 4, # 48 - 4f
+ 4, 4, 4, 4, 4, 4, 4, 4, # 50 - 57
+ 4, 4, 4, 4, 4, 4, 4, 4, # 58 - 5f
+ 4, 4, 4, 4, 4, 4, 4, 4, # 60 - 67
+ 4, 4, 4, 4, 4, 4, 4, 4, # 68 - 6f
+ 4, 4, 4, 4, 4, 4, 4, 4, # 70 - 77
+ 4, 4, 4, 4, 4, 4, 4, 4, # 78 - 7f
+ 5, 5, 5, 5, 5, 5, 5, 5, # 80 - 87
+ 5, 5, 5, 5, 5, 5, 1, 3, # 88 - 8f
+ 5, 5, 5, 5, 5, 5, 5, 5, # 90 - 97
+ 5, 5, 5, 5, 5, 5, 5, 5, # 98 - 9f
+ 5, 2, 2, 2, 2, 2, 2, 2, # a0 - a7
+ 2, 2, 2, 2, 2, 2, 2, 2, # a8 - af
+ 2, 2, 2, 2, 2, 2, 2, 2, # b0 - b7
+ 2, 2, 2, 2, 2, 2, 2, 2, # b8 - bf
+ 2, 2, 2, 2, 2, 2, 2, 2, # c0 - c7
+ 2, 2, 2, 2, 2, 2, 2, 2, # c8 - cf
+ 2, 2, 2, 2, 2, 2, 2, 2, # d0 - d7
+ 2, 2, 2, 2, 2, 2, 2, 2, # d8 - df
+ 0, 0, 0, 0, 0, 0, 0, 0, # e0 - e7
+ 0, 0, 0, 0, 0, 0, 0, 0, # e8 - ef
+ 0, 0, 0, 0, 0, 0, 0, 0, # f0 - f7
+ 0, 0, 0, 0, 0, 0, 0, 5 # f8 - ff
)
EUCJP_ST = (
@@ -162,100 +169,163 @@ EUCJP_ST = (
MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 3,MachineState.ERROR,#18-1f
3,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START#20-27
)
+# fmt: on
EUCJP_CHAR_LEN_TABLE = (2, 2, 2, 3, 1, 0)
-EUCJP_SM_MODEL = {'class_table': EUCJP_CLS,
- 'class_factor': 6,
- 'state_table': EUCJP_ST,
- 'char_len_table': EUCJP_CHAR_LEN_TABLE,
- 'name': 'EUC-JP'}
+EUCJP_SM_MODEL = {
+ "class_table": EUCJP_CLS,
+ "class_factor": 6,
+ "state_table": EUCJP_ST,
+ "char_len_table": EUCJP_CHAR_LEN_TABLE,
+ "name": "EUC-JP",
+}
# EUC-KR
-
+# fmt: off
EUCKR_CLS = (
- 1,1,1,1,1,1,1,1, # 00 - 07
- 1,1,1,1,1,1,0,0, # 08 - 0f
- 1,1,1,1,1,1,1,1, # 10 - 17
- 1,1,1,0,1,1,1,1, # 18 - 1f
- 1,1,1,1,1,1,1,1, # 20 - 27
- 1,1,1,1,1,1,1,1, # 28 - 2f
- 1,1,1,1,1,1,1,1, # 30 - 37
- 1,1,1,1,1,1,1,1, # 38 - 3f
- 1,1,1,1,1,1,1,1, # 40 - 47
+ 1, 1, 1, 1, 1, 1, 1, 1, # 00 - 07
+ 1, 1, 1, 1, 1, 1, 0, 0, # 08 - 0f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 10 - 17
+ 1, 1, 1, 0, 1, 1, 1, 1, # 18 - 1f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 20 - 27
+ 1, 1, 1, 1, 1, 1, 1, 1, # 28 - 2f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 30 - 37
+ 1, 1, 1, 1, 1, 1, 1, 1, # 38 - 3f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 40 - 47
+ 1, 1, 1, 1, 1, 1, 1, 1, # 48 - 4f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 50 - 57
+ 1, 1, 1, 1, 1, 1, 1, 1, # 58 - 5f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 60 - 67
+ 1, 1, 1, 1, 1, 1, 1, 1, # 68 - 6f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 70 - 77
+ 1, 1, 1, 1, 1, 1, 1, 1, # 78 - 7f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 80 - 87
+ 0, 0, 0, 0, 0, 0, 0, 0, # 88 - 8f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 90 - 97
+ 0, 0, 0, 0, 0, 0, 0, 0, # 98 - 9f
+ 0, 2, 2, 2, 2, 2, 2, 2, # a0 - a7
+ 2, 2, 2, 2, 2, 3, 3, 3, # a8 - af
+ 2, 2, 2, 2, 2, 2, 2, 2, # b0 - b7
+ 2, 2, 2, 2, 2, 2, 2, 2, # b8 - bf
+ 2, 2, 2, 2, 2, 2, 2, 2, # c0 - c7
+ 2, 3, 2, 2, 2, 2, 2, 2, # c8 - cf
+ 2, 2, 2, 2, 2, 2, 2, 2, # d0 - d7
+ 2, 2, 2, 2, 2, 2, 2, 2, # d8 - df
+ 2, 2, 2, 2, 2, 2, 2, 2, # e0 - e7
+ 2, 2, 2, 2, 2, 2, 2, 2, # e8 - ef
+ 2, 2, 2, 2, 2, 2, 2, 2, # f0 - f7
+ 2, 2, 2, 2, 2, 2, 2, 0 # f8 - ff
+)
+
+EUCKR_ST = (
+ MachineState.ERROR,MachineState.START, 3,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#00-07
+ MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START #08-0f
+)
+# fmt: on
+
+EUCKR_CHAR_LEN_TABLE = (0, 1, 2, 0)
+
+EUCKR_SM_MODEL = {
+ "class_table": EUCKR_CLS,
+ "class_factor": 4,
+ "state_table": EUCKR_ST,
+ "char_len_table": EUCKR_CHAR_LEN_TABLE,
+ "name": "EUC-KR",
+}
+
+# JOHAB
+# fmt: off
+JOHAB_CLS = (
+ 4,4,4,4,4,4,4,4, # 00 - 07
+ 4,4,4,4,4,4,0,0, # 08 - 0f
+ 4,4,4,4,4,4,4,4, # 10 - 17
+ 4,4,4,0,4,4,4,4, # 18 - 1f
+ 4,4,4,4,4,4,4,4, # 20 - 27
+ 4,4,4,4,4,4,4,4, # 28 - 2f
+ 4,3,3,3,3,3,3,3, # 30 - 37
+ 3,3,3,3,3,3,3,3, # 38 - 3f
+ 3,1,1,1,1,1,1,1, # 40 - 47
1,1,1,1,1,1,1,1, # 48 - 4f
1,1,1,1,1,1,1,1, # 50 - 57
1,1,1,1,1,1,1,1, # 58 - 5f
1,1,1,1,1,1,1,1, # 60 - 67
1,1,1,1,1,1,1,1, # 68 - 6f
1,1,1,1,1,1,1,1, # 70 - 77
- 1,1,1,1,1,1,1,1, # 78 - 7f
- 0,0,0,0,0,0,0,0, # 80 - 87
- 0,0,0,0,0,0,0,0, # 88 - 8f
- 0,0,0,0,0,0,0,0, # 90 - 97
- 0,0,0,0,0,0,0,0, # 98 - 9f
- 0,2,2,2,2,2,2,2, # a0 - a7
- 2,2,2,2,2,3,3,3, # a8 - af
- 2,2,2,2,2,2,2,2, # b0 - b7
- 2,2,2,2,2,2,2,2, # b8 - bf
- 2,2,2,2,2,2,2,2, # c0 - c7
- 2,3,2,2,2,2,2,2, # c8 - cf
- 2,2,2,2,2,2,2,2, # d0 - d7
- 2,2,2,2,2,2,2,2, # d8 - df
- 2,2,2,2,2,2,2,2, # e0 - e7
- 2,2,2,2,2,2,2,2, # e8 - ef
- 2,2,2,2,2,2,2,2, # f0 - f7
- 2,2,2,2,2,2,2,0 # f8 - ff
+ 1,1,1,1,1,1,1,2, # 78 - 7f
+ 6,6,6,6,8,8,8,8, # 80 - 87
+ 8,8,8,8,8,8,8,8, # 88 - 8f
+ 8,7,7,7,7,7,7,7, # 90 - 97
+ 7,7,7,7,7,7,7,7, # 98 - 9f
+ 7,7,7,7,7,7,7,7, # a0 - a7
+ 7,7,7,7,7,7,7,7, # a8 - af
+ 7,7,7,7,7,7,7,7, # b0 - b7
+ 7,7,7,7,7,7,7,7, # b8 - bf
+ 7,7,7,7,7,7,7,7, # c0 - c7
+ 7,7,7,7,7,7,7,7, # c8 - cf
+ 7,7,7,7,5,5,5,5, # d0 - d7
+ 5,9,9,9,9,9,9,5, # d8 - df
+ 9,9,9,9,9,9,9,9, # e0 - e7
+ 9,9,9,9,9,9,9,9, # e8 - ef
+ 9,9,9,9,9,9,9,9, # f0 - f7
+ 9,9,5,5,5,5,5,0 # f8 - ff
)
-EUCKR_ST = (
- MachineState.ERROR,MachineState.START, 3,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#00-07
- MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START #08-0f
+JOHAB_ST = (
+# cls = 0 1 2 3 4 5 6 7 8 9
+ MachineState.ERROR ,MachineState.START ,MachineState.START ,MachineState.START ,MachineState.START ,MachineState.ERROR ,MachineState.ERROR ,3 ,3 ,4 , # MachineState.START
+ MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME, # MachineState.ITS_ME
+ MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR ,MachineState.ERROR , # MachineState.ERROR
+ MachineState.ERROR ,MachineState.START ,MachineState.START ,MachineState.ERROR ,MachineState.ERROR ,MachineState.START ,MachineState.START ,MachineState.START ,MachineState.START ,MachineState.START , # 3
+ MachineState.ERROR ,MachineState.START ,MachineState.ERROR ,MachineState.START ,MachineState.ERROR ,MachineState.START ,MachineState.ERROR ,MachineState.START ,MachineState.ERROR ,MachineState.START , # 4
)
+# fmt: on
-EUCKR_CHAR_LEN_TABLE = (0, 1, 2, 0)
+JOHAB_CHAR_LEN_TABLE = (0, 1, 1, 1, 1, 0, 0, 2, 2, 2)
-EUCKR_SM_MODEL = {'class_table': EUCKR_CLS,
- 'class_factor': 4,
- 'state_table': EUCKR_ST,
- 'char_len_table': EUCKR_CHAR_LEN_TABLE,
- 'name': 'EUC-KR'}
+JOHAB_SM_MODEL = {
+ "class_table": JOHAB_CLS,
+ "class_factor": 10,
+ "state_table": JOHAB_ST,
+ "char_len_table": JOHAB_CHAR_LEN_TABLE,
+ "name": "Johab",
+}
# EUC-TW
-
+# fmt: off
EUCTW_CLS = (
- 2,2,2,2,2,2,2,2, # 00 - 07
- 2,2,2,2,2,2,0,0, # 08 - 0f
- 2,2,2,2,2,2,2,2, # 10 - 17
- 2,2,2,0,2,2,2,2, # 18 - 1f
- 2,2,2,2,2,2,2,2, # 20 - 27
- 2,2,2,2,2,2,2,2, # 28 - 2f
- 2,2,2,2,2,2,2,2, # 30 - 37
- 2,2,2,2,2,2,2,2, # 38 - 3f
- 2,2,2,2,2,2,2,2, # 40 - 47
- 2,2,2,2,2,2,2,2, # 48 - 4f
- 2,2,2,2,2,2,2,2, # 50 - 57
- 2,2,2,2,2,2,2,2, # 58 - 5f
- 2,2,2,2,2,2,2,2, # 60 - 67
- 2,2,2,2,2,2,2,2, # 68 - 6f
- 2,2,2,2,2,2,2,2, # 70 - 77
- 2,2,2,2,2,2,2,2, # 78 - 7f
- 0,0,0,0,0,0,0,0, # 80 - 87
- 0,0,0,0,0,0,6,0, # 88 - 8f
- 0,0,0,0,0,0,0,0, # 90 - 97
- 0,0,0,0,0,0,0,0, # 98 - 9f
- 0,3,4,4,4,4,4,4, # a0 - a7
- 5,5,1,1,1,1,1,1, # a8 - af
- 1,1,1,1,1,1,1,1, # b0 - b7
- 1,1,1,1,1,1,1,1, # b8 - bf
- 1,1,3,1,3,3,3,3, # c0 - c7
- 3,3,3,3,3,3,3,3, # c8 - cf
- 3,3,3,3,3,3,3,3, # d0 - d7
- 3,3,3,3,3,3,3,3, # d8 - df
- 3,3,3,3,3,3,3,3, # e0 - e7
- 3,3,3,3,3,3,3,3, # e8 - ef
- 3,3,3,3,3,3,3,3, # f0 - f7
- 3,3,3,3,3,3,3,0 # f8 - ff
+ 2, 2, 2, 2, 2, 2, 2, 2, # 00 - 07
+ 2, 2, 2, 2, 2, 2, 0, 0, # 08 - 0f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 10 - 17
+ 2, 2, 2, 0, 2, 2, 2, 2, # 18 - 1f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 20 - 27
+ 2, 2, 2, 2, 2, 2, 2, 2, # 28 - 2f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 30 - 37
+ 2, 2, 2, 2, 2, 2, 2, 2, # 38 - 3f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 40 - 47
+ 2, 2, 2, 2, 2, 2, 2, 2, # 48 - 4f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 50 - 57
+ 2, 2, 2, 2, 2, 2, 2, 2, # 58 - 5f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 60 - 67
+ 2, 2, 2, 2, 2, 2, 2, 2, # 68 - 6f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 70 - 77
+ 2, 2, 2, 2, 2, 2, 2, 2, # 78 - 7f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 80 - 87
+ 0, 0, 0, 0, 0, 0, 6, 0, # 88 - 8f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 90 - 97
+ 0, 0, 0, 0, 0, 0, 0, 0, # 98 - 9f
+ 0, 3, 4, 4, 4, 4, 4, 4, # a0 - a7
+ 5, 5, 1, 1, 1, 1, 1, 1, # a8 - af
+ 1, 1, 1, 1, 1, 1, 1, 1, # b0 - b7
+ 1, 1, 1, 1, 1, 1, 1, 1, # b8 - bf
+ 1, 1, 3, 1, 3, 3, 3, 3, # c0 - c7
+ 3, 3, 3, 3, 3, 3, 3, 3, # c8 - cf
+ 3, 3, 3, 3, 3, 3, 3, 3, # d0 - d7
+ 3, 3, 3, 3, 3, 3, 3, 3, # d8 - df
+ 3, 3, 3, 3, 3, 3, 3, 3, # e0 - e7
+ 3, 3, 3, 3, 3, 3, 3, 3, # e8 - ef
+ 3, 3, 3, 3, 3, 3, 3, 3, # f0 - f7
+ 3, 3, 3, 3, 3, 3, 3, 0 # f8 - ff
)
EUCTW_ST = (
@@ -266,50 +336,53 @@ EUCTW_ST = (
5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.ERROR,MachineState.START,MachineState.START,#20-27
MachineState.START,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START #28-2f
)
+# fmt: on
EUCTW_CHAR_LEN_TABLE = (0, 0, 1, 2, 2, 2, 3)
-EUCTW_SM_MODEL = {'class_table': EUCTW_CLS,
- 'class_factor': 7,
- 'state_table': EUCTW_ST,
- 'char_len_table': EUCTW_CHAR_LEN_TABLE,
- 'name': 'x-euc-tw'}
+EUCTW_SM_MODEL = {
+ "class_table": EUCTW_CLS,
+ "class_factor": 7,
+ "state_table": EUCTW_ST,
+ "char_len_table": EUCTW_CHAR_LEN_TABLE,
+ "name": "x-euc-tw",
+}
# GB2312
-
+# fmt: off
GB2312_CLS = (
- 1,1,1,1,1,1,1,1, # 00 - 07
- 1,1,1,1,1,1,0,0, # 08 - 0f
- 1,1,1,1,1,1,1,1, # 10 - 17
- 1,1,1,0,1,1,1,1, # 18 - 1f
- 1,1,1,1,1,1,1,1, # 20 - 27
- 1,1,1,1,1,1,1,1, # 28 - 2f
- 3,3,3,3,3,3,3,3, # 30 - 37
- 3,3,1,1,1,1,1,1, # 38 - 3f
- 2,2,2,2,2,2,2,2, # 40 - 47
- 2,2,2,2,2,2,2,2, # 48 - 4f
- 2,2,2,2,2,2,2,2, # 50 - 57
- 2,2,2,2,2,2,2,2, # 58 - 5f
- 2,2,2,2,2,2,2,2, # 60 - 67
- 2,2,2,2,2,2,2,2, # 68 - 6f
- 2,2,2,2,2,2,2,2, # 70 - 77
- 2,2,2,2,2,2,2,4, # 78 - 7f
- 5,6,6,6,6,6,6,6, # 80 - 87
- 6,6,6,6,6,6,6,6, # 88 - 8f
- 6,6,6,6,6,6,6,6, # 90 - 97
- 6,6,6,6,6,6,6,6, # 98 - 9f
- 6,6,6,6,6,6,6,6, # a0 - a7
- 6,6,6,6,6,6,6,6, # a8 - af
- 6,6,6,6,6,6,6,6, # b0 - b7
- 6,6,6,6,6,6,6,6, # b8 - bf
- 6,6,6,6,6,6,6,6, # c0 - c7
- 6,6,6,6,6,6,6,6, # c8 - cf
- 6,6,6,6,6,6,6,6, # d0 - d7
- 6,6,6,6,6,6,6,6, # d8 - df
- 6,6,6,6,6,6,6,6, # e0 - e7
- 6,6,6,6,6,6,6,6, # e8 - ef
- 6,6,6,6,6,6,6,6, # f0 - f7
- 6,6,6,6,6,6,6,0 # f8 - ff
+ 1, 1, 1, 1, 1, 1, 1, 1, # 00 - 07
+ 1, 1, 1, 1, 1, 1, 0, 0, # 08 - 0f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 10 - 17
+ 1, 1, 1, 0, 1, 1, 1, 1, # 18 - 1f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 20 - 27
+ 1, 1, 1, 1, 1, 1, 1, 1, # 28 - 2f
+ 3, 3, 3, 3, 3, 3, 3, 3, # 30 - 37
+ 3, 3, 1, 1, 1, 1, 1, 1, # 38 - 3f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 40 - 47
+ 2, 2, 2, 2, 2, 2, 2, 2, # 48 - 4f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 50 - 57
+ 2, 2, 2, 2, 2, 2, 2, 2, # 58 - 5f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 60 - 67
+ 2, 2, 2, 2, 2, 2, 2, 2, # 68 - 6f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 70 - 77
+ 2, 2, 2, 2, 2, 2, 2, 4, # 78 - 7f
+ 5, 6, 6, 6, 6, 6, 6, 6, # 80 - 87
+ 6, 6, 6, 6, 6, 6, 6, 6, # 88 - 8f
+ 6, 6, 6, 6, 6, 6, 6, 6, # 90 - 97
+ 6, 6, 6, 6, 6, 6, 6, 6, # 98 - 9f
+ 6, 6, 6, 6, 6, 6, 6, 6, # a0 - a7
+ 6, 6, 6, 6, 6, 6, 6, 6, # a8 - af
+ 6, 6, 6, 6, 6, 6, 6, 6, # b0 - b7
+ 6, 6, 6, 6, 6, 6, 6, 6, # b8 - bf
+ 6, 6, 6, 6, 6, 6, 6, 6, # c0 - c7
+ 6, 6, 6, 6, 6, 6, 6, 6, # c8 - cf
+ 6, 6, 6, 6, 6, 6, 6, 6, # d0 - d7
+ 6, 6, 6, 6, 6, 6, 6, 6, # d8 - df
+ 6, 6, 6, 6, 6, 6, 6, 6, # e0 - e7
+ 6, 6, 6, 6, 6, 6, 6, 6, # e8 - ef
+ 6, 6, 6, 6, 6, 6, 6, 6, # f0 - f7
+ 6, 6, 6, 6, 6, 6, 6, 0 # f8 - ff
)
GB2312_ST = (
@@ -320,6 +393,7 @@ GB2312_ST = (
MachineState.ERROR,MachineState.ERROR, 5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ERROR,#20-27
MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.START #28-2f
)
+# fmt: on
# To be accurate, the length of class 6 can be either 2 or 4.
# But it is not necessary to discriminate between the two since
@@ -328,100 +402,105 @@ GB2312_ST = (
# 2 here.
GB2312_CHAR_LEN_TABLE = (0, 1, 1, 1, 1, 1, 2)
-GB2312_SM_MODEL = {'class_table': GB2312_CLS,
- 'class_factor': 7,
- 'state_table': GB2312_ST,
- 'char_len_table': GB2312_CHAR_LEN_TABLE,
- 'name': 'GB2312'}
+GB2312_SM_MODEL = {
+ "class_table": GB2312_CLS,
+ "class_factor": 7,
+ "state_table": GB2312_ST,
+ "char_len_table": GB2312_CHAR_LEN_TABLE,
+ "name": "GB2312",
+}
# Shift_JIS
-
+# fmt: off
SJIS_CLS = (
- 1,1,1,1,1,1,1,1, # 00 - 07
- 1,1,1,1,1,1,0,0, # 08 - 0f
- 1,1,1,1,1,1,1,1, # 10 - 17
- 1,1,1,0,1,1,1,1, # 18 - 1f
- 1,1,1,1,1,1,1,1, # 20 - 27
- 1,1,1,1,1,1,1,1, # 28 - 2f
- 1,1,1,1,1,1,1,1, # 30 - 37
- 1,1,1,1,1,1,1,1, # 38 - 3f
- 2,2,2,2,2,2,2,2, # 40 - 47
- 2,2,2,2,2,2,2,2, # 48 - 4f
- 2,2,2,2,2,2,2,2, # 50 - 57
- 2,2,2,2,2,2,2,2, # 58 - 5f
- 2,2,2,2,2,2,2,2, # 60 - 67
- 2,2,2,2,2,2,2,2, # 68 - 6f
- 2,2,2,2,2,2,2,2, # 70 - 77
- 2,2,2,2,2,2,2,1, # 78 - 7f
- 3,3,3,3,3,2,2,3, # 80 - 87
- 3,3,3,3,3,3,3,3, # 88 - 8f
- 3,3,3,3,3,3,3,3, # 90 - 97
- 3,3,3,3,3,3,3,3, # 98 - 9f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 00 - 07
+ 1, 1, 1, 1, 1, 1, 0, 0, # 08 - 0f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 10 - 17
+ 1, 1, 1, 0, 1, 1, 1, 1, # 18 - 1f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 20 - 27
+ 1, 1, 1, 1, 1, 1, 1, 1, # 28 - 2f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 30 - 37
+ 1, 1, 1, 1, 1, 1, 1, 1, # 38 - 3f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 40 - 47
+ 2, 2, 2, 2, 2, 2, 2, 2, # 48 - 4f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 50 - 57
+ 2, 2, 2, 2, 2, 2, 2, 2, # 58 - 5f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 60 - 67
+ 2, 2, 2, 2, 2, 2, 2, 2, # 68 - 6f
+ 2, 2, 2, 2, 2, 2, 2, 2, # 70 - 77
+ 2, 2, 2, 2, 2, 2, 2, 1, # 78 - 7f
+ 3, 3, 3, 3, 3, 2, 2, 3, # 80 - 87
+ 3, 3, 3, 3, 3, 3, 3, 3, # 88 - 8f
+ 3, 3, 3, 3, 3, 3, 3, 3, # 90 - 97
+ 3, 3, 3, 3, 3, 3, 3, 3, # 98 - 9f
#0xa0 is illegal in sjis encoding, but some pages does
#contain such byte. We need to be more error forgiven.
- 2,2,2,2,2,2,2,2, # a0 - a7
- 2,2,2,2,2,2,2,2, # a8 - af
- 2,2,2,2,2,2,2,2, # b0 - b7
- 2,2,2,2,2,2,2,2, # b8 - bf
- 2,2,2,2,2,2,2,2, # c0 - c7
- 2,2,2,2,2,2,2,2, # c8 - cf
- 2,2,2,2,2,2,2,2, # d0 - d7
- 2,2,2,2,2,2,2,2, # d8 - df
- 3,3,3,3,3,3,3,3, # e0 - e7
- 3,3,3,3,3,4,4,4, # e8 - ef
- 3,3,3,3,3,3,3,3, # f0 - f7
- 3,3,3,3,3,0,0,0) # f8 - ff
-
+ 2, 2, 2, 2, 2, 2, 2, 2, # a0 - a7
+ 2, 2, 2, 2, 2, 2, 2, 2, # a8 - af
+ 2, 2, 2, 2, 2, 2, 2, 2, # b0 - b7
+ 2, 2, 2, 2, 2, 2, 2, 2, # b8 - bf
+ 2, 2, 2, 2, 2, 2, 2, 2, # c0 - c7
+ 2, 2, 2, 2, 2, 2, 2, 2, # c8 - cf
+ 2, 2, 2, 2, 2, 2, 2, 2, # d0 - d7
+ 2, 2, 2, 2, 2, 2, 2, 2, # d8 - df
+ 3, 3, 3, 3, 3, 3, 3, 3, # e0 - e7
+ 3, 3, 3, 3, 3, 4, 4, 4, # e8 - ef
+ 3, 3, 3, 3, 3, 3, 3, 3, # f0 - f7
+ 3, 3, 3, 3, 3, 0, 0, 0, # f8 - ff
+)
SJIS_ST = (
MachineState.ERROR,MachineState.START,MachineState.START, 3,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,#00-07
MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ITS_ME,#08-0f
MachineState.ITS_ME,MachineState.ITS_ME,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START #10-17
)
+# fmt: on
SJIS_CHAR_LEN_TABLE = (0, 1, 1, 2, 0, 0)
-SJIS_SM_MODEL = {'class_table': SJIS_CLS,
- 'class_factor': 6,
- 'state_table': SJIS_ST,
- 'char_len_table': SJIS_CHAR_LEN_TABLE,
- 'name': 'Shift_JIS'}
+SJIS_SM_MODEL = {
+ "class_table": SJIS_CLS,
+ "class_factor": 6,
+ "state_table": SJIS_ST,
+ "char_len_table": SJIS_CHAR_LEN_TABLE,
+ "name": "Shift_JIS",
+}
# UCS2-BE
-
+# fmt: off
UCS2BE_CLS = (
- 0,0,0,0,0,0,0,0, # 00 - 07
- 0,0,1,0,0,2,0,0, # 08 - 0f
- 0,0,0,0,0,0,0,0, # 10 - 17
- 0,0,0,3,0,0,0,0, # 18 - 1f
- 0,0,0,0,0,0,0,0, # 20 - 27
- 0,3,3,3,3,3,0,0, # 28 - 2f
- 0,0,0,0,0,0,0,0, # 30 - 37
- 0,0,0,0,0,0,0,0, # 38 - 3f
- 0,0,0,0,0,0,0,0, # 40 - 47
- 0,0,0,0,0,0,0,0, # 48 - 4f
- 0,0,0,0,0,0,0,0, # 50 - 57
- 0,0,0,0,0,0,0,0, # 58 - 5f
- 0,0,0,0,0,0,0,0, # 60 - 67
- 0,0,0,0,0,0,0,0, # 68 - 6f
- 0,0,0,0,0,0,0,0, # 70 - 77
- 0,0,0,0,0,0,0,0, # 78 - 7f
- 0,0,0,0,0,0,0,0, # 80 - 87
- 0,0,0,0,0,0,0,0, # 88 - 8f
- 0,0,0,0,0,0,0,0, # 90 - 97
- 0,0,0,0,0,0,0,0, # 98 - 9f
- 0,0,0,0,0,0,0,0, # a0 - a7
- 0,0,0,0,0,0,0,0, # a8 - af
- 0,0,0,0,0,0,0,0, # b0 - b7
- 0,0,0,0,0,0,0,0, # b8 - bf
- 0,0,0,0,0,0,0,0, # c0 - c7
- 0,0,0,0,0,0,0,0, # c8 - cf
- 0,0,0,0,0,0,0,0, # d0 - d7
- 0,0,0,0,0,0,0,0, # d8 - df
- 0,0,0,0,0,0,0,0, # e0 - e7
- 0,0,0,0,0,0,0,0, # e8 - ef
- 0,0,0,0,0,0,0,0, # f0 - f7
- 0,0,0,0,0,0,4,5 # f8 - ff
+ 0, 0, 0, 0, 0, 0, 0, 0, # 00 - 07
+ 0, 0, 1, 0, 0, 2, 0, 0, # 08 - 0f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 10 - 17
+ 0, 0, 0, 3, 0, 0, 0, 0, # 18 - 1f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 20 - 27
+ 0, 3, 3, 3, 3, 3, 0, 0, # 28 - 2f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 30 - 37
+ 0, 0, 0, 0, 0, 0, 0, 0, # 38 - 3f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 40 - 47
+ 0, 0, 0, 0, 0, 0, 0, 0, # 48 - 4f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 50 - 57
+ 0, 0, 0, 0, 0, 0, 0, 0, # 58 - 5f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 60 - 67
+ 0, 0, 0, 0, 0, 0, 0, 0, # 68 - 6f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 70 - 77
+ 0, 0, 0, 0, 0, 0, 0, 0, # 78 - 7f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 80 - 87
+ 0, 0, 0, 0, 0, 0, 0, 0, # 88 - 8f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 90 - 97
+ 0, 0, 0, 0, 0, 0, 0, 0, # 98 - 9f
+ 0, 0, 0, 0, 0, 0, 0, 0, # a0 - a7
+ 0, 0, 0, 0, 0, 0, 0, 0, # a8 - af
+ 0, 0, 0, 0, 0, 0, 0, 0, # b0 - b7
+ 0, 0, 0, 0, 0, 0, 0, 0, # b8 - bf
+ 0, 0, 0, 0, 0, 0, 0, 0, # c0 - c7
+ 0, 0, 0, 0, 0, 0, 0, 0, # c8 - cf
+ 0, 0, 0, 0, 0, 0, 0, 0, # d0 - d7
+ 0, 0, 0, 0, 0, 0, 0, 0, # d8 - df
+ 0, 0, 0, 0, 0, 0, 0, 0, # e0 - e7
+ 0, 0, 0, 0, 0, 0, 0, 0, # e8 - ef
+ 0, 0, 0, 0, 0, 0, 0, 0, # f0 - f7
+ 0, 0, 0, 0, 0, 0, 4, 5 # f8 - ff
)
UCS2BE_ST = (
@@ -433,50 +512,53 @@ UCS2BE_ST = (
5, 8, 6, 6,MachineState.ERROR, 6, 6, 6,#28-2f
6, 6, 6, 6,MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START #30-37
)
+# fmt: on
UCS2BE_CHAR_LEN_TABLE = (2, 2, 2, 0, 2, 2)
-UCS2BE_SM_MODEL = {'class_table': UCS2BE_CLS,
- 'class_factor': 6,
- 'state_table': UCS2BE_ST,
- 'char_len_table': UCS2BE_CHAR_LEN_TABLE,
- 'name': 'UTF-16BE'}
+UCS2BE_SM_MODEL = {
+ "class_table": UCS2BE_CLS,
+ "class_factor": 6,
+ "state_table": UCS2BE_ST,
+ "char_len_table": UCS2BE_CHAR_LEN_TABLE,
+ "name": "UTF-16BE",
+}
# UCS2-LE
-
+# fmt: off
UCS2LE_CLS = (
- 0,0,0,0,0,0,0,0, # 00 - 07
- 0,0,1,0,0,2,0,0, # 08 - 0f
- 0,0,0,0,0,0,0,0, # 10 - 17
- 0,0,0,3,0,0,0,0, # 18 - 1f
- 0,0,0,0,0,0,0,0, # 20 - 27
- 0,3,3,3,3,3,0,0, # 28 - 2f
- 0,0,0,0,0,0,0,0, # 30 - 37
- 0,0,0,0,0,0,0,0, # 38 - 3f
- 0,0,0,0,0,0,0,0, # 40 - 47
- 0,0,0,0,0,0,0,0, # 48 - 4f
- 0,0,0,0,0,0,0,0, # 50 - 57
- 0,0,0,0,0,0,0,0, # 58 - 5f
- 0,0,0,0,0,0,0,0, # 60 - 67
- 0,0,0,0,0,0,0,0, # 68 - 6f
- 0,0,0,0,0,0,0,0, # 70 - 77
- 0,0,0,0,0,0,0,0, # 78 - 7f
- 0,0,0,0,0,0,0,0, # 80 - 87
- 0,0,0,0,0,0,0,0, # 88 - 8f
- 0,0,0,0,0,0,0,0, # 90 - 97
- 0,0,0,0,0,0,0,0, # 98 - 9f
- 0,0,0,0,0,0,0,0, # a0 - a7
- 0,0,0,0,0,0,0,0, # a8 - af
- 0,0,0,0,0,0,0,0, # b0 - b7
- 0,0,0,0,0,0,0,0, # b8 - bf
- 0,0,0,0,0,0,0,0, # c0 - c7
- 0,0,0,0,0,0,0,0, # c8 - cf
- 0,0,0,0,0,0,0,0, # d0 - d7
- 0,0,0,0,0,0,0,0, # d8 - df
- 0,0,0,0,0,0,0,0, # e0 - e7
- 0,0,0,0,0,0,0,0, # e8 - ef
- 0,0,0,0,0,0,0,0, # f0 - f7
- 0,0,0,0,0,0,4,5 # f8 - ff
+ 0, 0, 0, 0, 0, 0, 0, 0, # 00 - 07
+ 0, 0, 1, 0, 0, 2, 0, 0, # 08 - 0f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 10 - 17
+ 0, 0, 0, 3, 0, 0, 0, 0, # 18 - 1f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 20 - 27
+ 0, 3, 3, 3, 3, 3, 0, 0, # 28 - 2f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 30 - 37
+ 0, 0, 0, 0, 0, 0, 0, 0, # 38 - 3f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 40 - 47
+ 0, 0, 0, 0, 0, 0, 0, 0, # 48 - 4f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 50 - 57
+ 0, 0, 0, 0, 0, 0, 0, 0, # 58 - 5f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 60 - 67
+ 0, 0, 0, 0, 0, 0, 0, 0, # 68 - 6f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 70 - 77
+ 0, 0, 0, 0, 0, 0, 0, 0, # 78 - 7f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 80 - 87
+ 0, 0, 0, 0, 0, 0, 0, 0, # 88 - 8f
+ 0, 0, 0, 0, 0, 0, 0, 0, # 90 - 97
+ 0, 0, 0, 0, 0, 0, 0, 0, # 98 - 9f
+ 0, 0, 0, 0, 0, 0, 0, 0, # a0 - a7
+ 0, 0, 0, 0, 0, 0, 0, 0, # a8 - af
+ 0, 0, 0, 0, 0, 0, 0, 0, # b0 - b7
+ 0, 0, 0, 0, 0, 0, 0, 0, # b8 - bf
+ 0, 0, 0, 0, 0, 0, 0, 0, # c0 - c7
+ 0, 0, 0, 0, 0, 0, 0, 0, # c8 - cf
+ 0, 0, 0, 0, 0, 0, 0, 0, # d0 - d7
+ 0, 0, 0, 0, 0, 0, 0, 0, # d8 - df
+ 0, 0, 0, 0, 0, 0, 0, 0, # e0 - e7
+ 0, 0, 0, 0, 0, 0, 0, 0, # e8 - ef
+ 0, 0, 0, 0, 0, 0, 0, 0, # f0 - f7
+ 0, 0, 0, 0, 0, 0, 4, 5 # f8 - ff
)
UCS2LE_ST = (
@@ -488,50 +570,53 @@ UCS2LE_ST = (
5, 5, 5,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR, 5, 5,#28-2f
5, 5, 5,MachineState.ERROR, 5,MachineState.ERROR,MachineState.START,MachineState.START #30-37
)
+# fmt: on
UCS2LE_CHAR_LEN_TABLE = (2, 2, 2, 2, 2, 2)
-UCS2LE_SM_MODEL = {'class_table': UCS2LE_CLS,
- 'class_factor': 6,
- 'state_table': UCS2LE_ST,
- 'char_len_table': UCS2LE_CHAR_LEN_TABLE,
- 'name': 'UTF-16LE'}
+UCS2LE_SM_MODEL = {
+ "class_table": UCS2LE_CLS,
+ "class_factor": 6,
+ "state_table": UCS2LE_ST,
+ "char_len_table": UCS2LE_CHAR_LEN_TABLE,
+ "name": "UTF-16LE",
+}
# UTF-8
-
+# fmt: off
UTF8_CLS = (
- 1,1,1,1,1,1,1,1, # 00 - 07 #allow 0x00 as a legal value
- 1,1,1,1,1,1,0,0, # 08 - 0f
- 1,1,1,1,1,1,1,1, # 10 - 17
- 1,1,1,0,1,1,1,1, # 18 - 1f
- 1,1,1,1,1,1,1,1, # 20 - 27
- 1,1,1,1,1,1,1,1, # 28 - 2f
- 1,1,1,1,1,1,1,1, # 30 - 37
- 1,1,1,1,1,1,1,1, # 38 - 3f
- 1,1,1,1,1,1,1,1, # 40 - 47
- 1,1,1,1,1,1,1,1, # 48 - 4f
- 1,1,1,1,1,1,1,1, # 50 - 57
- 1,1,1,1,1,1,1,1, # 58 - 5f
- 1,1,1,1,1,1,1,1, # 60 - 67
- 1,1,1,1,1,1,1,1, # 68 - 6f
- 1,1,1,1,1,1,1,1, # 70 - 77
- 1,1,1,1,1,1,1,1, # 78 - 7f
- 2,2,2,2,3,3,3,3, # 80 - 87
- 4,4,4,4,4,4,4,4, # 88 - 8f
- 4,4,4,4,4,4,4,4, # 90 - 97
- 4,4,4,4,4,4,4,4, # 98 - 9f
- 5,5,5,5,5,5,5,5, # a0 - a7
- 5,5,5,5,5,5,5,5, # a8 - af
- 5,5,5,5,5,5,5,5, # b0 - b7
- 5,5,5,5,5,5,5,5, # b8 - bf
- 0,0,6,6,6,6,6,6, # c0 - c7
- 6,6,6,6,6,6,6,6, # c8 - cf
- 6,6,6,6,6,6,6,6, # d0 - d7
- 6,6,6,6,6,6,6,6, # d8 - df
- 7,8,8,8,8,8,8,8, # e0 - e7
- 8,8,8,8,8,9,8,8, # e8 - ef
- 10,11,11,11,11,11,11,11, # f0 - f7
- 12,13,13,13,14,15,0,0 # f8 - ff
+ 1, 1, 1, 1, 1, 1, 1, 1, # 00 - 07 #allow 0x00 as a legal value
+ 1, 1, 1, 1, 1, 1, 0, 0, # 08 - 0f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 10 - 17
+ 1, 1, 1, 0, 1, 1, 1, 1, # 18 - 1f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 20 - 27
+ 1, 1, 1, 1, 1, 1, 1, 1, # 28 - 2f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 30 - 37
+ 1, 1, 1, 1, 1, 1, 1, 1, # 38 - 3f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 40 - 47
+ 1, 1, 1, 1, 1, 1, 1, 1, # 48 - 4f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 50 - 57
+ 1, 1, 1, 1, 1, 1, 1, 1, # 58 - 5f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 60 - 67
+ 1, 1, 1, 1, 1, 1, 1, 1, # 68 - 6f
+ 1, 1, 1, 1, 1, 1, 1, 1, # 70 - 77
+ 1, 1, 1, 1, 1, 1, 1, 1, # 78 - 7f
+ 2, 2, 2, 2, 3, 3, 3, 3, # 80 - 87
+ 4, 4, 4, 4, 4, 4, 4, 4, # 88 - 8f
+ 4, 4, 4, 4, 4, 4, 4, 4, # 90 - 97
+ 4, 4, 4, 4, 4, 4, 4, 4, # 98 - 9f
+ 5, 5, 5, 5, 5, 5, 5, 5, # a0 - a7
+ 5, 5, 5, 5, 5, 5, 5, 5, # a8 - af
+ 5, 5, 5, 5, 5, 5, 5, 5, # b0 - b7
+ 5, 5, 5, 5, 5, 5, 5, 5, # b8 - bf
+ 0, 0, 6, 6, 6, 6, 6, 6, # c0 - c7
+ 6, 6, 6, 6, 6, 6, 6, 6, # c8 - cf
+ 6, 6, 6, 6, 6, 6, 6, 6, # d0 - d7
+ 6, 6, 6, 6, 6, 6, 6, 6, # d8 - df
+ 7, 8, 8, 8, 8, 8, 8, 8, # e0 - e7
+ 8, 8, 8, 8, 8, 9, 8, 8, # e8 - ef
+ 10, 11, 11, 11, 11, 11, 11, 11, # f0 - f7
+ 12, 13, 13, 13, 14, 15, 0, 0 # f8 - ff
)
UTF8_ST = (
@@ -562,11 +647,14 @@ UTF8_ST = (
MachineState.ERROR,MachineState.ERROR,MachineState.START,MachineState.START,MachineState.START,MachineState.START,MachineState.ERROR,MachineState.ERROR,#c0-c7
MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR,MachineState.ERROR #c8-cf
)
+# fmt: on
UTF8_CHAR_LEN_TABLE = (0, 1, 0, 0, 0, 0, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6)
-UTF8_SM_MODEL = {'class_table': UTF8_CLS,
- 'class_factor': 16,
- 'state_table': UTF8_ST,
- 'char_len_table': UTF8_CHAR_LEN_TABLE,
- 'name': 'UTF-8'}
+UTF8_SM_MODEL = {
+ "class_table": UTF8_CLS,
+ "class_factor": 16,
+ "state_table": UTF8_ST,
+ "char_len_table": UTF8_CHAR_LEN_TABLE,
+ "name": "UTF-8",
+}
diff --git a/src/pip/_vendor/chardet/metadata/languages.py b/src/pip/_vendor/chardet/metadata/languages.py
index 3237d5abf..1d37884c3 100644
--- a/src/pip/_vendor/chardet/metadata/languages.py
+++ b/src/pip/_vendor/chardet/metadata/languages.py
@@ -1,19 +1,16 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
"""
Metadata about languages used by our model training code for our
SingleByteCharSetProbers. Could be used for other things in the future.
This code is based on the language metadata from the uchardet project.
"""
-from __future__ import absolute_import, print_function
from string import ascii_letters
+# TODO: Add Ukrainian (KOI8-U)
-# TODO: Add Ukranian (KOI8-U)
-class Language(object):
+class Language:
"""Metadata about a language useful for training models
:ivar name: The human name for the language, in English.
@@ -33,9 +30,17 @@ class Language(object):
Wikipedia for training data.
:type wiki_start_pages: list of str
"""
- def __init__(self, name=None, iso_code=None, use_ascii=True, charsets=None,
- alphabet=None, wiki_start_pages=None):
- super(Language, self).__init__()
+
+ def __init__(
+ self,
+ name=None,
+ iso_code=None,
+ use_ascii=True,
+ charsets=None,
+ alphabet=None,
+ wiki_start_pages=None,
+ ):
+ super().__init__()
self.name = name
self.iso_code = iso_code
self.use_ascii = use_ascii
@@ -46,265 +51,301 @@ class Language(object):
else:
alphabet = ascii_letters
elif not alphabet:
- raise ValueError('Must supply alphabet if use_ascii is False')
- self.alphabet = ''.join(sorted(set(alphabet))) if alphabet else None
+ raise ValueError("Must supply alphabet if use_ascii is False")
+ self.alphabet = "".join(sorted(set(alphabet))) if alphabet else None
self.wiki_start_pages = wiki_start_pages
def __repr__(self):
- return '{}({})'.format(self.__class__.__name__,
- ', '.join('{}={!r}'.format(k, v)
- for k, v in self.__dict__.items()
- if not k.startswith('_')))
+ param_str = ", ".join(
+ f"{k}={v!r}" for k, v in self.__dict__.items() if not k.startswith("_")
+ )
+ return f"{self.__class__.__name__}({param_str})"
-LANGUAGES = {'Arabic': Language(name='Arabic',
- iso_code='ar',
- use_ascii=False,
- # We only support encodings that use isolated
- # forms, because the current recommendation is
- # that the rendering system handles presentation
- # forms. This means we purposefully skip IBM864.
- charsets=['ISO-8859-6', 'WINDOWS-1256',
- 'CP720', 'CP864'],
- alphabet=u'ءآأؤإئابةتثجحخدذرزسشصضطظعغػؼؽؾؿـÙقكلمنهوىيًٌÙÙŽÙÙÙ‘',
- wiki_start_pages=[u'الصÙحة_الرئيسية']),
- 'Belarusian': Language(name='Belarusian',
- iso_code='be',
- use_ascii=False,
- charsets=['ISO-8859-5', 'WINDOWS-1251',
- 'IBM866', 'MacCyrillic'],
- alphabet=(u'ÐБВГДЕÐЖЗІЙКЛМÐОПРСТУЎФХЦЧШЫЬЭЮЯ'
- u'абвгдеёжзійклмнопрÑтуўфхцчшыьÑÑŽÑʼ'),
- wiki_start_pages=[u'ГалоўнаÑ_Ñтаронка']),
- 'Bulgarian': Language(name='Bulgarian',
- iso_code='bg',
- use_ascii=False,
- charsets=['ISO-8859-5', 'WINDOWS-1251',
- 'IBM855'],
- alphabet=(u'ÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЬЮЯ'
- u'абвгдежзийклмнопрÑтуфхцчшщъьюÑ'),
- wiki_start_pages=[u'Ðачална_Ñтраница']),
- 'Czech': Language(name='Czech',
- iso_code='cz',
- use_ascii=True,
- charsets=['ISO-8859-2', 'WINDOWS-1250'],
- alphabet=u'áÄÄéěíňóřšťúůýžÃČĎÉĚÃŇÓŘŠŤÚŮÃŽ',
- wiki_start_pages=[u'Hlavní_strana']),
- 'Danish': Language(name='Danish',
- iso_code='da',
- use_ascii=True,
- charsets=['ISO-8859-1', 'ISO-8859-15',
- 'WINDOWS-1252'],
- alphabet=u'æøåÆØÅ',
- wiki_start_pages=[u'Forside']),
- 'German': Language(name='German',
- iso_code='de',
- use_ascii=True,
- charsets=['ISO-8859-1', 'WINDOWS-1252'],
- alphabet=u'äöüßÄÖÜ',
- wiki_start_pages=[u'Wikipedia:Hauptseite']),
- 'Greek': Language(name='Greek',
- iso_code='el',
- use_ascii=False,
- charsets=['ISO-8859-7', 'WINDOWS-1253'],
- alphabet=(u'αβγδεζηθικλμνξοπÏσςτυφχψωάέήίόÏÏŽ'
- u'ΑΒΓΔΕΖΗΘΙΚΛΜÎΞΟΠΡΣΣΤΥΦΧΨΩΆΈΉΊΌΎÎ'),
- wiki_start_pages=[u'ΠÏλη:ΚÏÏια']),
- 'English': Language(name='English',
- iso_code='en',
- use_ascii=True,
- charsets=['ISO-8859-1', 'WINDOWS-1252'],
- wiki_start_pages=[u'Main_Page']),
- 'Esperanto': Language(name='Esperanto',
- iso_code='eo',
- # Q, W, X, and Y not used at all
- use_ascii=False,
- charsets=['ISO-8859-3'],
- alphabet=(u'abcĉdefgÄhÄ¥ijĵklmnoprsÅtuÅ­vz'
- u'ABCĈDEFGĜHĤIJĴKLMNOPRSŜTUŬVZ'),
- wiki_start_pages=[u'Vikipedio:ĈefpaÄo']),
- 'Spanish': Language(name='Spanish',
- iso_code='es',
- use_ascii=True,
- charsets=['ISO-8859-1', 'ISO-8859-15',
- 'WINDOWS-1252'],
- alphabet=u'ñáéíóúüÑÃÉÃÓÚÜ',
- wiki_start_pages=[u'Wikipedia:Portada']),
- 'Estonian': Language(name='Estonian',
- iso_code='et',
- use_ascii=False,
- charsets=['ISO-8859-4', 'ISO-8859-13',
- 'WINDOWS-1257'],
- # C, F, Š, Q, W, X, Y, Z, Ž are only for
- # loanwords
- alphabet=(u'ABDEGHIJKLMNOPRSTUVÕÄÖÜ'
- u'abdeghijklmnoprstuvõäöü'),
- wiki_start_pages=[u'Esileht']),
- 'Finnish': Language(name='Finnish',
- iso_code='fi',
- use_ascii=True,
- charsets=['ISO-8859-1', 'ISO-8859-15',
- 'WINDOWS-1252'],
- alphabet=u'ÅÄÖŠŽåäöšž',
- wiki_start_pages=[u'Wikipedia:Etusivu']),
- 'French': Language(name='French',
- iso_code='fr',
- use_ascii=True,
- charsets=['ISO-8859-1', 'ISO-8859-15',
- 'WINDOWS-1252'],
- alphabet=u'œàâçèéîïùûêŒÀÂÇÈÉÎÃÙÛÊ',
- wiki_start_pages=[u'Wikipédia:Accueil_principal',
- u'BÅ“uf (animal)']),
- 'Hebrew': Language(name='Hebrew',
- iso_code='he',
- use_ascii=False,
- charsets=['ISO-8859-8', 'WINDOWS-1255'],
- alphabet=u'×בגדהוזחטיךכל×מןנסעףפץצקרשתװױײ',
- wiki_start_pages=[u'עמוד_ר×שי']),
- 'Croatian': Language(name='Croatian',
- iso_code='hr',
- # Q, W, X, Y are only used for foreign words.
- use_ascii=False,
- charsets=['ISO-8859-2', 'WINDOWS-1250'],
- alphabet=(u'abcÄćdÄ‘efghijklmnoprsÅ¡tuvzž'
- u'ABCČĆDÄEFGHIJKLMNOPRSÅ TUVZŽ'),
- wiki_start_pages=[u'Glavna_stranica']),
- 'Hungarian': Language(name='Hungarian',
- iso_code='hu',
- # Q, W, X, Y are only used for foreign words.
- use_ascii=False,
- charsets=['ISO-8859-2', 'WINDOWS-1250'],
- alphabet=(u'abcdefghijklmnoprstuvzáéíóöőúüű'
- u'ABCDEFGHIJKLMNOPRSTUVZÃÉÃÓÖÅÚÜŰ'),
- wiki_start_pages=[u'Kezdőlap']),
- 'Italian': Language(name='Italian',
- iso_code='it',
- use_ascii=True,
- charsets=['ISO-8859-1', 'ISO-8859-15',
- 'WINDOWS-1252'],
- alphabet=u'ÀÈÉÌÒÓÙàèéìòóù',
- wiki_start_pages=[u'Pagina_principale']),
- 'Lithuanian': Language(name='Lithuanian',
- iso_code='lt',
- use_ascii=False,
- charsets=['ISO-8859-13', 'WINDOWS-1257',
- 'ISO-8859-4'],
- # Q, W, and X not used at all
- alphabet=(u'AĄBCČDEĘĖFGHIĮYJKLMNOPRSŠTUŲŪVZŽ'
- u'aÄ…bcÄdeęėfghiįyjklmnoprsÅ¡tuųūvzž'),
- wiki_start_pages=[u'Pagrindinis_puslapis']),
- 'Latvian': Language(name='Latvian',
- iso_code='lv',
- use_ascii=False,
- charsets=['ISO-8859-13', 'WINDOWS-1257',
- 'ISO-8859-4'],
- # Q, W, X, Y are only for loanwords
- alphabet=(u'AĀBCČDEĒFGĢHIĪJKĶLĻMNŅOPRSŠTUŪVZŽ'
- u'aÄbcÄdeÄ“fgÄ£hiÄ«jkÄ·lļmnņoprsÅ¡tuÅ«vzž'),
- wiki_start_pages=[u'SÄkumlapa']),
- 'Macedonian': Language(name='Macedonian',
- iso_code='mk',
- use_ascii=False,
- charsets=['ISO-8859-5', 'WINDOWS-1251',
- 'MacCyrillic', 'IBM855'],
- alphabet=(u'ÐБВГДЃЕЖЗЅИЈКЛЉМÐЊОПРСТЌУФХЦЧÐШ'
- u'абвгдѓежзѕијклљмнњопрÑтќуфхцчџш'),
- wiki_start_pages=[u'Главна_Ñтраница']),
- 'Dutch': Language(name='Dutch',
- iso_code='nl',
- use_ascii=True,
- charsets=['ISO-8859-1', 'WINDOWS-1252'],
- wiki_start_pages=[u'Hoofdpagina']),
- 'Polish': Language(name='Polish',
- iso_code='pl',
- # Q and X are only used for foreign words.
- use_ascii=False,
- charsets=['ISO-8859-2', 'WINDOWS-1250'],
- alphabet=(u'AÄ„BCĆDEĘFGHIJKLÅMNŃOÓPRSÅšTUWYZŹŻ'
- u'aąbcćdeęfghijklłmnńoóprsśtuwyzźż'),
- wiki_start_pages=[u'Wikipedia:Strona_główna']),
- 'Portuguese': Language(name='Portuguese',
- iso_code='pt',
- use_ascii=True,
- charsets=['ISO-8859-1', 'ISO-8859-15',
- 'WINDOWS-1252'],
- alphabet=u'ÃÂÃÀÇÉÊÃÓÔÕÚáâãàçéêíóôõú',
- wiki_start_pages=[u'Wikipédia:Página_principal']),
- 'Romanian': Language(name='Romanian',
- iso_code='ro',
- use_ascii=True,
- charsets=['ISO-8859-2', 'WINDOWS-1250'],
- alphabet=u'ăâîșțĂÂÎȘȚ',
- wiki_start_pages=[u'Pagina_principală']),
- 'Russian': Language(name='Russian',
- iso_code='ru',
- use_ascii=False,
- charsets=['ISO-8859-5', 'WINDOWS-1251',
- 'KOI8-R', 'MacCyrillic', 'IBM866',
- 'IBM855'],
- alphabet=(u'абвгдеёжзийклмнопрÑтуфхцчшщъыьÑÑŽÑ'
- u'ÐБВГДЕÐЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯ'),
- wiki_start_pages=[u'ЗаглавнаÑ_Ñтраница']),
- 'Slovak': Language(name='Slovak',
- iso_code='sk',
- use_ascii=True,
- charsets=['ISO-8859-2', 'WINDOWS-1250'],
- alphabet=u'áäÄÄéíĺľňóôŕšťúýžÃÄČĎÉÃĹĽŇÓÔŔŠŤÚÃŽ',
- wiki_start_pages=[u'Hlavná_stránka']),
- 'Slovene': Language(name='Slovene',
- iso_code='sl',
- # Q, W, X, Y are only used for foreign words.
- use_ascii=False,
- charsets=['ISO-8859-2', 'WINDOWS-1250'],
- alphabet=(u'abcÄdefghijklmnoprsÅ¡tuvzž'
- u'ABCČDEFGHIJKLMNOPRSŠTUVZŽ'),
- wiki_start_pages=[u'Glavna_stran']),
- # Serbian can be written in both Latin and Cyrillic, but there's no
- # simple way to get the Latin alphabet pages from Wikipedia through
- # the API, so for now we just support Cyrillic.
- 'Serbian': Language(name='Serbian',
- iso_code='sr',
- alphabet=(u'ÐБВГДЂЕЖЗИЈКЛЉМÐЊОПРСТЋУФХЦЧÐШ'
- u'абвгдђежзијклљмнњопрÑтћуфхцчџш'),
- charsets=['ISO-8859-5', 'WINDOWS-1251',
- 'MacCyrillic', 'IBM855'],
- wiki_start_pages=[u'Главна_Ñтрана']),
- 'Thai': Language(name='Thai',
- iso_code='th',
- use_ascii=False,
- charsets=['ISO-8859-11', 'TIS-620', 'CP874'],
- alphabet=u'à¸à¸‚ฃคฅฆงจฉชซฌà¸à¸Žà¸à¸à¸‘ฒณดตถทธนบปผà¸à¸žà¸Ÿà¸ à¸¡à¸¢à¸£à¸¤à¸¥à¸¦à¸§à¸¨à¸©à¸ªà¸«à¸¬à¸­à¸®à¸¯à¸°à¸±à¸²à¸³à¸´à¸µà¸¶à¸·à¸ºà¸¸à¸¹à¸¿à¹€à¹à¹‚ใไๅๆ็่้๊๋์à¹à¹Žà¹à¹à¹‘๒๓๔๕๖๗๘๙๚๛',
- wiki_start_pages=[u'หน้าหลัà¸']),
- 'Turkish': Language(name='Turkish',
- iso_code='tr',
- # Q, W, and X are not used by Turkish
- use_ascii=False,
- charsets=['ISO-8859-3', 'ISO-8859-9',
- 'WINDOWS-1254'],
- alphabet=(u'abcçdefgğhıijklmnoöprsştuüvyzâîû'
- u'ABCÇDEFGĞHIİJKLMNOÖPRSŞTUÜVYZÂÎÛ'),
- wiki_start_pages=[u'Ana_Sayfa']),
- 'Vietnamese': Language(name='Vietnamese',
- iso_code='vi',
- use_ascii=False,
- # Windows-1258 is the only common 8-bit
- # Vietnamese encoding supported by Python.
- # From Wikipedia:
- # For systems that lack support for Unicode,
- # dozens of 8-bit Vietnamese code pages are
- # available.[1] The most common are VISCII
- # (TCVN 5712:1993), VPS, and Windows-1258.[3]
- # Where ASCII is required, such as when
- # ensuring readability in plain text e-mail,
- # Vietnamese letters are often encoded
- # according to Vietnamese Quoted-Readable
- # (VIQR) or VSCII Mnemonic (VSCII-MNEM),[4]
- # though usage of either variable-width
- # scheme has declined dramatically following
- # the adoption of Unicode on the World Wide
- # Web.
- charsets=['WINDOWS-1258'],
- alphabet=(u'aăâbcdđeêghiklmnoôơpqrstuưvxy'
- u'AĂÂBCDÄEÊGHIKLMNOÔƠPQRSTUƯVXY'),
- wiki_start_pages=[u'Chữ_Quốc_ngữ']),
- }
+LANGUAGES = {
+ "Arabic": Language(
+ name="Arabic",
+ iso_code="ar",
+ use_ascii=False,
+ # We only support encodings that use isolated
+ # forms, because the current recommendation is
+ # that the rendering system handles presentation
+ # forms. This means we purposefully skip IBM864.
+ charsets=["ISO-8859-6", "WINDOWS-1256", "CP720", "CP864"],
+ alphabet="ءآأؤإئابةتثجحخدذرزسشصضطظعغػؼؽؾؿـÙقكلمنهوىيًٌÙÙŽÙÙÙ‘",
+ wiki_start_pages=["الصÙحة_الرئيسية"],
+ ),
+ "Belarusian": Language(
+ name="Belarusian",
+ iso_code="be",
+ use_ascii=False,
+ charsets=["ISO-8859-5", "WINDOWS-1251", "IBM866", "MacCyrillic"],
+ alphabet="ÐБВГДЕÐЖЗІЙКЛМÐОПРСТУЎФХЦЧШЫЬЭЮЯабвгдеёжзійклмнопрÑтуўфхцчшыьÑÑŽÑʼ",
+ wiki_start_pages=["ГалоўнаÑ_Ñтаронка"],
+ ),
+ "Bulgarian": Language(
+ name="Bulgarian",
+ iso_code="bg",
+ use_ascii=False,
+ charsets=["ISO-8859-5", "WINDOWS-1251", "IBM855"],
+ alphabet="ÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрÑтуфхцчшщъьюÑ",
+ wiki_start_pages=["Ðачална_Ñтраница"],
+ ),
+ "Czech": Language(
+ name="Czech",
+ iso_code="cz",
+ use_ascii=True,
+ charsets=["ISO-8859-2", "WINDOWS-1250"],
+ alphabet="áÄÄéěíňóřšťúůýžÃČĎÉĚÃŇÓŘŠŤÚŮÃŽ",
+ wiki_start_pages=["Hlavní_strana"],
+ ),
+ "Danish": Language(
+ name="Danish",
+ iso_code="da",
+ use_ascii=True,
+ charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252"],
+ alphabet="æøåÆØÅ",
+ wiki_start_pages=["Forside"],
+ ),
+ "German": Language(
+ name="German",
+ iso_code="de",
+ use_ascii=True,
+ charsets=["ISO-8859-1", "WINDOWS-1252"],
+ alphabet="äöüßÄÖÜ",
+ wiki_start_pages=["Wikipedia:Hauptseite"],
+ ),
+ "Greek": Language(
+ name="Greek",
+ iso_code="el",
+ use_ascii=False,
+ charsets=["ISO-8859-7", "WINDOWS-1253"],
+ alphabet="αβγδεζηθικλμνξοπÏσςτυφχψωάέήίόÏώΑΒΓΔΕΖΗΘΙΚΛΜÎΞΟΠΡΣΣΤΥΦΧΨΩΆΈΉΊΌΎÎ",
+ wiki_start_pages=["ΠÏλη:ΚÏÏια"],
+ ),
+ "English": Language(
+ name="English",
+ iso_code="en",
+ use_ascii=True,
+ charsets=["ISO-8859-1", "WINDOWS-1252"],
+ wiki_start_pages=["Main_Page"],
+ ),
+ "Esperanto": Language(
+ name="Esperanto",
+ iso_code="eo",
+ # Q, W, X, and Y not used at all
+ use_ascii=False,
+ charsets=["ISO-8859-3"],
+ alphabet="abcĉdefgÄhÄ¥ijĵklmnoprsÅtuÅ­vzABCĈDEFGÄœHĤIJÄ´KLMNOPRSÅœTUŬVZ",
+ wiki_start_pages=["Vikipedio:ĈefpaÄo"],
+ ),
+ "Spanish": Language(
+ name="Spanish",
+ iso_code="es",
+ use_ascii=True,
+ charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252"],
+ alphabet="ñáéíóúüÑÃÉÃÓÚÜ",
+ wiki_start_pages=["Wikipedia:Portada"],
+ ),
+ "Estonian": Language(
+ name="Estonian",
+ iso_code="et",
+ use_ascii=False,
+ charsets=["ISO-8859-4", "ISO-8859-13", "WINDOWS-1257"],
+ # C, F, Š, Q, W, X, Y, Z, Ž are only for
+ # loanwords
+ alphabet="ABDEGHIJKLMNOPRSTUVÕÄÖÜabdeghijklmnoprstuvõäöü",
+ wiki_start_pages=["Esileht"],
+ ),
+ "Finnish": Language(
+ name="Finnish",
+ iso_code="fi",
+ use_ascii=True,
+ charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252"],
+ alphabet="ÅÄÖŠŽåäöšž",
+ wiki_start_pages=["Wikipedia:Etusivu"],
+ ),
+ "French": Language(
+ name="French",
+ iso_code="fr",
+ use_ascii=True,
+ charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252"],
+ alphabet="œàâçèéîïùûêŒÀÂÇÈÉÎÃÙÛÊ",
+ wiki_start_pages=["Wikipédia:Accueil_principal", "Bœuf (animal)"],
+ ),
+ "Hebrew": Language(
+ name="Hebrew",
+ iso_code="he",
+ use_ascii=False,
+ charsets=["ISO-8859-8", "WINDOWS-1255"],
+ alphabet="×בגדהוזחטיךכל×מןנסעףפץצקרשתװױײ",
+ wiki_start_pages=["עמוד_ר×שי"],
+ ),
+ "Croatian": Language(
+ name="Croatian",
+ iso_code="hr",
+ # Q, W, X, Y are only used for foreign words.
+ use_ascii=False,
+ charsets=["ISO-8859-2", "WINDOWS-1250"],
+ alphabet="abcÄćdÄ‘efghijklmnoprsÅ¡tuvzžABCČĆDÄEFGHIJKLMNOPRSÅ TUVZŽ",
+ wiki_start_pages=["Glavna_stranica"],
+ ),
+ "Hungarian": Language(
+ name="Hungarian",
+ iso_code="hu",
+ # Q, W, X, Y are only used for foreign words.
+ use_ascii=False,
+ charsets=["ISO-8859-2", "WINDOWS-1250"],
+ alphabet="abcdefghijklmnoprstuvzáéíóöőúüűABCDEFGHIJKLMNOPRSTUVZÃÉÃÓÖÅÚÜŰ",
+ wiki_start_pages=["Kezdőlap"],
+ ),
+ "Italian": Language(
+ name="Italian",
+ iso_code="it",
+ use_ascii=True,
+ charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252"],
+ alphabet="ÀÈÉÌÒÓÙàèéìòóù",
+ wiki_start_pages=["Pagina_principale"],
+ ),
+ "Lithuanian": Language(
+ name="Lithuanian",
+ iso_code="lt",
+ use_ascii=False,
+ charsets=["ISO-8859-13", "WINDOWS-1257", "ISO-8859-4"],
+ # Q, W, and X not used at all
+ alphabet="AÄ„BCÄŒDEĘĖFGHIÄ®YJKLMNOPRSÅ TUŲŪVZŽaÄ…bcÄdeęėfghiįyjklmnoprsÅ¡tuųūvzž",
+ wiki_start_pages=["Pagrindinis_puslapis"],
+ ),
+ "Latvian": Language(
+ name="Latvian",
+ iso_code="lv",
+ use_ascii=False,
+ charsets=["ISO-8859-13", "WINDOWS-1257", "ISO-8859-4"],
+ # Q, W, X, Y are only for loanwords
+ alphabet="AÄ€BCÄŒDEÄ’FGÄ¢HIĪJKĶLÄ»MNÅ…OPRSÅ TUŪVZŽaÄbcÄdeÄ“fgÄ£hiÄ«jkÄ·lļmnņoprsÅ¡tuÅ«vzž",
+ wiki_start_pages=["SÄkumlapa"],
+ ),
+ "Macedonian": Language(
+ name="Macedonian",
+ iso_code="mk",
+ use_ascii=False,
+ charsets=["ISO-8859-5", "WINDOWS-1251", "MacCyrillic", "IBM855"],
+ alphabet="ÐБВГДЃЕЖЗЅИЈКЛЉМÐЊОПРСТЌУФХЦЧÐШабвгдѓежзѕијклљмнњопрÑтќуфхцчџш",
+ wiki_start_pages=["Главна_Ñтраница"],
+ ),
+ "Dutch": Language(
+ name="Dutch",
+ iso_code="nl",
+ use_ascii=True,
+ charsets=["ISO-8859-1", "WINDOWS-1252"],
+ wiki_start_pages=["Hoofdpagina"],
+ ),
+ "Polish": Language(
+ name="Polish",
+ iso_code="pl",
+ # Q and X are only used for foreign words.
+ use_ascii=False,
+ charsets=["ISO-8859-2", "WINDOWS-1250"],
+ alphabet="AÄ„BCĆDEĘFGHIJKLÅMNŃOÓPRSÅšTUWYZŹŻaÄ…bcćdeÄ™fghijklÅ‚mnÅ„oóprsÅ›tuwyzźż",
+ wiki_start_pages=["Wikipedia:Strona_główna"],
+ ),
+ "Portuguese": Language(
+ name="Portuguese",
+ iso_code="pt",
+ use_ascii=True,
+ charsets=["ISO-8859-1", "ISO-8859-15", "WINDOWS-1252"],
+ alphabet="ÃÂÃÀÇÉÊÃÓÔÕÚáâãàçéêíóôõú",
+ wiki_start_pages=["Wikipédia:Página_principal"],
+ ),
+ "Romanian": Language(
+ name="Romanian",
+ iso_code="ro",
+ use_ascii=True,
+ charsets=["ISO-8859-2", "WINDOWS-1250"],
+ alphabet="ăâîșțĂÂÎȘȚ",
+ wiki_start_pages=["Pagina_principală"],
+ ),
+ "Russian": Language(
+ name="Russian",
+ iso_code="ru",
+ use_ascii=False,
+ charsets=[
+ "ISO-8859-5",
+ "WINDOWS-1251",
+ "KOI8-R",
+ "MacCyrillic",
+ "IBM866",
+ "IBM855",
+ ],
+ alphabet="абвгдеёжзийклмнопрÑтуфхцчшщъыьÑÑŽÑÐБВГДЕÐЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯ",
+ wiki_start_pages=["ЗаглавнаÑ_Ñтраница"],
+ ),
+ "Slovak": Language(
+ name="Slovak",
+ iso_code="sk",
+ use_ascii=True,
+ charsets=["ISO-8859-2", "WINDOWS-1250"],
+ alphabet="áäÄÄéíĺľňóôŕšťúýžÃÄČĎÉÃĹĽŇÓÔŔŠŤÚÃŽ",
+ wiki_start_pages=["Hlavná_stránka"],
+ ),
+ "Slovene": Language(
+ name="Slovene",
+ iso_code="sl",
+ # Q, W, X, Y are only used for foreign words.
+ use_ascii=False,
+ charsets=["ISO-8859-2", "WINDOWS-1250"],
+ alphabet="abcÄdefghijklmnoprsÅ¡tuvzžABCÄŒDEFGHIJKLMNOPRSÅ TUVZŽ",
+ wiki_start_pages=["Glavna_stran"],
+ ),
+ # Serbian can be written in both Latin and Cyrillic, but there's no
+ # simple way to get the Latin alphabet pages from Wikipedia through
+ # the API, so for now we just support Cyrillic.
+ "Serbian": Language(
+ name="Serbian",
+ iso_code="sr",
+ alphabet="ÐБВГДЂЕЖЗИЈКЛЉМÐЊОПРСТЋУФХЦЧÐШабвгдђежзијклљмнњопрÑтћуфхцчџш",
+ charsets=["ISO-8859-5", "WINDOWS-1251", "MacCyrillic", "IBM855"],
+ wiki_start_pages=["Главна_Ñтрана"],
+ ),
+ "Thai": Language(
+ name="Thai",
+ iso_code="th",
+ use_ascii=False,
+ charsets=["ISO-8859-11", "TIS-620", "CP874"],
+ alphabet="à¸à¸‚ฃคฅฆงจฉชซฌà¸à¸Žà¸à¸à¸‘ฒณดตถทธนบปผà¸à¸žà¸Ÿà¸ à¸¡à¸¢à¸£à¸¤à¸¥à¸¦à¸§à¸¨à¸©à¸ªà¸«à¸¬à¸­à¸®à¸¯à¸°à¸±à¸²à¸³à¸´à¸µà¸¶à¸·à¸ºà¸¸à¸¹à¸¿à¹€à¹à¹‚ใไๅๆ็่้๊๋์à¹à¹Žà¹à¹à¹‘๒๓๔๕๖๗๘๙๚๛",
+ wiki_start_pages=["หน้าหลัà¸"],
+ ),
+ "Turkish": Language(
+ name="Turkish",
+ iso_code="tr",
+ # Q, W, and X are not used by Turkish
+ use_ascii=False,
+ charsets=["ISO-8859-3", "ISO-8859-9", "WINDOWS-1254"],
+ alphabet="abcçdefgğhıijklmnoöprsştuüvyzâîûABCÇDEFGĞHIİJKLMNOÖPRSŞTUÜVYZÂÎÛ",
+ wiki_start_pages=["Ana_Sayfa"],
+ ),
+ "Vietnamese": Language(
+ name="Vietnamese",
+ iso_code="vi",
+ use_ascii=False,
+ # Windows-1258 is the only common 8-bit
+ # Vietnamese encoding supported by Python.
+ # From Wikipedia:
+ # For systems that lack support for Unicode,
+ # dozens of 8-bit Vietnamese code pages are
+ # available.[1] The most common are VISCII
+ # (TCVN 5712:1993), VPS, and Windows-1258.[3]
+ # Where ASCII is required, such as when
+ # ensuring readability in plain text e-mail,
+ # Vietnamese letters are often encoded
+ # according to Vietnamese Quoted-Readable
+ # (VIQR) or VSCII Mnemonic (VSCII-MNEM),[4]
+ # though usage of either variable-width
+ # scheme has declined dramatically following
+ # the adoption of Unicode on the World Wide
+ # Web.
+ charsets=["WINDOWS-1258"],
+ alphabet="aăâbcdÄ‘eêghiklmnoôơpqrstuÆ°vxyAĂÂBCDÄEÊGHIKLMNOÔƠPQRSTUƯVXY",
+ wiki_start_pages=["Chữ_Quốc_ngữ"],
+ ),
+}
diff --git a/src/pip/_vendor/chardet/sbcharsetprober.py b/src/pip/_vendor/chardet/sbcharsetprober.py
index 46ba835c6..31d70e154 100644
--- a/src/pip/_vendor/chardet/sbcharsetprober.py
+++ b/src/pip/_vendor/chardet/sbcharsetprober.py
@@ -31,44 +31,49 @@ from collections import namedtuple
from .charsetprober import CharSetProber
from .enums import CharacterCategory, ProbingState, SequenceLikelihood
-
-SingleByteCharSetModel = namedtuple('SingleByteCharSetModel',
- ['charset_name',
- 'language',
- 'char_to_order_map',
- 'language_model',
- 'typical_positive_ratio',
- 'keep_ascii_letters',
- 'alphabet'])
+SingleByteCharSetModel = namedtuple(
+ "SingleByteCharSetModel",
+ [
+ "charset_name",
+ "language",
+ "char_to_order_map",
+ "language_model",
+ "typical_positive_ratio",
+ "keep_ascii_letters",
+ "alphabet",
+ ],
+)
class SingleByteCharSetProber(CharSetProber):
SAMPLE_SIZE = 64
- SB_ENOUGH_REL_THRESHOLD = 1024 # 0.25 * SAMPLE_SIZE^2
+ SB_ENOUGH_REL_THRESHOLD = 1024 # 0.25 * SAMPLE_SIZE^2
POSITIVE_SHORTCUT_THRESHOLD = 0.95
NEGATIVE_SHORTCUT_THRESHOLD = 0.05
- def __init__(self, model, reversed=False, name_prober=None):
- super(SingleByteCharSetProber, self).__init__()
+ def __init__(self, model, is_reversed=False, name_prober=None):
+ super().__init__()
self._model = model
# TRUE if we need to reverse every pair in the model lookup
- self._reversed = reversed
+ self._reversed = is_reversed
# Optional auxiliary prober for name decision
self._name_prober = name_prober
self._last_order = None
self._seq_counters = None
self._total_seqs = None
self._total_char = None
+ self._control_char = None
self._freq_char = None
self.reset()
def reset(self):
- super(SingleByteCharSetProber, self).reset()
+ super().reset()
# char order of last character
self._last_order = 255
self._seq_counters = [0] * SequenceLikelihood.get_num_categories()
self._total_seqs = 0
self._total_char = 0
+ self._control_char = 0
# characters that fall in our sampling range
self._freq_char = 0
@@ -76,20 +81,20 @@ class SingleByteCharSetProber(CharSetProber):
def charset_name(self):
if self._name_prober:
return self._name_prober.charset_name
- else:
- return self._model.charset_name
+ return self._model.charset_name
@property
def language(self):
if self._name_prober:
return self._name_prober.language
- else:
- return self._model.language
+ return self._model.language
def feed(self, byte_str):
# TODO: Make filter_international_words keep things in self.alphabet
if not self._model.keep_ascii_letters:
byte_str = self.filter_international_words(byte_str)
+ else:
+ byte_str = self.remove_xml_tags(byte_str)
if not byte_str:
return self.state
char_to_order_map = self._model.char_to_order_map
@@ -103,9 +108,6 @@ class SingleByteCharSetProber(CharSetProber):
# _total_char purposes.
if order < CharacterCategory.CONTROL:
self._total_char += 1
- # TODO: Follow uchardet's lead and discount confidence for frequent
- # control characters.
- # See https://github.com/BYVoid/uchardet/commit/55b4f23971db61
if order < self.SAMPLE_SIZE:
self._freq_char += 1
if self._last_order < self.SAMPLE_SIZE:
@@ -122,14 +124,17 @@ class SingleByteCharSetProber(CharSetProber):
if self._total_seqs > self.SB_ENOUGH_REL_THRESHOLD:
confidence = self.get_confidence()
if confidence > self.POSITIVE_SHORTCUT_THRESHOLD:
- self.logger.debug('%s confidence = %s, we have a winner',
- charset_name, confidence)
+ self.logger.debug(
+ "%s confidence = %s, we have a winner", charset_name, confidence
+ )
self._state = ProbingState.FOUND_IT
elif confidence < self.NEGATIVE_SHORTCUT_THRESHOLD:
- self.logger.debug('%s confidence = %s, below negative '
- 'shortcut threshhold %s', charset_name,
- confidence,
- self.NEGATIVE_SHORTCUT_THRESHOLD)
+ self.logger.debug(
+ "%s confidence = %s, below negative shortcut threshold %s",
+ charset_name,
+ confidence,
+ self.NEGATIVE_SHORTCUT_THRESHOLD,
+ )
self._state = ProbingState.NOT_ME
return self.state
@@ -137,8 +142,18 @@ class SingleByteCharSetProber(CharSetProber):
def get_confidence(self):
r = 0.01
if self._total_seqs > 0:
- r = ((1.0 * self._seq_counters[SequenceLikelihood.POSITIVE]) /
- self._total_seqs / self._model.typical_positive_ratio)
+ r = (
+ (
+ self._seq_counters[SequenceLikelihood.POSITIVE]
+ + 0.25 * self._seq_counters[SequenceLikelihood.LIKELY]
+ )
+ / self._total_seqs
+ / self._model.typical_positive_ratio
+ )
+ # The more control characters (proportionnaly to the size
+ # of the text), the less confident we become in the current
+ # charset.
+ r = r * (self._total_char - self._control_char) / self._total_char
r = r * self._freq_char / self._total_char
if r >= 1.0:
r = 0.99
diff --git a/src/pip/_vendor/chardet/sbcsgroupprober.py b/src/pip/_vendor/chardet/sbcsgroupprober.py
index bdeef4e15..cad001cb1 100644
--- a/src/pip/_vendor/chardet/sbcsgroupprober.py
+++ b/src/pip/_vendor/chardet/sbcsgroupprober.py
@@ -28,16 +28,20 @@
from .charsetgroupprober import CharSetGroupProber
from .hebrewprober import HebrewProber
-from .langbulgarianmodel import (ISO_8859_5_BULGARIAN_MODEL,
- WINDOWS_1251_BULGARIAN_MODEL)
+from .langbulgarianmodel import ISO_8859_5_BULGARIAN_MODEL, WINDOWS_1251_BULGARIAN_MODEL
from .langgreekmodel import ISO_8859_7_GREEK_MODEL, WINDOWS_1253_GREEK_MODEL
from .langhebrewmodel import WINDOWS_1255_HEBREW_MODEL
+
# from .langhungarianmodel import (ISO_8859_2_HUNGARIAN_MODEL,
# WINDOWS_1250_HUNGARIAN_MODEL)
-from .langrussianmodel import (IBM855_RUSSIAN_MODEL, IBM866_RUSSIAN_MODEL,
- ISO_8859_5_RUSSIAN_MODEL, KOI8_R_RUSSIAN_MODEL,
- MACCYRILLIC_RUSSIAN_MODEL,
- WINDOWS_1251_RUSSIAN_MODEL)
+from .langrussianmodel import (
+ IBM855_RUSSIAN_MODEL,
+ IBM866_RUSSIAN_MODEL,
+ ISO_8859_5_RUSSIAN_MODEL,
+ KOI8_R_RUSSIAN_MODEL,
+ MACCYRILLIC_RUSSIAN_MODEL,
+ WINDOWS_1251_RUSSIAN_MODEL,
+)
from .langthaimodel import TIS_620_THAI_MODEL
from .langturkishmodel import ISO_8859_9_TURKISH_MODEL
from .sbcharsetprober import SingleByteCharSetProber
@@ -45,16 +49,17 @@ from .sbcharsetprober import SingleByteCharSetProber
class SBCSGroupProber(CharSetGroupProber):
def __init__(self):
- super(SBCSGroupProber, self).__init__()
+ super().__init__()
hebrew_prober = HebrewProber()
- logical_hebrew_prober = SingleByteCharSetProber(WINDOWS_1255_HEBREW_MODEL,
- False, hebrew_prober)
+ logical_hebrew_prober = SingleByteCharSetProber(
+ WINDOWS_1255_HEBREW_MODEL, is_reversed=False, name_prober=hebrew_prober
+ )
# TODO: See if using ISO-8859-8 Hebrew model works better here, since
# it's actually the visual one
- visual_hebrew_prober = SingleByteCharSetProber(WINDOWS_1255_HEBREW_MODEL,
- True, hebrew_prober)
- hebrew_prober.set_model_probers(logical_hebrew_prober,
- visual_hebrew_prober)
+ visual_hebrew_prober = SingleByteCharSetProber(
+ WINDOWS_1255_HEBREW_MODEL, is_reversed=True, name_prober=hebrew_prober
+ )
+ hebrew_prober.set_model_probers(logical_hebrew_prober, visual_hebrew_prober)
# TODO: ORDER MATTERS HERE. I changed the order vs what was in master
# and several tests failed that did not before. Some thought
# should be put into the ordering, and we should consider making
diff --git a/src/pip/_vendor/chardet/sjisprober.py b/src/pip/_vendor/chardet/sjisprober.py
index 9e29623bd..3bcbdb71d 100644
--- a/src/pip/_vendor/chardet/sjisprober.py
+++ b/src/pip/_vendor/chardet/sjisprober.py
@@ -25,24 +25,24 @@
# 02110-1301 USA
######################### END LICENSE BLOCK #########################
-from .mbcharsetprober import MultiByteCharSetProber
-from .codingstatemachine import CodingStateMachine
from .chardistribution import SJISDistributionAnalysis
+from .codingstatemachine import CodingStateMachine
+from .enums import MachineState, ProbingState
from .jpcntx import SJISContextAnalysis
+from .mbcharsetprober import MultiByteCharSetProber
from .mbcssm import SJIS_SM_MODEL
-from .enums import ProbingState, MachineState
class SJISProber(MultiByteCharSetProber):
def __init__(self):
- super(SJISProber, self).__init__()
+ super().__init__()
self.coding_sm = CodingStateMachine(SJIS_SM_MODEL)
self.distribution_analyzer = SJISDistributionAnalysis()
self.context_analyzer = SJISContextAnalysis()
self.reset()
def reset(self):
- super(SJISProber, self).reset()
+ super().reset()
self.context_analyzer.reset()
@property
@@ -54,34 +54,40 @@ class SJISProber(MultiByteCharSetProber):
return "Japanese"
def feed(self, byte_str):
- for i in range(len(byte_str)):
- coding_state = self.coding_sm.next_state(byte_str[i])
+ for i, byte in enumerate(byte_str):
+ coding_state = self.coding_sm.next_state(byte)
if coding_state == MachineState.ERROR:
- self.logger.debug('%s %s prober hit error at byte %s',
- self.charset_name, self.language, i)
+ self.logger.debug(
+ "%s %s prober hit error at byte %s",
+ self.charset_name,
+ self.language,
+ i,
+ )
self._state = ProbingState.NOT_ME
break
- elif coding_state == MachineState.ITS_ME:
+ if coding_state == MachineState.ITS_ME:
self._state = ProbingState.FOUND_IT
break
- elif coding_state == MachineState.START:
+ if coding_state == MachineState.START:
char_len = self.coding_sm.get_current_charlen()
if i == 0:
- self._last_char[1] = byte_str[0]
- self.context_analyzer.feed(self._last_char[2 - char_len:],
- char_len)
+ self._last_char[1] = byte
+ self.context_analyzer.feed(
+ self._last_char[2 - char_len :], char_len
+ )
self.distribution_analyzer.feed(self._last_char, char_len)
else:
- self.context_analyzer.feed(byte_str[i + 1 - char_len:i + 3
- - char_len], char_len)
- self.distribution_analyzer.feed(byte_str[i - 1:i + 1],
- char_len)
+ self.context_analyzer.feed(
+ byte_str[i + 1 - char_len : i + 3 - char_len], char_len
+ )
+ self.distribution_analyzer.feed(byte_str[i - 1 : i + 1], char_len)
self._last_char[0] = byte_str[-1]
if self.state == ProbingState.DETECTING:
- if (self.context_analyzer.got_enough_data() and
- (self.get_confidence() > self.SHORTCUT_THRESHOLD)):
+ if self.context_analyzer.got_enough_data() and (
+ self.get_confidence() > self.SHORTCUT_THRESHOLD
+ ):
self._state = ProbingState.FOUND_IT
return self.state
diff --git a/src/pip/_vendor/chardet/universaldetector.py b/src/pip/_vendor/chardet/universaldetector.py
index 055a8ac1b..22fcf8290 100644
--- a/src/pip/_vendor/chardet/universaldetector.py
+++ b/src/pip/_vendor/chardet/universaldetector.py
@@ -46,9 +46,10 @@ from .escprober import EscCharSetProber
from .latin1prober import Latin1Prober
from .mbcsgroupprober import MBCSGroupProber
from .sbcsgroupprober import SBCSGroupProber
+from .utf1632prober import UTF1632Prober
-class UniversalDetector(object):
+class UniversalDetector:
"""
The ``UniversalDetector`` class underlies the ``chardet.detect`` function
and coordinates all of the different charset probers.
@@ -66,20 +67,23 @@ class UniversalDetector(object):
"""
MINIMUM_THRESHOLD = 0.20
- HIGH_BYTE_DETECTOR = re.compile(b'[\x80-\xFF]')
- ESC_DETECTOR = re.compile(b'(\033|~{)')
- WIN_BYTE_DETECTOR = re.compile(b'[\x80-\x9F]')
- ISO_WIN_MAP = {'iso-8859-1': 'Windows-1252',
- 'iso-8859-2': 'Windows-1250',
- 'iso-8859-5': 'Windows-1251',
- 'iso-8859-6': 'Windows-1256',
- 'iso-8859-7': 'Windows-1253',
- 'iso-8859-8': 'Windows-1255',
- 'iso-8859-9': 'Windows-1254',
- 'iso-8859-13': 'Windows-1257'}
+ HIGH_BYTE_DETECTOR = re.compile(b"[\x80-\xFF]")
+ ESC_DETECTOR = re.compile(b"(\033|~{)")
+ WIN_BYTE_DETECTOR = re.compile(b"[\x80-\x9F]")
+ ISO_WIN_MAP = {
+ "iso-8859-1": "Windows-1252",
+ "iso-8859-2": "Windows-1250",
+ "iso-8859-5": "Windows-1251",
+ "iso-8859-6": "Windows-1256",
+ "iso-8859-7": "Windows-1253",
+ "iso-8859-8": "Windows-1255",
+ "iso-8859-9": "Windows-1254",
+ "iso-8859-13": "Windows-1257",
+ }
def __init__(self, lang_filter=LanguageFilter.ALL):
self._esc_charset_prober = None
+ self._utf1632_prober = None
self._charset_probers = []
self.result = None
self.done = None
@@ -91,20 +95,34 @@ class UniversalDetector(object):
self._has_win_bytes = None
self.reset()
+ @property
+ def input_state(self):
+ return self._input_state
+
+ @property
+ def has_win_bytes(self):
+ return self._has_win_bytes
+
+ @property
+ def charset_probers(self):
+ return self._charset_probers
+
def reset(self):
"""
Reset the UniversalDetector and all of its probers back to their
initial states. This is called by ``__init__``, so you only need to
call this directly in between analyses of different documents.
"""
- self.result = {'encoding': None, 'confidence': 0.0, 'language': None}
+ self.result = {"encoding": None, "confidence": 0.0, "language": None}
self.done = False
self._got_data = False
self._has_win_bytes = False
self._input_state = InputState.PURE_ASCII
- self._last_char = b''
+ self._last_char = b""
if self._esc_charset_prober:
self._esc_charset_prober.reset()
+ if self._utf1632_prober:
+ self._utf1632_prober.reset()
for prober in self._charset_probers:
prober.reset()
@@ -125,7 +143,7 @@ class UniversalDetector(object):
if self.done:
return
- if not len(byte_str):
+ if not byte_str:
return
if not isinstance(byte_str, bytearray):
@@ -136,35 +154,36 @@ class UniversalDetector(object):
# If the data starts with BOM, we know it is UTF
if byte_str.startswith(codecs.BOM_UTF8):
# EF BB BF UTF-8 with BOM
- self.result = {'encoding': "UTF-8-SIG",
- 'confidence': 1.0,
- 'language': ''}
- elif byte_str.startswith((codecs.BOM_UTF32_LE,
- codecs.BOM_UTF32_BE)):
+ self.result = {
+ "encoding": "UTF-8-SIG",
+ "confidence": 1.0,
+ "language": "",
+ }
+ elif byte_str.startswith((codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE)):
# FF FE 00 00 UTF-32, little-endian BOM
# 00 00 FE FF UTF-32, big-endian BOM
- self.result = {'encoding': "UTF-32",
- 'confidence': 1.0,
- 'language': ''}
- elif byte_str.startswith(b'\xFE\xFF\x00\x00'):
+ self.result = {"encoding": "UTF-32", "confidence": 1.0, "language": ""}
+ elif byte_str.startswith(b"\xFE\xFF\x00\x00"):
# FE FF 00 00 UCS-4, unusual octet order BOM (3412)
- self.result = {'encoding': "X-ISO-10646-UCS-4-3412",
- 'confidence': 1.0,
- 'language': ''}
- elif byte_str.startswith(b'\x00\x00\xFF\xFE'):
+ self.result = {
+ "encoding": "X-ISO-10646-UCS-4-3412",
+ "confidence": 1.0,
+ "language": "",
+ }
+ elif byte_str.startswith(b"\x00\x00\xFF\xFE"):
# 00 00 FF FE UCS-4, unusual octet order BOM (2143)
- self.result = {'encoding': "X-ISO-10646-UCS-4-2143",
- 'confidence': 1.0,
- 'language': ''}
+ self.result = {
+ "encoding": "X-ISO-10646-UCS-4-2143",
+ "confidence": 1.0,
+ "language": "",
+ }
elif byte_str.startswith((codecs.BOM_LE, codecs.BOM_BE)):
# FF FE UTF-16, little endian BOM
# FE FF UTF-16, big endian BOM
- self.result = {'encoding': "UTF-16",
- 'confidence': 1.0,
- 'language': ''}
+ self.result = {"encoding": "UTF-16", "confidence": 1.0, "language": ""}
self._got_data = True
- if self.result['encoding'] is not None:
+ if self.result["encoding"] is not None:
self.done = True
return
@@ -173,12 +192,29 @@ class UniversalDetector(object):
if self._input_state == InputState.PURE_ASCII:
if self.HIGH_BYTE_DETECTOR.search(byte_str):
self._input_state = InputState.HIGH_BYTE
- elif self._input_state == InputState.PURE_ASCII and \
- self.ESC_DETECTOR.search(self._last_char + byte_str):
+ elif (
+ self._input_state == InputState.PURE_ASCII
+ and self.ESC_DETECTOR.search(self._last_char + byte_str)
+ ):
self._input_state = InputState.ESC_ASCII
self._last_char = byte_str[-1:]
+ # next we will look to see if it is appears to be either a UTF-16 or
+ # UTF-32 encoding
+ if not self._utf1632_prober:
+ self._utf1632_prober = UTF1632Prober()
+
+ if self._utf1632_prober.state == ProbingState.DETECTING:
+ if self._utf1632_prober.feed(byte_str) == ProbingState.FOUND_IT:
+ self.result = {
+ "encoding": self._utf1632_prober.charset_name,
+ "confidence": self._utf1632_prober.get_confidence(),
+ "language": "",
+ }
+ self.done = True
+ return
+
# If we've seen escape sequences, use the EscCharSetProber, which
# uses a simple state machine to check for known escape sequences in
# HZ and ISO-2022 encodings, since those are the only encodings that
@@ -187,12 +223,11 @@ class UniversalDetector(object):
if not self._esc_charset_prober:
self._esc_charset_prober = EscCharSetProber(self.lang_filter)
if self._esc_charset_prober.feed(byte_str) == ProbingState.FOUND_IT:
- self.result = {'encoding':
- self._esc_charset_prober.charset_name,
- 'confidence':
- self._esc_charset_prober.get_confidence(),
- 'language':
- self._esc_charset_prober.language}
+ self.result = {
+ "encoding": self._esc_charset_prober.charset_name,
+ "confidence": self._esc_charset_prober.get_confidence(),
+ "language": self._esc_charset_prober.language,
+ }
self.done = True
# If we've seen high bytes (i.e., those with values greater than 127),
# we need to do more complicated checks using all our multi-byte and
@@ -209,9 +244,11 @@ class UniversalDetector(object):
self._charset_probers.append(Latin1Prober())
for prober in self._charset_probers:
if prober.feed(byte_str) == ProbingState.FOUND_IT:
- self.result = {'encoding': prober.charset_name,
- 'confidence': prober.get_confidence(),
- 'language': prober.language}
+ self.result = {
+ "encoding": prober.charset_name,
+ "confidence": prober.get_confidence(),
+ "language": prober.language,
+ }
self.done = True
break
if self.WIN_BYTE_DETECTOR.search(byte_str):
@@ -231,13 +268,11 @@ class UniversalDetector(object):
self.done = True
if not self._got_data:
- self.logger.debug('no data received!')
+ self.logger.debug("no data received!")
# Default to ASCII if it is all we've seen so far
elif self._input_state == InputState.PURE_ASCII:
- self.result = {'encoding': 'ascii',
- 'confidence': 1.0,
- 'language': ''}
+ self.result = {"encoding": "ascii", "confidence": 1.0, "language": ""}
# If we have seen non-ASCII, return the best that met MINIMUM_THRESHOLD
elif self._input_state == InputState.HIGH_BYTE:
@@ -257,30 +292,37 @@ class UniversalDetector(object):
confidence = max_prober.get_confidence()
# Use Windows encoding name instead of ISO-8859 if we saw any
# extra Windows-specific bytes
- if lower_charset_name.startswith('iso-8859'):
+ if lower_charset_name.startswith("iso-8859"):
if self._has_win_bytes:
- charset_name = self.ISO_WIN_MAP.get(lower_charset_name,
- charset_name)
- self.result = {'encoding': charset_name,
- 'confidence': confidence,
- 'language': max_prober.language}
+ charset_name = self.ISO_WIN_MAP.get(
+ lower_charset_name, charset_name
+ )
+ self.result = {
+ "encoding": charset_name,
+ "confidence": confidence,
+ "language": max_prober.language,
+ }
# Log all prober confidences if none met MINIMUM_THRESHOLD
if self.logger.getEffectiveLevel() <= logging.DEBUG:
- if self.result['encoding'] is None:
- self.logger.debug('no probers hit minimum threshold')
+ if self.result["encoding"] is None:
+ self.logger.debug("no probers hit minimum threshold")
for group_prober in self._charset_probers:
if not group_prober:
continue
if isinstance(group_prober, CharSetGroupProber):
for prober in group_prober.probers:
- self.logger.debug('%s %s confidence = %s',
- prober.charset_name,
- prober.language,
- prober.get_confidence())
+ self.logger.debug(
+ "%s %s confidence = %s",
+ prober.charset_name,
+ prober.language,
+ prober.get_confidence(),
+ )
else:
- self.logger.debug('%s %s confidence = %s',
- group_prober.charset_name,
- group_prober.language,
- group_prober.get_confidence())
+ self.logger.debug(
+ "%s %s confidence = %s",
+ group_prober.charset_name,
+ group_prober.language,
+ group_prober.get_confidence(),
+ )
return self.result
diff --git a/src/pip/_vendor/chardet/utf1632prober.py b/src/pip/_vendor/chardet/utf1632prober.py
new file mode 100644
index 000000000..9fd1580b8
--- /dev/null
+++ b/src/pip/_vendor/chardet/utf1632prober.py
@@ -0,0 +1,223 @@
+######################## BEGIN LICENSE BLOCK ########################
+#
+# Contributor(s):
+# Jason Zavaglia
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+# 02110-1301 USA
+######################### END LICENSE BLOCK #########################
+from .charsetprober import CharSetProber
+from .enums import ProbingState
+
+
+class UTF1632Prober(CharSetProber):
+ """
+ This class simply looks for occurrences of zero bytes, and infers
+ whether the file is UTF16 or UTF32 (low-endian or big-endian)
+ For instance, files looking like ( \0 \0 \0 [nonzero] )+
+ have a good probability to be UTF32BE. Files looking like ( \0 [nonzero] )+
+ may be guessed to be UTF16BE, and inversely for little-endian varieties.
+ """
+
+ # how many logical characters to scan before feeling confident of prediction
+ MIN_CHARS_FOR_DETECTION = 20
+ # a fixed constant ratio of expected zeros or non-zeros in modulo-position.
+ EXPECTED_RATIO = 0.94
+
+ def __init__(self):
+ super().__init__()
+ self.position = 0
+ self.zeros_at_mod = [0] * 4
+ self.nonzeros_at_mod = [0] * 4
+ self._state = ProbingState.DETECTING
+ self.quad = [0, 0, 0, 0]
+ self.invalid_utf16be = False
+ self.invalid_utf16le = False
+ self.invalid_utf32be = False
+ self.invalid_utf32le = False
+ self.first_half_surrogate_pair_detected_16be = False
+ self.first_half_surrogate_pair_detected_16le = False
+ self.reset()
+
+ def reset(self):
+ super().reset()
+ self.position = 0
+ self.zeros_at_mod = [0] * 4
+ self.nonzeros_at_mod = [0] * 4
+ self._state = ProbingState.DETECTING
+ self.invalid_utf16be = False
+ self.invalid_utf16le = False
+ self.invalid_utf32be = False
+ self.invalid_utf32le = False
+ self.first_half_surrogate_pair_detected_16be = False
+ self.first_half_surrogate_pair_detected_16le = False
+ self.quad = [0, 0, 0, 0]
+
+ @property
+ def charset_name(self):
+ if self.is_likely_utf32be():
+ return "utf-32be"
+ if self.is_likely_utf32le():
+ return "utf-32le"
+ if self.is_likely_utf16be():
+ return "utf-16be"
+ if self.is_likely_utf16le():
+ return "utf-16le"
+ # default to something valid
+ return "utf-16"
+
+ @property
+ def language(self):
+ return ""
+
+ def approx_32bit_chars(self):
+ return max(1.0, self.position / 4.0)
+
+ def approx_16bit_chars(self):
+ return max(1.0, self.position / 2.0)
+
+ def is_likely_utf32be(self):
+ approx_chars = self.approx_32bit_chars()
+ return approx_chars >= self.MIN_CHARS_FOR_DETECTION and (
+ self.zeros_at_mod[0] / approx_chars > self.EXPECTED_RATIO
+ and self.zeros_at_mod[1] / approx_chars > self.EXPECTED_RATIO
+ and self.zeros_at_mod[2] / approx_chars > self.EXPECTED_RATIO
+ and self.nonzeros_at_mod[3] / approx_chars > self.EXPECTED_RATIO
+ and not self.invalid_utf32be
+ )
+
+ def is_likely_utf32le(self):
+ approx_chars = self.approx_32bit_chars()
+ return approx_chars >= self.MIN_CHARS_FOR_DETECTION and (
+ self.nonzeros_at_mod[0] / approx_chars > self.EXPECTED_RATIO
+ and self.zeros_at_mod[1] / approx_chars > self.EXPECTED_RATIO
+ and self.zeros_at_mod[2] / approx_chars > self.EXPECTED_RATIO
+ and self.zeros_at_mod[3] / approx_chars > self.EXPECTED_RATIO
+ and not self.invalid_utf32le
+ )
+
+ def is_likely_utf16be(self):
+ approx_chars = self.approx_16bit_chars()
+ return approx_chars >= self.MIN_CHARS_FOR_DETECTION and (
+ (self.nonzeros_at_mod[1] + self.nonzeros_at_mod[3]) / approx_chars
+ > self.EXPECTED_RATIO
+ and (self.zeros_at_mod[0] + self.zeros_at_mod[2]) / approx_chars
+ > self.EXPECTED_RATIO
+ and not self.invalid_utf16be
+ )
+
+ def is_likely_utf16le(self):
+ approx_chars = self.approx_16bit_chars()
+ return approx_chars >= self.MIN_CHARS_FOR_DETECTION and (
+ (self.nonzeros_at_mod[0] + self.nonzeros_at_mod[2]) / approx_chars
+ > self.EXPECTED_RATIO
+ and (self.zeros_at_mod[1] + self.zeros_at_mod[3]) / approx_chars
+ > self.EXPECTED_RATIO
+ and not self.invalid_utf16le
+ )
+
+ def validate_utf32_characters(self, quad):
+ """
+ Validate if the quad of bytes is valid UTF-32.
+
+ UTF-32 is valid in the range 0x00000000 - 0x0010FFFF
+ excluding 0x0000D800 - 0x0000DFFF
+
+ https://en.wikipedia.org/wiki/UTF-32
+ """
+ if (
+ quad[0] != 0
+ or quad[1] > 0x10
+ or (quad[0] == 0 and quad[1] == 0 and 0xD8 <= quad[2] <= 0xDF)
+ ):
+ self.invalid_utf32be = True
+ if (
+ quad[3] != 0
+ or quad[2] > 0x10
+ or (quad[3] == 0 and quad[2] == 0 and 0xD8 <= quad[1] <= 0xDF)
+ ):
+ self.invalid_utf32le = True
+
+ def validate_utf16_characters(self, pair):
+ """
+ Validate if the pair of bytes is valid UTF-16.
+
+ UTF-16 is valid in the range 0x0000 - 0xFFFF excluding 0xD800 - 0xFFFF
+ with an exception for surrogate pairs, which must be in the range
+ 0xD800-0xDBFF followed by 0xDC00-0xDFFF
+
+ https://en.wikipedia.org/wiki/UTF-16
+ """
+ if not self.first_half_surrogate_pair_detected_16be:
+ if 0xD8 <= pair[0] <= 0xDB:
+ self.first_half_surrogate_pair_detected_16be = True
+ elif 0xDC <= pair[0] <= 0xDF:
+ self.invalid_utf16be = True
+ else:
+ if 0xDC <= pair[0] <= 0xDF:
+ self.first_half_surrogate_pair_detected_16be = False
+ else:
+ self.invalid_utf16be = True
+
+ if not self.first_half_surrogate_pair_detected_16le:
+ if 0xD8 <= pair[1] <= 0xDB:
+ self.first_half_surrogate_pair_detected_16le = True
+ elif 0xDC <= pair[1] <= 0xDF:
+ self.invalid_utf16le = True
+ else:
+ if 0xDC <= pair[1] <= 0xDF:
+ self.first_half_surrogate_pair_detected_16le = False
+ else:
+ self.invalid_utf16le = True
+
+ def feed(self, byte_str):
+ for c in byte_str:
+ mod4 = self.position % 4
+ self.quad[mod4] = c
+ if mod4 == 3:
+ self.validate_utf32_characters(self.quad)
+ self.validate_utf16_characters(self.quad[0:2])
+ self.validate_utf16_characters(self.quad[2:4])
+ if c == 0:
+ self.zeros_at_mod[mod4] += 1
+ else:
+ self.nonzeros_at_mod[mod4] += 1
+ self.position += 1
+ return self.state
+
+ @property
+ def state(self):
+ if self._state in {ProbingState.NOT_ME, ProbingState.FOUND_IT}:
+ # terminal, decided states
+ return self._state
+ if self.get_confidence() > 0.80:
+ self._state = ProbingState.FOUND_IT
+ elif self.position > 4 * 1024:
+ # if we get to 4kb into the file, and we can't conclude it's UTF,
+ # let's give up
+ self._state = ProbingState.NOT_ME
+ return self._state
+
+ def get_confidence(self):
+ return (
+ 0.85
+ if (
+ self.is_likely_utf16le()
+ or self.is_likely_utf16be()
+ or self.is_likely_utf32le()
+ or self.is_likely_utf32be()
+ )
+ else 0.00
+ )
diff --git a/src/pip/_vendor/chardet/utf8prober.py b/src/pip/_vendor/chardet/utf8prober.py
index 6c3196cc2..3aae09e86 100644
--- a/src/pip/_vendor/chardet/utf8prober.py
+++ b/src/pip/_vendor/chardet/utf8prober.py
@@ -26,23 +26,22 @@
######################### END LICENSE BLOCK #########################
from .charsetprober import CharSetProber
-from .enums import ProbingState, MachineState
from .codingstatemachine import CodingStateMachine
+from .enums import MachineState, ProbingState
from .mbcssm import UTF8_SM_MODEL
-
class UTF8Prober(CharSetProber):
ONE_CHAR_PROB = 0.5
def __init__(self):
- super(UTF8Prober, self).__init__()
+ super().__init__()
self.coding_sm = CodingStateMachine(UTF8_SM_MODEL)
self._num_mb_chars = None
self.reset()
def reset(self):
- super(UTF8Prober, self).reset()
+ super().reset()
self.coding_sm.reset()
self._num_mb_chars = 0
@@ -60,10 +59,10 @@ class UTF8Prober(CharSetProber):
if coding_state == MachineState.ERROR:
self._state = ProbingState.NOT_ME
break
- elif coding_state == MachineState.ITS_ME:
+ if coding_state == MachineState.ITS_ME:
self._state = ProbingState.FOUND_IT
break
- elif coding_state == MachineState.START:
+ if coding_state == MachineState.START:
if self.coding_sm.get_current_charlen() >= 2:
self._num_mb_chars += 1
@@ -76,7 +75,6 @@ class UTF8Prober(CharSetProber):
def get_confidence(self):
unlike = 0.99
if self._num_mb_chars < 6:
- unlike *= self.ONE_CHAR_PROB ** self._num_mb_chars
+ unlike *= self.ONE_CHAR_PROB**self._num_mb_chars
return 1.0 - unlike
- else:
- return unlike
+ return unlike
diff --git a/src/pip/_vendor/chardet/version.py b/src/pip/_vendor/chardet/version.py
index 70369b9d6..a08a06b9a 100644
--- a/src/pip/_vendor/chardet/version.py
+++ b/src/pip/_vendor/chardet/version.py
@@ -5,5 +5,5 @@ from within setup.py and from chardet subpackages.
:author: Dan Blanchard (dan.blanchard@gmail.com)
"""
-__version__ = "4.0.0"
-VERSION = __version__.split('.')
+__version__ = "5.0.0"
+VERSION = __version__.split(".")
diff --git a/src/pip/_vendor/colorama/__init__.py b/src/pip/_vendor/colorama/__init__.py
index b149ed79b..9138a8cc8 100644
--- a/src/pip/_vendor/colorama/__init__.py
+++ b/src/pip/_vendor/colorama/__init__.py
@@ -3,4 +3,4 @@ from .initialise import init, deinit, reinit, colorama_text
from .ansi import Fore, Back, Style, Cursor
from .ansitowin32 import AnsiToWin32
-__version__ = '0.4.4'
+__version__ = '0.4.5'
diff --git a/src/pip/_vendor/colorama/ansitowin32.py b/src/pip/_vendor/colorama/ansitowin32.py
index 6039a0543..3db248baa 100644
--- a/src/pip/_vendor/colorama/ansitowin32.py
+++ b/src/pip/_vendor/colorama/ansitowin32.py
@@ -37,6 +37,12 @@ class StreamWrapper(object):
def __exit__(self, *args, **kwargs):
return self.__wrapped.__exit__(*args, **kwargs)
+ def __setstate__(self, state):
+ self.__dict__ = state
+
+ def __getstate__(self):
+ return self.__dict__
+
def write(self, text):
self.__convertor.write(text)
@@ -57,7 +63,9 @@ class StreamWrapper(object):
stream = self.__wrapped
try:
return stream.closed
- except AttributeError:
+ # AttributeError in the case that the stream doesn't support being closed
+ # ValueError for the case that the stream has already been detached when atexit runs
+ except (AttributeError, ValueError):
return True
diff --git a/src/pip/_vendor/distlib/__init__.py b/src/pip/_vendor/distlib/__init__.py
index 492c2c705..505556517 100644
--- a/src/pip/_vendor/distlib/__init__.py
+++ b/src/pip/_vendor/distlib/__init__.py
@@ -6,7 +6,7 @@
#
import logging
-__version__ = '0.3.2'
+__version__ = '0.3.5'
class DistlibException(Exception):
pass
diff --git a/src/pip/_vendor/distlib/_backport/__init__.py b/src/pip/_vendor/distlib/_backport/__init__.py
deleted file mode 100644
index f7dbf4c9a..000000000
--- a/src/pip/_vendor/distlib/_backport/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""Modules copied from Python 3 standard libraries, for internal use only.
-
-Individual classes and functions are found in d2._backport.misc. Intended
-usage is to always import things missing from 3.1 from that module: the
-built-in/stdlib objects will be used if found.
-"""
diff --git a/src/pip/_vendor/distlib/_backport/misc.py b/src/pip/_vendor/distlib/_backport/misc.py
deleted file mode 100644
index cfb318d34..000000000
--- a/src/pip/_vendor/distlib/_backport/misc.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2012 The Python Software Foundation.
-# See LICENSE.txt and CONTRIBUTORS.txt.
-#
-"""Backports for individual classes and functions."""
-
-import os
-import sys
-
-__all__ = ['cache_from_source', 'callable', 'fsencode']
-
-
-try:
- from imp import cache_from_source
-except ImportError:
- def cache_from_source(py_file, debug=__debug__):
- ext = debug and 'c' or 'o'
- return py_file + ext
-
-
-try:
- callable = callable
-except NameError:
- from collections import Callable
-
- def callable(obj):
- return isinstance(obj, Callable)
-
-
-try:
- fsencode = os.fsencode
-except AttributeError:
- def fsencode(filename):
- if isinstance(filename, bytes):
- return filename
- elif isinstance(filename, str):
- return filename.encode(sys.getfilesystemencoding())
- else:
- raise TypeError("expect bytes or str, not %s" %
- type(filename).__name__)
diff --git a/src/pip/_vendor/distlib/_backport/shutil.py b/src/pip/_vendor/distlib/_backport/shutil.py
deleted file mode 100644
index 10ed36253..000000000
--- a/src/pip/_vendor/distlib/_backport/shutil.py
+++ /dev/null
@@ -1,764 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2012 The Python Software Foundation.
-# See LICENSE.txt and CONTRIBUTORS.txt.
-#
-"""Utility functions for copying and archiving files and directory trees.
-
-XXX The functions here don't copy the resource fork or other metadata on Mac.
-
-"""
-
-import os
-import sys
-import stat
-from os.path import abspath
-import fnmatch
-try:
- from collections.abc import Callable
-except ImportError:
- from collections import Callable
-import errno
-from . import tarfile
-
-try:
- import bz2
- _BZ2_SUPPORTED = True
-except ImportError:
- _BZ2_SUPPORTED = False
-
-try:
- from pwd import getpwnam
-except ImportError:
- getpwnam = None
-
-try:
- from grp import getgrnam
-except ImportError:
- getgrnam = None
-
-__all__ = ["copyfileobj", "copyfile", "copymode", "copystat", "copy", "copy2",
- "copytree", "move", "rmtree", "Error", "SpecialFileError",
- "ExecError", "make_archive", "get_archive_formats",
- "register_archive_format", "unregister_archive_format",
- "get_unpack_formats", "register_unpack_format",
- "unregister_unpack_format", "unpack_archive", "ignore_patterns"]
-
-class Error(EnvironmentError):
- pass
-
-class SpecialFileError(EnvironmentError):
- """Raised when trying to do a kind of operation (e.g. copying) which is
- not supported on a special file (e.g. a named pipe)"""
-
-class ExecError(EnvironmentError):
- """Raised when a command could not be executed"""
-
-class ReadError(EnvironmentError):
- """Raised when an archive cannot be read"""
-
-class RegistryError(Exception):
- """Raised when a registry operation with the archiving
- and unpacking registries fails"""
-
-
-try:
- WindowsError
-except NameError:
- WindowsError = None
-
-def copyfileobj(fsrc, fdst, length=16*1024):
- """copy data from file-like object fsrc to file-like object fdst"""
- while 1:
- buf = fsrc.read(length)
- if not buf:
- break
- fdst.write(buf)
-
-def _samefile(src, dst):
- # Macintosh, Unix.
- if hasattr(os.path, 'samefile'):
- try:
- return os.path.samefile(src, dst)
- except OSError:
- return False
-
- # All other platforms: check for same pathname.
- return (os.path.normcase(os.path.abspath(src)) ==
- os.path.normcase(os.path.abspath(dst)))
-
-def copyfile(src, dst):
- """Copy data from src to dst"""
- if _samefile(src, dst):
- raise Error("`%s` and `%s` are the same file" % (src, dst))
-
- for fn in [src, dst]:
- try:
- st = os.stat(fn)
- except OSError:
- # File most likely does not exist
- pass
- else:
- # XXX What about other special files? (sockets, devices...)
- if stat.S_ISFIFO(st.st_mode):
- raise SpecialFileError("`%s` is a named pipe" % fn)
-
- with open(src, 'rb') as fsrc:
- with open(dst, 'wb') as fdst:
- copyfileobj(fsrc, fdst)
-
-def copymode(src, dst):
- """Copy mode bits from src to dst"""
- if hasattr(os, 'chmod'):
- st = os.stat(src)
- mode = stat.S_IMODE(st.st_mode)
- os.chmod(dst, mode)
-
-def copystat(src, dst):
- """Copy all stat info (mode bits, atime, mtime, flags) from src to dst"""
- st = os.stat(src)
- mode = stat.S_IMODE(st.st_mode)
- if hasattr(os, 'utime'):
- os.utime(dst, (st.st_atime, st.st_mtime))
- if hasattr(os, 'chmod'):
- os.chmod(dst, mode)
- if hasattr(os, 'chflags') and hasattr(st, 'st_flags'):
- try:
- os.chflags(dst, st.st_flags)
- except OSError as why:
- if (not hasattr(errno, 'EOPNOTSUPP') or
- why.errno != errno.EOPNOTSUPP):
- raise
-
-def copy(src, dst):
- """Copy data and mode bits ("cp src dst").
-
- The destination may be a directory.
-
- """
- if os.path.isdir(dst):
- dst = os.path.join(dst, os.path.basename(src))
- copyfile(src, dst)
- copymode(src, dst)
-
-def copy2(src, dst):
- """Copy data and all stat info ("cp -p src dst").
-
- The destination may be a directory.
-
- """
- if os.path.isdir(dst):
- dst = os.path.join(dst, os.path.basename(src))
- copyfile(src, dst)
- copystat(src, dst)
-
-def ignore_patterns(*patterns):
- """Function that can be used as copytree() ignore parameter.
-
- Patterns is a sequence of glob-style patterns
- that are used to exclude files"""
- def _ignore_patterns(path, names):
- ignored_names = []
- for pattern in patterns:
- ignored_names.extend(fnmatch.filter(names, pattern))
- return set(ignored_names)
- return _ignore_patterns
-
-def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
- ignore_dangling_symlinks=False):
- """Recursively copy a directory tree.
-
- The destination directory must not already exist.
- If exception(s) occur, an Error is raised with a list of reasons.
-
- If the optional symlinks flag is true, symbolic links in the
- source tree result in symbolic links in the destination tree; if
- it is false, the contents of the files pointed to by symbolic
- links are copied. If the file pointed by the symlink doesn't
- exist, an exception will be added in the list of errors raised in
- an Error exception at the end of the copy process.
-
- You can set the optional ignore_dangling_symlinks flag to true if you
- want to silence this exception. Notice that this has no effect on
- platforms that don't support os.symlink.
-
- The optional ignore argument is a callable. If given, it
- is called with the `src` parameter, which is the directory
- being visited by copytree(), and `names` which is the list of
- `src` contents, as returned by os.listdir():
-
- callable(src, names) -> ignored_names
-
- Since copytree() is called recursively, the callable will be
- called once for each directory that is copied. It returns a
- list of names relative to the `src` directory that should
- not be copied.
-
- The optional copy_function argument is a callable that will be used
- to copy each file. It will be called with the source path and the
- destination path as arguments. By default, copy2() is used, but any
- function that supports the same signature (like copy()) can be used.
-
- """
- names = os.listdir(src)
- if ignore is not None:
- ignored_names = ignore(src, names)
- else:
- ignored_names = set()
-
- os.makedirs(dst)
- errors = []
- for name in names:
- if name in ignored_names:
- continue
- srcname = os.path.join(src, name)
- dstname = os.path.join(dst, name)
- try:
- if os.path.islink(srcname):
- linkto = os.readlink(srcname)
- if symlinks:
- os.symlink(linkto, dstname)
- else:
- # ignore dangling symlink if the flag is on
- if not os.path.exists(linkto) and ignore_dangling_symlinks:
- continue
- # otherwise let the copy occurs. copy2 will raise an error
- copy_function(srcname, dstname)
- elif os.path.isdir(srcname):
- copytree(srcname, dstname, symlinks, ignore, copy_function)
- else:
- # Will raise a SpecialFileError for unsupported file types
- copy_function(srcname, dstname)
- # catch the Error from the recursive copytree so that we can
- # continue with other files
- except Error as err:
- errors.extend(err.args[0])
- except EnvironmentError as why:
- errors.append((srcname, dstname, str(why)))
- try:
- copystat(src, dst)
- except OSError as why:
- if WindowsError is not None and isinstance(why, WindowsError):
- # Copying file access times may fail on Windows
- pass
- else:
- errors.extend((src, dst, str(why)))
- if errors:
- raise Error(errors)
-
-def rmtree(path, ignore_errors=False, onerror=None):
- """Recursively delete a directory tree.
-
- If ignore_errors is set, errors are ignored; otherwise, if onerror
- is set, it is called to handle the error with arguments (func,
- path, exc_info) where func is os.listdir, os.remove, or os.rmdir;
- path is the argument to that function that caused it to fail; and
- exc_info is a tuple returned by sys.exc_info(). If ignore_errors
- is false and onerror is None, an exception is raised.
-
- """
- if ignore_errors:
- def onerror(*args):
- pass
- elif onerror is None:
- def onerror(*args):
- raise
- try:
- if os.path.islink(path):
- # symlinks to directories are forbidden, see bug #1669
- raise OSError("Cannot call rmtree on a symbolic link")
- except OSError:
- onerror(os.path.islink, path, sys.exc_info())
- # can't continue even if onerror hook returns
- return
- names = []
- try:
- names = os.listdir(path)
- except os.error:
- onerror(os.listdir, path, sys.exc_info())
- for name in names:
- fullname = os.path.join(path, name)
- try:
- mode = os.lstat(fullname).st_mode
- except os.error:
- mode = 0
- if stat.S_ISDIR(mode):
- rmtree(fullname, ignore_errors, onerror)
- else:
- try:
- os.remove(fullname)
- except os.error:
- onerror(os.remove, fullname, sys.exc_info())
- try:
- os.rmdir(path)
- except os.error:
- onerror(os.rmdir, path, sys.exc_info())
-
-
-def _basename(path):
- # A basename() variant which first strips the trailing slash, if present.
- # Thus we always get the last component of the path, even for directories.
- return os.path.basename(path.rstrip(os.path.sep))
-
-def move(src, dst):
- """Recursively move a file or directory to another location. This is
- similar to the Unix "mv" command.
-
- If the destination is a directory or a symlink to a directory, the source
- is moved inside the directory. The destination path must not already
- exist.
-
- If the destination already exists but is not a directory, it may be
- overwritten depending on os.rename() semantics.
-
- If the destination is on our current filesystem, then rename() is used.
- Otherwise, src is copied to the destination and then removed.
- A lot more could be done here... A look at a mv.c shows a lot of
- the issues this implementation glosses over.
-
- """
- real_dst = dst
- if os.path.isdir(dst):
- if _samefile(src, dst):
- # We might be on a case insensitive filesystem,
- # perform the rename anyway.
- os.rename(src, dst)
- return
-
- real_dst = os.path.join(dst, _basename(src))
- if os.path.exists(real_dst):
- raise Error("Destination path '%s' already exists" % real_dst)
- try:
- os.rename(src, real_dst)
- except OSError:
- if os.path.isdir(src):
- if _destinsrc(src, dst):
- raise Error("Cannot move a directory '%s' into itself '%s'." % (src, dst))
- copytree(src, real_dst, symlinks=True)
- rmtree(src)
- else:
- copy2(src, real_dst)
- os.unlink(src)
-
-def _destinsrc(src, dst):
- src = abspath(src)
- dst = abspath(dst)
- if not src.endswith(os.path.sep):
- src += os.path.sep
- if not dst.endswith(os.path.sep):
- dst += os.path.sep
- return dst.startswith(src)
-
-def _get_gid(name):
- """Returns a gid, given a group name."""
- if getgrnam is None or name is None:
- return None
- try:
- result = getgrnam(name)
- except KeyError:
- result = None
- if result is not None:
- return result[2]
- return None
-
-def _get_uid(name):
- """Returns an uid, given a user name."""
- if getpwnam is None or name is None:
- return None
- try:
- result = getpwnam(name)
- except KeyError:
- result = None
- if result is not None:
- return result[2]
- return None
-
-def _make_tarball(base_name, base_dir, compress="gzip", verbose=0, dry_run=0,
- owner=None, group=None, logger=None):
- """Create a (possibly compressed) tar file from all the files under
- 'base_dir'.
-
- 'compress' must be "gzip" (the default), "bzip2", or None.
-
- 'owner' and 'group' can be used to define an owner and a group for the
- archive that is being built. If not provided, the current owner and group
- will be used.
-
- The output tar file will be named 'base_name' + ".tar", possibly plus
- the appropriate compression extension (".gz", or ".bz2").
-
- Returns the output filename.
- """
- tar_compression = {'gzip': 'gz', None: ''}
- compress_ext = {'gzip': '.gz'}
-
- if _BZ2_SUPPORTED:
- tar_compression['bzip2'] = 'bz2'
- compress_ext['bzip2'] = '.bz2'
-
- # flags for compression program, each element of list will be an argument
- if compress is not None and compress not in compress_ext:
- raise ValueError("bad value for 'compress', or compression format not "
- "supported : {0}".format(compress))
-
- archive_name = base_name + '.tar' + compress_ext.get(compress, '')
- archive_dir = os.path.dirname(archive_name)
-
- if not os.path.exists(archive_dir):
- if logger is not None:
- logger.info("creating %s", archive_dir)
- if not dry_run:
- os.makedirs(archive_dir)
-
- # creating the tarball
- if logger is not None:
- logger.info('Creating tar archive')
-
- uid = _get_uid(owner)
- gid = _get_gid(group)
-
- def _set_uid_gid(tarinfo):
- if gid is not None:
- tarinfo.gid = gid
- tarinfo.gname = group
- if uid is not None:
- tarinfo.uid = uid
- tarinfo.uname = owner
- return tarinfo
-
- if not dry_run:
- tar = tarfile.open(archive_name, 'w|%s' % tar_compression[compress])
- try:
- tar.add(base_dir, filter=_set_uid_gid)
- finally:
- tar.close()
-
- return archive_name
-
-def _call_external_zip(base_dir, zip_filename, verbose=False, dry_run=False):
- # XXX see if we want to keep an external call here
- if verbose:
- zipoptions = "-r"
- else:
- zipoptions = "-rq"
- from distutils.errors import DistutilsExecError
- from distutils.spawn import spawn
- try:
- spawn(["zip", zipoptions, zip_filename, base_dir], dry_run=dry_run)
- except DistutilsExecError:
- # XXX really should distinguish between "couldn't find
- # external 'zip' command" and "zip failed".
- raise ExecError("unable to create zip file '%s': "
- "could neither import the 'zipfile' module nor "
- "find a standalone zip utility") % zip_filename
-
-def _make_zipfile(base_name, base_dir, verbose=0, dry_run=0, logger=None):
- """Create a zip file from all the files under 'base_dir'.
-
- The output zip file will be named 'base_name' + ".zip". Uses either the
- "zipfile" Python module (if available) or the InfoZIP "zip" utility
- (if installed and found on the default search path). If neither tool is
- available, raises ExecError. Returns the name of the output zip
- file.
- """
- zip_filename = base_name + ".zip"
- archive_dir = os.path.dirname(base_name)
-
- if not os.path.exists(archive_dir):
- if logger is not None:
- logger.info("creating %s", archive_dir)
- if not dry_run:
- os.makedirs(archive_dir)
-
- # If zipfile module is not available, try spawning an external 'zip'
- # command.
- try:
- import zipfile
- except ImportError:
- zipfile = None
-
- if zipfile is None:
- _call_external_zip(base_dir, zip_filename, verbose, dry_run)
- else:
- if logger is not None:
- logger.info("creating '%s' and adding '%s' to it",
- zip_filename, base_dir)
-
- if not dry_run:
- zip = zipfile.ZipFile(zip_filename, "w",
- compression=zipfile.ZIP_DEFLATED)
-
- for dirpath, dirnames, filenames in os.walk(base_dir):
- for name in filenames:
- path = os.path.normpath(os.path.join(dirpath, name))
- if os.path.isfile(path):
- zip.write(path, path)
- if logger is not None:
- logger.info("adding '%s'", path)
- zip.close()
-
- return zip_filename
-
-_ARCHIVE_FORMATS = {
- 'gztar': (_make_tarball, [('compress', 'gzip')], "gzip'ed tar-file"),
- 'bztar': (_make_tarball, [('compress', 'bzip2')], "bzip2'ed tar-file"),
- 'tar': (_make_tarball, [('compress', None)], "uncompressed tar file"),
- 'zip': (_make_zipfile, [], "ZIP file"),
- }
-
-if _BZ2_SUPPORTED:
- _ARCHIVE_FORMATS['bztar'] = (_make_tarball, [('compress', 'bzip2')],
- "bzip2'ed tar-file")
-
-def get_archive_formats():
- """Returns a list of supported formats for archiving and unarchiving.
-
- Each element of the returned sequence is a tuple (name, description)
- """
- formats = [(name, registry[2]) for name, registry in
- _ARCHIVE_FORMATS.items()]
- formats.sort()
- return formats
-
-def register_archive_format(name, function, extra_args=None, description=''):
- """Registers an archive format.
-
- name is the name of the format. function is the callable that will be
- used to create archives. If provided, extra_args is a sequence of
- (name, value) tuples that will be passed as arguments to the callable.
- description can be provided to describe the format, and will be returned
- by the get_archive_formats() function.
- """
- if extra_args is None:
- extra_args = []
- if not isinstance(function, Callable):
- raise TypeError('The %s object is not callable' % function)
- if not isinstance(extra_args, (tuple, list)):
- raise TypeError('extra_args needs to be a sequence')
- for element in extra_args:
- if not isinstance(element, (tuple, list)) or len(element) !=2:
- raise TypeError('extra_args elements are : (arg_name, value)')
-
- _ARCHIVE_FORMATS[name] = (function, extra_args, description)
-
-def unregister_archive_format(name):
- del _ARCHIVE_FORMATS[name]
-
-def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0,
- dry_run=0, owner=None, group=None, logger=None):
- """Create an archive file (eg. zip or tar).
-
- 'base_name' is the name of the file to create, minus any format-specific
- extension; 'format' is the archive format: one of "zip", "tar", "bztar"
- or "gztar".
-
- 'root_dir' is a directory that will be the root directory of the
- archive; ie. we typically chdir into 'root_dir' before creating the
- archive. 'base_dir' is the directory where we start archiving from;
- ie. 'base_dir' will be the common prefix of all files and
- directories in the archive. 'root_dir' and 'base_dir' both default
- to the current directory. Returns the name of the archive file.
-
- 'owner' and 'group' are used when creating a tar archive. By default,
- uses the current owner and group.
- """
- save_cwd = os.getcwd()
- if root_dir is not None:
- if logger is not None:
- logger.debug("changing into '%s'", root_dir)
- base_name = os.path.abspath(base_name)
- if not dry_run:
- os.chdir(root_dir)
-
- if base_dir is None:
- base_dir = os.curdir
-
- kwargs = {'dry_run': dry_run, 'logger': logger}
-
- try:
- format_info = _ARCHIVE_FORMATS[format]
- except KeyError:
- raise ValueError("unknown archive format '%s'" % format)
-
- func = format_info[0]
- for arg, val in format_info[1]:
- kwargs[arg] = val
-
- if format != 'zip':
- kwargs['owner'] = owner
- kwargs['group'] = group
-
- try:
- filename = func(base_name, base_dir, **kwargs)
- finally:
- if root_dir is not None:
- if logger is not None:
- logger.debug("changing back to '%s'", save_cwd)
- os.chdir(save_cwd)
-
- return filename
-
-
-def get_unpack_formats():
- """Returns a list of supported formats for unpacking.
-
- Each element of the returned sequence is a tuple
- (name, extensions, description)
- """
- formats = [(name, info[0], info[3]) for name, info in
- _UNPACK_FORMATS.items()]
- formats.sort()
- return formats
-
-def _check_unpack_options(extensions, function, extra_args):
- """Checks what gets registered as an unpacker."""
- # first make sure no other unpacker is registered for this extension
- existing_extensions = {}
- for name, info in _UNPACK_FORMATS.items():
- for ext in info[0]:
- existing_extensions[ext] = name
-
- for extension in extensions:
- if extension in existing_extensions:
- msg = '%s is already registered for "%s"'
- raise RegistryError(msg % (extension,
- existing_extensions[extension]))
-
- if not isinstance(function, Callable):
- raise TypeError('The registered function must be a callable')
-
-
-def register_unpack_format(name, extensions, function, extra_args=None,
- description=''):
- """Registers an unpack format.
-
- `name` is the name of the format. `extensions` is a list of extensions
- corresponding to the format.
-
- `function` is the callable that will be
- used to unpack archives. The callable will receive archives to unpack.
- If it's unable to handle an archive, it needs to raise a ReadError
- exception.
-
- If provided, `extra_args` is a sequence of
- (name, value) tuples that will be passed as arguments to the callable.
- description can be provided to describe the format, and will be returned
- by the get_unpack_formats() function.
- """
- if extra_args is None:
- extra_args = []
- _check_unpack_options(extensions, function, extra_args)
- _UNPACK_FORMATS[name] = extensions, function, extra_args, description
-
-def unregister_unpack_format(name):
- """Removes the pack format from the registry."""
- del _UNPACK_FORMATS[name]
-
-def _ensure_directory(path):
- """Ensure that the parent directory of `path` exists"""
- dirname = os.path.dirname(path)
- if not os.path.isdir(dirname):
- os.makedirs(dirname)
-
-def _unpack_zipfile(filename, extract_dir):
- """Unpack zip `filename` to `extract_dir`
- """
- try:
- import zipfile
- except ImportError:
- raise ReadError('zlib not supported, cannot unpack this archive.')
-
- if not zipfile.is_zipfile(filename):
- raise ReadError("%s is not a zip file" % filename)
-
- zip = zipfile.ZipFile(filename)
- try:
- for info in zip.infolist():
- name = info.filename
-
- # don't extract absolute paths or ones with .. in them
- if name.startswith('/') or '..' in name:
- continue
-
- target = os.path.join(extract_dir, *name.split('/'))
- if not target:
- continue
-
- _ensure_directory(target)
- if not name.endswith('/'):
- # file
- data = zip.read(info.filename)
- f = open(target, 'wb')
- try:
- f.write(data)
- finally:
- f.close()
- del data
- finally:
- zip.close()
-
-def _unpack_tarfile(filename, extract_dir):
- """Unpack tar/tar.gz/tar.bz2 `filename` to `extract_dir`
- """
- try:
- tarobj = tarfile.open(filename)
- except tarfile.TarError:
- raise ReadError(
- "%s is not a compressed or uncompressed tar file" % filename)
- try:
- tarobj.extractall(extract_dir)
- finally:
- tarobj.close()
-
-_UNPACK_FORMATS = {
- 'gztar': (['.tar.gz', '.tgz'], _unpack_tarfile, [], "gzip'ed tar-file"),
- 'tar': (['.tar'], _unpack_tarfile, [], "uncompressed tar file"),
- 'zip': (['.zip'], _unpack_zipfile, [], "ZIP file")
- }
-
-if _BZ2_SUPPORTED:
- _UNPACK_FORMATS['bztar'] = (['.bz2'], _unpack_tarfile, [],
- "bzip2'ed tar-file")
-
-def _find_unpack_format(filename):
- for name, info in _UNPACK_FORMATS.items():
- for extension in info[0]:
- if filename.endswith(extension):
- return name
- return None
-
-def unpack_archive(filename, extract_dir=None, format=None):
- """Unpack an archive.
-
- `filename` is the name of the archive.
-
- `extract_dir` is the name of the target directory, where the archive
- is unpacked. If not provided, the current working directory is used.
-
- `format` is the archive format: one of "zip", "tar", or "gztar". Or any
- other registered format. If not provided, unpack_archive will use the
- filename extension and see if an unpacker was registered for that
- extension.
-
- In case none is found, a ValueError is raised.
- """
- if extract_dir is None:
- extract_dir = os.getcwd()
-
- if format is not None:
- try:
- format_info = _UNPACK_FORMATS[format]
- except KeyError:
- raise ValueError("Unknown unpack format '{0}'".format(format))
-
- func = format_info[1]
- func(filename, extract_dir, **dict(format_info[2]))
- else:
- # we need to look at the registered unpackers supported extensions
- format = _find_unpack_format(filename)
- if format is None:
- raise ReadError("Unknown archive format '{0}'".format(filename))
-
- func = _UNPACK_FORMATS[format][1]
- kwargs = dict(_UNPACK_FORMATS[format][2])
- func(filename, extract_dir, **kwargs)
diff --git a/src/pip/_vendor/distlib/_backport/sysconfig.cfg b/src/pip/_vendor/distlib/_backport/sysconfig.cfg
deleted file mode 100644
index 1746bd01c..000000000
--- a/src/pip/_vendor/distlib/_backport/sysconfig.cfg
+++ /dev/null
@@ -1,84 +0,0 @@
-[posix_prefix]
-# Configuration directories. Some of these come straight out of the
-# configure script. They are for implementing the other variables, not to
-# be used directly in [resource_locations].
-confdir = /etc
-datadir = /usr/share
-libdir = /usr/lib
-statedir = /var
-# User resource directory
-local = ~/.local/{distribution.name}
-
-stdlib = {base}/lib/python{py_version_short}
-platstdlib = {platbase}/lib/python{py_version_short}
-purelib = {base}/lib/python{py_version_short}/site-packages
-platlib = {platbase}/lib/python{py_version_short}/site-packages
-include = {base}/include/python{py_version_short}{abiflags}
-platinclude = {platbase}/include/python{py_version_short}{abiflags}
-data = {base}
-
-[posix_home]
-stdlib = {base}/lib/python
-platstdlib = {base}/lib/python
-purelib = {base}/lib/python
-platlib = {base}/lib/python
-include = {base}/include/python
-platinclude = {base}/include/python
-scripts = {base}/bin
-data = {base}
-
-[nt]
-stdlib = {base}/Lib
-platstdlib = {base}/Lib
-purelib = {base}/Lib/site-packages
-platlib = {base}/Lib/site-packages
-include = {base}/Include
-platinclude = {base}/Include
-scripts = {base}/Scripts
-data = {base}
-
-[os2]
-stdlib = {base}/Lib
-platstdlib = {base}/Lib
-purelib = {base}/Lib/site-packages
-platlib = {base}/Lib/site-packages
-include = {base}/Include
-platinclude = {base}/Include
-scripts = {base}/Scripts
-data = {base}
-
-[os2_home]
-stdlib = {userbase}/lib/python{py_version_short}
-platstdlib = {userbase}/lib/python{py_version_short}
-purelib = {userbase}/lib/python{py_version_short}/site-packages
-platlib = {userbase}/lib/python{py_version_short}/site-packages
-include = {userbase}/include/python{py_version_short}
-scripts = {userbase}/bin
-data = {userbase}
-
-[nt_user]
-stdlib = {userbase}/Python{py_version_nodot}
-platstdlib = {userbase}/Python{py_version_nodot}
-purelib = {userbase}/Python{py_version_nodot}/site-packages
-platlib = {userbase}/Python{py_version_nodot}/site-packages
-include = {userbase}/Python{py_version_nodot}/Include
-scripts = {userbase}/Scripts
-data = {userbase}
-
-[posix_user]
-stdlib = {userbase}/lib/python{py_version_short}
-platstdlib = {userbase}/lib/python{py_version_short}
-purelib = {userbase}/lib/python{py_version_short}/site-packages
-platlib = {userbase}/lib/python{py_version_short}/site-packages
-include = {userbase}/include/python{py_version_short}
-scripts = {userbase}/bin
-data = {userbase}
-
-[osx_framework_user]
-stdlib = {userbase}/lib/python
-platstdlib = {userbase}/lib/python
-purelib = {userbase}/lib/python/site-packages
-platlib = {userbase}/lib/python/site-packages
-include = {userbase}/include
-scripts = {userbase}/bin
-data = {userbase}
diff --git a/src/pip/_vendor/distlib/_backport/sysconfig.py b/src/pip/_vendor/distlib/_backport/sysconfig.py
deleted file mode 100644
index b470a373c..000000000
--- a/src/pip/_vendor/distlib/_backport/sysconfig.py
+++ /dev/null
@@ -1,786 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2012 The Python Software Foundation.
-# See LICENSE.txt and CONTRIBUTORS.txt.
-#
-"""Access to Python's configuration information."""
-
-import codecs
-import os
-import re
-import sys
-from os.path import pardir, realpath
-try:
- import configparser
-except ImportError:
- import ConfigParser as configparser
-
-
-__all__ = [
- 'get_config_h_filename',
- 'get_config_var',
- 'get_config_vars',
- 'get_makefile_filename',
- 'get_path',
- 'get_path_names',
- 'get_paths',
- 'get_platform',
- 'get_python_version',
- 'get_scheme_names',
- 'parse_config_h',
-]
-
-
-def _safe_realpath(path):
- try:
- return realpath(path)
- except OSError:
- return path
-
-
-if sys.executable:
- _PROJECT_BASE = os.path.dirname(_safe_realpath(sys.executable))
-else:
- # sys.executable can be empty if argv[0] has been changed and Python is
- # unable to retrieve the real program name
- _PROJECT_BASE = _safe_realpath(os.getcwd())
-
-if os.name == "nt" and "pcbuild" in _PROJECT_BASE[-8:].lower():
- _PROJECT_BASE = _safe_realpath(os.path.join(_PROJECT_BASE, pardir))
-# PC/VS7.1
-if os.name == "nt" and "\\pc\\v" in _PROJECT_BASE[-10:].lower():
- _PROJECT_BASE = _safe_realpath(os.path.join(_PROJECT_BASE, pardir, pardir))
-# PC/AMD64
-if os.name == "nt" and "\\pcbuild\\amd64" in _PROJECT_BASE[-14:].lower():
- _PROJECT_BASE = _safe_realpath(os.path.join(_PROJECT_BASE, pardir, pardir))
-
-
-def is_python_build():
- for fn in ("Setup.dist", "Setup.local"):
- if os.path.isfile(os.path.join(_PROJECT_BASE, "Modules", fn)):
- return True
- return False
-
-_PYTHON_BUILD = is_python_build()
-
-_cfg_read = False
-
-def _ensure_cfg_read():
- global _cfg_read
- if not _cfg_read:
- from ..resources import finder
- backport_package = __name__.rsplit('.', 1)[0]
- _finder = finder(backport_package)
- _cfgfile = _finder.find('sysconfig.cfg')
- assert _cfgfile, 'sysconfig.cfg exists'
- with _cfgfile.as_stream() as s:
- _SCHEMES.readfp(s)
- if _PYTHON_BUILD:
- for scheme in ('posix_prefix', 'posix_home'):
- _SCHEMES.set(scheme, 'include', '{srcdir}/Include')
- _SCHEMES.set(scheme, 'platinclude', '{projectbase}/.')
-
- _cfg_read = True
-
-
-_SCHEMES = configparser.RawConfigParser()
-_VAR_REPL = re.compile(r'\{([^{]*?)\}')
-
-def _expand_globals(config):
- _ensure_cfg_read()
- if config.has_section('globals'):
- globals = config.items('globals')
- else:
- globals = tuple()
-
- sections = config.sections()
- for section in sections:
- if section == 'globals':
- continue
- for option, value in globals:
- if config.has_option(section, option):
- continue
- config.set(section, option, value)
- config.remove_section('globals')
-
- # now expanding local variables defined in the cfg file
- #
- for section in config.sections():
- variables = dict(config.items(section))
-
- def _replacer(matchobj):
- name = matchobj.group(1)
- if name in variables:
- return variables[name]
- return matchobj.group(0)
-
- for option, value in config.items(section):
- config.set(section, option, _VAR_REPL.sub(_replacer, value))
-
-#_expand_globals(_SCHEMES)
-
-_PY_VERSION = '%s.%s.%s' % sys.version_info[:3]
-_PY_VERSION_SHORT = '%s.%s' % sys.version_info[:2]
-_PY_VERSION_SHORT_NO_DOT = '%s%s' % sys.version_info[:2]
-_PREFIX = os.path.normpath(sys.prefix)
-_EXEC_PREFIX = os.path.normpath(sys.exec_prefix)
-_CONFIG_VARS = None
-_USER_BASE = None
-
-
-def _subst_vars(path, local_vars):
- """In the string `path`, replace tokens like {some.thing} with the
- corresponding value from the map `local_vars`.
-
- If there is no corresponding value, leave the token unchanged.
- """
- def _replacer(matchobj):
- name = matchobj.group(1)
- if name in local_vars:
- return local_vars[name]
- elif name in os.environ:
- return os.environ[name]
- return matchobj.group(0)
- return _VAR_REPL.sub(_replacer, path)
-
-
-def _extend_dict(target_dict, other_dict):
- target_keys = target_dict.keys()
- for key, value in other_dict.items():
- if key in target_keys:
- continue
- target_dict[key] = value
-
-
-def _expand_vars(scheme, vars):
- res = {}
- if vars is None:
- vars = {}
- _extend_dict(vars, get_config_vars())
-
- for key, value in _SCHEMES.items(scheme):
- if os.name in ('posix', 'nt'):
- value = os.path.expanduser(value)
- res[key] = os.path.normpath(_subst_vars(value, vars))
- return res
-
-
-def format_value(value, vars):
- def _replacer(matchobj):
- name = matchobj.group(1)
- if name in vars:
- return vars[name]
- return matchobj.group(0)
- return _VAR_REPL.sub(_replacer, value)
-
-
-def _get_default_scheme():
- if os.name == 'posix':
- # the default scheme for posix is posix_prefix
- return 'posix_prefix'
- return os.name
-
-
-def _getuserbase():
- env_base = os.environ.get("PYTHONUSERBASE", None)
-
- def joinuser(*args):
- return os.path.expanduser(os.path.join(*args))
-
- # what about 'os2emx', 'riscos' ?
- if os.name == "nt":
- base = os.environ.get("APPDATA") or "~"
- if env_base:
- return env_base
- else:
- return joinuser(base, "Python")
-
- if sys.platform == "darwin":
- framework = get_config_var("PYTHONFRAMEWORK")
- if framework:
- if env_base:
- return env_base
- else:
- return joinuser("~", "Library", framework, "%d.%d" %
- sys.version_info[:2])
-
- if env_base:
- return env_base
- else:
- return joinuser("~", ".local")
-
-
-def _parse_makefile(filename, vars=None):
- """Parse a Makefile-style file.
-
- A dictionary containing name/value pairs is returned. If an
- optional dictionary is passed in as the second argument, it is
- used instead of a new dictionary.
- """
- # Regexes needed for parsing Makefile (and similar syntaxes,
- # like old-style Setup files).
- _variable_rx = re.compile(r"([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)")
- _findvar1_rx = re.compile(r"\$\(([A-Za-z][A-Za-z0-9_]*)\)")
- _findvar2_rx = re.compile(r"\${([A-Za-z][A-Za-z0-9_]*)}")
-
- if vars is None:
- vars = {}
- done = {}
- notdone = {}
-
- with codecs.open(filename, encoding='utf-8', errors="surrogateescape") as f:
- lines = f.readlines()
-
- for line in lines:
- if line.startswith('#') or line.strip() == '':
- continue
- m = _variable_rx.match(line)
- if m:
- n, v = m.group(1, 2)
- v = v.strip()
- # `$$' is a literal `$' in make
- tmpv = v.replace('$$', '')
-
- if "$" in tmpv:
- notdone[n] = v
- else:
- try:
- v = int(v)
- except ValueError:
- # insert literal `$'
- done[n] = v.replace('$$', '$')
- else:
- done[n] = v
-
- # do variable interpolation here
- variables = list(notdone.keys())
-
- # Variables with a 'PY_' prefix in the makefile. These need to
- # be made available without that prefix through sysconfig.
- # Special care is needed to ensure that variable expansion works, even
- # if the expansion uses the name without a prefix.
- renamed_variables = ('CFLAGS', 'LDFLAGS', 'CPPFLAGS')
-
- while len(variables) > 0:
- for name in tuple(variables):
- value = notdone[name]
- m = _findvar1_rx.search(value) or _findvar2_rx.search(value)
- if m is not None:
- n = m.group(1)
- found = True
- if n in done:
- item = str(done[n])
- elif n in notdone:
- # get it on a subsequent round
- found = False
- elif n in os.environ:
- # do it like make: fall back to environment
- item = os.environ[n]
-
- elif n in renamed_variables:
- if (name.startswith('PY_') and
- name[3:] in renamed_variables):
- item = ""
-
- elif 'PY_' + n in notdone:
- found = False
-
- else:
- item = str(done['PY_' + n])
-
- else:
- done[n] = item = ""
-
- if found:
- after = value[m.end():]
- value = value[:m.start()] + item + after
- if "$" in after:
- notdone[name] = value
- else:
- try:
- value = int(value)
- except ValueError:
- done[name] = value.strip()
- else:
- done[name] = value
- variables.remove(name)
-
- if (name.startswith('PY_') and
- name[3:] in renamed_variables):
-
- name = name[3:]
- if name not in done:
- done[name] = value
-
- else:
- # bogus variable reference (e.g. "prefix=$/opt/python");
- # just drop it since we can't deal
- done[name] = value
- variables.remove(name)
-
- # strip spurious spaces
- for k, v in done.items():
- if isinstance(v, str):
- done[k] = v.strip()
-
- # save the results in the global dictionary
- vars.update(done)
- return vars
-
-
-def get_makefile_filename():
- """Return the path of the Makefile."""
- if _PYTHON_BUILD:
- return os.path.join(_PROJECT_BASE, "Makefile")
- if hasattr(sys, 'abiflags'):
- config_dir_name = 'config-%s%s' % (_PY_VERSION_SHORT, sys.abiflags)
- else:
- config_dir_name = 'config'
- return os.path.join(get_path('stdlib'), config_dir_name, 'Makefile')
-
-
-def _init_posix(vars):
- """Initialize the module as appropriate for POSIX systems."""
- # load the installed Makefile:
- makefile = get_makefile_filename()
- try:
- _parse_makefile(makefile, vars)
- except IOError as e:
- msg = "invalid Python installation: unable to open %s" % makefile
- if hasattr(e, "strerror"):
- msg = msg + " (%s)" % e.strerror
- raise IOError(msg)
- # load the installed pyconfig.h:
- config_h = get_config_h_filename()
- try:
- with open(config_h) as f:
- parse_config_h(f, vars)
- except IOError as e:
- msg = "invalid Python installation: unable to open %s" % config_h
- if hasattr(e, "strerror"):
- msg = msg + " (%s)" % e.strerror
- raise IOError(msg)
- # On AIX, there are wrong paths to the linker scripts in the Makefile
- # -- these paths are relative to the Python source, but when installed
- # the scripts are in another directory.
- if _PYTHON_BUILD:
- vars['LDSHARED'] = vars['BLDSHARED']
-
-
-def _init_non_posix(vars):
- """Initialize the module as appropriate for NT"""
- # set basic install directories
- vars['LIBDEST'] = get_path('stdlib')
- vars['BINLIBDEST'] = get_path('platstdlib')
- vars['INCLUDEPY'] = get_path('include')
- vars['SO'] = '.pyd'
- vars['EXE'] = '.exe'
- vars['VERSION'] = _PY_VERSION_SHORT_NO_DOT
- vars['BINDIR'] = os.path.dirname(_safe_realpath(sys.executable))
-
-#
-# public APIs
-#
-
-
-def parse_config_h(fp, vars=None):
- """Parse a config.h-style file.
-
- A dictionary containing name/value pairs is returned. If an
- optional dictionary is passed in as the second argument, it is
- used instead of a new dictionary.
- """
- if vars is None:
- vars = {}
- define_rx = re.compile("#define ([A-Z][A-Za-z0-9_]+) (.*)\n")
- undef_rx = re.compile("/[*] #undef ([A-Z][A-Za-z0-9_]+) [*]/\n")
-
- while True:
- line = fp.readline()
- if not line:
- break
- m = define_rx.match(line)
- if m:
- n, v = m.group(1, 2)
- try:
- v = int(v)
- except ValueError:
- pass
- vars[n] = v
- else:
- m = undef_rx.match(line)
- if m:
- vars[m.group(1)] = 0
- return vars
-
-
-def get_config_h_filename():
- """Return the path of pyconfig.h."""
- if _PYTHON_BUILD:
- if os.name == "nt":
- inc_dir = os.path.join(_PROJECT_BASE, "PC")
- else:
- inc_dir = _PROJECT_BASE
- else:
- inc_dir = get_path('platinclude')
- return os.path.join(inc_dir, 'pyconfig.h')
-
-
-def get_scheme_names():
- """Return a tuple containing the schemes names."""
- return tuple(sorted(_SCHEMES.sections()))
-
-
-def get_path_names():
- """Return a tuple containing the paths names."""
- # xxx see if we want a static list
- return _SCHEMES.options('posix_prefix')
-
-
-def get_paths(scheme=_get_default_scheme(), vars=None, expand=True):
- """Return a mapping containing an install scheme.
-
- ``scheme`` is the install scheme name. If not provided, it will
- return the default scheme for the current platform.
- """
- _ensure_cfg_read()
- if expand:
- return _expand_vars(scheme, vars)
- else:
- return dict(_SCHEMES.items(scheme))
-
-
-def get_path(name, scheme=_get_default_scheme(), vars=None, expand=True):
- """Return a path corresponding to the scheme.
-
- ``scheme`` is the install scheme name.
- """
- return get_paths(scheme, vars, expand)[name]
-
-
-def get_config_vars(*args):
- """With no arguments, return a dictionary of all configuration
- variables relevant for the current platform.
-
- On Unix, this means every variable defined in Python's installed Makefile;
- On Windows and Mac OS it's a much smaller set.
-
- With arguments, return a list of values that result from looking up
- each argument in the configuration variable dictionary.
- """
- global _CONFIG_VARS
- if _CONFIG_VARS is None:
- _CONFIG_VARS = {}
- # Normalized versions of prefix and exec_prefix are handy to have;
- # in fact, these are the standard versions used most places in the
- # distutils2 module.
- _CONFIG_VARS['prefix'] = _PREFIX
- _CONFIG_VARS['exec_prefix'] = _EXEC_PREFIX
- _CONFIG_VARS['py_version'] = _PY_VERSION
- _CONFIG_VARS['py_version_short'] = _PY_VERSION_SHORT
- _CONFIG_VARS['py_version_nodot'] = _PY_VERSION[0] + _PY_VERSION[2]
- _CONFIG_VARS['base'] = _PREFIX
- _CONFIG_VARS['platbase'] = _EXEC_PREFIX
- _CONFIG_VARS['projectbase'] = _PROJECT_BASE
- try:
- _CONFIG_VARS['abiflags'] = sys.abiflags
- except AttributeError:
- # sys.abiflags may not be defined on all platforms.
- _CONFIG_VARS['abiflags'] = ''
-
- if os.name in ('nt', 'os2'):
- _init_non_posix(_CONFIG_VARS)
- if os.name == 'posix':
- _init_posix(_CONFIG_VARS)
- # Setting 'userbase' is done below the call to the
- # init function to enable using 'get_config_var' in
- # the init-function.
- if sys.version >= '2.6':
- _CONFIG_VARS['userbase'] = _getuserbase()
-
- if 'srcdir' not in _CONFIG_VARS:
- _CONFIG_VARS['srcdir'] = _PROJECT_BASE
- else:
- _CONFIG_VARS['srcdir'] = _safe_realpath(_CONFIG_VARS['srcdir'])
-
- # Convert srcdir into an absolute path if it appears necessary.
- # Normally it is relative to the build directory. However, during
- # testing, for example, we might be running a non-installed python
- # from a different directory.
- if _PYTHON_BUILD and os.name == "posix":
- base = _PROJECT_BASE
- try:
- cwd = os.getcwd()
- except OSError:
- cwd = None
- if (not os.path.isabs(_CONFIG_VARS['srcdir']) and
- base != cwd):
- # srcdir is relative and we are not in the same directory
- # as the executable. Assume executable is in the build
- # directory and make srcdir absolute.
- srcdir = os.path.join(base, _CONFIG_VARS['srcdir'])
- _CONFIG_VARS['srcdir'] = os.path.normpath(srcdir)
-
- if sys.platform == 'darwin':
- kernel_version = os.uname()[2] # Kernel version (8.4.3)
- major_version = int(kernel_version.split('.')[0])
-
- if major_version < 8:
- # On Mac OS X before 10.4, check if -arch and -isysroot
- # are in CFLAGS or LDFLAGS and remove them if they are.
- # This is needed when building extensions on a 10.3 system
- # using a universal build of python.
- for key in ('LDFLAGS', 'BASECFLAGS',
- # a number of derived variables. These need to be
- # patched up as well.
- 'CFLAGS', 'PY_CFLAGS', 'BLDSHARED'):
- flags = _CONFIG_VARS[key]
- flags = re.sub(r'-arch\s+\w+\s', ' ', flags)
- flags = re.sub('-isysroot [^ \t]*', ' ', flags)
- _CONFIG_VARS[key] = flags
- else:
- # Allow the user to override the architecture flags using
- # an environment variable.
- # NOTE: This name was introduced by Apple in OSX 10.5 and
- # is used by several scripting languages distributed with
- # that OS release.
- if 'ARCHFLAGS' in os.environ:
- arch = os.environ['ARCHFLAGS']
- for key in ('LDFLAGS', 'BASECFLAGS',
- # a number of derived variables. These need to be
- # patched up as well.
- 'CFLAGS', 'PY_CFLAGS', 'BLDSHARED'):
-
- flags = _CONFIG_VARS[key]
- flags = re.sub(r'-arch\s+\w+\s', ' ', flags)
- flags = flags + ' ' + arch
- _CONFIG_VARS[key] = flags
-
- # If we're on OSX 10.5 or later and the user tries to
- # compiles an extension using an SDK that is not present
- # on the current machine it is better to not use an SDK
- # than to fail.
- #
- # The major usecase for this is users using a Python.org
- # binary installer on OSX 10.6: that installer uses
- # the 10.4u SDK, but that SDK is not installed by default
- # when you install Xcode.
- #
- CFLAGS = _CONFIG_VARS.get('CFLAGS', '')
- m = re.search(r'-isysroot\s+(\S+)', CFLAGS)
- if m is not None:
- sdk = m.group(1)
- if not os.path.exists(sdk):
- for key in ('LDFLAGS', 'BASECFLAGS',
- # a number of derived variables. These need to be
- # patched up as well.
- 'CFLAGS', 'PY_CFLAGS', 'BLDSHARED'):
-
- flags = _CONFIG_VARS[key]
- flags = re.sub(r'-isysroot\s+\S+(\s|$)', ' ', flags)
- _CONFIG_VARS[key] = flags
-
- if args:
- vals = []
- for name in args:
- vals.append(_CONFIG_VARS.get(name))
- return vals
- else:
- return _CONFIG_VARS
-
-
-def get_config_var(name):
- """Return the value of a single variable using the dictionary returned by
- 'get_config_vars()'.
-
- Equivalent to get_config_vars().get(name)
- """
- return get_config_vars().get(name)
-
-
-def get_platform():
- """Return a string that identifies the current platform.
-
- This is used mainly to distinguish platform-specific build directories and
- platform-specific built distributions. Typically includes the OS name
- and version and the architecture (as supplied by 'os.uname()'),
- although the exact information included depends on the OS; eg. for IRIX
- the architecture isn't particularly important (IRIX only runs on SGI
- hardware), but for Linux the kernel version isn't particularly
- important.
-
- Examples of returned values:
- linux-i586
- linux-alpha (?)
- solaris-2.6-sun4u
- irix-5.3
- irix64-6.2
-
- Windows will return one of:
- win-amd64 (64bit Windows on AMD64 (aka x86_64, Intel64, EM64T, etc)
- win-ia64 (64bit Windows on Itanium)
- win32 (all others - specifically, sys.platform is returned)
-
- For other non-POSIX platforms, currently just returns 'sys.platform'.
- """
- if os.name == 'nt':
- # sniff sys.version for architecture.
- prefix = " bit ("
- i = sys.version.find(prefix)
- if i == -1:
- return sys.platform
- j = sys.version.find(")", i)
- look = sys.version[i+len(prefix):j].lower()
- if look == 'amd64':
- return 'win-amd64'
- if look == 'itanium':
- return 'win-ia64'
- return sys.platform
-
- if os.name != "posix" or not hasattr(os, 'uname'):
- # XXX what about the architecture? NT is Intel or Alpha,
- # Mac OS is M68k or PPC, etc.
- return sys.platform
-
- # Try to distinguish various flavours of Unix
- osname, host, release, version, machine = os.uname()
-
- # Convert the OS name to lowercase, remove '/' characters
- # (to accommodate BSD/OS), and translate spaces (for "Power Macintosh")
- osname = osname.lower().replace('/', '')
- machine = machine.replace(' ', '_')
- machine = machine.replace('/', '-')
-
- if osname[:5] == "linux":
- # At least on Linux/Intel, 'machine' is the processor --
- # i386, etc.
- # XXX what about Alpha, SPARC, etc?
- return "%s-%s" % (osname, machine)
- elif osname[:5] == "sunos":
- if release[0] >= "5": # SunOS 5 == Solaris 2
- osname = "solaris"
- release = "%d.%s" % (int(release[0]) - 3, release[2:])
- # fall through to standard osname-release-machine representation
- elif osname[:4] == "irix": # could be "irix64"!
- return "%s-%s" % (osname, release)
- elif osname[:3] == "aix":
- return "%s-%s.%s" % (osname, version, release)
- elif osname[:6] == "cygwin":
- osname = "cygwin"
- rel_re = re.compile(r'[\d.]+')
- m = rel_re.match(release)
- if m:
- release = m.group()
- elif osname[:6] == "darwin":
- #
- # For our purposes, we'll assume that the system version from
- # distutils' perspective is what MACOSX_DEPLOYMENT_TARGET is set
- # to. This makes the compatibility story a bit more sane because the
- # machine is going to compile and link as if it were
- # MACOSX_DEPLOYMENT_TARGET.
- cfgvars = get_config_vars()
- macver = cfgvars.get('MACOSX_DEPLOYMENT_TARGET')
-
- if True:
- # Always calculate the release of the running machine,
- # needed to determine if we can build fat binaries or not.
-
- macrelease = macver
- # Get the system version. Reading this plist is a documented
- # way to get the system version (see the documentation for
- # the Gestalt Manager)
- try:
- f = open('/System/Library/CoreServices/SystemVersion.plist')
- except IOError:
- # We're on a plain darwin box, fall back to the default
- # behaviour.
- pass
- else:
- try:
- m = re.search(r'<key>ProductUserVisibleVersion</key>\s*'
- r'<string>(.*?)</string>', f.read())
- finally:
- f.close()
- if m is not None:
- macrelease = '.'.join(m.group(1).split('.')[:2])
- # else: fall back to the default behaviour
-
- if not macver:
- macver = macrelease
-
- if macver:
- release = macver
- osname = "macosx"
-
- if ((macrelease + '.') >= '10.4.' and
- '-arch' in get_config_vars().get('CFLAGS', '').strip()):
- # The universal build will build fat binaries, but not on
- # systems before 10.4
- #
- # Try to detect 4-way universal builds, those have machine-type
- # 'universal' instead of 'fat'.
-
- machine = 'fat'
- cflags = get_config_vars().get('CFLAGS')
-
- archs = re.findall(r'-arch\s+(\S+)', cflags)
- archs = tuple(sorted(set(archs)))
-
- if len(archs) == 1:
- machine = archs[0]
- elif archs == ('i386', 'ppc'):
- machine = 'fat'
- elif archs == ('i386', 'x86_64'):
- machine = 'intel'
- elif archs == ('i386', 'ppc', 'x86_64'):
- machine = 'fat3'
- elif archs == ('ppc64', 'x86_64'):
- machine = 'fat64'
- elif archs == ('i386', 'ppc', 'ppc64', 'x86_64'):
- machine = 'universal'
- else:
- raise ValueError(
- "Don't know machine value for archs=%r" % (archs,))
-
- elif machine == 'i386':
- # On OSX the machine type returned by uname is always the
- # 32-bit variant, even if the executable architecture is
- # the 64-bit variant
- if sys.maxsize >= 2**32:
- machine = 'x86_64'
-
- elif machine in ('PowerPC', 'Power_Macintosh'):
- # Pick a sane name for the PPC architecture.
- # See 'i386' case
- if sys.maxsize >= 2**32:
- machine = 'ppc64'
- else:
- machine = 'ppc'
-
- return "%s-%s-%s" % (osname, release, machine)
-
-
-def get_python_version():
- return _PY_VERSION_SHORT
-
-
-def _print_dict(title, data):
- for index, (key, value) in enumerate(sorted(data.items())):
- if index == 0:
- print('%s: ' % (title))
- print('\t%s = "%s"' % (key, value))
-
-
-def _main():
- """Display all information sysconfig detains."""
- print('Platform: "%s"' % get_platform())
- print('Python version: "%s"' % get_python_version())
- print('Current installation scheme: "%s"' % _get_default_scheme())
- print()
- _print_dict('Paths', get_paths())
- print()
- _print_dict('Variables', get_config_vars())
-
-
-if __name__ == '__main__':
- _main()
diff --git a/src/pip/_vendor/distlib/_backport/tarfile.py b/src/pip/_vendor/distlib/_backport/tarfile.py
deleted file mode 100644
index d66d85663..000000000
--- a/src/pip/_vendor/distlib/_backport/tarfile.py
+++ /dev/null
@@ -1,2607 +0,0 @@
-#-------------------------------------------------------------------
-# tarfile.py
-#-------------------------------------------------------------------
-# Copyright (C) 2002 Lars Gustaebel <lars@gustaebel.de>
-# All rights reserved.
-#
-# 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.
-#
-from __future__ import print_function
-
-"""Read from and write to tar format archives.
-"""
-
-__version__ = "$Revision$"
-
-version = "0.9.0"
-__author__ = "Lars Gust\u00e4bel (lars@gustaebel.de)"
-__date__ = "$Date: 2011-02-25 17:42:01 +0200 (Fri, 25 Feb 2011) $"
-__cvsid__ = "$Id: tarfile.py 88586 2011-02-25 15:42:01Z marc-andre.lemburg $"
-__credits__ = "Gustavo Niemeyer, Niels Gust\u00e4bel, Richard Townsend."
-
-#---------
-# Imports
-#---------
-import sys
-import os
-import stat
-import errno
-import time
-import struct
-import copy
-import re
-
-try:
- import grp, pwd
-except ImportError:
- grp = pwd = None
-
-# os.symlink on Windows prior to 6.0 raises NotImplementedError
-symlink_exception = (AttributeError, NotImplementedError)
-try:
- # WindowsError (1314) will be raised if the caller does not hold the
- # SeCreateSymbolicLinkPrivilege privilege
- symlink_exception += (WindowsError,)
-except NameError:
- pass
-
-# from tarfile import *
-__all__ = ["TarFile", "TarInfo", "is_tarfile", "TarError"]
-
-if sys.version_info[0] < 3:
- import __builtin__ as builtins
-else:
- import builtins
-
-_open = builtins.open # Since 'open' is TarFile.open
-
-#---------------------------------------------------------
-# tar constants
-#---------------------------------------------------------
-NUL = b"\0" # the null character
-BLOCKSIZE = 512 # length of processing blocks
-RECORDSIZE = BLOCKSIZE * 20 # length of records
-GNU_MAGIC = b"ustar \0" # magic gnu tar string
-POSIX_MAGIC = b"ustar\x0000" # magic posix tar string
-
-LENGTH_NAME = 100 # maximum length of a filename
-LENGTH_LINK = 100 # maximum length of a linkname
-LENGTH_PREFIX = 155 # maximum length of the prefix field
-
-REGTYPE = b"0" # regular file
-AREGTYPE = b"\0" # regular file
-LNKTYPE = b"1" # link (inside tarfile)
-SYMTYPE = b"2" # symbolic link
-CHRTYPE = b"3" # character special device
-BLKTYPE = b"4" # block special device
-DIRTYPE = b"5" # directory
-FIFOTYPE = b"6" # fifo special device
-CONTTYPE = b"7" # contiguous file
-
-GNUTYPE_LONGNAME = b"L" # GNU tar longname
-GNUTYPE_LONGLINK = b"K" # GNU tar longlink
-GNUTYPE_SPARSE = b"S" # GNU tar sparse file
-
-XHDTYPE = b"x" # POSIX.1-2001 extended header
-XGLTYPE = b"g" # POSIX.1-2001 global header
-SOLARIS_XHDTYPE = b"X" # Solaris extended header
-
-USTAR_FORMAT = 0 # POSIX.1-1988 (ustar) format
-GNU_FORMAT = 1 # GNU tar format
-PAX_FORMAT = 2 # POSIX.1-2001 (pax) format
-DEFAULT_FORMAT = GNU_FORMAT
-
-#---------------------------------------------------------
-# tarfile constants
-#---------------------------------------------------------
-# File types that tarfile supports:
-SUPPORTED_TYPES = (REGTYPE, AREGTYPE, LNKTYPE,
- SYMTYPE, DIRTYPE, FIFOTYPE,
- CONTTYPE, CHRTYPE, BLKTYPE,
- GNUTYPE_LONGNAME, GNUTYPE_LONGLINK,
- GNUTYPE_SPARSE)
-
-# File types that will be treated as a regular file.
-REGULAR_TYPES = (REGTYPE, AREGTYPE,
- CONTTYPE, GNUTYPE_SPARSE)
-
-# File types that are part of the GNU tar format.
-GNU_TYPES = (GNUTYPE_LONGNAME, GNUTYPE_LONGLINK,
- GNUTYPE_SPARSE)
-
-# Fields from a pax header that override a TarInfo attribute.
-PAX_FIELDS = ("path", "linkpath", "size", "mtime",
- "uid", "gid", "uname", "gname")
-
-# Fields from a pax header that are affected by hdrcharset.
-PAX_NAME_FIELDS = set(("path", "linkpath", "uname", "gname"))
-
-# Fields in a pax header that are numbers, all other fields
-# are treated as strings.
-PAX_NUMBER_FIELDS = {
- "atime": float,
- "ctime": float,
- "mtime": float,
- "uid": int,
- "gid": int,
- "size": int
-}
-
-#---------------------------------------------------------
-# Bits used in the mode field, values in octal.
-#---------------------------------------------------------
-S_IFLNK = 0o120000 # symbolic link
-S_IFREG = 0o100000 # regular file
-S_IFBLK = 0o060000 # block device
-S_IFDIR = 0o040000 # directory
-S_IFCHR = 0o020000 # character device
-S_IFIFO = 0o010000 # fifo
-
-TSUID = 0o4000 # set UID on execution
-TSGID = 0o2000 # set GID on execution
-TSVTX = 0o1000 # reserved
-
-TUREAD = 0o400 # read by owner
-TUWRITE = 0o200 # write by owner
-TUEXEC = 0o100 # execute/search by owner
-TGREAD = 0o040 # read by group
-TGWRITE = 0o020 # write by group
-TGEXEC = 0o010 # execute/search by group
-TOREAD = 0o004 # read by other
-TOWRITE = 0o002 # write by other
-TOEXEC = 0o001 # execute/search by other
-
-#---------------------------------------------------------
-# initialization
-#---------------------------------------------------------
-if os.name in ("nt", "ce"):
- ENCODING = "utf-8"
-else:
- ENCODING = sys.getfilesystemencoding()
-
-#---------------------------------------------------------
-# Some useful functions
-#---------------------------------------------------------
-
-def stn(s, length, encoding, errors):
- """Convert a string to a null-terminated bytes object.
- """
- s = s.encode(encoding, errors)
- return s[:length] + (length - len(s)) * NUL
-
-def nts(s, encoding, errors):
- """Convert a null-terminated bytes object to a string.
- """
- p = s.find(b"\0")
- if p != -1:
- s = s[:p]
- return s.decode(encoding, errors)
-
-def nti(s):
- """Convert a number field to a python number.
- """
- # There are two possible encodings for a number field, see
- # itn() below.
- if s[0] != chr(0o200):
- try:
- n = int(nts(s, "ascii", "strict") or "0", 8)
- except ValueError:
- raise InvalidHeaderError("invalid header")
- else:
- n = 0
- for i in range(len(s) - 1):
- n <<= 8
- n += ord(s[i + 1])
- return n
-
-def itn(n, digits=8, format=DEFAULT_FORMAT):
- """Convert a python number to a number field.
- """
- # POSIX 1003.1-1988 requires numbers to be encoded as a string of
- # octal digits followed by a null-byte, this allows values up to
- # (8**(digits-1))-1. GNU tar allows storing numbers greater than
- # that if necessary. A leading 0o200 byte indicates this particular
- # encoding, the following digits-1 bytes are a big-endian
- # representation. This allows values up to (256**(digits-1))-1.
- if 0 <= n < 8 ** (digits - 1):
- s = ("%0*o" % (digits - 1, n)).encode("ascii") + NUL
- else:
- if format != GNU_FORMAT or n >= 256 ** (digits - 1):
- raise ValueError("overflow in number field")
-
- if n < 0:
- # XXX We mimic GNU tar's behaviour with negative numbers,
- # this could raise OverflowError.
- n = struct.unpack("L", struct.pack("l", n))[0]
-
- s = bytearray()
- for i in range(digits - 1):
- s.insert(0, n & 0o377)
- n >>= 8
- s.insert(0, 0o200)
- return s
-
-def calc_chksums(buf):
- """Calculate the checksum for a member's header by summing up all
- characters except for the chksum field which is treated as if
- it was filled with spaces. According to the GNU tar sources,
- some tars (Sun and NeXT) calculate chksum with signed char,
- which will be different if there are chars in the buffer with
- the high bit set. So we calculate two checksums, unsigned and
- signed.
- """
- unsigned_chksum = 256 + sum(struct.unpack("148B", buf[:148]) + struct.unpack("356B", buf[156:512]))
- signed_chksum = 256 + sum(struct.unpack("148b", buf[:148]) + struct.unpack("356b", buf[156:512]))
- return unsigned_chksum, signed_chksum
-
-def copyfileobj(src, dst, length=None):
- """Copy length bytes from fileobj src to fileobj dst.
- If length is None, copy the entire content.
- """
- if length == 0:
- return
- if length is None:
- while True:
- buf = src.read(16*1024)
- if not buf:
- break
- dst.write(buf)
- return
-
- BUFSIZE = 16 * 1024
- blocks, remainder = divmod(length, BUFSIZE)
- for b in range(blocks):
- buf = src.read(BUFSIZE)
- if len(buf) < BUFSIZE:
- raise IOError("end of file reached")
- dst.write(buf)
-
- if remainder != 0:
- buf = src.read(remainder)
- if len(buf) < remainder:
- raise IOError("end of file reached")
- dst.write(buf)
- return
-
-filemode_table = (
- ((S_IFLNK, "l"),
- (S_IFREG, "-"),
- (S_IFBLK, "b"),
- (S_IFDIR, "d"),
- (S_IFCHR, "c"),
- (S_IFIFO, "p")),
-
- ((TUREAD, "r"),),
- ((TUWRITE, "w"),),
- ((TUEXEC|TSUID, "s"),
- (TSUID, "S"),
- (TUEXEC, "x")),
-
- ((TGREAD, "r"),),
- ((TGWRITE, "w"),),
- ((TGEXEC|TSGID, "s"),
- (TSGID, "S"),
- (TGEXEC, "x")),
-
- ((TOREAD, "r"),),
- ((TOWRITE, "w"),),
- ((TOEXEC|TSVTX, "t"),
- (TSVTX, "T"),
- (TOEXEC, "x"))
-)
-
-def filemode(mode):
- """Convert a file's mode to a string of the form
- -rwxrwxrwx.
- Used by TarFile.list()
- """
- perm = []
- for table in filemode_table:
- for bit, char in table:
- if mode & bit == bit:
- perm.append(char)
- break
- else:
- perm.append("-")
- return "".join(perm)
-
-class TarError(Exception):
- """Base exception."""
- pass
-class ExtractError(TarError):
- """General exception for extract errors."""
- pass
-class ReadError(TarError):
- """Exception for unreadable tar archives."""
- pass
-class CompressionError(TarError):
- """Exception for unavailable compression methods."""
- pass
-class StreamError(TarError):
- """Exception for unsupported operations on stream-like TarFiles."""
- pass
-class HeaderError(TarError):
- """Base exception for header errors."""
- pass
-class EmptyHeaderError(HeaderError):
- """Exception for empty headers."""
- pass
-class TruncatedHeaderError(HeaderError):
- """Exception for truncated headers."""
- pass
-class EOFHeaderError(HeaderError):
- """Exception for end of file headers."""
- pass
-class InvalidHeaderError(HeaderError):
- """Exception for invalid headers."""
- pass
-class SubsequentHeaderError(HeaderError):
- """Exception for missing and invalid extended headers."""
- pass
-
-#---------------------------
-# internal stream interface
-#---------------------------
-class _LowLevelFile(object):
- """Low-level file object. Supports reading and writing.
- It is used instead of a regular file object for streaming
- access.
- """
-
- def __init__(self, name, mode):
- mode = {
- "r": os.O_RDONLY,
- "w": os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
- }[mode]
- if hasattr(os, "O_BINARY"):
- mode |= os.O_BINARY
- self.fd = os.open(name, mode, 0o666)
-
- def close(self):
- os.close(self.fd)
-
- def read(self, size):
- return os.read(self.fd, size)
-
- def write(self, s):
- os.write(self.fd, s)
-
-class _Stream(object):
- """Class that serves as an adapter between TarFile and
- a stream-like object. The stream-like object only
- needs to have a read() or write() method and is accessed
- blockwise. Use of gzip or bzip2 compression is possible.
- A stream-like object could be for example: sys.stdin,
- sys.stdout, a socket, a tape device etc.
-
- _Stream is intended to be used only internally.
- """
-
- def __init__(self, name, mode, comptype, fileobj, bufsize):
- """Construct a _Stream object.
- """
- self._extfileobj = True
- if fileobj is None:
- fileobj = _LowLevelFile(name, mode)
- self._extfileobj = False
-
- if comptype == '*':
- # Enable transparent compression detection for the
- # stream interface
- fileobj = _StreamProxy(fileobj)
- comptype = fileobj.getcomptype()
-
- self.name = name or ""
- self.mode = mode
- self.comptype = comptype
- self.fileobj = fileobj
- self.bufsize = bufsize
- self.buf = b""
- self.pos = 0
- self.closed = False
-
- try:
- if comptype == "gz":
- try:
- import zlib
- except ImportError:
- raise CompressionError("zlib module is not available")
- self.zlib = zlib
- self.crc = zlib.crc32(b"")
- if mode == "r":
- self._init_read_gz()
- else:
- self._init_write_gz()
-
- if comptype == "bz2":
- try:
- import bz2
- except ImportError:
- raise CompressionError("bz2 module is not available")
- if mode == "r":
- self.dbuf = b""
- self.cmp = bz2.BZ2Decompressor()
- else:
- self.cmp = bz2.BZ2Compressor()
- except:
- if not self._extfileobj:
- self.fileobj.close()
- self.closed = True
- raise
-
- def __del__(self):
- if hasattr(self, "closed") and not self.closed:
- self.close()
-
- def _init_write_gz(self):
- """Initialize for writing with gzip compression.
- """
- self.cmp = self.zlib.compressobj(9, self.zlib.DEFLATED,
- -self.zlib.MAX_WBITS,
- self.zlib.DEF_MEM_LEVEL,
- 0)
- timestamp = struct.pack("<L", int(time.time()))
- self.__write(b"\037\213\010\010" + timestamp + b"\002\377")
- if self.name.endswith(".gz"):
- self.name = self.name[:-3]
- # RFC1952 says we must use ISO-8859-1 for the FNAME field.
- self.__write(self.name.encode("iso-8859-1", "replace") + NUL)
-
- def write(self, s):
- """Write string s to the stream.
- """
- if self.comptype == "gz":
- self.crc = self.zlib.crc32(s, self.crc)
- self.pos += len(s)
- if self.comptype != "tar":
- s = self.cmp.compress(s)
- self.__write(s)
-
- def __write(self, s):
- """Write string s to the stream if a whole new block
- is ready to be written.
- """
- self.buf += s
- while len(self.buf) > self.bufsize:
- self.fileobj.write(self.buf[:self.bufsize])
- self.buf = self.buf[self.bufsize:]
-
- def close(self):
- """Close the _Stream object. No operation should be
- done on it afterwards.
- """
- if self.closed:
- return
-
- if self.mode == "w" and self.comptype != "tar":
- self.buf += self.cmp.flush()
-
- if self.mode == "w" and self.buf:
- self.fileobj.write(self.buf)
- self.buf = b""
- if self.comptype == "gz":
- # The native zlib crc is an unsigned 32-bit integer, but
- # the Python wrapper implicitly casts that to a signed C
- # long. So, on a 32-bit box self.crc may "look negative",
- # while the same crc on a 64-bit box may "look positive".
- # To avoid irksome warnings from the `struct` module, force
- # it to look positive on all boxes.
- self.fileobj.write(struct.pack("<L", self.crc & 0xffffffff))
- self.fileobj.write(struct.pack("<L", self.pos & 0xffffFFFF))
-
- if not self._extfileobj:
- self.fileobj.close()
-
- self.closed = True
-
- def _init_read_gz(self):
- """Initialize for reading a gzip compressed fileobj.
- """
- self.cmp = self.zlib.decompressobj(-self.zlib.MAX_WBITS)
- self.dbuf = b""
-
- # taken from gzip.GzipFile with some alterations
- if self.__read(2) != b"\037\213":
- raise ReadError("not a gzip file")
- if self.__read(1) != b"\010":
- raise CompressionError("unsupported compression method")
-
- flag = ord(self.__read(1))
- self.__read(6)
-
- if flag & 4:
- xlen = ord(self.__read(1)) + 256 * ord(self.__read(1))
- self.read(xlen)
- if flag & 8:
- while True:
- s = self.__read(1)
- if not s or s == NUL:
- break
- if flag & 16:
- while True:
- s = self.__read(1)
- if not s or s == NUL:
- break
- if flag & 2:
- self.__read(2)
-
- def tell(self):
- """Return the stream's file pointer position.
- """
- return self.pos
-
- def seek(self, pos=0):
- """Set the stream's file pointer to pos. Negative seeking
- is forbidden.
- """
- if pos - self.pos >= 0:
- blocks, remainder = divmod(pos - self.pos, self.bufsize)
- for i in range(blocks):
- self.read(self.bufsize)
- self.read(remainder)
- else:
- raise StreamError("seeking backwards is not allowed")
- return self.pos
-
- def read(self, size=None):
- """Return the next size number of bytes from the stream.
- If size is not defined, return all bytes of the stream
- up to EOF.
- """
- if size is None:
- t = []
- while True:
- buf = self._read(self.bufsize)
- if not buf:
- break
- t.append(buf)
- buf = "".join(t)
- else:
- buf = self._read(size)
- self.pos += len(buf)
- return buf
-
- def _read(self, size):
- """Return size bytes from the stream.
- """
- if self.comptype == "tar":
- return self.__read(size)
-
- c = len(self.dbuf)
- while c < size:
- buf = self.__read(self.bufsize)
- if not buf:
- break
- try:
- buf = self.cmp.decompress(buf)
- except IOError:
- raise ReadError("invalid compressed data")
- self.dbuf += buf
- c += len(buf)
- buf = self.dbuf[:size]
- self.dbuf = self.dbuf[size:]
- return buf
-
- def __read(self, size):
- """Return size bytes from stream. If internal buffer is empty,
- read another block from the stream.
- """
- c = len(self.buf)
- while c < size:
- buf = self.fileobj.read(self.bufsize)
- if not buf:
- break
- self.buf += buf
- c += len(buf)
- buf = self.buf[:size]
- self.buf = self.buf[size:]
- return buf
-# class _Stream
-
-class _StreamProxy(object):
- """Small proxy class that enables transparent compression
- detection for the Stream interface (mode 'r|*').
- """
-
- def __init__(self, fileobj):
- self.fileobj = fileobj
- self.buf = self.fileobj.read(BLOCKSIZE)
-
- def read(self, size):
- self.read = self.fileobj.read
- return self.buf
-
- def getcomptype(self):
- if self.buf.startswith(b"\037\213\010"):
- return "gz"
- if self.buf.startswith(b"BZh91"):
- return "bz2"
- return "tar"
-
- def close(self):
- self.fileobj.close()
-# class StreamProxy
-
-class _BZ2Proxy(object):
- """Small proxy class that enables external file object
- support for "r:bz2" and "w:bz2" modes. This is actually
- a workaround for a limitation in bz2 module's BZ2File
- class which (unlike gzip.GzipFile) has no support for
- a file object argument.
- """
-
- blocksize = 16 * 1024
-
- def __init__(self, fileobj, mode):
- self.fileobj = fileobj
- self.mode = mode
- self.name = getattr(self.fileobj, "name", None)
- self.init()
-
- def init(self):
- import bz2
- self.pos = 0
- if self.mode == "r":
- self.bz2obj = bz2.BZ2Decompressor()
- self.fileobj.seek(0)
- self.buf = b""
- else:
- self.bz2obj = bz2.BZ2Compressor()
-
- def read(self, size):
- x = len(self.buf)
- while x < size:
- raw = self.fileobj.read(self.blocksize)
- if not raw:
- break
- data = self.bz2obj.decompress(raw)
- self.buf += data
- x += len(data)
-
- buf = self.buf[:size]
- self.buf = self.buf[size:]
- self.pos += len(buf)
- return buf
-
- def seek(self, pos):
- if pos < self.pos:
- self.init()
- self.read(pos - self.pos)
-
- def tell(self):
- return self.pos
-
- def write(self, data):
- self.pos += len(data)
- raw = self.bz2obj.compress(data)
- self.fileobj.write(raw)
-
- def close(self):
- if self.mode == "w":
- raw = self.bz2obj.flush()
- self.fileobj.write(raw)
-# class _BZ2Proxy
-
-#------------------------
-# Extraction file object
-#------------------------
-class _FileInFile(object):
- """A thin wrapper around an existing file object that
- provides a part of its data as an individual file
- object.
- """
-
- def __init__(self, fileobj, offset, size, blockinfo=None):
- self.fileobj = fileobj
- self.offset = offset
- self.size = size
- self.position = 0
-
- if blockinfo is None:
- blockinfo = [(0, size)]
-
- # Construct a map with data and zero blocks.
- self.map_index = 0
- self.map = []
- lastpos = 0
- realpos = self.offset
- for offset, size in blockinfo:
- if offset > lastpos:
- self.map.append((False, lastpos, offset, None))
- self.map.append((True, offset, offset + size, realpos))
- realpos += size
- lastpos = offset + size
- if lastpos < self.size:
- self.map.append((False, lastpos, self.size, None))
-
- def seekable(self):
- if not hasattr(self.fileobj, "seekable"):
- # XXX gzip.GzipFile and bz2.BZ2File
- return True
- return self.fileobj.seekable()
-
- def tell(self):
- """Return the current file position.
- """
- return self.position
-
- def seek(self, position):
- """Seek to a position in the file.
- """
- self.position = position
-
- def read(self, size=None):
- """Read data from the file.
- """
- if size is None:
- size = self.size - self.position
- else:
- size = min(size, self.size - self.position)
-
- buf = b""
- while size > 0:
- while True:
- data, start, stop, offset = self.map[self.map_index]
- if start <= self.position < stop:
- break
- else:
- self.map_index += 1
- if self.map_index == len(self.map):
- self.map_index = 0
- length = min(size, stop - self.position)
- if data:
- self.fileobj.seek(offset + (self.position - start))
- buf += self.fileobj.read(length)
- else:
- buf += NUL * length
- size -= length
- self.position += length
- return buf
-#class _FileInFile
-
-
-class ExFileObject(object):
- """File-like object for reading an archive member.
- Is returned by TarFile.extractfile().
- """
- blocksize = 1024
-
- def __init__(self, tarfile, tarinfo):
- self.fileobj = _FileInFile(tarfile.fileobj,
- tarinfo.offset_data,
- tarinfo.size,
- tarinfo.sparse)
- self.name = tarinfo.name
- self.mode = "r"
- self.closed = False
- self.size = tarinfo.size
-
- self.position = 0
- self.buffer = b""
-
- def readable(self):
- return True
-
- def writable(self):
- return False
-
- def seekable(self):
- return self.fileobj.seekable()
-
- def read(self, size=None):
- """Read at most size bytes from the file. If size is not
- present or None, read all data until EOF is reached.
- """
- if self.closed:
- raise ValueError("I/O operation on closed file")
-
- buf = b""
- if self.buffer:
- if size is None:
- buf = self.buffer
- self.buffer = b""
- else:
- buf = self.buffer[:size]
- self.buffer = self.buffer[size:]
-
- if size is None:
- buf += self.fileobj.read()
- else:
- buf += self.fileobj.read(size - len(buf))
-
- self.position += len(buf)
- return buf
-
- # XXX TextIOWrapper uses the read1() method.
- read1 = read
-
- def readline(self, size=-1):
- """Read one entire line from the file. If size is present
- and non-negative, return a string with at most that
- size, which may be an incomplete line.
- """
- if self.closed:
- raise ValueError("I/O operation on closed file")
-
- pos = self.buffer.find(b"\n") + 1
- if pos == 0:
- # no newline found.
- while True:
- buf = self.fileobj.read(self.blocksize)
- self.buffer += buf
- if not buf or b"\n" in buf:
- pos = self.buffer.find(b"\n") + 1
- if pos == 0:
- # no newline found.
- pos = len(self.buffer)
- break
-
- if size != -1:
- pos = min(size, pos)
-
- buf = self.buffer[:pos]
- self.buffer = self.buffer[pos:]
- self.position += len(buf)
- return buf
-
- def readlines(self):
- """Return a list with all remaining lines.
- """
- result = []
- while True:
- line = self.readline()
- if not line: break
- result.append(line)
- return result
-
- def tell(self):
- """Return the current file position.
- """
- if self.closed:
- raise ValueError("I/O operation on closed file")
-
- return self.position
-
- def seek(self, pos, whence=os.SEEK_SET):
- """Seek to a position in the file.
- """
- if self.closed:
- raise ValueError("I/O operation on closed file")
-
- if whence == os.SEEK_SET:
- self.position = min(max(pos, 0), self.size)
- elif whence == os.SEEK_CUR:
- if pos < 0:
- self.position = max(self.position + pos, 0)
- else:
- self.position = min(self.position + pos, self.size)
- elif whence == os.SEEK_END:
- self.position = max(min(self.size + pos, self.size), 0)
- else:
- raise ValueError("Invalid argument")
-
- self.buffer = b""
- self.fileobj.seek(self.position)
-
- def close(self):
- """Close the file object.
- """
- self.closed = True
-
- def __iter__(self):
- """Get an iterator over the file's lines.
- """
- while True:
- line = self.readline()
- if not line:
- break
- yield line
-#class ExFileObject
-
-#------------------
-# Exported Classes
-#------------------
-class TarInfo(object):
- """Informational class which holds the details about an
- archive member given by a tar header block.
- TarInfo objects are returned by TarFile.getmember(),
- TarFile.getmembers() and TarFile.gettarinfo() and are
- usually created internally.
- """
-
- __slots__ = ("name", "mode", "uid", "gid", "size", "mtime",
- "chksum", "type", "linkname", "uname", "gname",
- "devmajor", "devminor",
- "offset", "offset_data", "pax_headers", "sparse",
- "tarfile", "_sparse_structs", "_link_target")
-
- def __init__(self, name=""):
- """Construct a TarInfo object. name is the optional name
- of the member.
- """
- self.name = name # member name
- self.mode = 0o644 # file permissions
- self.uid = 0 # user id
- self.gid = 0 # group id
- self.size = 0 # file size
- self.mtime = 0 # modification time
- self.chksum = 0 # header checksum
- self.type = REGTYPE # member type
- self.linkname = "" # link name
- self.uname = "" # user name
- self.gname = "" # group name
- self.devmajor = 0 # device major number
- self.devminor = 0 # device minor number
-
- self.offset = 0 # the tar header starts here
- self.offset_data = 0 # the file's data starts here
-
- self.sparse = None # sparse member information
- self.pax_headers = {} # pax header information
-
- # In pax headers the "name" and "linkname" field are called
- # "path" and "linkpath".
- def _getpath(self):
- return self.name
- def _setpath(self, name):
- self.name = name
- path = property(_getpath, _setpath)
-
- def _getlinkpath(self):
- return self.linkname
- def _setlinkpath(self, linkname):
- self.linkname = linkname
- linkpath = property(_getlinkpath, _setlinkpath)
-
- def __repr__(self):
- return "<%s %r at %#x>" % (self.__class__.__name__,self.name,id(self))
-
- def get_info(self):
- """Return the TarInfo's attributes as a dictionary.
- """
- info = {
- "name": self.name,
- "mode": self.mode & 0o7777,
- "uid": self.uid,
- "gid": self.gid,
- "size": self.size,
- "mtime": self.mtime,
- "chksum": self.chksum,
- "type": self.type,
- "linkname": self.linkname,
- "uname": self.uname,
- "gname": self.gname,
- "devmajor": self.devmajor,
- "devminor": self.devminor
- }
-
- if info["type"] == DIRTYPE and not info["name"].endswith("/"):
- info["name"] += "/"
-
- return info
-
- def tobuf(self, format=DEFAULT_FORMAT, encoding=ENCODING, errors="surrogateescape"):
- """Return a tar header as a string of 512 byte blocks.
- """
- info = self.get_info()
-
- if format == USTAR_FORMAT:
- return self.create_ustar_header(info, encoding, errors)
- elif format == GNU_FORMAT:
- return self.create_gnu_header(info, encoding, errors)
- elif format == PAX_FORMAT:
- return self.create_pax_header(info, encoding)
- else:
- raise ValueError("invalid format")
-
- def create_ustar_header(self, info, encoding, errors):
- """Return the object as a ustar header block.
- """
- info["magic"] = POSIX_MAGIC
-
- if len(info["linkname"]) > LENGTH_LINK:
- raise ValueError("linkname is too long")
-
- if len(info["name"]) > LENGTH_NAME:
- info["prefix"], info["name"] = self._posix_split_name(info["name"])
-
- return self._create_header(info, USTAR_FORMAT, encoding, errors)
-
- def create_gnu_header(self, info, encoding, errors):
- """Return the object as a GNU header block sequence.
- """
- info["magic"] = GNU_MAGIC
-
- buf = b""
- if len(info["linkname"]) > LENGTH_LINK:
- buf += self._create_gnu_long_header(info["linkname"], GNUTYPE_LONGLINK, encoding, errors)
-
- if len(info["name"]) > LENGTH_NAME:
- buf += self._create_gnu_long_header(info["name"], GNUTYPE_LONGNAME, encoding, errors)
-
- return buf + self._create_header(info, GNU_FORMAT, encoding, errors)
-
- def create_pax_header(self, info, encoding):
- """Return the object as a ustar header block. If it cannot be
- represented this way, prepend a pax extended header sequence
- with supplement information.
- """
- info["magic"] = POSIX_MAGIC
- pax_headers = self.pax_headers.copy()
-
- # Test string fields for values that exceed the field length or cannot
- # be represented in ASCII encoding.
- for name, hname, length in (
- ("name", "path", LENGTH_NAME), ("linkname", "linkpath", LENGTH_LINK),
- ("uname", "uname", 32), ("gname", "gname", 32)):
-
- if hname in pax_headers:
- # The pax header has priority.
- continue
-
- # Try to encode the string as ASCII.
- try:
- info[name].encode("ascii", "strict")
- except UnicodeEncodeError:
- pax_headers[hname] = info[name]
- continue
-
- if len(info[name]) > length:
- pax_headers[hname] = info[name]
-
- # Test number fields for values that exceed the field limit or values
- # that like to be stored as float.
- for name, digits in (("uid", 8), ("gid", 8), ("size", 12), ("mtime", 12)):
- if name in pax_headers:
- # The pax header has priority. Avoid overflow.
- info[name] = 0
- continue
-
- val = info[name]
- if not 0 <= val < 8 ** (digits - 1) or isinstance(val, float):
- pax_headers[name] = str(val)
- info[name] = 0
-
- # Create a pax extended header if necessary.
- if pax_headers:
- buf = self._create_pax_generic_header(pax_headers, XHDTYPE, encoding)
- else:
- buf = b""
-
- return buf + self._create_header(info, USTAR_FORMAT, "ascii", "replace")
-
- @classmethod
- def create_pax_global_header(cls, pax_headers):
- """Return the object as a pax global header block sequence.
- """
- return cls._create_pax_generic_header(pax_headers, XGLTYPE, "utf8")
-
- def _posix_split_name(self, name):
- """Split a name longer than 100 chars into a prefix
- and a name part.
- """
- prefix = name[:LENGTH_PREFIX + 1]
- while prefix and prefix[-1] != "/":
- prefix = prefix[:-1]
-
- name = name[len(prefix):]
- prefix = prefix[:-1]
-
- if not prefix or len(name) > LENGTH_NAME:
- raise ValueError("name is too long")
- return prefix, name
-
- @staticmethod
- def _create_header(info, format, encoding, errors):
- """Return a header block. info is a dictionary with file
- information, format must be one of the *_FORMAT constants.
- """
- parts = [
- stn(info.get("name", ""), 100, encoding, errors),
- itn(info.get("mode", 0) & 0o7777, 8, format),
- itn(info.get("uid", 0), 8, format),
- itn(info.get("gid", 0), 8, format),
- itn(info.get("size", 0), 12, format),
- itn(info.get("mtime", 0), 12, format),
- b" ", # checksum field
- info.get("type", REGTYPE),
- stn(info.get("linkname", ""), 100, encoding, errors),
- info.get("magic", POSIX_MAGIC),
- stn(info.get("uname", ""), 32, encoding, errors),
- stn(info.get("gname", ""), 32, encoding, errors),
- itn(info.get("devmajor", 0), 8, format),
- itn(info.get("devminor", 0), 8, format),
- stn(info.get("prefix", ""), 155, encoding, errors)
- ]
-
- buf = struct.pack("%ds" % BLOCKSIZE, b"".join(parts))
- chksum = calc_chksums(buf[-BLOCKSIZE:])[0]
- buf = buf[:-364] + ("%06o\0" % chksum).encode("ascii") + buf[-357:]
- return buf
-
- @staticmethod
- def _create_payload(payload):
- """Return the string payload filled with zero bytes
- up to the next 512 byte border.
- """
- blocks, remainder = divmod(len(payload), BLOCKSIZE)
- if remainder > 0:
- payload += (BLOCKSIZE - remainder) * NUL
- return payload
-
- @classmethod
- def _create_gnu_long_header(cls, name, type, encoding, errors):
- """Return a GNUTYPE_LONGNAME or GNUTYPE_LONGLINK sequence
- for name.
- """
- name = name.encode(encoding, errors) + NUL
-
- info = {}
- info["name"] = "././@LongLink"
- info["type"] = type
- info["size"] = len(name)
- info["magic"] = GNU_MAGIC
-
- # create extended header + name blocks.
- return cls._create_header(info, USTAR_FORMAT, encoding, errors) + \
- cls._create_payload(name)
-
- @classmethod
- def _create_pax_generic_header(cls, pax_headers, type, encoding):
- """Return a POSIX.1-2008 extended or global header sequence
- that contains a list of keyword, value pairs. The values
- must be strings.
- """
- # Check if one of the fields contains surrogate characters and thereby
- # forces hdrcharset=BINARY, see _proc_pax() for more information.
- binary = False
- for keyword, value in pax_headers.items():
- try:
- value.encode("utf8", "strict")
- except UnicodeEncodeError:
- binary = True
- break
-
- records = b""
- if binary:
- # Put the hdrcharset field at the beginning of the header.
- records += b"21 hdrcharset=BINARY\n"
-
- for keyword, value in pax_headers.items():
- keyword = keyword.encode("utf8")
- if binary:
- # Try to restore the original byte representation of `value'.
- # Needless to say, that the encoding must match the string.
- value = value.encode(encoding, "surrogateescape")
- else:
- value = value.encode("utf8")
-
- l = len(keyword) + len(value) + 3 # ' ' + '=' + '\n'
- n = p = 0
- while True:
- n = l + len(str(p))
- if n == p:
- break
- p = n
- records += bytes(str(p), "ascii") + b" " + keyword + b"=" + value + b"\n"
-
- # We use a hardcoded "././@PaxHeader" name like star does
- # instead of the one that POSIX recommends.
- info = {}
- info["name"] = "././@PaxHeader"
- info["type"] = type
- info["size"] = len(records)
- info["magic"] = POSIX_MAGIC
-
- # Create pax header + record blocks.
- return cls._create_header(info, USTAR_FORMAT, "ascii", "replace") + \
- cls._create_payload(records)
-
- @classmethod
- def frombuf(cls, buf, encoding, errors):
- """Construct a TarInfo object from a 512 byte bytes object.
- """
- if len(buf) == 0:
- raise EmptyHeaderError("empty header")
- if len(buf) != BLOCKSIZE:
- raise TruncatedHeaderError("truncated header")
- if buf.count(NUL) == BLOCKSIZE:
- raise EOFHeaderError("end of file header")
-
- chksum = nti(buf[148:156])
- if chksum not in calc_chksums(buf):
- raise InvalidHeaderError("bad checksum")
-
- obj = cls()
- obj.name = nts(buf[0:100], encoding, errors)
- obj.mode = nti(buf[100:108])
- obj.uid = nti(buf[108:116])
- obj.gid = nti(buf[116:124])
- obj.size = nti(buf[124:136])
- obj.mtime = nti(buf[136:148])
- obj.chksum = chksum
- obj.type = buf[156:157]
- obj.linkname = nts(buf[157:257], encoding, errors)
- obj.uname = nts(buf[265:297], encoding, errors)
- obj.gname = nts(buf[297:329], encoding, errors)
- obj.devmajor = nti(buf[329:337])
- obj.devminor = nti(buf[337:345])
- prefix = nts(buf[345:500], encoding, errors)
-
- # Old V7 tar format represents a directory as a regular
- # file with a trailing slash.
- if obj.type == AREGTYPE and obj.name.endswith("/"):
- obj.type = DIRTYPE
-
- # The old GNU sparse format occupies some of the unused
- # space in the buffer for up to 4 sparse structures.
- # Save the them for later processing in _proc_sparse().
- if obj.type == GNUTYPE_SPARSE:
- pos = 386
- structs = []
- for i in range(4):
- try:
- offset = nti(buf[pos:pos + 12])
- numbytes = nti(buf[pos + 12:pos + 24])
- except ValueError:
- break
- structs.append((offset, numbytes))
- pos += 24
- isextended = bool(buf[482])
- origsize = nti(buf[483:495])
- obj._sparse_structs = (structs, isextended, origsize)
-
- # Remove redundant slashes from directories.
- if obj.isdir():
- obj.name = obj.name.rstrip("/")
-
- # Reconstruct a ustar longname.
- if prefix and obj.type not in GNU_TYPES:
- obj.name = prefix + "/" + obj.name
- return obj
-
- @classmethod
- def fromtarfile(cls, tarfile):
- """Return the next TarInfo object from TarFile object
- tarfile.
- """
- buf = tarfile.fileobj.read(BLOCKSIZE)
- obj = cls.frombuf(buf, tarfile.encoding, tarfile.errors)
- obj.offset = tarfile.fileobj.tell() - BLOCKSIZE
- return obj._proc_member(tarfile)
-
- #--------------------------------------------------------------------------
- # The following are methods that are called depending on the type of a
- # member. The entry point is _proc_member() which can be overridden in a
- # subclass to add custom _proc_*() methods. A _proc_*() method MUST
- # implement the following
- # operations:
- # 1. Set self.offset_data to the position where the data blocks begin,
- # if there is data that follows.
- # 2. Set tarfile.offset to the position where the next member's header will
- # begin.
- # 3. Return self or another valid TarInfo object.
- def _proc_member(self, tarfile):
- """Choose the right processing method depending on
- the type and call it.
- """
- if self.type in (GNUTYPE_LONGNAME, GNUTYPE_LONGLINK):
- return self._proc_gnulong(tarfile)
- elif self.type == GNUTYPE_SPARSE:
- return self._proc_sparse(tarfile)
- elif self.type in (XHDTYPE, XGLTYPE, SOLARIS_XHDTYPE):
- return self._proc_pax(tarfile)
- else:
- return self._proc_builtin(tarfile)
-
- def _proc_builtin(self, tarfile):
- """Process a builtin type or an unknown type which
- will be treated as a regular file.
- """
- self.offset_data = tarfile.fileobj.tell()
- offset = self.offset_data
- if self.isreg() or self.type not in SUPPORTED_TYPES:
- # Skip the following data blocks.
- offset += self._block(self.size)
- tarfile.offset = offset
-
- # Patch the TarInfo object with saved global
- # header information.
- self._apply_pax_info(tarfile.pax_headers, tarfile.encoding, tarfile.errors)
-
- return self
-
- def _proc_gnulong(self, tarfile):
- """Process the blocks that hold a GNU longname
- or longlink member.
- """
- buf = tarfile.fileobj.read(self._block(self.size))
-
- # Fetch the next header and process it.
- try:
- next = self.fromtarfile(tarfile)
- except HeaderError:
- raise SubsequentHeaderError("missing or bad subsequent header")
-
- # Patch the TarInfo object from the next header with
- # the longname information.
- next.offset = self.offset
- if self.type == GNUTYPE_LONGNAME:
- next.name = nts(buf, tarfile.encoding, tarfile.errors)
- elif self.type == GNUTYPE_LONGLINK:
- next.linkname = nts(buf, tarfile.encoding, tarfile.errors)
-
- return next
-
- def _proc_sparse(self, tarfile):
- """Process a GNU sparse header plus extra headers.
- """
- # We already collected some sparse structures in frombuf().
- structs, isextended, origsize = self._sparse_structs
- del self._sparse_structs
-
- # Collect sparse structures from extended header blocks.
- while isextended:
- buf = tarfile.fileobj.read(BLOCKSIZE)
- pos = 0
- for i in range(21):
- try:
- offset = nti(buf[pos:pos + 12])
- numbytes = nti(buf[pos + 12:pos + 24])
- except ValueError:
- break
- if offset and numbytes:
- structs.append((offset, numbytes))
- pos += 24
- isextended = bool(buf[504])
- self.sparse = structs
-
- self.offset_data = tarfile.fileobj.tell()
- tarfile.offset = self.offset_data + self._block(self.size)
- self.size = origsize
- return self
-
- def _proc_pax(self, tarfile):
- """Process an extended or global header as described in
- POSIX.1-2008.
- """
- # Read the header information.
- buf = tarfile.fileobj.read(self._block(self.size))
-
- # A pax header stores supplemental information for either
- # the following file (extended) or all following files
- # (global).
- if self.type == XGLTYPE:
- pax_headers = tarfile.pax_headers
- else:
- pax_headers = tarfile.pax_headers.copy()
-
- # Check if the pax header contains a hdrcharset field. This tells us
- # the encoding of the path, linkpath, uname and gname fields. Normally,
- # these fields are UTF-8 encoded but since POSIX.1-2008 tar
- # implementations are allowed to store them as raw binary strings if
- # the translation to UTF-8 fails.
- match = re.search(br"\d+ hdrcharset=([^\n]+)\n", buf)
- if match is not None:
- pax_headers["hdrcharset"] = match.group(1).decode("utf8")
-
- # For the time being, we don't care about anything other than "BINARY".
- # The only other value that is currently allowed by the standard is
- # "ISO-IR 10646 2000 UTF-8" in other words UTF-8.
- hdrcharset = pax_headers.get("hdrcharset")
- if hdrcharset == "BINARY":
- encoding = tarfile.encoding
- else:
- encoding = "utf8"
-
- # Parse pax header information. A record looks like that:
- # "%d %s=%s\n" % (length, keyword, value). length is the size
- # of the complete record including the length field itself and
- # the newline. keyword and value are both UTF-8 encoded strings.
- regex = re.compile(br"(\d+) ([^=]+)=")
- pos = 0
- while True:
- match = regex.match(buf, pos)
- if not match:
- break
-
- length, keyword = match.groups()
- length = int(length)
- value = buf[match.end(2) + 1:match.start(1) + length - 1]
-
- # Normally, we could just use "utf8" as the encoding and "strict"
- # as the error handler, but we better not take the risk. For
- # example, GNU tar <= 1.23 is known to store filenames it cannot
- # translate to UTF-8 as raw strings (unfortunately without a
- # hdrcharset=BINARY header).
- # We first try the strict standard encoding, and if that fails we
- # fall back on the user's encoding and error handler.
- keyword = self._decode_pax_field(keyword, "utf8", "utf8",
- tarfile.errors)
- if keyword in PAX_NAME_FIELDS:
- value = self._decode_pax_field(value, encoding, tarfile.encoding,
- tarfile.errors)
- else:
- value = self._decode_pax_field(value, "utf8", "utf8",
- tarfile.errors)
-
- pax_headers[keyword] = value
- pos += length
-
- # Fetch the next header.
- try:
- next = self.fromtarfile(tarfile)
- except HeaderError:
- raise SubsequentHeaderError("missing or bad subsequent header")
-
- # Process GNU sparse information.
- if "GNU.sparse.map" in pax_headers:
- # GNU extended sparse format version 0.1.
- self._proc_gnusparse_01(next, pax_headers)
-
- elif "GNU.sparse.size" in pax_headers:
- # GNU extended sparse format version 0.0.
- self._proc_gnusparse_00(next, pax_headers, buf)
-
- elif pax_headers.get("GNU.sparse.major") == "1" and pax_headers.get("GNU.sparse.minor") == "0":
- # GNU extended sparse format version 1.0.
- self._proc_gnusparse_10(next, pax_headers, tarfile)
-
- if self.type in (XHDTYPE, SOLARIS_XHDTYPE):
- # Patch the TarInfo object with the extended header info.
- next._apply_pax_info(pax_headers, tarfile.encoding, tarfile.errors)
- next.offset = self.offset
-
- if "size" in pax_headers:
- # If the extended header replaces the size field,
- # we need to recalculate the offset where the next
- # header starts.
- offset = next.offset_data
- if next.isreg() or next.type not in SUPPORTED_TYPES:
- offset += next._block(next.size)
- tarfile.offset = offset
-
- return next
-
- def _proc_gnusparse_00(self, next, pax_headers, buf):
- """Process a GNU tar extended sparse header, version 0.0.
- """
- offsets = []
- for match in re.finditer(br"\d+ GNU.sparse.offset=(\d+)\n", buf):
- offsets.append(int(match.group(1)))
- numbytes = []
- for match in re.finditer(br"\d+ GNU.sparse.numbytes=(\d+)\n", buf):
- numbytes.append(int(match.group(1)))
- next.sparse = list(zip(offsets, numbytes))
-
- def _proc_gnusparse_01(self, next, pax_headers):
- """Process a GNU tar extended sparse header, version 0.1.
- """
- sparse = [int(x) for x in pax_headers["GNU.sparse.map"].split(",")]
- next.sparse = list(zip(sparse[::2], sparse[1::2]))
-
- def _proc_gnusparse_10(self, next, pax_headers, tarfile):
- """Process a GNU tar extended sparse header, version 1.0.
- """
- fields = None
- sparse = []
- buf = tarfile.fileobj.read(BLOCKSIZE)
- fields, buf = buf.split(b"\n", 1)
- fields = int(fields)
- while len(sparse) < fields * 2:
- if b"\n" not in buf:
- buf += tarfile.fileobj.read(BLOCKSIZE)
- number, buf = buf.split(b"\n", 1)
- sparse.append(int(number))
- next.offset_data = tarfile.fileobj.tell()
- next.sparse = list(zip(sparse[::2], sparse[1::2]))
-
- def _apply_pax_info(self, pax_headers, encoding, errors):
- """Replace fields with supplemental information from a previous
- pax extended or global header.
- """
- for keyword, value in pax_headers.items():
- if keyword == "GNU.sparse.name":
- setattr(self, "path", value)
- elif keyword == "GNU.sparse.size":
- setattr(self, "size", int(value))
- elif keyword == "GNU.sparse.realsize":
- setattr(self, "size", int(value))
- elif keyword in PAX_FIELDS:
- if keyword in PAX_NUMBER_FIELDS:
- try:
- value = PAX_NUMBER_FIELDS[keyword](value)
- except ValueError:
- value = 0
- if keyword == "path":
- value = value.rstrip("/")
- setattr(self, keyword, value)
-
- self.pax_headers = pax_headers.copy()
-
- def _decode_pax_field(self, value, encoding, fallback_encoding, fallback_errors):
- """Decode a single field from a pax record.
- """
- try:
- return value.decode(encoding, "strict")
- except UnicodeDecodeError:
- return value.decode(fallback_encoding, fallback_errors)
-
- def _block(self, count):
- """Round up a byte count by BLOCKSIZE and return it,
- e.g. _block(834) => 1024.
- """
- blocks, remainder = divmod(count, BLOCKSIZE)
- if remainder:
- blocks += 1
- return blocks * BLOCKSIZE
-
- def isreg(self):
- return self.type in REGULAR_TYPES
- def isfile(self):
- return self.isreg()
- def isdir(self):
- return self.type == DIRTYPE
- def issym(self):
- return self.type == SYMTYPE
- def islnk(self):
- return self.type == LNKTYPE
- def ischr(self):
- return self.type == CHRTYPE
- def isblk(self):
- return self.type == BLKTYPE
- def isfifo(self):
- return self.type == FIFOTYPE
- def issparse(self):
- return self.sparse is not None
- def isdev(self):
- return self.type in (CHRTYPE, BLKTYPE, FIFOTYPE)
-# class TarInfo
-
-class TarFile(object):
- """The TarFile Class provides an interface to tar archives.
- """
-
- debug = 0 # May be set from 0 (no msgs) to 3 (all msgs)
-
- dereference = False # If true, add content of linked file to the
- # tar file, else the link.
-
- ignore_zeros = False # If true, skips empty or invalid blocks and
- # continues processing.
-
- errorlevel = 1 # If 0, fatal errors only appear in debug
- # messages (if debug >= 0). If > 0, errors
- # are passed to the caller as exceptions.
-
- format = DEFAULT_FORMAT # The format to use when creating an archive.
-
- encoding = ENCODING # Encoding for 8-bit character strings.
-
- errors = None # Error handler for unicode conversion.
-
- tarinfo = TarInfo # The default TarInfo class to use.
-
- fileobject = ExFileObject # The default ExFileObject class to use.
-
- def __init__(self, name=None, mode="r", fileobj=None, format=None,
- tarinfo=None, dereference=None, ignore_zeros=None, encoding=None,
- errors="surrogateescape", pax_headers=None, debug=None, errorlevel=None):
- """Open an (uncompressed) tar archive `name'. `mode' is either 'r' to
- read from an existing archive, 'a' to append data to an existing
- file or 'w' to create a new file overwriting an existing one. `mode'
- defaults to 'r'.
- If `fileobj' is given, it is used for reading or writing data. If it
- can be determined, `mode' is overridden by `fileobj's mode.
- `fileobj' is not closed, when TarFile is closed.
- """
- if len(mode) > 1 or mode not in "raw":
- raise ValueError("mode must be 'r', 'a' or 'w'")
- self.mode = mode
- self._mode = {"r": "rb", "a": "r+b", "w": "wb"}[mode]
-
- if not fileobj:
- if self.mode == "a" and not os.path.exists(name):
- # Create nonexistent files in append mode.
- self.mode = "w"
- self._mode = "wb"
- fileobj = bltn_open(name, self._mode)
- self._extfileobj = False
- else:
- if name is None and hasattr(fileobj, "name"):
- name = fileobj.name
- if hasattr(fileobj, "mode"):
- self._mode = fileobj.mode
- self._extfileobj = True
- self.name = os.path.abspath(name) if name else None
- self.fileobj = fileobj
-
- # Init attributes.
- if format is not None:
- self.format = format
- if tarinfo is not None:
- self.tarinfo = tarinfo
- if dereference is not None:
- self.dereference = dereference
- if ignore_zeros is not None:
- self.ignore_zeros = ignore_zeros
- if encoding is not None:
- self.encoding = encoding
- self.errors = errors
-
- if pax_headers is not None and self.format == PAX_FORMAT:
- self.pax_headers = pax_headers
- else:
- self.pax_headers = {}
-
- if debug is not None:
- self.debug = debug
- if errorlevel is not None:
- self.errorlevel = errorlevel
-
- # Init datastructures.
- self.closed = False
- self.members = [] # list of members as TarInfo objects
- self._loaded = False # flag if all members have been read
- self.offset = self.fileobj.tell()
- # current position in the archive file
- self.inodes = {} # dictionary caching the inodes of
- # archive members already added
-
- try:
- if self.mode == "r":
- self.firstmember = None
- self.firstmember = self.next()
-
- if self.mode == "a":
- # Move to the end of the archive,
- # before the first empty block.
- while True:
- self.fileobj.seek(self.offset)
- try:
- tarinfo = self.tarinfo.fromtarfile(self)
- self.members.append(tarinfo)
- except EOFHeaderError:
- self.fileobj.seek(self.offset)
- break
- except HeaderError as e:
- raise ReadError(str(e))
-
- if self.mode in "aw":
- self._loaded = True
-
- if self.pax_headers:
- buf = self.tarinfo.create_pax_global_header(self.pax_headers.copy())
- self.fileobj.write(buf)
- self.offset += len(buf)
- except:
- if not self._extfileobj:
- self.fileobj.close()
- self.closed = True
- raise
-
- #--------------------------------------------------------------------------
- # Below are the classmethods which act as alternate constructors to the
- # TarFile class. The open() method is the only one that is needed for
- # public use; it is the "super"-constructor and is able to select an
- # adequate "sub"-constructor for a particular compression using the mapping
- # from OPEN_METH.
- #
- # This concept allows one to subclass TarFile without losing the comfort of
- # the super-constructor. A sub-constructor is registered and made available
- # by adding it to the mapping in OPEN_METH.
-
- @classmethod
- def open(cls, name=None, mode="r", fileobj=None, bufsize=RECORDSIZE, **kwargs):
- """Open a tar archive for reading, writing or appending. Return
- an appropriate TarFile class.
-
- mode:
- 'r' or 'r:*' open for reading with transparent compression
- 'r:' open for reading exclusively uncompressed
- 'r:gz' open for reading with gzip compression
- 'r:bz2' open for reading with bzip2 compression
- 'a' or 'a:' open for appending, creating the file if necessary
- 'w' or 'w:' open for writing without compression
- 'w:gz' open for writing with gzip compression
- 'w:bz2' open for writing with bzip2 compression
-
- 'r|*' open a stream of tar blocks with transparent compression
- 'r|' open an uncompressed stream of tar blocks for reading
- 'r|gz' open a gzip compressed stream of tar blocks
- 'r|bz2' open a bzip2 compressed stream of tar blocks
- 'w|' open an uncompressed stream for writing
- 'w|gz' open a gzip compressed stream for writing
- 'w|bz2' open a bzip2 compressed stream for writing
- """
-
- if not name and not fileobj:
- raise ValueError("nothing to open")
-
- if mode in ("r", "r:*"):
- # Find out which *open() is appropriate for opening the file.
- for comptype in cls.OPEN_METH:
- func = getattr(cls, cls.OPEN_METH[comptype])
- if fileobj is not None:
- saved_pos = fileobj.tell()
- try:
- return func(name, "r", fileobj, **kwargs)
- except (ReadError, CompressionError) as e:
- if fileobj is not None:
- fileobj.seek(saved_pos)
- continue
- raise ReadError("file could not be opened successfully")
-
- elif ":" in mode:
- filemode, comptype = mode.split(":", 1)
- filemode = filemode or "r"
- comptype = comptype or "tar"
-
- # Select the *open() function according to
- # given compression.
- if comptype in cls.OPEN_METH:
- func = getattr(cls, cls.OPEN_METH[comptype])
- else:
- raise CompressionError("unknown compression type %r" % comptype)
- return func(name, filemode, fileobj, **kwargs)
-
- elif "|" in mode:
- filemode, comptype = mode.split("|", 1)
- filemode = filemode or "r"
- comptype = comptype or "tar"
-
- if filemode not in "rw":
- raise ValueError("mode must be 'r' or 'w'")
-
- stream = _Stream(name, filemode, comptype, fileobj, bufsize)
- try:
- t = cls(name, filemode, stream, **kwargs)
- except:
- stream.close()
- raise
- t._extfileobj = False
- return t
-
- elif mode in "aw":
- return cls.taropen(name, mode, fileobj, **kwargs)
-
- raise ValueError("undiscernible mode")
-
- @classmethod
- def taropen(cls, name, mode="r", fileobj=None, **kwargs):
- """Open uncompressed tar archive name for reading or writing.
- """
- if len(mode) > 1 or mode not in "raw":
- raise ValueError("mode must be 'r', 'a' or 'w'")
- return cls(name, mode, fileobj, **kwargs)
-
- @classmethod
- def gzopen(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs):
- """Open gzip compressed tar archive name for reading or writing.
- Appending is not allowed.
- """
- if len(mode) > 1 or mode not in "rw":
- raise ValueError("mode must be 'r' or 'w'")
-
- try:
- import gzip
- gzip.GzipFile
- except (ImportError, AttributeError):
- raise CompressionError("gzip module is not available")
-
- extfileobj = fileobj is not None
- try:
- fileobj = gzip.GzipFile(name, mode + "b", compresslevel, fileobj)
- t = cls.taropen(name, mode, fileobj, **kwargs)
- except IOError:
- if not extfileobj and fileobj is not None:
- fileobj.close()
- if fileobj is None:
- raise
- raise ReadError("not a gzip file")
- except:
- if not extfileobj and fileobj is not None:
- fileobj.close()
- raise
- t._extfileobj = extfileobj
- return t
-
- @classmethod
- def bz2open(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs):
- """Open bzip2 compressed tar archive name for reading or writing.
- Appending is not allowed.
- """
- if len(mode) > 1 or mode not in "rw":
- raise ValueError("mode must be 'r' or 'w'.")
-
- try:
- import bz2
- except ImportError:
- raise CompressionError("bz2 module is not available")
-
- if fileobj is not None:
- fileobj = _BZ2Proxy(fileobj, mode)
- else:
- fileobj = bz2.BZ2File(name, mode, compresslevel=compresslevel)
-
- try:
- t = cls.taropen(name, mode, fileobj, **kwargs)
- except (IOError, EOFError):
- fileobj.close()
- raise ReadError("not a bzip2 file")
- t._extfileobj = False
- return t
-
- # All *open() methods are registered here.
- OPEN_METH = {
- "tar": "taropen", # uncompressed tar
- "gz": "gzopen", # gzip compressed tar
- "bz2": "bz2open" # bzip2 compressed tar
- }
-
- #--------------------------------------------------------------------------
- # The public methods which TarFile provides:
-
- def close(self):
- """Close the TarFile. In write-mode, two finishing zero blocks are
- appended to the archive.
- """
- if self.closed:
- return
-
- if self.mode in "aw":
- self.fileobj.write(NUL * (BLOCKSIZE * 2))
- self.offset += (BLOCKSIZE * 2)
- # fill up the end with zero-blocks
- # (like option -b20 for tar does)
- blocks, remainder = divmod(self.offset, RECORDSIZE)
- if remainder > 0:
- self.fileobj.write(NUL * (RECORDSIZE - remainder))
-
- if not self._extfileobj:
- self.fileobj.close()
- self.closed = True
-
- def getmember(self, name):
- """Return a TarInfo object for member `name'. If `name' can not be
- found in the archive, KeyError is raised. If a member occurs more
- than once in the archive, its last occurrence is assumed to be the
- most up-to-date version.
- """
- tarinfo = self._getmember(name)
- if tarinfo is None:
- raise KeyError("filename %r not found" % name)
- return tarinfo
-
- def getmembers(self):
- """Return the members of the archive as a list of TarInfo objects. The
- list has the same order as the members in the archive.
- """
- self._check()
- if not self._loaded: # if we want to obtain a list of
- self._load() # all members, we first have to
- # scan the whole archive.
- return self.members
-
- def getnames(self):
- """Return the members of the archive as a list of their names. It has
- the same order as the list returned by getmembers().
- """
- return [tarinfo.name for tarinfo in self.getmembers()]
-
- def gettarinfo(self, name=None, arcname=None, fileobj=None):
- """Create a TarInfo object for either the file `name' or the file
- object `fileobj' (using os.fstat on its file descriptor). You can
- modify some of the TarInfo's attributes before you add it using
- addfile(). If given, `arcname' specifies an alternative name for the
- file in the archive.
- """
- self._check("aw")
-
- # When fileobj is given, replace name by
- # fileobj's real name.
- if fileobj is not None:
- name = fileobj.name
-
- # Building the name of the member in the archive.
- # Backward slashes are converted to forward slashes,
- # Absolute paths are turned to relative paths.
- if arcname is None:
- arcname = name
- drv, arcname = os.path.splitdrive(arcname)
- arcname = arcname.replace(os.sep, "/")
- arcname = arcname.lstrip("/")
-
- # Now, fill the TarInfo object with
- # information specific for the file.
- tarinfo = self.tarinfo()
- tarinfo.tarfile = self
-
- # Use os.stat or os.lstat, depending on platform
- # and if symlinks shall be resolved.
- if fileobj is None:
- if hasattr(os, "lstat") and not self.dereference:
- statres = os.lstat(name)
- else:
- statres = os.stat(name)
- else:
- statres = os.fstat(fileobj.fileno())
- linkname = ""
-
- stmd = statres.st_mode
- if stat.S_ISREG(stmd):
- inode = (statres.st_ino, statres.st_dev)
- if not self.dereference and statres.st_nlink > 1 and \
- inode in self.inodes and arcname != self.inodes[inode]:
- # Is it a hardlink to an already
- # archived file?
- type = LNKTYPE
- linkname = self.inodes[inode]
- else:
- # The inode is added only if its valid.
- # For win32 it is always 0.
- type = REGTYPE
- if inode[0]:
- self.inodes[inode] = arcname
- elif stat.S_ISDIR(stmd):
- type = DIRTYPE
- elif stat.S_ISFIFO(stmd):
- type = FIFOTYPE
- elif stat.S_ISLNK(stmd):
- type = SYMTYPE
- linkname = os.readlink(name)
- elif stat.S_ISCHR(stmd):
- type = CHRTYPE
- elif stat.S_ISBLK(stmd):
- type = BLKTYPE
- else:
- return None
-
- # Fill the TarInfo object with all
- # information we can get.
- tarinfo.name = arcname
- tarinfo.mode = stmd
- tarinfo.uid = statres.st_uid
- tarinfo.gid = statres.st_gid
- if type == REGTYPE:
- tarinfo.size = statres.st_size
- else:
- tarinfo.size = 0
- tarinfo.mtime = statres.st_mtime
- tarinfo.type = type
- tarinfo.linkname = linkname
- if pwd:
- try:
- tarinfo.uname = pwd.getpwuid(tarinfo.uid)[0]
- except KeyError:
- pass
- if grp:
- try:
- tarinfo.gname = grp.getgrgid(tarinfo.gid)[0]
- except KeyError:
- pass
-
- if type in (CHRTYPE, BLKTYPE):
- if hasattr(os, "major") and hasattr(os, "minor"):
- tarinfo.devmajor = os.major(statres.st_rdev)
- tarinfo.devminor = os.minor(statres.st_rdev)
- return tarinfo
-
- def list(self, verbose=True):
- """Print a table of contents to sys.stdout. If `verbose' is False, only
- the names of the members are printed. If it is True, an `ls -l'-like
- output is produced.
- """
- self._check()
-
- for tarinfo in self:
- if verbose:
- print(filemode(tarinfo.mode), end=' ')
- print("%s/%s" % (tarinfo.uname or tarinfo.uid,
- tarinfo.gname or tarinfo.gid), end=' ')
- if tarinfo.ischr() or tarinfo.isblk():
- print("%10s" % ("%d,%d" \
- % (tarinfo.devmajor, tarinfo.devminor)), end=' ')
- else:
- print("%10d" % tarinfo.size, end=' ')
- print("%d-%02d-%02d %02d:%02d:%02d" \
- % time.localtime(tarinfo.mtime)[:6], end=' ')
-
- print(tarinfo.name + ("/" if tarinfo.isdir() else ""), end=' ')
-
- if verbose:
- if tarinfo.issym():
- print("->", tarinfo.linkname, end=' ')
- if tarinfo.islnk():
- print("link to", tarinfo.linkname, end=' ')
- print()
-
- def add(self, name, arcname=None, recursive=True, exclude=None, filter=None):
- """Add the file `name' to the archive. `name' may be any type of file
- (directory, fifo, symbolic link, etc.). If given, `arcname'
- specifies an alternative name for the file in the archive.
- Directories are added recursively by default. This can be avoided by
- setting `recursive' to False. `exclude' is a function that should
- return True for each filename to be excluded. `filter' is a function
- that expects a TarInfo object argument and returns the changed
- TarInfo object, if it returns None the TarInfo object will be
- excluded from the archive.
- """
- self._check("aw")
-
- if arcname is None:
- arcname = name
-
- # Exclude pathnames.
- if exclude is not None:
- import warnings
- warnings.warn("use the filter argument instead",
- DeprecationWarning, 2)
- if exclude(name):
- self._dbg(2, "tarfile: Excluded %r" % name)
- return
-
- # Skip if somebody tries to archive the archive...
- if self.name is not None and os.path.abspath(name) == self.name:
- self._dbg(2, "tarfile: Skipped %r" % name)
- return
-
- self._dbg(1, name)
-
- # Create a TarInfo object from the file.
- tarinfo = self.gettarinfo(name, arcname)
-
- if tarinfo is None:
- self._dbg(1, "tarfile: Unsupported type %r" % name)
- return
-
- # Change or exclude the TarInfo object.
- if filter is not None:
- tarinfo = filter(tarinfo)
- if tarinfo is None:
- self._dbg(2, "tarfile: Excluded %r" % name)
- return
-
- # Append the tar header and data to the archive.
- if tarinfo.isreg():
- f = bltn_open(name, "rb")
- self.addfile(tarinfo, f)
- f.close()
-
- elif tarinfo.isdir():
- self.addfile(tarinfo)
- if recursive:
- for f in os.listdir(name):
- self.add(os.path.join(name, f), os.path.join(arcname, f),
- recursive, exclude, filter=filter)
-
- else:
- self.addfile(tarinfo)
-
- def addfile(self, tarinfo, fileobj=None):
- """Add the TarInfo object `tarinfo' to the archive. If `fileobj' is
- given, tarinfo.size bytes are read from it and added to the archive.
- You can create TarInfo objects using gettarinfo().
- On Windows platforms, `fileobj' should always be opened with mode
- 'rb' to avoid irritation about the file size.
- """
- self._check("aw")
-
- tarinfo = copy.copy(tarinfo)
-
- buf = tarinfo.tobuf(self.format, self.encoding, self.errors)
- self.fileobj.write(buf)
- self.offset += len(buf)
-
- # If there's data to follow, append it.
- if fileobj is not None:
- copyfileobj(fileobj, self.fileobj, tarinfo.size)
- blocks, remainder = divmod(tarinfo.size, BLOCKSIZE)
- if remainder > 0:
- self.fileobj.write(NUL * (BLOCKSIZE - remainder))
- blocks += 1
- self.offset += blocks * BLOCKSIZE
-
- self.members.append(tarinfo)
-
- def extractall(self, path=".", members=None):
- """Extract all members from the archive to the current working
- directory and set owner, modification time and permissions on
- directories afterwards. `path' specifies a different directory
- to extract to. `members' is optional and must be a subset of the
- list returned by getmembers().
- """
- directories = []
-
- if members is None:
- members = self
-
- for tarinfo in members:
- if tarinfo.isdir():
- # Extract directories with a safe mode.
- directories.append(tarinfo)
- tarinfo = copy.copy(tarinfo)
- tarinfo.mode = 0o700
- # Do not set_attrs directories, as we will do that further down
- self.extract(tarinfo, path, set_attrs=not tarinfo.isdir())
-
- # Reverse sort directories.
- directories.sort(key=lambda a: a.name)
- directories.reverse()
-
- # Set correct owner, mtime and filemode on directories.
- for tarinfo in directories:
- dirpath = os.path.join(path, tarinfo.name)
- try:
- self.chown(tarinfo, dirpath)
- self.utime(tarinfo, dirpath)
- self.chmod(tarinfo, dirpath)
- except ExtractError as e:
- if self.errorlevel > 1:
- raise
- else:
- self._dbg(1, "tarfile: %s" % e)
-
- def extract(self, member, path="", set_attrs=True):
- """Extract a member from the archive to the current working directory,
- using its full name. Its file information is extracted as accurately
- as possible. `member' may be a filename or a TarInfo object. You can
- specify a different directory using `path'. File attributes (owner,
- mtime, mode) are set unless `set_attrs' is False.
- """
- self._check("r")
-
- if isinstance(member, str):
- tarinfo = self.getmember(member)
- else:
- tarinfo = member
-
- # Prepare the link target for makelink().
- if tarinfo.islnk():
- tarinfo._link_target = os.path.join(path, tarinfo.linkname)
-
- try:
- self._extract_member(tarinfo, os.path.join(path, tarinfo.name),
- set_attrs=set_attrs)
- except EnvironmentError as e:
- if self.errorlevel > 0:
- raise
- else:
- if e.filename is None:
- self._dbg(1, "tarfile: %s" % e.strerror)
- else:
- self._dbg(1, "tarfile: %s %r" % (e.strerror, e.filename))
- except ExtractError as e:
- if self.errorlevel > 1:
- raise
- else:
- self._dbg(1, "tarfile: %s" % e)
-
- def extractfile(self, member):
- """Extract a member from the archive as a file object. `member' may be
- a filename or a TarInfo object. If `member' is a regular file, a
- file-like object is returned. If `member' is a link, a file-like
- object is constructed from the link's target. If `member' is none of
- the above, None is returned.
- The file-like object is read-only and provides the following
- methods: read(), readline(), readlines(), seek() and tell()
- """
- self._check("r")
-
- if isinstance(member, str):
- tarinfo = self.getmember(member)
- else:
- tarinfo = member
-
- if tarinfo.isreg():
- return self.fileobject(self, tarinfo)
-
- elif tarinfo.type not in SUPPORTED_TYPES:
- # If a member's type is unknown, it is treated as a
- # regular file.
- return self.fileobject(self, tarinfo)
-
- elif tarinfo.islnk() or tarinfo.issym():
- if isinstance(self.fileobj, _Stream):
- # A small but ugly workaround for the case that someone tries
- # to extract a (sym)link as a file-object from a non-seekable
- # stream of tar blocks.
- raise StreamError("cannot extract (sym)link as file object")
- else:
- # A (sym)link's file object is its target's file object.
- return self.extractfile(self._find_link_target(tarinfo))
- else:
- # If there's no data associated with the member (directory, chrdev,
- # blkdev, etc.), return None instead of a file object.
- return None
-
- def _extract_member(self, tarinfo, targetpath, set_attrs=True):
- """Extract the TarInfo object tarinfo to a physical
- file called targetpath.
- """
- # Fetch the TarInfo object for the given name
- # and build the destination pathname, replacing
- # forward slashes to platform specific separators.
- targetpath = targetpath.rstrip("/")
- targetpath = targetpath.replace("/", os.sep)
-
- # Create all upper directories.
- upperdirs = os.path.dirname(targetpath)
- if upperdirs and not os.path.exists(upperdirs):
- # Create directories that are not part of the archive with
- # default permissions.
- os.makedirs(upperdirs)
-
- if tarinfo.islnk() or tarinfo.issym():
- self._dbg(1, "%s -> %s" % (tarinfo.name, tarinfo.linkname))
- else:
- self._dbg(1, tarinfo.name)
-
- if tarinfo.isreg():
- self.makefile(tarinfo, targetpath)
- elif tarinfo.isdir():
- self.makedir(tarinfo, targetpath)
- elif tarinfo.isfifo():
- self.makefifo(tarinfo, targetpath)
- elif tarinfo.ischr() or tarinfo.isblk():
- self.makedev(tarinfo, targetpath)
- elif tarinfo.islnk() or tarinfo.issym():
- self.makelink(tarinfo, targetpath)
- elif tarinfo.type not in SUPPORTED_TYPES:
- self.makeunknown(tarinfo, targetpath)
- else:
- self.makefile(tarinfo, targetpath)
-
- if set_attrs:
- self.chown(tarinfo, targetpath)
- if not tarinfo.issym():
- self.chmod(tarinfo, targetpath)
- self.utime(tarinfo, targetpath)
-
- #--------------------------------------------------------------------------
- # Below are the different file methods. They are called via
- # _extract_member() when extract() is called. They can be replaced in a
- # subclass to implement other functionality.
-
- def makedir(self, tarinfo, targetpath):
- """Make a directory called targetpath.
- """
- try:
- # Use a safe mode for the directory, the real mode is set
- # later in _extract_member().
- os.mkdir(targetpath, 0o700)
- except EnvironmentError as e:
- if e.errno != errno.EEXIST:
- raise
-
- def makefile(self, tarinfo, targetpath):
- """Make a file called targetpath.
- """
- source = self.fileobj
- source.seek(tarinfo.offset_data)
- target = bltn_open(targetpath, "wb")
- if tarinfo.sparse is not None:
- for offset, size in tarinfo.sparse:
- target.seek(offset)
- copyfileobj(source, target, size)
- else:
- copyfileobj(source, target, tarinfo.size)
- target.seek(tarinfo.size)
- target.truncate()
- target.close()
-
- def makeunknown(self, tarinfo, targetpath):
- """Make a file from a TarInfo object with an unknown type
- at targetpath.
- """
- self.makefile(tarinfo, targetpath)
- self._dbg(1, "tarfile: Unknown file type %r, " \
- "extracted as regular file." % tarinfo.type)
-
- def makefifo(self, tarinfo, targetpath):
- """Make a fifo called targetpath.
- """
- if hasattr(os, "mkfifo"):
- os.mkfifo(targetpath)
- else:
- raise ExtractError("fifo not supported by system")
-
- def makedev(self, tarinfo, targetpath):
- """Make a character or block device called targetpath.
- """
- if not hasattr(os, "mknod") or not hasattr(os, "makedev"):
- raise ExtractError("special devices not supported by system")
-
- mode = tarinfo.mode
- if tarinfo.isblk():
- mode |= stat.S_IFBLK
- else:
- mode |= stat.S_IFCHR
-
- os.mknod(targetpath, mode,
- os.makedev(tarinfo.devmajor, tarinfo.devminor))
-
- def makelink(self, tarinfo, targetpath):
- """Make a (symbolic) link called targetpath. If it cannot be created
- (platform limitation), we try to make a copy of the referenced file
- instead of a link.
- """
- try:
- # For systems that support symbolic and hard links.
- if tarinfo.issym():
- os.symlink(tarinfo.linkname, targetpath)
- else:
- # See extract().
- if os.path.exists(tarinfo._link_target):
- os.link(tarinfo._link_target, targetpath)
- else:
- self._extract_member(self._find_link_target(tarinfo),
- targetpath)
- except symlink_exception:
- if tarinfo.issym():
- linkpath = os.path.join(os.path.dirname(tarinfo.name),
- tarinfo.linkname)
- else:
- linkpath = tarinfo.linkname
- else:
- try:
- self._extract_member(self._find_link_target(tarinfo),
- targetpath)
- except KeyError:
- raise ExtractError("unable to resolve link inside archive")
-
- def chown(self, tarinfo, targetpath):
- """Set owner of targetpath according to tarinfo.
- """
- if pwd and hasattr(os, "geteuid") and os.geteuid() == 0:
- # We have to be root to do so.
- try:
- g = grp.getgrnam(tarinfo.gname)[2]
- except KeyError:
- g = tarinfo.gid
- try:
- u = pwd.getpwnam(tarinfo.uname)[2]
- except KeyError:
- u = tarinfo.uid
- try:
- if tarinfo.issym() and hasattr(os, "lchown"):
- os.lchown(targetpath, u, g)
- else:
- if sys.platform != "os2emx":
- os.chown(targetpath, u, g)
- except EnvironmentError as e:
- raise ExtractError("could not change owner")
-
- def chmod(self, tarinfo, targetpath):
- """Set file permissions of targetpath according to tarinfo.
- """
- if hasattr(os, 'chmod'):
- try:
- os.chmod(targetpath, tarinfo.mode)
- except EnvironmentError as e:
- raise ExtractError("could not change mode")
-
- def utime(self, tarinfo, targetpath):
- """Set modification time of targetpath according to tarinfo.
- """
- if not hasattr(os, 'utime'):
- return
- try:
- os.utime(targetpath, (tarinfo.mtime, tarinfo.mtime))
- except EnvironmentError as e:
- raise ExtractError("could not change modification time")
-
- #--------------------------------------------------------------------------
- def next(self):
- """Return the next member of the archive as a TarInfo object, when
- TarFile is opened for reading. Return None if there is no more
- available.
- """
- self._check("ra")
- if self.firstmember is not None:
- m = self.firstmember
- self.firstmember = None
- return m
-
- # Read the next block.
- self.fileobj.seek(self.offset)
- tarinfo = None
- while True:
- try:
- tarinfo = self.tarinfo.fromtarfile(self)
- except EOFHeaderError as e:
- if self.ignore_zeros:
- self._dbg(2, "0x%X: %s" % (self.offset, e))
- self.offset += BLOCKSIZE
- continue
- except InvalidHeaderError as e:
- if self.ignore_zeros:
- self._dbg(2, "0x%X: %s" % (self.offset, e))
- self.offset += BLOCKSIZE
- continue
- elif self.offset == 0:
- raise ReadError(str(e))
- except EmptyHeaderError:
- if self.offset == 0:
- raise ReadError("empty file")
- except TruncatedHeaderError as e:
- if self.offset == 0:
- raise ReadError(str(e))
- except SubsequentHeaderError as e:
- raise ReadError(str(e))
- break
-
- if tarinfo is not None:
- self.members.append(tarinfo)
- else:
- self._loaded = True
-
- return tarinfo
-
- #--------------------------------------------------------------------------
- # Little helper methods:
-
- def _getmember(self, name, tarinfo=None, normalize=False):
- """Find an archive member by name from bottom to top.
- If tarinfo is given, it is used as the starting point.
- """
- # Ensure that all members have been loaded.
- members = self.getmembers()
-
- # Limit the member search list up to tarinfo.
- if tarinfo is not None:
- members = members[:members.index(tarinfo)]
-
- if normalize:
- name = os.path.normpath(name)
-
- for member in reversed(members):
- if normalize:
- member_name = os.path.normpath(member.name)
- else:
- member_name = member.name
-
- if name == member_name:
- return member
-
- def _load(self):
- """Read through the entire archive file and look for readable
- members.
- """
- while True:
- tarinfo = self.next()
- if tarinfo is None:
- break
- self._loaded = True
-
- def _check(self, mode=None):
- """Check if TarFile is still open, and if the operation's mode
- corresponds to TarFile's mode.
- """
- if self.closed:
- raise IOError("%s is closed" % self.__class__.__name__)
- if mode is not None and self.mode not in mode:
- raise IOError("bad operation for mode %r" % self.mode)
-
- def _find_link_target(self, tarinfo):
- """Find the target member of a symlink or hardlink member in the
- archive.
- """
- if tarinfo.issym():
- # Always search the entire archive.
- linkname = os.path.dirname(tarinfo.name) + "/" + tarinfo.linkname
- limit = None
- else:
- # Search the archive before the link, because a hard link is
- # just a reference to an already archived file.
- linkname = tarinfo.linkname
- limit = tarinfo
-
- member = self._getmember(linkname, tarinfo=limit, normalize=True)
- if member is None:
- raise KeyError("linkname %r not found" % linkname)
- return member
-
- def __iter__(self):
- """Provide an iterator object.
- """
- if self._loaded:
- return iter(self.members)
- else:
- return TarIter(self)
-
- def _dbg(self, level, msg):
- """Write debugging output to sys.stderr.
- """
- if level <= self.debug:
- print(msg, file=sys.stderr)
-
- def __enter__(self):
- self._check()
- return self
-
- def __exit__(self, type, value, traceback):
- if type is None:
- self.close()
- else:
- # An exception occurred. We must not call close() because
- # it would try to write end-of-archive blocks and padding.
- if not self._extfileobj:
- self.fileobj.close()
- self.closed = True
-# class TarFile
-
-class TarIter(object):
- """Iterator Class.
-
- for tarinfo in TarFile(...):
- suite...
- """
-
- def __init__(self, tarfile):
- """Construct a TarIter object.
- """
- self.tarfile = tarfile
- self.index = 0
- def __iter__(self):
- """Return iterator object.
- """
- return self
-
- def __next__(self):
- """Return the next item using TarFile's next() method.
- When all members have been read, set TarFile as _loaded.
- """
- # Fix for SF #1100429: Under rare circumstances it can
- # happen that getmembers() is called during iteration,
- # which will cause TarIter to stop prematurely.
- if not self.tarfile._loaded:
- tarinfo = self.tarfile.next()
- if not tarinfo:
- self.tarfile._loaded = True
- raise StopIteration
- else:
- try:
- tarinfo = self.tarfile.members[self.index]
- except IndexError:
- raise StopIteration
- self.index += 1
- return tarinfo
-
- next = __next__ # for Python 2.x
-
-#--------------------
-# exported functions
-#--------------------
-def is_tarfile(name):
- """Return True if name points to a tar archive that we
- are able to handle, else return False.
- """
- try:
- t = open(name)
- t.close()
- return True
- except TarError:
- return False
-
-bltn_open = open
-open = TarFile.open
diff --git a/src/pip/_vendor/distlib/compat.py b/src/pip/_vendor/distlib/compat.py
index c316fd973..1fe3d225a 100644
--- a/src/pip/_vendor/distlib/compat.py
+++ b/src/pip/_vendor/distlib/compat.py
@@ -22,7 +22,6 @@ if sys.version_info[0] < 3: # pragma: no cover
from types import FileType as file_type
import __builtin__ as builtins
import ConfigParser as configparser
- from ._backport import shutil
from urlparse import urlparse, urlunparse, urljoin, urlsplit, urlunsplit
from urllib import (urlretrieve, quote as _quote, unquote, url2pathname,
pathname2url, ContentTooShortError, splittype)
@@ -48,17 +47,18 @@ if sys.version_info[0] < 3: # pragma: no cover
from itertools import ifilter as filter
from itertools import ifilterfalse as filterfalse
- _userprog = None
- def splituser(host):
- """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'."""
- global _userprog
- if _userprog is None:
- import re
- _userprog = re.compile('^(.*)@(.*)$')
+ # Leaving this around for now, in case it needs resurrecting in some way
+ # _userprog = None
+ # def splituser(host):
+ # """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'."""
+ # global _userprog
+ # if _userprog is None:
+ # import re
+ # _userprog = re.compile('^(.*)@(.*)$')
- match = _userprog.match(host)
- if match: return match.group(1, 2)
- return None, host
+ # match = _userprog.match(host)
+ # if match: return match.group(1, 2)
+ # return None, host
else: # pragma: no cover
from io import StringIO
@@ -68,7 +68,7 @@ else: # pragma: no cover
import builtins
import configparser
import shutil
- from urllib.parse import (urlparse, urlunparse, urljoin, splituser, quote,
+ from urllib.parse import (urlparse, urlunparse, urljoin, quote,
unquote, urlsplit, urlunsplit, splittype)
from urllib.request import (urlopen, urlretrieve, Request, url2pathname,
pathname2url,
@@ -88,6 +88,7 @@ else: # pragma: no cover
from itertools import filterfalse
filter = filter
+
try:
from ssl import match_hostname, CertificateError
except ImportError: # pragma: no cover
@@ -311,10 +312,8 @@ except ImportError: # pragma: no cover
return 'IronPython'
return 'CPython'
-try:
- import sysconfig
-except ImportError: # pragma: no cover
- from ._backport import sysconfig
+import shutil
+import sysconfig
try:
callable = callable
@@ -616,18 +615,15 @@ except ImportError: # pragma: no cover
try:
from importlib.util import cache_from_source # Python >= 3.4
except ImportError: # pragma: no cover
- try:
- from imp import cache_from_source
- except ImportError: # pragma: no cover
- def cache_from_source(path, debug_override=None):
- assert path.endswith('.py')
- if debug_override is None:
- debug_override = __debug__
- if debug_override:
- suffix = 'c'
- else:
- suffix = 'o'
- return path + suffix
+ def cache_from_source(path, debug_override=None):
+ assert path.endswith('.py')
+ if debug_override is None:
+ debug_override = __debug__
+ if debug_override:
+ suffix = 'c'
+ else:
+ suffix = 'o'
+ return path + suffix
try:
from collections import OrderedDict
diff --git a/src/pip/_vendor/distlib/database.py b/src/pip/_vendor/distlib/database.py
index 0a90c300b..5db5d7f50 100644
--- a/src/pip/_vendor/distlib/database.py
+++ b/src/pip/_vendor/distlib/database.py
@@ -132,29 +132,35 @@ class DistributionPath(object):
r = finder.find(entry)
if not r or r.path in seen:
continue
- if self._include_dist and entry.endswith(DISTINFO_EXT):
- possible_filenames = [METADATA_FILENAME,
- WHEEL_METADATA_FILENAME,
- LEGACY_METADATA_FILENAME]
- for metadata_filename in possible_filenames:
- metadata_path = posixpath.join(entry, metadata_filename)
- pydist = finder.find(metadata_path)
- if pydist:
- break
- else:
- continue
+ try:
+ if self._include_dist and entry.endswith(DISTINFO_EXT):
+ possible_filenames = [METADATA_FILENAME,
+ WHEEL_METADATA_FILENAME,
+ LEGACY_METADATA_FILENAME]
+ for metadata_filename in possible_filenames:
+ metadata_path = posixpath.join(entry, metadata_filename)
+ pydist = finder.find(metadata_path)
+ if pydist:
+ break
+ else:
+ continue
- with contextlib.closing(pydist.as_stream()) as stream:
- metadata = Metadata(fileobj=stream, scheme='legacy')
- logger.debug('Found %s', r.path)
- seen.add(r.path)
- yield new_dist_class(r.path, metadata=metadata,
- env=self)
- elif self._include_egg and entry.endswith(('.egg-info',
- '.egg')):
- logger.debug('Found %s', r.path)
- seen.add(r.path)
- yield old_dist_class(r.path, self)
+ with contextlib.closing(pydist.as_stream()) as stream:
+ metadata = Metadata(fileobj=stream, scheme='legacy')
+ logger.debug('Found %s', r.path)
+ seen.add(r.path)
+ yield new_dist_class(r.path, metadata=metadata,
+ env=self)
+ elif self._include_egg and entry.endswith(('.egg-info',
+ '.egg')):
+ logger.debug('Found %s', r.path)
+ seen.add(r.path)
+ yield old_dist_class(r.path, self)
+ except Exception as e:
+ msg = 'Unable to read distribution at %s, perhaps due to bad metadata: %s'
+ logger.warning(msg, r.path, e)
+ import warnings
+ warnings.warn(msg % (r.path, e), stacklevel=2)
def _generate_cache(self):
"""
@@ -379,8 +385,9 @@ class Distribution(object):
def _get_requirements(self, req_attr):
md = self.metadata
- logger.debug('Getting requirements from metadata %r', md.todict())
reqts = getattr(md, req_attr)
+ logger.debug('%s: got requirements %r from metadata: %r', self.name, req_attr,
+ reqts)
return set(md.get_requirements(reqts, extras=self.extras,
env=self.context))
@@ -1308,22 +1315,26 @@ def get_required_dists(dists, dist):
:param dists: a list of distributions
:param dist: a distribution, member of *dists* for which we are interested
+ in finding the dependencies.
"""
if dist not in dists:
raise DistlibException('given distribution %r is not a member '
'of the list' % dist.name)
graph = make_graph(dists)
- req = [] # required distributions
+ req = set() # required distributions
todo = graph.adjacency_list[dist] # list of nodes we should inspect
+ seen = set(t[0] for t in todo) # already added to todo
while todo:
d = todo.pop()[0]
- req.append(d)
- for pred in graph.adjacency_list[d]:
- if pred not in req:
+ req.add(d)
+ pred_list = graph.adjacency_list[d]
+ for pred in pred_list:
+ d = pred[0]
+ if d not in req and d not in seen:
+ seen.add(d)
todo.append(pred)
-
return req
diff --git a/src/pip/_vendor/distlib/index.py b/src/pip/_vendor/distlib/index.py
index b1fbbf8e8..9b6d129ed 100644
--- a/src/pip/_vendor/distlib/index.py
+++ b/src/pip/_vendor/distlib/index.py
@@ -12,7 +12,7 @@ import subprocess
import tempfile
try:
from threading import Thread
-except ImportError:
+except ImportError: # pragma: no cover
from dummy_threading import Thread
from . import DistlibException
@@ -104,7 +104,7 @@ class PackageIndex(object):
pm.add_password(self.realm, netloc, self.username, self.password)
self.password_handler = HTTPBasicAuthHandler(pm)
- def register(self, metadata):
+ def register(self, metadata): # pragma: no cover
"""
Register a distribution on PyPI, using the provided metadata.
@@ -142,8 +142,7 @@ class PackageIndex(object):
logger.debug('%s: %s' % (name, s))
stream.close()
- def get_sign_command(self, filename, signer, sign_password,
- keystore=None):
+ def get_sign_command(self, filename, signer, sign_password, keystore=None): # pragma: no cover
"""
Return a suitable command for signing a file.
@@ -206,7 +205,7 @@ class PackageIndex(object):
t2.join()
return p.returncode, stdout, stderr
- def sign_file(self, filename, signer, sign_password, keystore=None):
+ def sign_file(self, filename, signer, sign_password, keystore=None): # pragma: no cover
"""
Sign a file.
@@ -286,7 +285,7 @@ class PackageIndex(object):
request = self.encode_request(d.items(), files)
return self.send_request(request)
- def upload_documentation(self, metadata, doc_dir):
+ def upload_documentation(self, metadata, doc_dir): # pragma: no cover
"""
Upload documentation to the index.
@@ -499,7 +498,7 @@ class PackageIndex(object):
}
return Request(self.url, body, headers)
- def search(self, terms, operator=None):
+ def search(self, terms, operator=None): # pragma: no cover
if isinstance(terms, string_types):
terms = {'name': terms}
rpc_proxy = ServerProxy(self.url, timeout=3.0)
diff --git a/src/pip/_vendor/distlib/locators.py b/src/pip/_vendor/distlib/locators.py
index 0c7d63914..966ebc0e3 100644
--- a/src/pip/_vendor/distlib/locators.py
+++ b/src/pip/_vendor/distlib/locators.py
@@ -633,7 +633,7 @@ class SimpleScrapingLocator(Locator):
self._threads = []
for i in range(self.num_workers):
t = threading.Thread(target=self._fetch)
- t.setDaemon(True)
+ t.daemon = True
t.start()
self._threads.append(t)
@@ -1053,9 +1053,9 @@ class AggregatingLocator(Locator):
# We use a legacy scheme simply because most of the dists on PyPI use legacy
-# versions which don't conform to PEP 426 / PEP 440.
+# versions which don't conform to PEP 440.
default_locator = AggregatingLocator(
- JSONLocator(),
+ # JSONLocator(), # don't use as PEP 426 is withdrawn
SimpleScrapingLocator('https://pypi.org/simple/',
timeout=3.0),
scheme='legacy')
diff --git a/src/pip/_vendor/distlib/markers.py b/src/pip/_vendor/distlib/markers.py
index 923a832b2..9dc684103 100644
--- a/src/pip/_vendor/distlib/markers.py
+++ b/src/pip/_vendor/distlib/markers.py
@@ -13,19 +13,29 @@ Parser for the environment markers micro-language defined in PEP 508.
# as ~= and === which aren't in Python, necessitating a different approach.
import os
+import re
import sys
import platform
from .compat import string_types
from .util import in_venv, parse_marker
+from .version import NormalizedVersion as NV
__all__ = ['interpret']
+_VERSION_PATTERN = re.compile(r'((\d+(\.\d+)*\w*)|\'(\d+(\.\d+)*\w*)\'|\"(\d+(\.\d+)*\w*)\")')
+
def _is_literal(o):
if not isinstance(o, string_types) or not o:
return False
return o[0] in '\'"'
+def _get_versions(s):
+ result = []
+ for m in _VERSION_PATTERN.finditer(s):
+ result.append(NV(m.groups()[0]))
+ return set(result)
+
class Evaluator(object):
"""
This class is used to evaluate marker expessions.
@@ -70,9 +80,18 @@ class Evaluator(object):
lhs = self.evaluate(elhs, context)
rhs = self.evaluate(erhs, context)
+ if ((elhs == 'python_version' or erhs == 'python_version') and
+ op in ('<', '<=', '>', '>=', '===', '==', '!=', '~=')):
+ lhs = NV(lhs)
+ rhs = NV(rhs)
+ elif elhs == 'python_version' and op in ('in', 'not in'):
+ lhs = NV(lhs)
+ rhs = _get_versions(rhs)
result = self.operations[op](lhs, rhs)
return result
+_DIGITS = re.compile(r'\d+\.\d+')
+
def default_context():
def format_full_version(info):
version = '%s.%s.%s' % (info.major, info.minor, info.micro)
@@ -88,6 +107,9 @@ def default_context():
implementation_version = '0'
implementation_name = ''
+ ppv = platform.python_version()
+ m = _DIGITS.match(ppv)
+ pv = m.group(0)
result = {
'implementation_name': implementation_name,
'implementation_version': implementation_version,
@@ -98,8 +120,8 @@ def default_context():
'platform_system': platform.system(),
'platform_version': platform.version(),
'platform_in_venv': str(in_venv()),
- 'python_full_version': platform.python_version(),
- 'python_version': platform.python_version()[:3],
+ 'python_full_version': ppv,
+ 'python_version': pv,
'sys_platform': sys.platform,
}
return result
diff --git a/src/pip/_vendor/distlib/metadata.py b/src/pip/_vendor/distlib/metadata.py
index 6a26b0ab2..c329e1977 100644
--- a/src/pip/_vendor/distlib/metadata.py
+++ b/src/pip/_vendor/distlib/metadata.py
@@ -5,7 +5,7 @@
#
"""Implementation of the Metadata for Python packages PEPs.
-Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1 and withdrawn 2.0).
+Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1 and 2.2).
"""
from __future__ import unicode_literals
@@ -100,12 +100,17 @@ _566_FIELDS = _426_FIELDS + ('Description-Content-Type',
_566_MARKERS = ('Description-Content-Type',)
+_643_MARKERS = ('Dynamic', 'License-File')
+
+_643_FIELDS = _566_FIELDS + _643_MARKERS
+
_ALL_FIELDS = set()
_ALL_FIELDS.update(_241_FIELDS)
_ALL_FIELDS.update(_314_FIELDS)
_ALL_FIELDS.update(_345_FIELDS)
_ALL_FIELDS.update(_426_FIELDS)
_ALL_FIELDS.update(_566_FIELDS)
+_ALL_FIELDS.update(_643_FIELDS)
EXTRA_RE = re.compile(r'''extra\s*==\s*("([^"]+)"|'([^']+)')''')
@@ -121,7 +126,10 @@ def _version2fieldlist(version):
# avoid adding field names if already there
return _345_FIELDS + tuple(f for f in _566_FIELDS if f not in _345_FIELDS)
elif version == '2.0':
- return _426_FIELDS
+ raise ValueError('Metadata 2.0 is withdrawn and not supported')
+ # return _426_FIELDS
+ elif version == '2.2':
+ return _643_FIELDS
raise MetadataUnrecognizedVersionError(version)
@@ -139,7 +147,7 @@ def _best_version(fields):
continue
keys.append(key)
- possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.0', '2.1']
+ possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.1', '2.2'] # 2.0 removed
# first let's try to see if a field is not part of one of the version
for key in keys:
@@ -159,9 +167,12 @@ def _best_version(fields):
if key != 'Description': # In 2.1, description allowed after headers
possible_versions.remove('2.1')
logger.debug('Removed 2.1 due to %s', key)
- if key not in _426_FIELDS and '2.0' in possible_versions:
- possible_versions.remove('2.0')
- logger.debug('Removed 2.0 due to %s', key)
+ if key not in _643_FIELDS and '2.2' in possible_versions:
+ possible_versions.remove('2.2')
+ logger.debug('Removed 2.2 due to %s', key)
+ # if key not in _426_FIELDS and '2.0' in possible_versions:
+ # possible_versions.remove('2.0')
+ # logger.debug('Removed 2.0 due to %s', key)
# possible_version contains qualified versions
if len(possible_versions) == 1:
@@ -174,16 +185,18 @@ def _best_version(fields):
is_1_1 = '1.1' in possible_versions and _has_marker(keys, _314_MARKERS)
is_1_2 = '1.2' in possible_versions and _has_marker(keys, _345_MARKERS)
is_2_1 = '2.1' in possible_versions and _has_marker(keys, _566_MARKERS)
- is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS)
- if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_0) > 1:
- raise MetadataConflictError('You used incompatible 1.1/1.2/2.0/2.1 fields')
+ # is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS)
+ is_2_2 = '2.2' in possible_versions and _has_marker(keys, _643_MARKERS)
+ if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_2) > 1:
+ raise MetadataConflictError('You used incompatible 1.1/1.2/2.1/2.2 fields')
- # we have the choice, 1.0, or 1.2, or 2.0
+ # we have the choice, 1.0, or 1.2, 2.1 or 2.2
# - 1.0 has a broken Summary field but works with all tools
# - 1.1 is to avoid
# - 1.2 fixes Summary but has little adoption
- # - 2.0 adds more features and is very new
- if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_0:
+ # - 2.1 adds more features
+ # - 2.2 is the latest
+ if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_2:
# we couldn't find any specific marker
if PKG_INFO_PREFERRED_VERSION in possible_versions:
return PKG_INFO_PREFERRED_VERSION
@@ -193,8 +206,10 @@ def _best_version(fields):
return '1.2'
if is_2_1:
return '2.1'
+ # if is_2_2:
+ # return '2.2'
- return '2.0'
+ return '2.2'
# This follows the rules about transforming keys as described in
# https://www.python.org/dev/peps/pep-0566/#id17
@@ -210,7 +225,7 @@ _LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes',
'Requires', 'Provides', 'Obsoletes-Dist',
'Provides-Dist', 'Requires-Dist', 'Requires-External',
'Project-URL', 'Supported-Platform', 'Setup-Requires-Dist',
- 'Provides-Extra', 'Extension')
+ 'Provides-Extra', 'Extension', 'License-File')
_LISTTUPLEFIELDS = ('Project-URL',)
_ELEMENTSFIELD = ('Keywords',)
@@ -602,7 +617,7 @@ LEGACY_METADATA_FILENAME = 'METADATA'
class Metadata(object):
"""
- The metadata of a release. This implementation uses 2.0 (JSON)
+ The metadata of a release. This implementation uses 2.1
metadata where possible. If not possible, it wraps a LegacyMetadata
instance which handles the key-value metadata format.
"""
@@ -611,6 +626,8 @@ class Metadata(object):
NAME_MATCHER = re.compile('^[0-9A-Z]([0-9A-Z_.-]*[0-9A-Z])?$', re.I)
+ FIELDNAME_MATCHER = re.compile('^[A-Z]([0-9A-Z-]*[0-9A-Z])?$', re.I)
+
VERSION_MATCHER = PEP440_VERSION_RE
SUMMARY_MATCHER = re.compile('.{1,2047}')
@@ -638,6 +655,7 @@ class Metadata(object):
'name': (NAME_MATCHER, ('legacy',)),
'version': (VERSION_MATCHER, ('legacy',)),
'summary': (SUMMARY_MATCHER, ('legacy',)),
+ 'dynamic': (FIELDNAME_MATCHER, ('legacy',)),
}
__slots__ = ('_legacy', '_data', 'scheme')
diff --git a/src/pip/_vendor/distlib/scripts.py b/src/pip/_vendor/distlib/scripts.py
index 1ac01dde5..d2706242b 100644
--- a/src/pip/_vendor/distlib/scripts.py
+++ b/src/pip/_vendor/distlib/scripts.py
@@ -10,11 +10,13 @@ import os
import re
import struct
import sys
+import time
+from zipfile import ZipInfo
from .compat import sysconfig, detect_encoding, ZipFile
from .resources import finder
from .util import (FileOperator, get_export_entry, convert_path,
- get_executable, in_venv)
+ get_executable, get_platform, in_venv)
logger = logging.getLogger(__name__)
@@ -170,6 +172,11 @@ class ScriptMaker(object):
sysconfig.get_config_var('BINDIR'),
'python%s%s' % (sysconfig.get_config_var('VERSION'),
sysconfig.get_config_var('EXE')))
+ if not os.path.isfile(executable):
+ # for Python builds from source on Windows, no Python executables with
+ # a version suffix are created, so we use python.exe
+ executable = os.path.join(sysconfig.get_config_var('BINDIR'),
+ 'python%s' % (sysconfig.get_config_var('EXE')))
if options:
executable = self._get_alternate_executable(executable, options)
@@ -244,7 +251,13 @@ class ScriptMaker(object):
launcher = self._get_launcher('w')
stream = BytesIO()
with ZipFile(stream, 'w') as zf:
- zf.writestr('__main__.py', script_bytes)
+ source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH')
+ if source_date_epoch:
+ date_time = time.gmtime(int(source_date_epoch))[:6]
+ zinfo = ZipInfo(filename='__main__.py', date_time=date_time)
+ zf.writestr(zinfo, script_bytes)
+ else:
+ zf.writestr('__main__.py', script_bytes)
zip_data = stream.getvalue()
script_bytes = launcher + shebang + zip_data
for name in names:
@@ -379,7 +392,8 @@ class ScriptMaker(object):
bits = '64'
else:
bits = '32'
- name = '%s%s.exe' % (kind, bits)
+ platform_suffix = '-arm' if get_platform() == 'win-arm64' else ''
+ name = '%s%s%s.exe' % (kind, bits, platform_suffix)
# Issue 31: don't hardcode an absolute package name, but
# determine it relative to the current package
distlib_package = __name__.rsplit('.', 1)[0]
diff --git a/src/pip/_vendor/distlib/t32.exe b/src/pip/_vendor/distlib/t32.exe
index 8932a18e4..0aaa386d7 100644
--- a/src/pip/_vendor/distlib/t32.exe
+++ b/src/pip/_vendor/distlib/t32.exe
Binary files differ
diff --git a/src/pip/_vendor/distlib/t64-arm.exe b/src/pip/_vendor/distlib/t64-arm.exe
new file mode 100644
index 000000000..a759e270d
--- /dev/null
+++ b/src/pip/_vendor/distlib/t64-arm.exe
Binary files differ
diff --git a/src/pip/_vendor/distlib/t64.exe b/src/pip/_vendor/distlib/t64.exe
index 325b8057c..82fe2d99e 100644
--- a/src/pip/_vendor/distlib/t64.exe
+++ b/src/pip/_vendor/distlib/t64.exe
Binary files differ
diff --git a/src/pip/_vendor/distlib/util.py b/src/pip/_vendor/distlib/util.py
index b9e2c695c..dd01849d9 100644
--- a/src/pip/_vendor/distlib/util.py
+++ b/src/pip/_vendor/distlib/util.py
@@ -215,6 +215,10 @@ def parse_requirement(req):
if not ver_remaining or ver_remaining[0] != ',':
break
ver_remaining = ver_remaining[1:].lstrip()
+ # Some packages have a trailing comma which would break things
+ # See issue #148
+ if not ver_remaining:
+ break
m = COMPARE_OP.match(ver_remaining)
if not m:
raise SyntaxError('invalid constraint: %s' % ver_remaining)
@@ -1428,29 +1432,19 @@ if ssl:
self.sock = sock
self._tunnel()
- if not hasattr(ssl, 'SSLContext'):
- # For 2.x
- if self.ca_certs:
- cert_reqs = ssl.CERT_REQUIRED
- else:
- cert_reqs = ssl.CERT_NONE
- self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
- cert_reqs=cert_reqs,
- ssl_version=ssl.PROTOCOL_SSLv23,
- ca_certs=self.ca_certs)
- else: # pragma: no cover
- context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
- if hasattr(ssl, 'OP_NO_SSLv2'):
- context.options |= ssl.OP_NO_SSLv2
- if self.cert_file:
- context.load_cert_chain(self.cert_file, self.key_file)
- kwargs = {}
- if self.ca_certs:
- context.verify_mode = ssl.CERT_REQUIRED
- context.load_verify_locations(cafile=self.ca_certs)
- if getattr(ssl, 'HAS_SNI', False):
- kwargs['server_hostname'] = self.host
- self.sock = context.wrap_socket(sock, **kwargs)
+ context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
+ if hasattr(ssl, 'OP_NO_SSLv2'):
+ context.options |= ssl.OP_NO_SSLv2
+ if self.cert_file:
+ context.load_cert_chain(self.cert_file, self.key_file)
+ kwargs = {}
+ if self.ca_certs:
+ context.verify_mode = ssl.CERT_REQUIRED
+ context.load_verify_locations(cafile=self.ca_certs)
+ if getattr(ssl, 'HAS_SNI', False):
+ kwargs['server_hostname'] = self.host
+
+ self.sock = context.wrap_socket(sock, **kwargs)
if self.ca_certs and self.check_domain:
try:
match_hostname(self.sock.getpeercert(), self.host)
@@ -1509,25 +1503,6 @@ if ssl:
#
# XML-RPC with timeouts
#
-
-_ver_info = sys.version_info[:2]
-
-if _ver_info == (2, 6):
- class HTTP(httplib.HTTP):
- def __init__(self, host='', port=None, **kwargs):
- if port == 0: # 0 means use port 0, not the default port
- port = None
- self._setup(self._connection_class(host, port, **kwargs))
-
-
- if ssl:
- class HTTPS(httplib.HTTPS):
- def __init__(self, host='', port=None, **kwargs):
- if port == 0: # 0 means use port 0, not the default port
- port = None
- self._setup(self._connection_class(host, port, **kwargs))
-
-
class Transport(xmlrpclib.Transport):
def __init__(self, timeout, use_datetime=0):
self.timeout = timeout
@@ -1535,14 +1510,10 @@ class Transport(xmlrpclib.Transport):
def make_connection(self, host):
h, eh, x509 = self.get_host_info(host)
- if _ver_info == (2, 6):
- result = HTTP(h, timeout=self.timeout)
- else:
- if not self._connection or host != self._connection[0]:
- self._extra_headers = eh
- self._connection = host, httplib.HTTPConnection(h)
- result = self._connection[1]
- return result
+ if not self._connection or host != self._connection[0]:
+ self._extra_headers = eh
+ self._connection = host, httplib.HTTPConnection(h)
+ return self._connection[1]
if ssl:
class SafeTransport(xmlrpclib.SafeTransport):
@@ -1555,15 +1526,11 @@ if ssl:
if not kwargs:
kwargs = {}
kwargs['timeout'] = self.timeout
- if _ver_info == (2, 6):
- result = HTTPS(host, None, **kwargs)
- else:
- if not self._connection or host != self._connection[0]:
- self._extra_headers = eh
- self._connection = host, httplib.HTTPSConnection(h, None,
- **kwargs)
- result = self._connection[1]
- return result
+ if not self._connection or host != self._connection[0]:
+ self._extra_headers = eh
+ self._connection = host, httplib.HTTPSConnection(h, None,
+ **kwargs)
+ return self._connection[1]
class ServerProxy(xmlrpclib.ServerProxy):
diff --git a/src/pip/_vendor/distlib/version.py b/src/pip/_vendor/distlib/version.py
index 86c069a7c..c7c8bb6ff 100644
--- a/src/pip/_vendor/distlib/version.py
+++ b/src/pip/_vendor/distlib/version.py
@@ -194,7 +194,7 @@ def _pep_440_key(s):
if not groups[0]:
epoch = 0
else:
- epoch = int(groups[0])
+ epoch = int(groups[0][:-1])
pre = groups[4:6]
post = groups[7:9]
dev = groups[10:12]
diff --git a/src/pip/_vendor/distlib/w32.exe b/src/pip/_vendor/distlib/w32.exe
index e6439e9e4..f2e73aa0c 100644
--- a/src/pip/_vendor/distlib/w32.exe
+++ b/src/pip/_vendor/distlib/w32.exe
Binary files differ
diff --git a/src/pip/_vendor/distlib/w64-arm.exe b/src/pip/_vendor/distlib/w64-arm.exe
new file mode 100644
index 000000000..b998321c4
--- /dev/null
+++ b/src/pip/_vendor/distlib/w64-arm.exe
Binary files differ
diff --git a/src/pip/_vendor/distlib/w64.exe b/src/pip/_vendor/distlib/w64.exe
index 46139dbf9..9a6b894db 100644
--- a/src/pip/_vendor/distlib/w64.exe
+++ b/src/pip/_vendor/distlib/w64.exe
Binary files differ
diff --git a/src/pip/_vendor/distlib/wheel.py b/src/pip/_vendor/distlib/wheel.py
index 5262c8323..028c2d99b 100644
--- a/src/pip/_vendor/distlib/wheel.py
+++ b/src/pip/_vendor/distlib/wheel.py
@@ -11,7 +11,6 @@ import codecs
import datetime
from email import message_from_file
import hashlib
-import imp
import json
import logging
import os
@@ -47,10 +46,7 @@ else:
VER_SUFFIX = sysconfig.get_config_var('py_version_nodot')
if not VER_SUFFIX: # pragma: no cover
- if sys.version_info[1] >= 10:
- VER_SUFFIX = '%s_%s' % sys.version_info[:2] # PEP 641 (draft)
- else:
- VER_SUFFIX = '%s%s' % sys.version_info[:2]
+ VER_SUFFIX = '%s%s' % sys.version_info[:2]
PYVER = 'py' + VER_SUFFIX
IMPVER = IMP_PREFIX + VER_SUFFIX
@@ -64,10 +60,18 @@ else:
parts = ['cp', VER_SUFFIX]
if sysconfig.get_config_var('Py_DEBUG'):
parts.append('d')
- if sysconfig.get_config_var('WITH_PYMALLOC'):
- parts.append('m')
- if sysconfig.get_config_var('Py_UNICODE_SIZE') == 4:
- parts.append('u')
+ if IMP_PREFIX == 'cp':
+ vi = sys.version_info[:2]
+ if vi < (3, 8):
+ wpm = sysconfig.get_config_var('WITH_PYMALLOC')
+ if wpm is None:
+ wpm = True
+ if wpm:
+ parts.append('m')
+ if vi < (3, 3):
+ us = sysconfig.get_config_var('Py_UNICODE_SIZE')
+ if us == 4 or (us is None and sys.maxunicode == 0x10FFFF):
+ parts.append('u')
return ''.join(parts)
ABI = _derive_abi()
del _derive_abi
@@ -98,6 +102,29 @@ if os.sep == '/':
else:
to_posix = lambda o: o.replace(os.sep, '/')
+if sys.version_info[0] < 3:
+ import imp
+else:
+ imp = None
+ import importlib.machinery
+ import importlib.util
+
+def _get_suffixes():
+ if imp:
+ return [s[0] for s in imp.get_suffixes()]
+ else:
+ return importlib.machinery.EXTENSION_SUFFIXES
+
+def _load_dynamic(name, path):
+ # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
+ if imp:
+ return imp.load_dynamic(name, path)
+ else:
+ spec = importlib.util.spec_from_file_location(name, path)
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[name] = module
+ spec.loader.exec_module(module)
+ return module
class Mounter(object):
def __init__(self):
@@ -127,7 +154,7 @@ class Mounter(object):
else:
if fullname not in self.libs:
raise ImportError('unable to find extension for %s' % fullname)
- result = imp.load_dynamic(fullname, self.libs[fullname])
+ result = _load_dynamic(fullname, self.libs[fullname])
result.__loader__ = self
parts = fullname.rsplit('.', 1)
if len(parts) > 1:
@@ -304,10 +331,9 @@ class Wheel(object):
result = base64.urlsafe_b64encode(result).rstrip(b'=').decode('ascii')
return hash_kind, result
- def write_record(self, records, record_path, base):
+ def write_record(self, records, record_path, archive_record_path):
records = list(records) # make a copy, as mutated
- p = to_posix(os.path.relpath(record_path, base))
- records.append((p, '', ''))
+ records.append((archive_record_path, '', ''))
with CSVWriter(record_path) as writer:
for row in records:
writer.writerow(row)
@@ -324,8 +350,8 @@ class Wheel(object):
records.append((ap, digest, size))
p = os.path.join(distinfo, 'RECORD')
- self.write_record(records, p, libdir)
ap = to_posix(os.path.join(info_dir, 'RECORD'))
+ self.write_record(records, p, ap)
archive_paths.append((ap, p))
def build_zip(self, pathname, archive_paths):
@@ -968,7 +994,7 @@ def compatible_tags():
versions.append(''.join([major, str(minor)]))
abis = []
- for suffix, _, _ in imp.get_suffixes():
+ for suffix in _get_suffixes():
if suffix.startswith('.abi'):
abis.append(suffix.split('.', 2)[1])
abis.sort()
diff --git a/src/pip/_vendor/distro.pyi b/src/pip/_vendor/distro.pyi
deleted file mode 100644
index c7ea94b37..000000000
--- a/src/pip/_vendor/distro.pyi
+++ /dev/null
@@ -1 +0,0 @@
-from distro import * \ No newline at end of file
diff --git a/src/pip/_vendor/distro.LICENSE b/src/pip/_vendor/distro/LICENSE
index e06d20818..e06d20818 100644
--- a/src/pip/_vendor/distro.LICENSE
+++ b/src/pip/_vendor/distro/LICENSE
diff --git a/src/pip/_vendor/distro/__init__.py b/src/pip/_vendor/distro/__init__.py
new file mode 100644
index 000000000..7686fe85a
--- /dev/null
+++ b/src/pip/_vendor/distro/__init__.py
@@ -0,0 +1,54 @@
+from .distro import (
+ NORMALIZED_DISTRO_ID,
+ NORMALIZED_LSB_ID,
+ NORMALIZED_OS_ID,
+ LinuxDistribution,
+ __version__,
+ build_number,
+ codename,
+ distro_release_attr,
+ distro_release_info,
+ id,
+ info,
+ like,
+ linux_distribution,
+ lsb_release_attr,
+ lsb_release_info,
+ major_version,
+ minor_version,
+ name,
+ os_release_attr,
+ os_release_info,
+ uname_attr,
+ uname_info,
+ version,
+ version_parts,
+)
+
+__all__ = [
+ "NORMALIZED_DISTRO_ID",
+ "NORMALIZED_LSB_ID",
+ "NORMALIZED_OS_ID",
+ "LinuxDistribution",
+ "build_number",
+ "codename",
+ "distro_release_attr",
+ "distro_release_info",
+ "id",
+ "info",
+ "like",
+ "linux_distribution",
+ "lsb_release_attr",
+ "lsb_release_info",
+ "major_version",
+ "minor_version",
+ "name",
+ "os_release_attr",
+ "os_release_info",
+ "uname_attr",
+ "uname_info",
+ "version",
+ "version_parts",
+]
+
+__version__ = __version__
diff --git a/src/pip/_vendor/distro/__main__.py b/src/pip/_vendor/distro/__main__.py
new file mode 100644
index 000000000..0c01d5b08
--- /dev/null
+++ b/src/pip/_vendor/distro/__main__.py
@@ -0,0 +1,4 @@
+from .distro import main
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pip/_vendor/distro.py b/src/pip/_vendor/distro/distro.py
index 0611b62a3..49066ae83 100644
--- a/src/pip/_vendor/distro.py
+++ b/src/pip/_vendor/distro/distro.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
# Copyright 2015,2016,2017 Nir Cohen
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,26 +21,60 @@ machine-readable distro ID, or version information.
It is the recommended replacement for Python's original
:py:func:`platform.linux_distribution` function, but it provides much more
functionality. An alternative implementation became necessary because Python
-3.5 deprecated this function, and Python 3.8 will remove it altogether.
-Its predecessor function :py:func:`platform.dist` was already
-deprecated since Python 2.6 and will also be removed in Python 3.8.
-Still, there are many cases in which access to OS distribution information
-is needed. See `Python issue 1322 <https://bugs.python.org/issue1322>`_ for
-more information.
+3.5 deprecated this function, and Python 3.8 removed it altogether. Its
+predecessor function :py:func:`platform.dist` was already deprecated since
+Python 2.6 and removed in Python 3.8. Still, there are many cases in which
+access to OS distribution information is needed. See `Python issue 1322
+<https://bugs.python.org/issue1322>`_ for more information.
"""
+import argparse
+import json
+import logging
import os
import re
-import sys
-import json
import shlex
-import logging
-import argparse
import subprocess
+import sys
+import warnings
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ Optional,
+ Sequence,
+ TextIO,
+ Tuple,
+ Type,
+)
+
+try:
+ from typing import TypedDict
+except ImportError:
+ # Python 3.7
+ TypedDict = dict
+__version__ = "1.7.0"
-_UNIXCONFDIR = os.environ.get('UNIXCONFDIR', '/etc')
-_OS_RELEASE_BASENAME = 'os-release'
+
+class VersionDict(TypedDict):
+ major: str
+ minor: str
+ build_number: str
+
+
+class InfoDict(TypedDict):
+ id: str
+ version: str
+ version_parts: VersionDict
+ like: str
+ codename: str
+
+
+_UNIXCONFDIR = os.environ.get("UNIXCONFDIR", "/etc")
+_UNIXUSRLIBDIR = os.environ.get("UNIXUSRLIBDIR", "/usr/lib")
+_OS_RELEASE_BASENAME = "os-release"
#: Translation table for normalizing the "ID" attribute defined in os-release
#: files, for use by the :func:`distro.id` method.
@@ -49,7 +84,8 @@ _OS_RELEASE_BASENAME = 'os-release'
#:
#: * Value: Normalized value.
NORMALIZED_OS_ID = {
- 'ol': 'oracle', # Oracle Linux
+ "ol": "oracle", # Oracle Linux
+ "opensuse-leap": "opensuse", # Newer versions of OpenSuSE report as opensuse-leap
}
#: Translation table for normalizing the "Distributor ID" attribute returned by
@@ -60,11 +96,11 @@ NORMALIZED_OS_ID = {
#:
#: * Value: Normalized value.
NORMALIZED_LSB_ID = {
- 'enterpriseenterpriseas': 'oracle', # Oracle Enterprise Linux 4
- 'enterpriseenterpriseserver': 'oracle', # Oracle Linux 5
- 'redhatenterpriseworkstation': 'rhel', # RHEL 6, 7 Workstation
- 'redhatenterpriseserver': 'rhel', # RHEL 6, 7 Server
- 'redhatenterprisecomputenode': 'rhel', # RHEL 6 ComputeNode
+ "enterpriseenterpriseas": "oracle", # Oracle Enterprise Linux 4
+ "enterpriseenterpriseserver": "oracle", # Oracle Linux 5
+ "redhatenterpriseworkstation": "rhel", # RHEL 6, 7 Workstation
+ "redhatenterpriseserver": "rhel", # RHEL 6, 7 Server
+ "redhatenterprisecomputenode": "rhel", # RHEL 6 ComputeNode
}
#: Translation table for normalizing the distro ID derived from the file name
@@ -75,30 +111,38 @@ NORMALIZED_LSB_ID = {
#:
#: * Value: Normalized value.
NORMALIZED_DISTRO_ID = {
- 'redhat': 'rhel', # RHEL 6.x, 7.x
+ "redhat": "rhel", # RHEL 6.x, 7.x
}
# Pattern for content of distro release file (reversed)
_DISTRO_RELEASE_CONTENT_REVERSED_PATTERN = re.compile(
- r'(?:[^)]*\)(.*)\()? *(?:STL )?([\d.+\-a-z]*\d) *(?:esaeler *)?(.+)')
+ r"(?:[^)]*\)(.*)\()? *(?:STL )?([\d.+\-a-z]*\d) *(?:esaeler *)?(.+)"
+)
# Pattern for base file name of distro release file
-_DISTRO_RELEASE_BASENAME_PATTERN = re.compile(
- r'(\w+)[-_](release|version)$')
+_DISTRO_RELEASE_BASENAME_PATTERN = re.compile(r"(\w+)[-_](release|version)$")
# Base file names to be ignored when searching for distro release file
_DISTRO_RELEASE_IGNORE_BASENAMES = (
- 'debian_version',
- 'lsb-release',
- 'oem-release',
+ "debian_version",
+ "lsb-release",
+ "oem-release",
_OS_RELEASE_BASENAME,
- 'system-release',
- 'plesk-release',
+ "system-release",
+ "plesk-release",
+ "iredmail-release",
)
-def linux_distribution(full_distribution_name=True):
+def linux_distribution(full_distribution_name: bool = True) -> Tuple[str, str, str]:
"""
+ .. deprecated:: 1.6.0
+
+ :func:`distro.linux_distribution()` is deprecated. It should only be
+ used as a compatibility shim with Python's
+ :py:func:`platform.linux_distribution()`. Please use :func:`distro.id`,
+ :func:`distro.version` and :func:`distro.name` instead.
+
Return information about the current OS distribution as a tuple
``(id_name, version, codename)`` with items as follows:
@@ -107,7 +151,8 @@ def linux_distribution(full_distribution_name=True):
* ``version``: The result of :func:`distro.version`.
- * ``codename``: The result of :func:`distro.codename`.
+ * ``codename``: The extra item (usually in parentheses) after the
+ os-release version number, or the result of :func:`distro.codename`.
The interface of this function is compatible with the original
:py:func:`platform.linux_distribution` function, supporting a subset of
@@ -122,10 +167,17 @@ def linux_distribution(full_distribution_name=True):
method normalizes the distro ID string to a reliable machine-readable value
for a number of popular OS distributions.
"""
+ warnings.warn(
+ "distro.linux_distribution() is deprecated. It should only be used as a "
+ "compatibility shim with Python's platform.linux_distribution(). Please use "
+ "distro.id(), distro.version() and distro.name() instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
return _distro.linux_distribution(full_distribution_name)
-def id():
+def id() -> str:
"""
Return the distro ID of the current distribution, as a
machine-readable string.
@@ -146,7 +198,7 @@ def id():
"fedora" Fedora
"sles" SUSE Linux Enterprise Server
"opensuse" openSUSE
- "amazon" Amazon Linux
+ "amzn" Amazon Linux
"arch" Arch Linux
"cloudlinux" CloudLinux OS
"exherbo" Exherbo Linux
@@ -167,6 +219,8 @@ def id():
"netbsd" NetBSD
"freebsd" FreeBSD
"midnightbsd" MidnightBSD
+ "rocky" Rocky Linux
+ "aix" AIX
============== =========================================
If you have a need to get distros for reliable IDs added into this set,
@@ -204,7 +258,7 @@ def id():
return _distro.id()
-def name(pretty=False):
+def name(pretty: bool = False) -> str:
"""
Return the name of the current OS distribution, as a human-readable
string.
@@ -243,7 +297,7 @@ def name(pretty=False):
return _distro.name(pretty)
-def version(pretty=False, best=False):
+def version(pretty: bool = False, best: bool = False) -> str:
"""
Return the version of the current OS distribution, as a human-readable
string.
@@ -259,6 +313,10 @@ def version(pretty=False, best=False):
sources in a fixed priority order does not always yield the most precise
version (e.g. for Debian 8.2, or CentOS 7.1).
+ Some other distributions may not provide this kind of information. In these
+ cases, an empty string would be returned. This behavior can be observed
+ with rolling releases distributions (e.g. Arch Linux).
+
The *best* parameter can be used to control the approach for the returned
version:
@@ -287,7 +345,7 @@ def version(pretty=False, best=False):
return _distro.version(pretty, best)
-def version_parts(best=False):
+def version_parts(best: bool = False) -> Tuple[str, str, str]:
"""
Return the version of the current OS distribution as a tuple
``(major, minor, build_number)`` with items as follows:
@@ -304,7 +362,7 @@ def version_parts(best=False):
return _distro.version_parts(best)
-def major_version(best=False):
+def major_version(best: bool = False) -> str:
"""
Return the major version of the current OS distribution, as a string,
if provided.
@@ -317,7 +375,7 @@ def major_version(best=False):
return _distro.major_version(best)
-def minor_version(best=False):
+def minor_version(best: bool = False) -> str:
"""
Return the minor version of the current OS distribution, as a string,
if provided.
@@ -330,7 +388,7 @@ def minor_version(best=False):
return _distro.minor_version(best)
-def build_number(best=False):
+def build_number(best: bool = False) -> str:
"""
Return the build number of the current OS distribution, as a string,
if provided.
@@ -343,7 +401,7 @@ def build_number(best=False):
return _distro.build_number(best)
-def like():
+def like() -> str:
"""
Return a space-separated list of distro IDs of distributions that are
closely related to the current OS distribution in regards to packaging
@@ -360,7 +418,7 @@ def like():
return _distro.like()
-def codename():
+def codename() -> str:
"""
Return the codename for the release of the current OS distribution,
as a string.
@@ -384,7 +442,7 @@ def codename():
return _distro.codename()
-def info(pretty=False, best=False):
+def info(pretty: bool = False, best: bool = False) -> InfoDict:
"""
Return certain machine-readable information items about the current OS
distribution in a dictionary, as shown in the following example:
@@ -428,7 +486,7 @@ def info(pretty=False, best=False):
return _distro.info(pretty, best)
-def os_release_info():
+def os_release_info() -> Dict[str, str]:
"""
Return a dictionary containing key-value pairs for the information items
from the os-release file data source of the current OS distribution.
@@ -438,7 +496,7 @@ def os_release_info():
return _distro.os_release_info()
-def lsb_release_info():
+def lsb_release_info() -> Dict[str, str]:
"""
Return a dictionary containing key-value pairs for the information items
from the lsb_release command data source of the current OS distribution.
@@ -449,7 +507,7 @@ def lsb_release_info():
return _distro.lsb_release_info()
-def distro_release_info():
+def distro_release_info() -> Dict[str, str]:
"""
Return a dictionary containing key-value pairs for the information items
from the distro release file data source of the current OS distribution.
@@ -459,7 +517,7 @@ def distro_release_info():
return _distro.distro_release_info()
-def uname_info():
+def uname_info() -> Dict[str, str]:
"""
Return a dictionary containing key-value pairs for the information items
from the distro release file data source of the current OS distribution.
@@ -467,7 +525,7 @@ def uname_info():
return _distro.uname_info()
-def os_release_attr(attribute):
+def os_release_attr(attribute: str) -> str:
"""
Return a single named information item from the os-release file data source
of the current OS distribution.
@@ -486,7 +544,7 @@ def os_release_attr(attribute):
return _distro.os_release_attr(attribute)
-def lsb_release_attr(attribute):
+def lsb_release_attr(attribute: str) -> str:
"""
Return a single named information item from the lsb_release command output
data source of the current OS distribution.
@@ -506,7 +564,7 @@ def lsb_release_attr(attribute):
return _distro.lsb_release_attr(attribute)
-def distro_release_attr(attribute):
+def distro_release_attr(attribute: str) -> str:
"""
Return a single named information item from the distro release file
data source of the current OS distribution.
@@ -525,7 +583,7 @@ def distro_release_attr(attribute):
return _distro.distro_release_attr(attribute)
-def uname_attr(attribute):
+def uname_attr(attribute: str) -> str:
"""
Return a single named information item from the distro release file
data source of the current OS distribution.
@@ -542,22 +600,27 @@ def uname_attr(attribute):
return _distro.uname_attr(attribute)
-class cached_property(object):
- """A version of @property which caches the value. On access, it calls the
- underlying function and sets the value in `__dict__` so future accesses
- will not re-call the property.
- """
- def __init__(self, f):
- self._fname = f.__name__
- self._f = f
+try:
+ from functools import cached_property
+except ImportError:
+ # Python < 3.8
+ class cached_property: # type: ignore
+ """A version of @property which caches the value. On access, it calls the
+ underlying function and sets the value in `__dict__` so future accesses
+ will not re-call the property.
+ """
- def __get__(self, obj, owner):
- assert obj is not None, 'call {} on an instance'.format(self._fname)
- ret = obj.__dict__[self._fname] = self._f(obj)
- return ret
+ def __init__(self, f: Callable[[Any], Any]) -> None:
+ self._fname = f.__name__
+ self._f = f
+ def __get__(self, obj: Any, owner: Type[Any]) -> Any:
+ assert obj is not None, f"call {self._fname} on an instance"
+ ret = obj.__dict__[self._fname] = self._f(obj)
+ return ret
-class LinuxDistribution(object):
+
+class LinuxDistribution:
"""
Provides information about a OS distribution.
@@ -575,11 +638,15 @@ class LinuxDistribution(object):
lsb_release command.
"""
- def __init__(self,
- include_lsb=True,
- os_release_file='',
- distro_release_file='',
- include_uname=True):
+ def __init__(
+ self,
+ include_lsb: Optional[bool] = None,
+ os_release_file: str = "",
+ distro_release_file: str = "",
+ include_uname: Optional[bool] = None,
+ root_dir: Optional[str] = None,
+ include_oslevel: Optional[bool] = None,
+ ) -> None:
"""
The initialization method of this class gathers information from the
available data sources, and stores that in private instance attributes.
@@ -618,6 +685,15 @@ class LinuxDistribution(object):
the program execution path the data source for the uname command will
be empty.
+ * ``root_dir`` (string): The absolute path to the root directory to use
+ to find distro-related information files. Note that ``include_*``
+ parameters must not be enabled in combination with ``root_dir``.
+
+ * ``include_oslevel`` (bool): Controls whether (AIX) oslevel command
+ output is included as a data source. If the oslevel command is not
+ available in the program execution path the data source will be
+ empty.
+
Public instance attributes:
* ``os_release_file`` (string): The path name of the
@@ -635,40 +711,86 @@ class LinuxDistribution(object):
parameter. This controls whether the uname information will
be loaded.
+ * ``include_oslevel`` (bool): The result of the ``include_oslevel``
+ parameter. This controls whether (AIX) oslevel information will be
+ loaded.
+
+ * ``root_dir`` (string): The result of the ``root_dir`` parameter.
+ The absolute path to the root directory to use to find distro-related
+ information files.
+
Raises:
- * :py:exc:`IOError`: Some I/O issue with an os-release file or distro
- release file.
+ * :py:exc:`ValueError`: Initialization parameters combination is not
+ supported.
- * :py:exc:`subprocess.CalledProcessError`: The lsb_release command had
- some issue (other than not being available in the program execution
- path).
+ * :py:exc:`OSError`: Some I/O issue with an os-release file or distro
+ release file.
* :py:exc:`UnicodeError`: A data source has unexpected characters or
uses an unexpected encoding.
"""
- self.os_release_file = os_release_file or \
- os.path.join(_UNIXCONFDIR, _OS_RELEASE_BASENAME)
- self.distro_release_file = distro_release_file or '' # updated later
- self.include_lsb = include_lsb
- self.include_uname = include_uname
-
- def __repr__(self):
- """Return repr of all info
- """
- return \
- "LinuxDistribution(" \
- "os_release_file={self.os_release_file!r}, " \
- "distro_release_file={self.distro_release_file!r}, " \
- "include_lsb={self.include_lsb!r}, " \
- "include_uname={self.include_uname!r}, " \
- "_os_release_info={self._os_release_info!r}, " \
- "_lsb_release_info={self._lsb_release_info!r}, " \
- "_distro_release_info={self._distro_release_info!r}, " \
- "_uname_info={self._uname_info!r})".format(
- self=self)
-
- def linux_distribution(self, full_distribution_name=True):
+ self.root_dir = root_dir
+ self.etc_dir = os.path.join(root_dir, "etc") if root_dir else _UNIXCONFDIR
+ self.usr_lib_dir = (
+ os.path.join(root_dir, "usr/lib") if root_dir else _UNIXUSRLIBDIR
+ )
+
+ if os_release_file:
+ self.os_release_file = os_release_file
+ else:
+ etc_dir_os_release_file = os.path.join(self.etc_dir, _OS_RELEASE_BASENAME)
+ usr_lib_os_release_file = os.path.join(
+ self.usr_lib_dir, _OS_RELEASE_BASENAME
+ )
+
+ # NOTE: The idea is to respect order **and** have it set
+ # at all times for API backwards compatibility.
+ if os.path.isfile(etc_dir_os_release_file) or not os.path.isfile(
+ usr_lib_os_release_file
+ ):
+ self.os_release_file = etc_dir_os_release_file
+ else:
+ self.os_release_file = usr_lib_os_release_file
+
+ self.distro_release_file = distro_release_file or "" # updated later
+
+ is_root_dir_defined = root_dir is not None
+ if is_root_dir_defined and (include_lsb or include_uname or include_oslevel):
+ raise ValueError(
+ "Including subprocess data sources from specific root_dir is disallowed"
+ " to prevent false information"
+ )
+ self.include_lsb = (
+ include_lsb if include_lsb is not None else not is_root_dir_defined
+ )
+ self.include_uname = (
+ include_uname if include_uname is not None else not is_root_dir_defined
+ )
+ self.include_oslevel = (
+ include_oslevel if include_oslevel is not None else not is_root_dir_defined
+ )
+
+ def __repr__(self) -> str:
+ """Return repr of all info"""
+ return (
+ "LinuxDistribution("
+ "os_release_file={self.os_release_file!r}, "
+ "distro_release_file={self.distro_release_file!r}, "
+ "include_lsb={self.include_lsb!r}, "
+ "include_uname={self.include_uname!r}, "
+ "include_oslevel={self.include_oslevel!r}, "
+ "root_dir={self.root_dir!r}, "
+ "_os_release_info={self._os_release_info!r}, "
+ "_lsb_release_info={self._lsb_release_info!r}, "
+ "_distro_release_info={self._distro_release_info!r}, "
+ "_uname_info={self._uname_info!r}, "
+ "_oslevel_info={self._oslevel_info!r})".format(self=self)
+ )
+
+ def linux_distribution(
+ self, full_distribution_name: bool = True
+ ) -> Tuple[str, str, str]:
"""
Return information about the OS distribution that is compatible
with Python's :func:`platform.linux_distribution`, supporting a subset
@@ -679,92 +801,100 @@ class LinuxDistribution(object):
return (
self.name() if full_distribution_name else self.id(),
self.version(),
- self.codename()
+ self._os_release_info.get("release_codename") or self.codename(),
)
- def id(self):
+ def id(self) -> str:
"""Return the distro ID of the OS distribution, as a string.
For details, see :func:`distro.id`.
"""
- def normalize(distro_id, table):
- distro_id = distro_id.lower().replace(' ', '_')
+
+ def normalize(distro_id: str, table: Dict[str, str]) -> str:
+ distro_id = distro_id.lower().replace(" ", "_")
return table.get(distro_id, distro_id)
- distro_id = self.os_release_attr('id')
+ distro_id = self.os_release_attr("id")
if distro_id:
return normalize(distro_id, NORMALIZED_OS_ID)
- distro_id = self.lsb_release_attr('distributor_id')
+ distro_id = self.lsb_release_attr("distributor_id")
if distro_id:
return normalize(distro_id, NORMALIZED_LSB_ID)
- distro_id = self.distro_release_attr('id')
+ distro_id = self.distro_release_attr("id")
if distro_id:
return normalize(distro_id, NORMALIZED_DISTRO_ID)
- distro_id = self.uname_attr('id')
+ distro_id = self.uname_attr("id")
if distro_id:
return normalize(distro_id, NORMALIZED_DISTRO_ID)
- return ''
+ return ""
- def name(self, pretty=False):
+ def name(self, pretty: bool = False) -> str:
"""
Return the name of the OS distribution, as a string.
For details, see :func:`distro.name`.
"""
- name = self.os_release_attr('name') \
- or self.lsb_release_attr('distributor_id') \
- or self.distro_release_attr('name') \
- or self.uname_attr('name')
+ name = (
+ self.os_release_attr("name")
+ or self.lsb_release_attr("distributor_id")
+ or self.distro_release_attr("name")
+ or self.uname_attr("name")
+ )
if pretty:
- name = self.os_release_attr('pretty_name') \
- or self.lsb_release_attr('description')
+ name = self.os_release_attr("pretty_name") or self.lsb_release_attr(
+ "description"
+ )
if not name:
- name = self.distro_release_attr('name') \
- or self.uname_attr('name')
+ name = self.distro_release_attr("name") or self.uname_attr("name")
version = self.version(pretty=True)
if version:
- name = name + ' ' + version
- return name or ''
+ name = f"{name} {version}"
+ return name or ""
- def version(self, pretty=False, best=False):
+ def version(self, pretty: bool = False, best: bool = False) -> str:
"""
Return the version of the OS distribution, as a string.
For details, see :func:`distro.version`.
"""
versions = [
- self.os_release_attr('version_id'),
- self.lsb_release_attr('release'),
- self.distro_release_attr('version_id'),
- self._parse_distro_release_content(
- self.os_release_attr('pretty_name')).get('version_id', ''),
+ self.os_release_attr("version_id"),
+ self.lsb_release_attr("release"),
+ self.distro_release_attr("version_id"),
+ self._parse_distro_release_content(self.os_release_attr("pretty_name")).get(
+ "version_id", ""
+ ),
self._parse_distro_release_content(
- self.lsb_release_attr('description')).get('version_id', ''),
- self.uname_attr('release')
+ self.lsb_release_attr("description")
+ ).get("version_id", ""),
+ self.uname_attr("release"),
]
- version = ''
+ if self.uname_attr("id").startswith("aix"):
+ # On AIX platforms, prefer oslevel command output.
+ versions.insert(0, self.oslevel_info())
+ version = ""
if best:
# This algorithm uses the last version in priority order that has
# the best precision. If the versions are not in conflict, that
# does not matter; otherwise, using the last one instead of the
# first one might be considered a surprise.
for v in versions:
- if v.count(".") > version.count(".") or version == '':
+ if v.count(".") > version.count(".") or version == "":
version = v
else:
for v in versions:
- if v != '':
+ if v != "":
version = v
break
if pretty and version and self.codename():
- version = '{0} ({1})'.format(version, self.codename())
+ version = f"{version} ({self.codename()})"
return version
- def version_parts(self, best=False):
+ def version_parts(self, best: bool = False) -> Tuple[str, str, str]:
"""
Return the version of the OS distribution, as a tuple of version
numbers.
@@ -773,14 +903,14 @@ class LinuxDistribution(object):
"""
version_str = self.version(best=best)
if version_str:
- version_regex = re.compile(r'(\d+)\.?(\d+)?\.?(\d+)?')
+ version_regex = re.compile(r"(\d+)\.?(\d+)?\.?(\d+)?")
matches = version_regex.match(version_str)
if matches:
major, minor, build_number = matches.groups()
- return major, minor or '', build_number or ''
- return '', '', ''
+ return major, minor or "", build_number or ""
+ return "", "", ""
- def major_version(self, best=False):
+ def major_version(self, best: bool = False) -> str:
"""
Return the major version number of the current distribution.
@@ -788,7 +918,7 @@ class LinuxDistribution(object):
"""
return self.version_parts(best)[0]
- def minor_version(self, best=False):
+ def minor_version(self, best: bool = False) -> str:
"""
Return the minor version number of the current distribution.
@@ -796,7 +926,7 @@ class LinuxDistribution(object):
"""
return self.version_parts(best)[1]
- def build_number(self, best=False):
+ def build_number(self, best: bool = False) -> str:
"""
Return the build number of the current distribution.
@@ -804,15 +934,15 @@ class LinuxDistribution(object):
"""
return self.version_parts(best)[2]
- def like(self):
+ def like(self) -> str:
"""
Return the IDs of distributions that are like the OS distribution.
For details, see :func:`distro.like`.
"""
- return self.os_release_attr('id_like') or ''
+ return self.os_release_attr("id_like") or ""
- def codename(self):
+ def codename(self) -> str:
"""
Return the codename of the OS distribution.
@@ -821,13 +951,15 @@ class LinuxDistribution(object):
try:
# Handle os_release specially since distros might purposefully set
# this to empty string to have no codename
- return self._os_release_info['codename']
+ return self._os_release_info["codename"]
except KeyError:
- return self.lsb_release_attr('codename') \
- or self.distro_release_attr('codename') \
- or ''
+ return (
+ self.lsb_release_attr("codename")
+ or self.distro_release_attr("codename")
+ or ""
+ )
- def info(self, pretty=False, best=False):
+ def info(self, pretty: bool = False, best: bool = False) -> InfoDict:
"""
Return certain machine-readable information about the OS
distribution.
@@ -840,13 +972,13 @@ class LinuxDistribution(object):
version_parts=dict(
major=self.major_version(best),
minor=self.minor_version(best),
- build_number=self.build_number(best)
+ build_number=self.build_number(best),
),
like=self.like(),
codename=self.codename(),
)
- def os_release_info(self):
+ def os_release_info(self) -> Dict[str, str]:
"""
Return a dictionary containing key-value pairs for the information
items from the os-release file data source of the OS distribution.
@@ -855,7 +987,7 @@ class LinuxDistribution(object):
"""
return self._os_release_info
- def lsb_release_info(self):
+ def lsb_release_info(self) -> Dict[str, str]:
"""
Return a dictionary containing key-value pairs for the information
items from the lsb_release command data source of the OS
@@ -865,7 +997,7 @@ class LinuxDistribution(object):
"""
return self._lsb_release_info
- def distro_release_info(self):
+ def distro_release_info(self) -> Dict[str, str]:
"""
Return a dictionary containing key-value pairs for the information
items from the distro release file data source of the OS
@@ -875,7 +1007,7 @@ class LinuxDistribution(object):
"""
return self._distro_release_info
- def uname_info(self):
+ def uname_info(self) -> Dict[str, str]:
"""
Return a dictionary containing key-value pairs for the information
items from the uname command data source of the OS distribution.
@@ -884,44 +1016,50 @@ class LinuxDistribution(object):
"""
return self._uname_info
- def os_release_attr(self, attribute):
+ def oslevel_info(self) -> str:
+ """
+ Return AIX' oslevel command output.
+ """
+ return self._oslevel_info
+
+ def os_release_attr(self, attribute: str) -> str:
"""
Return a single named information item from the os-release file data
source of the OS distribution.
For details, see :func:`distro.os_release_attr`.
"""
- return self._os_release_info.get(attribute, '')
+ return self._os_release_info.get(attribute, "")
- def lsb_release_attr(self, attribute):
+ def lsb_release_attr(self, attribute: str) -> str:
"""
Return a single named information item from the lsb_release command
output data source of the OS distribution.
For details, see :func:`distro.lsb_release_attr`.
"""
- return self._lsb_release_info.get(attribute, '')
+ return self._lsb_release_info.get(attribute, "")
- def distro_release_attr(self, attribute):
+ def distro_release_attr(self, attribute: str) -> str:
"""
Return a single named information item from the distro release file
data source of the OS distribution.
For details, see :func:`distro.distro_release_attr`.
"""
- return self._distro_release_info.get(attribute, '')
+ return self._distro_release_info.get(attribute, "")
- def uname_attr(self, attribute):
+ def uname_attr(self, attribute: str) -> str:
"""
Return a single named information item from the uname command
output data source of the OS distribution.
- For details, see :func:`distro.uname_release_attr`.
+ For details, see :func:`distro.uname_attr`.
"""
- return self._uname_info.get(attribute, '')
+ return self._uname_info.get(attribute, "")
@cached_property
- def _os_release_info(self):
+ def _os_release_info(self) -> Dict[str, str]:
"""
Get the information items from the specified os-release file.
@@ -929,12 +1067,12 @@ class LinuxDistribution(object):
A dictionary containing all information items.
"""
if os.path.isfile(self.os_release_file):
- with open(self.os_release_file) as release_file:
+ with open(self.os_release_file, encoding="utf-8") as release_file:
return self._parse_os_release_content(release_file)
return {}
@staticmethod
- def _parse_os_release_content(lines):
+ def _parse_os_release_content(lines: TextIO) -> Dict[str, str]:
"""
Parse the lines of an os-release file.
@@ -951,16 +1089,6 @@ class LinuxDistribution(object):
lexer = shlex.shlex(lines, posix=True)
lexer.whitespace_split = True
- # The shlex module defines its `wordchars` variable using literals,
- # making it dependent on the encoding of the Python source file.
- # In Python 2.6 and 2.7, the shlex source file is encoded in
- # 'iso-8859-1', and the `wordchars` variable is defined as a byte
- # string. This causes a UnicodeDecodeError to be raised when the
- # parsed content is a unicode object. The following fix resolves that
- # (... but it should be fixed in shlex...):
- if sys.version_info[0] == 2 and isinstance(lexer.wordchars, bytes):
- lexer.wordchars = lexer.wordchars.decode('iso-8859-1')
-
tokens = list(lexer)
for token in tokens:
# At this point, all shell-like parsing has been done (i.e.
@@ -969,37 +1097,32 @@ class LinuxDistribution(object):
# stripped, etc.), so the tokens are now either:
# * variable assignments: var=value
# * commands or their arguments (not allowed in os-release)
- if '=' in token:
- k, v = token.split('=', 1)
+ # Ignore any tokens that are not variable assignments
+ if "=" in token:
+ k, v = token.split("=", 1)
props[k.lower()] = v
- else:
- # Ignore any tokens that are not variable assignments
- pass
- if 'version_codename' in props:
+ if "version" in props:
+ # extract release codename (if any) from version attribute
+ match = re.search(r"\((\D+)\)|,\s*(\D+)", props["version"])
+ if match:
+ release_codename = match.group(1) or match.group(2)
+ props["codename"] = props["release_codename"] = release_codename
+
+ if "version_codename" in props:
# os-release added a version_codename field. Use that in
# preference to anything else Note that some distros purposefully
# do not have code names. They should be setting
# version_codename=""
- props['codename'] = props['version_codename']
- elif 'ubuntu_codename' in props:
+ props["codename"] = props["version_codename"]
+ elif "ubuntu_codename" in props:
# Same as above but a non-standard field name used on older Ubuntus
- props['codename'] = props['ubuntu_codename']
- elif 'version' in props:
- # If there is no version_codename, parse it from the version
- codename = re.search(r'(\(\D+\))|,(\s+)?\D+', props['version'])
- if codename:
- codename = codename.group()
- codename = codename.strip('()')
- codename = codename.strip(',')
- codename = codename.strip()
- # codename appears within paranthese.
- props['codename'] = codename
+ props["codename"] = props["ubuntu_codename"]
return props
@cached_property
- def _lsb_release_info(self):
+ def _lsb_release_info(self) -> Dict[str, str]:
"""
Get the information items from the lsb_release command output.
@@ -1008,17 +1131,17 @@ class LinuxDistribution(object):
"""
if not self.include_lsb:
return {}
- with open(os.devnull, 'w') as devnull:
- try:
- cmd = ('lsb_release', '-a')
- stdout = subprocess.check_output(cmd, stderr=devnull)
- except OSError: # Command not found
- return {}
+ try:
+ cmd = ("lsb_release", "-a")
+ stdout = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
+ # Command not found or lsb_release returned error
+ except (OSError, subprocess.CalledProcessError):
+ return {}
content = self._to_str(stdout).splitlines()
return self._parse_lsb_release_content(content)
@staticmethod
- def _parse_lsb_release_content(lines):
+ def _parse_lsb_release_content(lines: Iterable[str]) -> Dict[str, str]:
"""
Parse the output of the lsb_release command.
@@ -1033,58 +1156,62 @@ class LinuxDistribution(object):
"""
props = {}
for line in lines:
- kv = line.strip('\n').split(':', 1)
+ kv = line.strip("\n").split(":", 1)
if len(kv) != 2:
# Ignore lines without colon.
continue
k, v = kv
- props.update({k.replace(' ', '_').lower(): v.strip()})
+ props.update({k.replace(" ", "_").lower(): v.strip()})
return props
@cached_property
- def _uname_info(self):
- with open(os.devnull, 'w') as devnull:
- try:
- cmd = ('uname', '-rs')
- stdout = subprocess.check_output(cmd, stderr=devnull)
- except OSError:
- return {}
+ def _uname_info(self) -> Dict[str, str]:
+ if not self.include_uname:
+ return {}
+ try:
+ cmd = ("uname", "-rs")
+ stdout = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
+ except OSError:
+ return {}
content = self._to_str(stdout).splitlines()
return self._parse_uname_content(content)
+ @cached_property
+ def _oslevel_info(self) -> str:
+ if not self.include_oslevel:
+ return ""
+ try:
+ stdout = subprocess.check_output("oslevel", stderr=subprocess.DEVNULL)
+ except (OSError, subprocess.CalledProcessError):
+ return ""
+ return self._to_str(stdout).strip()
+
@staticmethod
- def _parse_uname_content(lines):
+ def _parse_uname_content(lines: Sequence[str]) -> Dict[str, str]:
+ if not lines:
+ return {}
props = {}
- match = re.search(r'^([^\s]+)\s+([\d\.]+)', lines[0].strip())
+ match = re.search(r"^([^\s]+)\s+([\d\.]+)", lines[0].strip())
if match:
name, version = match.groups()
# This is to prevent the Linux kernel version from
# appearing as the 'best' version on otherwise
# identifiable distributions.
- if name == 'Linux':
+ if name == "Linux":
return {}
- props['id'] = name.lower()
- props['name'] = name
- props['release'] = version
+ props["id"] = name.lower()
+ props["name"] = name
+ props["release"] = version
return props
@staticmethod
- def _to_str(text):
+ def _to_str(bytestring: bytes) -> str:
encoding = sys.getfilesystemencoding()
- encoding = 'utf-8' if encoding == 'ascii' else encoding
-
- if sys.version_info[0] >= 3:
- if isinstance(text, bytes):
- return text.decode(encoding)
- else:
- if isinstance(text, unicode): # noqa
- return text.encode(encoding)
-
- return text
+ return bytestring.decode(encoding)
@cached_property
- def _distro_release_info(self):
+ def _distro_release_info(self) -> Dict[str, str]:
"""
Get the information items from the specified distro release file.
@@ -1094,23 +1221,21 @@ class LinuxDistribution(object):
if self.distro_release_file:
# If it was specified, we use it and parse what we can, even if
# its file name or content does not match the expected pattern.
- distro_info = self._parse_distro_release_file(
- self.distro_release_file)
+ distro_info = self._parse_distro_release_file(self.distro_release_file)
basename = os.path.basename(self.distro_release_file)
# The file name pattern for user-specified distro release files
# is somewhat more tolerant (compared to when searching for the
# file), because we want to use what was specified as best as
# possible.
match = _DISTRO_RELEASE_BASENAME_PATTERN.match(basename)
- if 'name' in distro_info \
- and 'cloudlinux' in distro_info['name'].lower():
- distro_info['id'] = 'cloudlinux'
+ if "name" in distro_info and "cloudlinux" in distro_info["name"].lower():
+ distro_info["id"] = "cloudlinux"
elif match:
- distro_info['id'] = match.group(1)
+ distro_info["id"] = match.group(1)
return distro_info
else:
try:
- basenames = os.listdir(_UNIXCONFDIR)
+ basenames = os.listdir(self.etc_dir)
# We sort for repeatability in cases where there are multiple
# distro specific files; e.g. CentOS, Oracle, Enterprise all
# containing `redhat-release` on top of their own.
@@ -1120,38 +1245,41 @@ class LinuxDistribution(object):
# sure about the *-release files. Check common entries of
# /etc for information. If they turn out to not be there the
# error is handled in `_parse_distro_release_file()`.
- basenames = ['SuSE-release',
- 'arch-release',
- 'base-release',
- 'centos-release',
- 'fedora-release',
- 'gentoo-release',
- 'mageia-release',
- 'mandrake-release',
- 'mandriva-release',
- 'mandrivalinux-release',
- 'manjaro-release',
- 'oracle-release',
- 'redhat-release',
- 'sl-release',
- 'slackware-version']
+ basenames = [
+ "SuSE-release",
+ "arch-release",
+ "base-release",
+ "centos-release",
+ "fedora-release",
+ "gentoo-release",
+ "mageia-release",
+ "mandrake-release",
+ "mandriva-release",
+ "mandrivalinux-release",
+ "manjaro-release",
+ "oracle-release",
+ "redhat-release",
+ "rocky-release",
+ "sl-release",
+ "slackware-version",
+ ]
for basename in basenames:
if basename in _DISTRO_RELEASE_IGNORE_BASENAMES:
continue
match = _DISTRO_RELEASE_BASENAME_PATTERN.match(basename)
if match:
- filepath = os.path.join(_UNIXCONFDIR, basename)
+ filepath = os.path.join(self.etc_dir, basename)
distro_info = self._parse_distro_release_file(filepath)
- if 'name' in distro_info:
+ if "name" in distro_info:
# The name is always present if the pattern matches
self.distro_release_file = filepath
- distro_info['id'] = match.group(1)
- if 'cloudlinux' in distro_info['name'].lower():
- distro_info['id'] = 'cloudlinux'
+ distro_info["id"] = match.group(1)
+ if "cloudlinux" in distro_info["name"].lower():
+ distro_info["id"] = "cloudlinux"
return distro_info
return {}
- def _parse_distro_release_file(self, filepath):
+ def _parse_distro_release_file(self, filepath: str) -> Dict[str, str]:
"""
Parse a distro release file.
@@ -1163,18 +1291,18 @@ class LinuxDistribution(object):
A dictionary containing all information items.
"""
try:
- with open(filepath) as fp:
+ with open(filepath, encoding="utf-8") as fp:
# Only parse the first line. For instance, on SLES there
# are multiple lines. We don't want them...
return self._parse_distro_release_content(fp.readline())
- except (OSError, IOError):
+ except OSError:
# Ignore not being able to read a specific, seemingly version
# related file.
- # See https://github.com/nir0s/distro/issues/162
+ # See https://github.com/python-distro/distro/issues/162
return {}
@staticmethod
- def _parse_distro_release_content(line):
+ def _parse_distro_release_content(line: str) -> Dict[str, str]:
"""
Parse a line from a distro release file.
@@ -1185,46 +1313,62 @@ class LinuxDistribution(object):
Returns:
A dictionary containing all information items.
"""
- matches = _DISTRO_RELEASE_CONTENT_REVERSED_PATTERN.match(
- line.strip()[::-1])
+ matches = _DISTRO_RELEASE_CONTENT_REVERSED_PATTERN.match(line.strip()[::-1])
distro_info = {}
if matches:
# regexp ensures non-None
- distro_info['name'] = matches.group(3)[::-1]
+ distro_info["name"] = matches.group(3)[::-1]
if matches.group(2):
- distro_info['version_id'] = matches.group(2)[::-1]
+ distro_info["version_id"] = matches.group(2)[::-1]
if matches.group(1):
- distro_info['codename'] = matches.group(1)[::-1]
+ distro_info["codename"] = matches.group(1)[::-1]
elif line:
- distro_info['name'] = line.strip()
+ distro_info["name"] = line.strip()
return distro_info
_distro = LinuxDistribution()
-def main():
+def main() -> None:
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.StreamHandler(sys.stdout))
parser = argparse.ArgumentParser(description="OS distro info tool")
parser.add_argument(
- '--json',
- '-j',
- help="Output in machine readable format",
- action="store_true")
+ "--json", "-j", help="Output in machine readable format", action="store_true"
+ )
+
+ parser.add_argument(
+ "--root-dir",
+ "-r",
+ type=str,
+ dest="root_dir",
+ help="Path to the root filesystem directory (defaults to /)",
+ )
+
args = parser.parse_args()
+ if args.root_dir:
+ dist = LinuxDistribution(
+ include_lsb=False,
+ include_uname=False,
+ include_oslevel=False,
+ root_dir=args.root_dir,
+ )
+ else:
+ dist = _distro
+
if args.json:
- logger.info(json.dumps(info(), indent=4, sort_keys=True))
+ logger.info(json.dumps(dist.info(), indent=4, sort_keys=True))
else:
- logger.info('Name: %s', name(pretty=True))
- distribution_version = version(pretty=True)
- logger.info('Version: %s', distribution_version)
- distribution_codename = codename()
- logger.info('Codename: %s', distribution_codename)
+ logger.info("Name: %s", dist.name(pretty=True))
+ distribution_version = dist.version(pretty=True)
+ logger.info("Version: %s", distribution_version)
+ distribution_codename = dist.codename()
+ logger.info("Codename: %s", distribution_codename)
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/src/pip/_vendor/distro/py.typed b/src/pip/_vendor/distro/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pip/_vendor/distro/py.typed
diff --git a/src/pip/_vendor/html5lib.pyi b/src/pip/_vendor/html5lib.pyi
deleted file mode 100644
index 9bc9af95e..000000000
--- a/src/pip/_vendor/html5lib.pyi
+++ /dev/null
@@ -1 +0,0 @@
-from html5lib import * \ No newline at end of file
diff --git a/src/pip/_vendor/html5lib/LICENSE b/src/pip/_vendor/html5lib/LICENSE
deleted file mode 100644
index c87fa7a00..000000000
--- a/src/pip/_vendor/html5lib/LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-Copyright (c) 2006-2013 James Graham and other contributors
-
-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.
diff --git a/src/pip/_vendor/html5lib/__init__.py b/src/pip/_vendor/html5lib/__init__.py
deleted file mode 100644
index d1d82f157..000000000
--- a/src/pip/_vendor/html5lib/__init__.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""
-HTML parsing library based on the `WHATWG HTML specification
-<https://whatwg.org/html>`_. The parser is designed to be compatible with
-existing HTML found in the wild and implements well-defined error recovery that
-is largely compatible with modern desktop web browsers.
-
-Example usage::
-
- from pip._vendor import html5lib
- with open("my_document.html", "rb") as f:
- tree = html5lib.parse(f)
-
-For convenience, this module re-exports the following names:
-
-* :func:`~.html5parser.parse`
-* :func:`~.html5parser.parseFragment`
-* :class:`~.html5parser.HTMLParser`
-* :func:`~.treebuilders.getTreeBuilder`
-* :func:`~.treewalkers.getTreeWalker`
-* :func:`~.serializer.serialize`
-"""
-
-from __future__ import absolute_import, division, unicode_literals
-
-from .html5parser import HTMLParser, parse, parseFragment
-from .treebuilders import getTreeBuilder
-from .treewalkers import getTreeWalker
-from .serializer import serialize
-
-__all__ = ["HTMLParser", "parse", "parseFragment", "getTreeBuilder",
- "getTreeWalker", "serialize"]
-
-# this has to be at the top level, see how setup.py parses this
-#: Distribution version number.
-__version__ = "1.1"
diff --git a/src/pip/_vendor/html5lib/_ihatexml.py b/src/pip/_vendor/html5lib/_ihatexml.py
deleted file mode 100644
index 3ff803c19..000000000
--- a/src/pip/_vendor/html5lib/_ihatexml.py
+++ /dev/null
@@ -1,289 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-import re
-import warnings
-
-from .constants import DataLossWarning
-
-baseChar = """
-[#x0041-#x005A] | [#x0061-#x007A] | [#x00C0-#x00D6] | [#x00D8-#x00F6] |
-[#x00F8-#x00FF] | [#x0100-#x0131] | [#x0134-#x013E] | [#x0141-#x0148] |
-[#x014A-#x017E] | [#x0180-#x01C3] | [#x01CD-#x01F0] | [#x01F4-#x01F5] |
-[#x01FA-#x0217] | [#x0250-#x02A8] | [#x02BB-#x02C1] | #x0386 |
-[#x0388-#x038A] | #x038C | [#x038E-#x03A1] | [#x03A3-#x03CE] |
-[#x03D0-#x03D6] | #x03DA | #x03DC | #x03DE | #x03E0 | [#x03E2-#x03F3] |
-[#x0401-#x040C] | [#x040E-#x044F] | [#x0451-#x045C] | [#x045E-#x0481] |
-[#x0490-#x04C4] | [#x04C7-#x04C8] | [#x04CB-#x04CC] | [#x04D0-#x04EB] |
-[#x04EE-#x04F5] | [#x04F8-#x04F9] | [#x0531-#x0556] | #x0559 |
-[#x0561-#x0586] | [#x05D0-#x05EA] | [#x05F0-#x05F2] | [#x0621-#x063A] |
-[#x0641-#x064A] | [#x0671-#x06B7] | [#x06BA-#x06BE] | [#x06C0-#x06CE] |
-[#x06D0-#x06D3] | #x06D5 | [#x06E5-#x06E6] | [#x0905-#x0939] | #x093D |
-[#x0958-#x0961] | [#x0985-#x098C] | [#x098F-#x0990] | [#x0993-#x09A8] |
-[#x09AA-#x09B0] | #x09B2 | [#x09B6-#x09B9] | [#x09DC-#x09DD] |
-[#x09DF-#x09E1] | [#x09F0-#x09F1] | [#x0A05-#x0A0A] | [#x0A0F-#x0A10] |
-[#x0A13-#x0A28] | [#x0A2A-#x0A30] | [#x0A32-#x0A33] | [#x0A35-#x0A36] |
-[#x0A38-#x0A39] | [#x0A59-#x0A5C] | #x0A5E | [#x0A72-#x0A74] |
-[#x0A85-#x0A8B] | #x0A8D | [#x0A8F-#x0A91] | [#x0A93-#x0AA8] |
-[#x0AAA-#x0AB0] | [#x0AB2-#x0AB3] | [#x0AB5-#x0AB9] | #x0ABD | #x0AE0 |
-[#x0B05-#x0B0C] | [#x0B0F-#x0B10] | [#x0B13-#x0B28] | [#x0B2A-#x0B30] |
-[#x0B32-#x0B33] | [#x0B36-#x0B39] | #x0B3D | [#x0B5C-#x0B5D] |
-[#x0B5F-#x0B61] | [#x0B85-#x0B8A] | [#x0B8E-#x0B90] | [#x0B92-#x0B95] |
-[#x0B99-#x0B9A] | #x0B9C | [#x0B9E-#x0B9F] | [#x0BA3-#x0BA4] |
-[#x0BA8-#x0BAA] | [#x0BAE-#x0BB5] | [#x0BB7-#x0BB9] | [#x0C05-#x0C0C] |
-[#x0C0E-#x0C10] | [#x0C12-#x0C28] | [#x0C2A-#x0C33] | [#x0C35-#x0C39] |
-[#x0C60-#x0C61] | [#x0C85-#x0C8C] | [#x0C8E-#x0C90] | [#x0C92-#x0CA8] |
-[#x0CAA-#x0CB3] | [#x0CB5-#x0CB9] | #x0CDE | [#x0CE0-#x0CE1] |
-[#x0D05-#x0D0C] | [#x0D0E-#x0D10] | [#x0D12-#x0D28] | [#x0D2A-#x0D39] |
-[#x0D60-#x0D61] | [#x0E01-#x0E2E] | #x0E30 | [#x0E32-#x0E33] |
-[#x0E40-#x0E45] | [#x0E81-#x0E82] | #x0E84 | [#x0E87-#x0E88] | #x0E8A |
-#x0E8D | [#x0E94-#x0E97] | [#x0E99-#x0E9F] | [#x0EA1-#x0EA3] | #x0EA5 |
-#x0EA7 | [#x0EAA-#x0EAB] | [#x0EAD-#x0EAE] | #x0EB0 | [#x0EB2-#x0EB3] |
-#x0EBD | [#x0EC0-#x0EC4] | [#x0F40-#x0F47] | [#x0F49-#x0F69] |
-[#x10A0-#x10C5] | [#x10D0-#x10F6] | #x1100 | [#x1102-#x1103] |
-[#x1105-#x1107] | #x1109 | [#x110B-#x110C] | [#x110E-#x1112] | #x113C |
-#x113E | #x1140 | #x114C | #x114E | #x1150 | [#x1154-#x1155] | #x1159 |
-[#x115F-#x1161] | #x1163 | #x1165 | #x1167 | #x1169 | [#x116D-#x116E] |
-[#x1172-#x1173] | #x1175 | #x119E | #x11A8 | #x11AB | [#x11AE-#x11AF] |
-[#x11B7-#x11B8] | #x11BA | [#x11BC-#x11C2] | #x11EB | #x11F0 | #x11F9 |
-[#x1E00-#x1E9B] | [#x1EA0-#x1EF9] | [#x1F00-#x1F15] | [#x1F18-#x1F1D] |
-[#x1F20-#x1F45] | [#x1F48-#x1F4D] | [#x1F50-#x1F57] | #x1F59 | #x1F5B |
-#x1F5D | [#x1F5F-#x1F7D] | [#x1F80-#x1FB4] | [#x1FB6-#x1FBC] | #x1FBE |
-[#x1FC2-#x1FC4] | [#x1FC6-#x1FCC] | [#x1FD0-#x1FD3] | [#x1FD6-#x1FDB] |
-[#x1FE0-#x1FEC] | [#x1FF2-#x1FF4] | [#x1FF6-#x1FFC] | #x2126 |
-[#x212A-#x212B] | #x212E | [#x2180-#x2182] | [#x3041-#x3094] |
-[#x30A1-#x30FA] | [#x3105-#x312C] | [#xAC00-#xD7A3]"""
-
-ideographic = """[#x4E00-#x9FA5] | #x3007 | [#x3021-#x3029]"""
-
-combiningCharacter = """
-[#x0300-#x0345] | [#x0360-#x0361] | [#x0483-#x0486] | [#x0591-#x05A1] |
-[#x05A3-#x05B9] | [#x05BB-#x05BD] | #x05BF | [#x05C1-#x05C2] | #x05C4 |
-[#x064B-#x0652] | #x0670 | [#x06D6-#x06DC] | [#x06DD-#x06DF] |
-[#x06E0-#x06E4] | [#x06E7-#x06E8] | [#x06EA-#x06ED] | [#x0901-#x0903] |
-#x093C | [#x093E-#x094C] | #x094D | [#x0951-#x0954] | [#x0962-#x0963] |
-[#x0981-#x0983] | #x09BC | #x09BE | #x09BF | [#x09C0-#x09C4] |
-[#x09C7-#x09C8] | [#x09CB-#x09CD] | #x09D7 | [#x09E2-#x09E3] | #x0A02 |
-#x0A3C | #x0A3E | #x0A3F | [#x0A40-#x0A42] | [#x0A47-#x0A48] |
-[#x0A4B-#x0A4D] | [#x0A70-#x0A71] | [#x0A81-#x0A83] | #x0ABC |
-[#x0ABE-#x0AC5] | [#x0AC7-#x0AC9] | [#x0ACB-#x0ACD] | [#x0B01-#x0B03] |
-#x0B3C | [#x0B3E-#x0B43] | [#x0B47-#x0B48] | [#x0B4B-#x0B4D] |
-[#x0B56-#x0B57] | [#x0B82-#x0B83] | [#x0BBE-#x0BC2] | [#x0BC6-#x0BC8] |
-[#x0BCA-#x0BCD] | #x0BD7 | [#x0C01-#x0C03] | [#x0C3E-#x0C44] |
-[#x0C46-#x0C48] | [#x0C4A-#x0C4D] | [#x0C55-#x0C56] | [#x0C82-#x0C83] |
-[#x0CBE-#x0CC4] | [#x0CC6-#x0CC8] | [#x0CCA-#x0CCD] | [#x0CD5-#x0CD6] |
-[#x0D02-#x0D03] | [#x0D3E-#x0D43] | [#x0D46-#x0D48] | [#x0D4A-#x0D4D] |
-#x0D57 | #x0E31 | [#x0E34-#x0E3A] | [#x0E47-#x0E4E] | #x0EB1 |
-[#x0EB4-#x0EB9] | [#x0EBB-#x0EBC] | [#x0EC8-#x0ECD] | [#x0F18-#x0F19] |
-#x0F35 | #x0F37 | #x0F39 | #x0F3E | #x0F3F | [#x0F71-#x0F84] |
-[#x0F86-#x0F8B] | [#x0F90-#x0F95] | #x0F97 | [#x0F99-#x0FAD] |
-[#x0FB1-#x0FB7] | #x0FB9 | [#x20D0-#x20DC] | #x20E1 | [#x302A-#x302F] |
-#x3099 | #x309A"""
-
-digit = """
-[#x0030-#x0039] | [#x0660-#x0669] | [#x06F0-#x06F9] | [#x0966-#x096F] |
-[#x09E6-#x09EF] | [#x0A66-#x0A6F] | [#x0AE6-#x0AEF] | [#x0B66-#x0B6F] |
-[#x0BE7-#x0BEF] | [#x0C66-#x0C6F] | [#x0CE6-#x0CEF] | [#x0D66-#x0D6F] |
-[#x0E50-#x0E59] | [#x0ED0-#x0ED9] | [#x0F20-#x0F29]"""
-
-extender = """
-#x00B7 | #x02D0 | #x02D1 | #x0387 | #x0640 | #x0E46 | #x0EC6 | #x3005 |
-#[#x3031-#x3035] | [#x309D-#x309E] | [#x30FC-#x30FE]"""
-
-letter = " | ".join([baseChar, ideographic])
-
-# Without the
-name = " | ".join([letter, digit, ".", "-", "_", combiningCharacter,
- extender])
-nameFirst = " | ".join([letter, "_"])
-
-reChar = re.compile(r"#x([\d|A-F]{4,4})")
-reCharRange = re.compile(r"\[#x([\d|A-F]{4,4})-#x([\d|A-F]{4,4})\]")
-
-
-def charStringToList(chars):
- charRanges = [item.strip() for item in chars.split(" | ")]
- rv = []
- for item in charRanges:
- foundMatch = False
- for regexp in (reChar, reCharRange):
- match = regexp.match(item)
- if match is not None:
- rv.append([hexToInt(item) for item in match.groups()])
- if len(rv[-1]) == 1:
- rv[-1] = rv[-1] * 2
- foundMatch = True
- break
- if not foundMatch:
- assert len(item) == 1
-
- rv.append([ord(item)] * 2)
- rv = normaliseCharList(rv)
- return rv
-
-
-def normaliseCharList(charList):
- charList = sorted(charList)
- for item in charList:
- assert item[1] >= item[0]
- rv = []
- i = 0
- while i < len(charList):
- j = 1
- rv.append(charList[i])
- while i + j < len(charList) and charList[i + j][0] <= rv[-1][1] + 1:
- rv[-1][1] = charList[i + j][1]
- j += 1
- i += j
- return rv
-
-
-# We don't really support characters above the BMP :(
-max_unicode = int("FFFF", 16)
-
-
-def missingRanges(charList):
- rv = []
- if charList[0] != 0:
- rv.append([0, charList[0][0] - 1])
- for i, item in enumerate(charList[:-1]):
- rv.append([item[1] + 1, charList[i + 1][0] - 1])
- if charList[-1][1] != max_unicode:
- rv.append([charList[-1][1] + 1, max_unicode])
- return rv
-
-
-def listToRegexpStr(charList):
- rv = []
- for item in charList:
- if item[0] == item[1]:
- rv.append(escapeRegexp(chr(item[0])))
- else:
- rv.append(escapeRegexp(chr(item[0])) + "-" +
- escapeRegexp(chr(item[1])))
- return "[%s]" % "".join(rv)
-
-
-def hexToInt(hex_str):
- return int(hex_str, 16)
-
-
-def escapeRegexp(string):
- specialCharacters = (".", "^", "$", "*", "+", "?", "{", "}",
- "[", "]", "|", "(", ")", "-")
- for char in specialCharacters:
- string = string.replace(char, "\\" + char)
-
- return string
-
-# output from the above
-nonXmlNameBMPRegexp = re.compile('[\x00-,/:-@\\[-\\^`\\{-\xb6\xb8-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u02cf\u02d2-\u02ff\u0346-\u035f\u0362-\u0385\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482\u0487-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u0590\u05a2\u05ba\u05be\u05c0\u05c3\u05c5-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u063f\u0653-\u065f\u066a-\u066f\u06b8-\u06b9\u06bf\u06cf\u06d4\u06e9\u06ee-\u06ef\u06fa-\u0900\u0904\u093a-\u093b\u094e-\u0950\u0955-\u0957\u0964-\u0965\u0970-\u0980\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09bb\u09bd\u09c5-\u09c6\u09c9-\u09ca\u09ce-\u09d6\u09d8-\u09db\u09de\u09e4-\u09e5\u09f2-\u0a01\u0a03-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a3b\u0a3d\u0a43-\u0a46\u0a49-\u0a4a\u0a4e-\u0a58\u0a5d\u0a5f-\u0a65\u0a75-\u0a80\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abb\u0ac6\u0aca\u0ace-\u0adf\u0ae1-\u0ae5\u0af0-\u0b00\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3b\u0b44-\u0b46\u0b49-\u0b4a\u0b4e-\u0b55\u0b58-\u0b5b\u0b5e\u0b62-\u0b65\u0b70-\u0b81\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0bbd\u0bc3-\u0bc5\u0bc9\u0bce-\u0bd6\u0bd8-\u0be6\u0bf0-\u0c00\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c3d\u0c45\u0c49\u0c4e-\u0c54\u0c57-\u0c5f\u0c62-\u0c65\u0c70-\u0c81\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cbd\u0cc5\u0cc9\u0cce-\u0cd4\u0cd7-\u0cdd\u0cdf\u0ce2-\u0ce5\u0cf0-\u0d01\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d3d\u0d44-\u0d45\u0d49\u0d4e-\u0d56\u0d58-\u0d5f\u0d62-\u0d65\u0d70-\u0e00\u0e2f\u0e3b-\u0e3f\u0e4f\u0e5a-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eba\u0ebe-\u0ebf\u0ec5\u0ec7\u0ece-\u0ecf\u0eda-\u0f17\u0f1a-\u0f1f\u0f2a-\u0f34\u0f36\u0f38\u0f3a-\u0f3d\u0f48\u0f6a-\u0f70\u0f85\u0f8c-\u0f8f\u0f96\u0f98\u0fae-\u0fb0\u0fb8\u0fba-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u20cf\u20dd-\u20e0\u20e2-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3004\u3006\u3008-\u3020\u3030\u3036-\u3040\u3095-\u3098\u309b-\u309c\u309f-\u30a0\u30fb\u30ff-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') # noqa
-
-nonXmlNameFirstBMPRegexp = re.compile('[\x00-@\\[-\\^`\\{-\xbf\xd7\xf7\u0132-\u0133\u013f-\u0140\u0149\u017f\u01c4-\u01cc\u01f1-\u01f3\u01f6-\u01f9\u0218-\u024f\u02a9-\u02ba\u02c2-\u0385\u0387\u038b\u038d\u03a2\u03cf\u03d7-\u03d9\u03db\u03dd\u03df\u03e1\u03f4-\u0400\u040d\u0450\u045d\u0482-\u048f\u04c5-\u04c6\u04c9-\u04ca\u04cd-\u04cf\u04ec-\u04ed\u04f6-\u04f7\u04fa-\u0530\u0557-\u0558\u055a-\u0560\u0587-\u05cf\u05eb-\u05ef\u05f3-\u0620\u063b-\u0640\u064b-\u0670\u06b8-\u06b9\u06bf\u06cf\u06d4\u06d6-\u06e4\u06e7-\u0904\u093a-\u093c\u093e-\u0957\u0962-\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09db\u09de\u09e2-\u09ef\u09f2-\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a58\u0a5d\u0a5f-\u0a71\u0a75-\u0a84\u0a8c\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abc\u0abe-\u0adf\u0ae1-\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34-\u0b35\u0b3a-\u0b3c\u0b3e-\u0b5b\u0b5e\u0b62-\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bb6\u0bba-\u0c04\u0c0d\u0c11\u0c29\u0c34\u0c3a-\u0c5f\u0c62-\u0c84\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cdd\u0cdf\u0ce2-\u0d04\u0d0d\u0d11\u0d29\u0d3a-\u0d5f\u0d62-\u0e00\u0e2f\u0e31\u0e34-\u0e3f\u0e46-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eaf\u0eb1\u0eb4-\u0ebc\u0ebe-\u0ebf\u0ec5-\u0f3f\u0f48\u0f6a-\u109f\u10c6-\u10cf\u10f7-\u10ff\u1101\u1104\u1108\u110a\u110d\u1113-\u113b\u113d\u113f\u1141-\u114b\u114d\u114f\u1151-\u1153\u1156-\u1158\u115a-\u115e\u1162\u1164\u1166\u1168\u116a-\u116c\u116f-\u1171\u1174\u1176-\u119d\u119f-\u11a7\u11a9-\u11aa\u11ac-\u11ad\u11b0-\u11b6\u11b9\u11bb\u11c3-\u11ea\u11ec-\u11ef\u11f1-\u11f8\u11fa-\u1dff\u1e9c-\u1e9f\u1efa-\u1eff\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fbd\u1fbf-\u1fc1\u1fc5\u1fcd-\u1fcf\u1fd4-\u1fd5\u1fdc-\u1fdf\u1fed-\u1ff1\u1ff5\u1ffd-\u2125\u2127-\u2129\u212c-\u212d\u212f-\u217f\u2183-\u3006\u3008-\u3020\u302a-\u3040\u3095-\u30a0\u30fb-\u3104\u312d-\u4dff\u9fa6-\uabff\ud7a4-\uffff]') # noqa
-
-# Simpler things
-nonPubidCharRegexp = re.compile("[^\x20\x0D\x0Aa-zA-Z0-9\\-'()+,./:=?;!*#@$_%]")
-
-
-class InfosetFilter(object):
- replacementRegexp = re.compile(r"U[\dA-F]{5,5}")
-
- def __init__(self,
- dropXmlnsLocalName=False,
- dropXmlnsAttrNs=False,
- preventDoubleDashComments=False,
- preventDashAtCommentEnd=False,
- replaceFormFeedCharacters=True,
- preventSingleQuotePubid=False):
-
- self.dropXmlnsLocalName = dropXmlnsLocalName
- self.dropXmlnsAttrNs = dropXmlnsAttrNs
-
- self.preventDoubleDashComments = preventDoubleDashComments
- self.preventDashAtCommentEnd = preventDashAtCommentEnd
-
- self.replaceFormFeedCharacters = replaceFormFeedCharacters
-
- self.preventSingleQuotePubid = preventSingleQuotePubid
-
- self.replaceCache = {}
-
- def coerceAttribute(self, name, namespace=None):
- if self.dropXmlnsLocalName and name.startswith("xmlns:"):
- warnings.warn("Attributes cannot begin with xmlns", DataLossWarning)
- return None
- elif (self.dropXmlnsAttrNs and
- namespace == "http://www.w3.org/2000/xmlns/"):
- warnings.warn("Attributes cannot be in the xml namespace", DataLossWarning)
- return None
- else:
- return self.toXmlName(name)
-
- def coerceElement(self, name):
- return self.toXmlName(name)
-
- def coerceComment(self, data):
- if self.preventDoubleDashComments:
- while "--" in data:
- warnings.warn("Comments cannot contain adjacent dashes", DataLossWarning)
- data = data.replace("--", "- -")
- if data.endswith("-"):
- warnings.warn("Comments cannot end in a dash", DataLossWarning)
- data += " "
- return data
-
- def coerceCharacters(self, data):
- if self.replaceFormFeedCharacters:
- for _ in range(data.count("\x0C")):
- warnings.warn("Text cannot contain U+000C", DataLossWarning)
- data = data.replace("\x0C", " ")
- # Other non-xml characters
- return data
-
- def coercePubid(self, data):
- dataOutput = data
- for char in nonPubidCharRegexp.findall(data):
- warnings.warn("Coercing non-XML pubid", DataLossWarning)
- replacement = self.getReplacementCharacter(char)
- dataOutput = dataOutput.replace(char, replacement)
- if self.preventSingleQuotePubid and dataOutput.find("'") >= 0:
- warnings.warn("Pubid cannot contain single quote", DataLossWarning)
- dataOutput = dataOutput.replace("'", self.getReplacementCharacter("'"))
- return dataOutput
-
- def toXmlName(self, name):
- nameFirst = name[0]
- nameRest = name[1:]
- m = nonXmlNameFirstBMPRegexp.match(nameFirst)
- if m:
- warnings.warn("Coercing non-XML name: %s" % name, DataLossWarning)
- nameFirstOutput = self.getReplacementCharacter(nameFirst)
- else:
- nameFirstOutput = nameFirst
-
- nameRestOutput = nameRest
- replaceChars = set(nonXmlNameBMPRegexp.findall(nameRest))
- for char in replaceChars:
- warnings.warn("Coercing non-XML name: %s" % name, DataLossWarning)
- replacement = self.getReplacementCharacter(char)
- nameRestOutput = nameRestOutput.replace(char, replacement)
- return nameFirstOutput + nameRestOutput
-
- def getReplacementCharacter(self, char):
- if char in self.replaceCache:
- replacement = self.replaceCache[char]
- else:
- replacement = self.escapeChar(char)
- return replacement
-
- def fromXmlName(self, name):
- for item in set(self.replacementRegexp.findall(name)):
- name = name.replace(item, self.unescapeChar(item))
- return name
-
- def escapeChar(self, char):
- replacement = "U%05X" % ord(char)
- self.replaceCache[char] = replacement
- return replacement
-
- def unescapeChar(self, charcode):
- return chr(int(charcode[1:], 16))
diff --git a/src/pip/_vendor/html5lib/_inputstream.py b/src/pip/_vendor/html5lib/_inputstream.py
deleted file mode 100644
index e0bb37602..000000000
--- a/src/pip/_vendor/html5lib/_inputstream.py
+++ /dev/null
@@ -1,918 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from pip._vendor.six import text_type
-from pip._vendor.six.moves import http_client, urllib
-
-import codecs
-import re
-from io import BytesIO, StringIO
-
-from pip._vendor import webencodings
-
-from .constants import EOF, spaceCharacters, asciiLetters, asciiUppercase
-from .constants import _ReparseException
-from . import _utils
-
-# Non-unicode versions of constants for use in the pre-parser
-spaceCharactersBytes = frozenset([item.encode("ascii") for item in spaceCharacters])
-asciiLettersBytes = frozenset([item.encode("ascii") for item in asciiLetters])
-asciiUppercaseBytes = frozenset([item.encode("ascii") for item in asciiUppercase])
-spacesAngleBrackets = spaceCharactersBytes | frozenset([b">", b"<"])
-
-
-invalid_unicode_no_surrogate = "[\u0001-\u0008\u000B\u000E-\u001F\u007F-\u009F\uFDD0-\uFDEF\uFFFE\uFFFF\U0001FFFE\U0001FFFF\U0002FFFE\U0002FFFF\U0003FFFE\U0003FFFF\U0004FFFE\U0004FFFF\U0005FFFE\U0005FFFF\U0006FFFE\U0006FFFF\U0007FFFE\U0007FFFF\U0008FFFE\U0008FFFF\U0009FFFE\U0009FFFF\U000AFFFE\U000AFFFF\U000BFFFE\U000BFFFF\U000CFFFE\U000CFFFF\U000DFFFE\U000DFFFF\U000EFFFE\U000EFFFF\U000FFFFE\U000FFFFF\U0010FFFE\U0010FFFF]" # noqa
-
-if _utils.supports_lone_surrogates:
- # Use one extra step of indirection and create surrogates with
- # eval. Not using this indirection would introduce an illegal
- # unicode literal on platforms not supporting such lone
- # surrogates.
- assert invalid_unicode_no_surrogate[-1] == "]" and invalid_unicode_no_surrogate.count("]") == 1
- invalid_unicode_re = re.compile(invalid_unicode_no_surrogate[:-1] +
- eval('"\\uD800-\\uDFFF"') + # pylint:disable=eval-used
- "]")
-else:
- invalid_unicode_re = re.compile(invalid_unicode_no_surrogate)
-
-non_bmp_invalid_codepoints = {0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE,
- 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF,
- 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE,
- 0x8FFFF, 0x9FFFE, 0x9FFFF, 0xAFFFE, 0xAFFFF,
- 0xBFFFE, 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE,
- 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF,
- 0x10FFFE, 0x10FFFF}
-
-ascii_punctuation_re = re.compile("[\u0009-\u000D\u0020-\u002F\u003A-\u0040\u005C\u005B-\u0060\u007B-\u007E]")
-
-# Cache for charsUntil()
-charsUntilRegEx = {}
-
-
-class BufferedStream(object):
- """Buffering for streams that do not have buffering of their own
-
- The buffer is implemented as a list of chunks on the assumption that
- joining many strings will be slow since it is O(n**2)
- """
-
- def __init__(self, stream):
- self.stream = stream
- self.buffer = []
- self.position = [-1, 0] # chunk number, offset
-
- def tell(self):
- pos = 0
- for chunk in self.buffer[:self.position[0]]:
- pos += len(chunk)
- pos += self.position[1]
- return pos
-
- def seek(self, pos):
- assert pos <= self._bufferedBytes()
- offset = pos
- i = 0
- while len(self.buffer[i]) < offset:
- offset -= len(self.buffer[i])
- i += 1
- self.position = [i, offset]
-
- def read(self, bytes):
- if not self.buffer:
- return self._readStream(bytes)
- elif (self.position[0] == len(self.buffer) and
- self.position[1] == len(self.buffer[-1])):
- return self._readStream(bytes)
- else:
- return self._readFromBuffer(bytes)
-
- def _bufferedBytes(self):
- return sum([len(item) for item in self.buffer])
-
- def _readStream(self, bytes):
- data = self.stream.read(bytes)
- self.buffer.append(data)
- self.position[0] += 1
- self.position[1] = len(data)
- return data
-
- def _readFromBuffer(self, bytes):
- remainingBytes = bytes
- rv = []
- bufferIndex = self.position[0]
- bufferOffset = self.position[1]
- while bufferIndex < len(self.buffer) and remainingBytes != 0:
- assert remainingBytes > 0
- bufferedData = self.buffer[bufferIndex]
-
- if remainingBytes <= len(bufferedData) - bufferOffset:
- bytesToRead = remainingBytes
- self.position = [bufferIndex, bufferOffset + bytesToRead]
- else:
- bytesToRead = len(bufferedData) - bufferOffset
- self.position = [bufferIndex, len(bufferedData)]
- bufferIndex += 1
- rv.append(bufferedData[bufferOffset:bufferOffset + bytesToRead])
- remainingBytes -= bytesToRead
-
- bufferOffset = 0
-
- if remainingBytes:
- rv.append(self._readStream(remainingBytes))
-
- return b"".join(rv)
-
-
-def HTMLInputStream(source, **kwargs):
- # Work around Python bug #20007: read(0) closes the connection.
- # http://bugs.python.org/issue20007
- if (isinstance(source, http_client.HTTPResponse) or
- # Also check for addinfourl wrapping HTTPResponse
- (isinstance(source, urllib.response.addbase) and
- isinstance(source.fp, http_client.HTTPResponse))):
- isUnicode = False
- elif hasattr(source, "read"):
- isUnicode = isinstance(source.read(0), text_type)
- else:
- isUnicode = isinstance(source, text_type)
-
- if isUnicode:
- encodings = [x for x in kwargs if x.endswith("_encoding")]
- if encodings:
- raise TypeError("Cannot set an encoding with a unicode input, set %r" % encodings)
-
- return HTMLUnicodeInputStream(source, **kwargs)
- else:
- return HTMLBinaryInputStream(source, **kwargs)
-
-
-class HTMLUnicodeInputStream(object):
- """Provides a unicode stream of characters to the HTMLTokenizer.
-
- This class takes care of character encoding and removing or replacing
- incorrect byte-sequences and also provides column and line tracking.
-
- """
-
- _defaultChunkSize = 10240
-
- def __init__(self, source):
- """Initialises the HTMLInputStream.
-
- HTMLInputStream(source, [encoding]) -> Normalized stream from source
- for use by html5lib.
-
- source can be either a file-object, local filename or a string.
-
- The optional encoding parameter must be a string that indicates
- the encoding. If specified, that encoding will be used,
- regardless of any BOM or later declaration (such as in a meta
- element)
-
- """
-
- if not _utils.supports_lone_surrogates:
- # Such platforms will have already checked for such
- # surrogate errors, so no need to do this checking.
- self.reportCharacterErrors = None
- elif len("\U0010FFFF") == 1:
- self.reportCharacterErrors = self.characterErrorsUCS4
- else:
- self.reportCharacterErrors = self.characterErrorsUCS2
-
- # List of where new lines occur
- self.newLines = [0]
-
- self.charEncoding = (lookupEncoding("utf-8"), "certain")
- self.dataStream = self.openStream(source)
-
- self.reset()
-
- def reset(self):
- self.chunk = ""
- self.chunkSize = 0
- self.chunkOffset = 0
- self.errors = []
-
- # number of (complete) lines in previous chunks
- self.prevNumLines = 0
- # number of columns in the last line of the previous chunk
- self.prevNumCols = 0
-
- # Deal with CR LF and surrogates split over chunk boundaries
- self._bufferedCharacter = None
-
- def openStream(self, source):
- """Produces a file object from source.
-
- source can be either a file object, local filename or a string.
-
- """
- # Already a file object
- if hasattr(source, 'read'):
- stream = source
- else:
- stream = StringIO(source)
-
- return stream
-
- def _position(self, offset):
- chunk = self.chunk
- nLines = chunk.count('\n', 0, offset)
- positionLine = self.prevNumLines + nLines
- lastLinePos = chunk.rfind('\n', 0, offset)
- if lastLinePos == -1:
- positionColumn = self.prevNumCols + offset
- else:
- positionColumn = offset - (lastLinePos + 1)
- return (positionLine, positionColumn)
-
- def position(self):
- """Returns (line, col) of the current position in the stream."""
- line, col = self._position(self.chunkOffset)
- return (line + 1, col)
-
- def char(self):
- """ Read one character from the stream or queue if available. Return
- EOF when EOF is reached.
- """
- # Read a new chunk from the input stream if necessary
- if self.chunkOffset >= self.chunkSize:
- if not self.readChunk():
- return EOF
-
- chunkOffset = self.chunkOffset
- char = self.chunk[chunkOffset]
- self.chunkOffset = chunkOffset + 1
-
- return char
-
- def readChunk(self, chunkSize=None):
- if chunkSize is None:
- chunkSize = self._defaultChunkSize
-
- self.prevNumLines, self.prevNumCols = self._position(self.chunkSize)
-
- self.chunk = ""
- self.chunkSize = 0
- self.chunkOffset = 0
-
- data = self.dataStream.read(chunkSize)
-
- # Deal with CR LF and surrogates broken across chunks
- if self._bufferedCharacter:
- data = self._bufferedCharacter + data
- self._bufferedCharacter = None
- elif not data:
- # We have no more data, bye-bye stream
- return False
-
- if len(data) > 1:
- lastv = ord(data[-1])
- if lastv == 0x0D or 0xD800 <= lastv <= 0xDBFF:
- self._bufferedCharacter = data[-1]
- data = data[:-1]
-
- if self.reportCharacterErrors:
- self.reportCharacterErrors(data)
-
- # Replace invalid characters
- data = data.replace("\r\n", "\n")
- data = data.replace("\r", "\n")
-
- self.chunk = data
- self.chunkSize = len(data)
-
- return True
-
- def characterErrorsUCS4(self, data):
- for _ in range(len(invalid_unicode_re.findall(data))):
- self.errors.append("invalid-codepoint")
-
- def characterErrorsUCS2(self, data):
- # Someone picked the wrong compile option
- # You lose
- skip = False
- for match in invalid_unicode_re.finditer(data):
- if skip:
- continue
- codepoint = ord(match.group())
- pos = match.start()
- # Pretty sure there should be endianness issues here
- if _utils.isSurrogatePair(data[pos:pos + 2]):
- # We have a surrogate pair!
- char_val = _utils.surrogatePairToCodepoint(data[pos:pos + 2])
- if char_val in non_bmp_invalid_codepoints:
- self.errors.append("invalid-codepoint")
- skip = True
- elif (codepoint >= 0xD800 and codepoint <= 0xDFFF and
- pos == len(data) - 1):
- self.errors.append("invalid-codepoint")
- else:
- skip = False
- self.errors.append("invalid-codepoint")
-
- def charsUntil(self, characters, opposite=False):
- """ Returns a string of characters from the stream up to but not
- including any character in 'characters' or EOF. 'characters' must be
- a container that supports the 'in' method and iteration over its
- characters.
- """
-
- # Use a cache of regexps to find the required characters
- try:
- chars = charsUntilRegEx[(characters, opposite)]
- except KeyError:
- if __debug__:
- for c in characters:
- assert(ord(c) < 128)
- regex = "".join(["\\x%02x" % ord(c) for c in characters])
- if not opposite:
- regex = "^%s" % regex
- chars = charsUntilRegEx[(characters, opposite)] = re.compile("[%s]+" % regex)
-
- rv = []
-
- while True:
- # Find the longest matching prefix
- m = chars.match(self.chunk, self.chunkOffset)
- if m is None:
- # If nothing matched, and it wasn't because we ran out of chunk,
- # then stop
- if self.chunkOffset != self.chunkSize:
- break
- else:
- end = m.end()
- # If not the whole chunk matched, return everything
- # up to the part that didn't match
- if end != self.chunkSize:
- rv.append(self.chunk[self.chunkOffset:end])
- self.chunkOffset = end
- break
- # If the whole remainder of the chunk matched,
- # use it all and read the next chunk
- rv.append(self.chunk[self.chunkOffset:])
- if not self.readChunk():
- # Reached EOF
- break
-
- r = "".join(rv)
- return r
-
- def unget(self, char):
- # Only one character is allowed to be ungotten at once - it must
- # be consumed again before any further call to unget
- if char is not EOF:
- if self.chunkOffset == 0:
- # unget is called quite rarely, so it's a good idea to do
- # more work here if it saves a bit of work in the frequently
- # called char and charsUntil.
- # So, just prepend the ungotten character onto the current
- # chunk:
- self.chunk = char + self.chunk
- self.chunkSize += 1
- else:
- self.chunkOffset -= 1
- assert self.chunk[self.chunkOffset] == char
-
-
-class HTMLBinaryInputStream(HTMLUnicodeInputStream):
- """Provides a unicode stream of characters to the HTMLTokenizer.
-
- This class takes care of character encoding and removing or replacing
- incorrect byte-sequences and also provides column and line tracking.
-
- """
-
- def __init__(self, source, override_encoding=None, transport_encoding=None,
- same_origin_parent_encoding=None, likely_encoding=None,
- default_encoding="windows-1252", useChardet=True):
- """Initialises the HTMLInputStream.
-
- HTMLInputStream(source, [encoding]) -> Normalized stream from source
- for use by html5lib.
-
- source can be either a file-object, local filename or a string.
-
- The optional encoding parameter must be a string that indicates
- the encoding. If specified, that encoding will be used,
- regardless of any BOM or later declaration (such as in a meta
- element)
-
- """
- # Raw Stream - for unicode objects this will encode to utf-8 and set
- # self.charEncoding as appropriate
- self.rawStream = self.openStream(source)
-
- HTMLUnicodeInputStream.__init__(self, self.rawStream)
-
- # Encoding Information
- # Number of bytes to use when looking for a meta element with
- # encoding information
- self.numBytesMeta = 1024
- # Number of bytes to use when using detecting encoding using chardet
- self.numBytesChardet = 100
- # Things from args
- self.override_encoding = override_encoding
- self.transport_encoding = transport_encoding
- self.same_origin_parent_encoding = same_origin_parent_encoding
- self.likely_encoding = likely_encoding
- self.default_encoding = default_encoding
-
- # Determine encoding
- self.charEncoding = self.determineEncoding(useChardet)
- assert self.charEncoding[0] is not None
-
- # Call superclass
- self.reset()
-
- def reset(self):
- self.dataStream = self.charEncoding[0].codec_info.streamreader(self.rawStream, 'replace')
- HTMLUnicodeInputStream.reset(self)
-
- def openStream(self, source):
- """Produces a file object from source.
-
- source can be either a file object, local filename or a string.
-
- """
- # Already a file object
- if hasattr(source, 'read'):
- stream = source
- else:
- stream = BytesIO(source)
-
- try:
- stream.seek(stream.tell())
- except Exception:
- stream = BufferedStream(stream)
-
- return stream
-
- def determineEncoding(self, chardet=True):
- # BOMs take precedence over everything
- # This will also read past the BOM if present
- charEncoding = self.detectBOM(), "certain"
- if charEncoding[0] is not None:
- return charEncoding
-
- # If we've been overridden, we've been overridden
- charEncoding = lookupEncoding(self.override_encoding), "certain"
- if charEncoding[0] is not None:
- return charEncoding
-
- # Now check the transport layer
- charEncoding = lookupEncoding(self.transport_encoding), "certain"
- if charEncoding[0] is not None:
- return charEncoding
-
- # Look for meta elements with encoding information
- charEncoding = self.detectEncodingMeta(), "tentative"
- if charEncoding[0] is not None:
- return charEncoding
-
- # Parent document encoding
- charEncoding = lookupEncoding(self.same_origin_parent_encoding), "tentative"
- if charEncoding[0] is not None and not charEncoding[0].name.startswith("utf-16"):
- return charEncoding
-
- # "likely" encoding
- charEncoding = lookupEncoding(self.likely_encoding), "tentative"
- if charEncoding[0] is not None:
- return charEncoding
-
- # Guess with chardet, if available
- if chardet:
- try:
- from pip._vendor.chardet.universaldetector import UniversalDetector
- except ImportError:
- pass
- else:
- buffers = []
- detector = UniversalDetector()
- while not detector.done:
- buffer = self.rawStream.read(self.numBytesChardet)
- assert isinstance(buffer, bytes)
- if not buffer:
- break
- buffers.append(buffer)
- detector.feed(buffer)
- detector.close()
- encoding = lookupEncoding(detector.result['encoding'])
- self.rawStream.seek(0)
- if encoding is not None:
- return encoding, "tentative"
-
- # Try the default encoding
- charEncoding = lookupEncoding(self.default_encoding), "tentative"
- if charEncoding[0] is not None:
- return charEncoding
-
- # Fallback to html5lib's default if even that hasn't worked
- return lookupEncoding("windows-1252"), "tentative"
-
- def changeEncoding(self, newEncoding):
- assert self.charEncoding[1] != "certain"
- newEncoding = lookupEncoding(newEncoding)
- if newEncoding is None:
- return
- if newEncoding.name in ("utf-16be", "utf-16le"):
- newEncoding = lookupEncoding("utf-8")
- assert newEncoding is not None
- elif newEncoding == self.charEncoding[0]:
- self.charEncoding = (self.charEncoding[0], "certain")
- else:
- self.rawStream.seek(0)
- self.charEncoding = (newEncoding, "certain")
- self.reset()
- raise _ReparseException("Encoding changed from %s to %s" % (self.charEncoding[0], newEncoding))
-
- def detectBOM(self):
- """Attempts to detect at BOM at the start of the stream. If
- an encoding can be determined from the BOM return the name of the
- encoding otherwise return None"""
- bomDict = {
- codecs.BOM_UTF8: 'utf-8',
- codecs.BOM_UTF16_LE: 'utf-16le', codecs.BOM_UTF16_BE: 'utf-16be',
- codecs.BOM_UTF32_LE: 'utf-32le', codecs.BOM_UTF32_BE: 'utf-32be'
- }
-
- # Go to beginning of file and read in 4 bytes
- string = self.rawStream.read(4)
- assert isinstance(string, bytes)
-
- # Try detecting the BOM using bytes from the string
- encoding = bomDict.get(string[:3]) # UTF-8
- seek = 3
- if not encoding:
- # Need to detect UTF-32 before UTF-16
- encoding = bomDict.get(string) # UTF-32
- seek = 4
- if not encoding:
- encoding = bomDict.get(string[:2]) # UTF-16
- seek = 2
-
- # Set the read position past the BOM if one was found, otherwise
- # set it to the start of the stream
- if encoding:
- self.rawStream.seek(seek)
- return lookupEncoding(encoding)
- else:
- self.rawStream.seek(0)
- return None
-
- def detectEncodingMeta(self):
- """Report the encoding declared by the meta element
- """
- buffer = self.rawStream.read(self.numBytesMeta)
- assert isinstance(buffer, bytes)
- parser = EncodingParser(buffer)
- self.rawStream.seek(0)
- encoding = parser.getEncoding()
-
- if encoding is not None and encoding.name in ("utf-16be", "utf-16le"):
- encoding = lookupEncoding("utf-8")
-
- return encoding
-
-
-class EncodingBytes(bytes):
- """String-like object with an associated position and various extra methods
- If the position is ever greater than the string length then an exception is
- raised"""
- def __new__(self, value):
- assert isinstance(value, bytes)
- return bytes.__new__(self, value.lower())
-
- def __init__(self, value):
- # pylint:disable=unused-argument
- self._position = -1
-
- def __iter__(self):
- return self
-
- def __next__(self):
- p = self._position = self._position + 1
- if p >= len(self):
- raise StopIteration
- elif p < 0:
- raise TypeError
- return self[p:p + 1]
-
- def next(self):
- # Py2 compat
- return self.__next__()
-
- def previous(self):
- p = self._position
- if p >= len(self):
- raise StopIteration
- elif p < 0:
- raise TypeError
- self._position = p = p - 1
- return self[p:p + 1]
-
- def setPosition(self, position):
- if self._position >= len(self):
- raise StopIteration
- self._position = position
-
- def getPosition(self):
- if self._position >= len(self):
- raise StopIteration
- if self._position >= 0:
- return self._position
- else:
- return None
-
- position = property(getPosition, setPosition)
-
- def getCurrentByte(self):
- return self[self.position:self.position + 1]
-
- currentByte = property(getCurrentByte)
-
- def skip(self, chars=spaceCharactersBytes):
- """Skip past a list of characters"""
- p = self.position # use property for the error-checking
- while p < len(self):
- c = self[p:p + 1]
- if c not in chars:
- self._position = p
- return c
- p += 1
- self._position = p
- return None
-
- def skipUntil(self, chars):
- p = self.position
- while p < len(self):
- c = self[p:p + 1]
- if c in chars:
- self._position = p
- return c
- p += 1
- self._position = p
- return None
-
- def matchBytes(self, bytes):
- """Look for a sequence of bytes at the start of a string. If the bytes
- are found return True and advance the position to the byte after the
- match. Otherwise return False and leave the position alone"""
- rv = self.startswith(bytes, self.position)
- if rv:
- self.position += len(bytes)
- return rv
-
- def jumpTo(self, bytes):
- """Look for the next sequence of bytes matching a given sequence. If
- a match is found advance the position to the last byte of the match"""
- try:
- self._position = self.index(bytes, self.position) + len(bytes) - 1
- except ValueError:
- raise StopIteration
- return True
-
-
-class EncodingParser(object):
- """Mini parser for detecting character encoding from meta elements"""
-
- def __init__(self, data):
- """string - the data to work on for encoding detection"""
- self.data = EncodingBytes(data)
- self.encoding = None
-
- def getEncoding(self):
- if b"<meta" not in self.data:
- return None
-
- methodDispatch = (
- (b"<!--", self.handleComment),
- (b"<meta", self.handleMeta),
- (b"</", self.handlePossibleEndTag),
- (b"<!", self.handleOther),
- (b"<?", self.handleOther),
- (b"<", self.handlePossibleStartTag))
- for _ in self.data:
- keepParsing = True
- try:
- self.data.jumpTo(b"<")
- except StopIteration:
- break
- for key, method in methodDispatch:
- if self.data.matchBytes(key):
- try:
- keepParsing = method()
- break
- except StopIteration:
- keepParsing = False
- break
- if not keepParsing:
- break
-
- return self.encoding
-
- def handleComment(self):
- """Skip over comments"""
- return self.data.jumpTo(b"-->")
-
- def handleMeta(self):
- if self.data.currentByte not in spaceCharactersBytes:
- # if we have <meta not followed by a space so just keep going
- return True
- # We have a valid meta element we want to search for attributes
- hasPragma = False
- pendingEncoding = None
- while True:
- # Try to find the next attribute after the current position
- attr = self.getAttribute()
- if attr is None:
- return True
- else:
- if attr[0] == b"http-equiv":
- hasPragma = attr[1] == b"content-type"
- if hasPragma and pendingEncoding is not None:
- self.encoding = pendingEncoding
- return False
- elif attr[0] == b"charset":
- tentativeEncoding = attr[1]
- codec = lookupEncoding(tentativeEncoding)
- if codec is not None:
- self.encoding = codec
- return False
- elif attr[0] == b"content":
- contentParser = ContentAttrParser(EncodingBytes(attr[1]))
- tentativeEncoding = contentParser.parse()
- if tentativeEncoding is not None:
- codec = lookupEncoding(tentativeEncoding)
- if codec is not None:
- if hasPragma:
- self.encoding = codec
- return False
- else:
- pendingEncoding = codec
-
- def handlePossibleStartTag(self):
- return self.handlePossibleTag(False)
-
- def handlePossibleEndTag(self):
- next(self.data)
- return self.handlePossibleTag(True)
-
- def handlePossibleTag(self, endTag):
- data = self.data
- if data.currentByte not in asciiLettersBytes:
- # If the next byte is not an ascii letter either ignore this
- # fragment (possible start tag case) or treat it according to
- # handleOther
- if endTag:
- data.previous()
- self.handleOther()
- return True
-
- c = data.skipUntil(spacesAngleBrackets)
- if c == b"<":
- # return to the first step in the overall "two step" algorithm
- # reprocessing the < byte
- data.previous()
- else:
- # Read all attributes
- attr = self.getAttribute()
- while attr is not None:
- attr = self.getAttribute()
- return True
-
- def handleOther(self):
- return self.data.jumpTo(b">")
-
- def getAttribute(self):
- """Return a name,value pair for the next attribute in the stream,
- if one is found, or None"""
- data = self.data
- # Step 1 (skip chars)
- c = data.skip(spaceCharactersBytes | frozenset([b"/"]))
- assert c is None or len(c) == 1
- # Step 2
- if c in (b">", None):
- return None
- # Step 3
- attrName = []
- attrValue = []
- # Step 4 attribute name
- while True:
- if c == b"=" and attrName:
- break
- elif c in spaceCharactersBytes:
- # Step 6!
- c = data.skip()
- break
- elif c in (b"/", b">"):
- return b"".join(attrName), b""
- elif c in asciiUppercaseBytes:
- attrName.append(c.lower())
- elif c is None:
- return None
- else:
- attrName.append(c)
- # Step 5
- c = next(data)
- # Step 7
- if c != b"=":
- data.previous()
- return b"".join(attrName), b""
- # Step 8
- next(data)
- # Step 9
- c = data.skip()
- # Step 10
- if c in (b"'", b'"'):
- # 10.1
- quoteChar = c
- while True:
- # 10.2
- c = next(data)
- # 10.3
- if c == quoteChar:
- next(data)
- return b"".join(attrName), b"".join(attrValue)
- # 10.4
- elif c in asciiUppercaseBytes:
- attrValue.append(c.lower())
- # 10.5
- else:
- attrValue.append(c)
- elif c == b">":
- return b"".join(attrName), b""
- elif c in asciiUppercaseBytes:
- attrValue.append(c.lower())
- elif c is None:
- return None
- else:
- attrValue.append(c)
- # Step 11
- while True:
- c = next(data)
- if c in spacesAngleBrackets:
- return b"".join(attrName), b"".join(attrValue)
- elif c in asciiUppercaseBytes:
- attrValue.append(c.lower())
- elif c is None:
- return None
- else:
- attrValue.append(c)
-
-
-class ContentAttrParser(object):
- def __init__(self, data):
- assert isinstance(data, bytes)
- self.data = data
-
- def parse(self):
- try:
- # Check if the attr name is charset
- # otherwise return
- self.data.jumpTo(b"charset")
- self.data.position += 1
- self.data.skip()
- if not self.data.currentByte == b"=":
- # If there is no = sign keep looking for attrs
- return None
- self.data.position += 1
- self.data.skip()
- # Look for an encoding between matching quote marks
- if self.data.currentByte in (b'"', b"'"):
- quoteMark = self.data.currentByte
- self.data.position += 1
- oldPosition = self.data.position
- if self.data.jumpTo(quoteMark):
- return self.data[oldPosition:self.data.position]
- else:
- return None
- else:
- # Unquoted value
- oldPosition = self.data.position
- try:
- self.data.skipUntil(spaceCharactersBytes)
- return self.data[oldPosition:self.data.position]
- except StopIteration:
- # Return the whole remaining value
- return self.data[oldPosition:]
- except StopIteration:
- return None
-
-
-def lookupEncoding(encoding):
- """Return the python codec name corresponding to an encoding or None if the
- string doesn't correspond to a valid encoding."""
- if isinstance(encoding, bytes):
- try:
- encoding = encoding.decode("ascii")
- except UnicodeDecodeError:
- return None
-
- if encoding is not None:
- try:
- return webencodings.lookup(encoding)
- except AttributeError:
- return None
- else:
- return None
diff --git a/src/pip/_vendor/html5lib/_tokenizer.py b/src/pip/_vendor/html5lib/_tokenizer.py
deleted file mode 100644
index 5f00253e2..000000000
--- a/src/pip/_vendor/html5lib/_tokenizer.py
+++ /dev/null
@@ -1,1735 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from pip._vendor.six import unichr as chr
-
-from collections import deque, OrderedDict
-from sys import version_info
-
-from .constants import spaceCharacters
-from .constants import entities
-from .constants import asciiLetters, asciiUpper2Lower
-from .constants import digits, hexDigits, EOF
-from .constants import tokenTypes, tagTokenTypes
-from .constants import replacementCharacters
-
-from ._inputstream import HTMLInputStream
-
-from ._trie import Trie
-
-entitiesTrie = Trie(entities)
-
-if version_info >= (3, 7):
- attributeMap = dict
-else:
- attributeMap = OrderedDict
-
-
-class HTMLTokenizer(object):
- """ This class takes care of tokenizing HTML.
-
- * self.currentToken
- Holds the token that is currently being processed.
-
- * self.state
- Holds a reference to the method to be invoked... XXX
-
- * self.stream
- Points to HTMLInputStream object.
- """
-
- def __init__(self, stream, parser=None, **kwargs):
-
- self.stream = HTMLInputStream(stream, **kwargs)
- self.parser = parser
-
- # Setup the initial tokenizer state
- self.escapeFlag = False
- self.lastFourChars = []
- self.state = self.dataState
- self.escape = False
-
- # The current token being created
- self.currentToken = None
- super(HTMLTokenizer, self).__init__()
-
- def __iter__(self):
- """ This is where the magic happens.
-
- We do our usually processing through the states and when we have a token
- to return we yield the token which pauses processing until the next token
- is requested.
- """
- self.tokenQueue = deque([])
- # Start processing. When EOF is reached self.state will return False
- # instead of True and the loop will terminate.
- while self.state():
- while self.stream.errors:
- yield {"type": tokenTypes["ParseError"], "data": self.stream.errors.pop(0)}
- while self.tokenQueue:
- yield self.tokenQueue.popleft()
-
- def consumeNumberEntity(self, isHex):
- """This function returns either U+FFFD or the character based on the
- decimal or hexadecimal representation. It also discards ";" if present.
- If not present self.tokenQueue.append({"type": tokenTypes["ParseError"]}) is invoked.
- """
-
- allowed = digits
- radix = 10
- if isHex:
- allowed = hexDigits
- radix = 16
-
- charStack = []
-
- # Consume all the characters that are in range while making sure we
- # don't hit an EOF.
- c = self.stream.char()
- while c in allowed and c is not EOF:
- charStack.append(c)
- c = self.stream.char()
-
- # Convert the set of characters consumed to an int.
- charAsInt = int("".join(charStack), radix)
-
- # Certain characters get replaced with others
- if charAsInt in replacementCharacters:
- char = replacementCharacters[charAsInt]
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "illegal-codepoint-for-numeric-entity",
- "datavars": {"charAsInt": charAsInt}})
- elif ((0xD800 <= charAsInt <= 0xDFFF) or
- (charAsInt > 0x10FFFF)):
- char = "\uFFFD"
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "illegal-codepoint-for-numeric-entity",
- "datavars": {"charAsInt": charAsInt}})
- else:
- # Should speed up this check somehow (e.g. move the set to a constant)
- if ((0x0001 <= charAsInt <= 0x0008) or
- (0x000E <= charAsInt <= 0x001F) or
- (0x007F <= charAsInt <= 0x009F) or
- (0xFDD0 <= charAsInt <= 0xFDEF) or
- charAsInt in frozenset([0x000B, 0xFFFE, 0xFFFF, 0x1FFFE,
- 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE,
- 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE,
- 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE,
- 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE,
- 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE,
- 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE,
- 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE,
- 0xFFFFF, 0x10FFFE, 0x10FFFF])):
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data":
- "illegal-codepoint-for-numeric-entity",
- "datavars": {"charAsInt": charAsInt}})
- try:
- # Try/except needed as UCS-2 Python builds' unichar only works
- # within the BMP.
- char = chr(charAsInt)
- except ValueError:
- v = charAsInt - 0x10000
- char = chr(0xD800 | (v >> 10)) + chr(0xDC00 | (v & 0x3FF))
-
- # Discard the ; if present. Otherwise, put it back on the queue and
- # invoke parseError on parser.
- if c != ";":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "numeric-entity-without-semicolon"})
- self.stream.unget(c)
-
- return char
-
- def consumeEntity(self, allowedChar=None, fromAttribute=False):
- # Initialise to the default output for when no entity is matched
- output = "&"
-
- charStack = [self.stream.char()]
- if (charStack[0] in spaceCharacters or charStack[0] in (EOF, "<", "&") or
- (allowedChar is not None and allowedChar == charStack[0])):
- self.stream.unget(charStack[0])
-
- elif charStack[0] == "#":
- # Read the next character to see if it's hex or decimal
- hex = False
- charStack.append(self.stream.char())
- if charStack[-1] in ("x", "X"):
- hex = True
- charStack.append(self.stream.char())
-
- # charStack[-1] should be the first digit
- if (hex and charStack[-1] in hexDigits) \
- or (not hex and charStack[-1] in digits):
- # At least one digit found, so consume the whole number
- self.stream.unget(charStack[-1])
- output = self.consumeNumberEntity(hex)
- else:
- # No digits found
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "expected-numeric-entity"})
- self.stream.unget(charStack.pop())
- output = "&" + "".join(charStack)
-
- else:
- # At this point in the process might have named entity. Entities
- # are stored in the global variable "entities".
- #
- # Consume characters and compare to these to a substring of the
- # entity names in the list until the substring no longer matches.
- while (charStack[-1] is not EOF):
- if not entitiesTrie.has_keys_with_prefix("".join(charStack)):
- break
- charStack.append(self.stream.char())
-
- # At this point we have a string that starts with some characters
- # that may match an entity
- # Try to find the longest entity the string will match to take care
- # of &noti for instance.
- try:
- entityName = entitiesTrie.longest_prefix("".join(charStack[:-1]))
- entityLength = len(entityName)
- except KeyError:
- entityName = None
-
- if entityName is not None:
- if entityName[-1] != ";":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "named-entity-without-semicolon"})
- if (entityName[-1] != ";" and fromAttribute and
- (charStack[entityLength] in asciiLetters or
- charStack[entityLength] in digits or
- charStack[entityLength] == "=")):
- self.stream.unget(charStack.pop())
- output = "&" + "".join(charStack)
- else:
- output = entities[entityName]
- self.stream.unget(charStack.pop())
- output += "".join(charStack[entityLength:])
- else:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-named-entity"})
- self.stream.unget(charStack.pop())
- output = "&" + "".join(charStack)
-
- if fromAttribute:
- self.currentToken["data"][-1][1] += output
- else:
- if output in spaceCharacters:
- tokenType = "SpaceCharacters"
- else:
- tokenType = "Characters"
- self.tokenQueue.append({"type": tokenTypes[tokenType], "data": output})
-
- def processEntityInAttribute(self, allowedChar):
- """This method replaces the need for "entityInAttributeValueState".
- """
- self.consumeEntity(allowedChar=allowedChar, fromAttribute=True)
-
- def emitCurrentToken(self):
- """This method is a generic handler for emitting the tags. It also sets
- the state to "data" because that's what's needed after a token has been
- emitted.
- """
- token = self.currentToken
- # Add token to the queue to be yielded
- if (token["type"] in tagTokenTypes):
- token["name"] = token["name"].translate(asciiUpper2Lower)
- if token["type"] == tokenTypes["StartTag"]:
- raw = token["data"]
- data = attributeMap(raw)
- if len(raw) > len(data):
- # we had some duplicated attribute, fix so first wins
- data.update(raw[::-1])
- token["data"] = data
-
- if token["type"] == tokenTypes["EndTag"]:
- if token["data"]:
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "attributes-in-end-tag"})
- if token["selfClosing"]:
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "self-closing-flag-on-end-tag"})
- self.tokenQueue.append(token)
- self.state = self.dataState
-
- # Below are the various tokenizer states worked out.
- def dataState(self):
- data = self.stream.char()
- if data == "&":
- self.state = self.entityDataState
- elif data == "<":
- self.state = self.tagOpenState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "\u0000"})
- elif data is EOF:
- # Tokenization ends.
- return False
- elif data in spaceCharacters:
- # Directly after emitting a token you switch back to the "data
- # state". At that point spaceCharacters are important so they are
- # emitted separately.
- self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data":
- data + self.stream.charsUntil(spaceCharacters, True)})
- # No need to update lastFourChars here, since the first space will
- # have already been appended to lastFourChars and will have broken
- # any <!-- or --> sequences
- else:
- chars = self.stream.charsUntil(("&", "<", "\u0000"))
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
- data + chars})
- return True
-
- def entityDataState(self):
- self.consumeEntity()
- self.state = self.dataState
- return True
-
- def rcdataState(self):
- data = self.stream.char()
- if data == "&":
- self.state = self.characterReferenceInRcdata
- elif data == "<":
- self.state = self.rcdataLessThanSignState
- elif data == EOF:
- # Tokenization ends.
- return False
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "\uFFFD"})
- elif data in spaceCharacters:
- # Directly after emitting a token you switch back to the "data
- # state". At that point spaceCharacters are important so they are
- # emitted separately.
- self.tokenQueue.append({"type": tokenTypes["SpaceCharacters"], "data":
- data + self.stream.charsUntil(spaceCharacters, True)})
- # No need to update lastFourChars here, since the first space will
- # have already been appended to lastFourChars and will have broken
- # any <!-- or --> sequences
- else:
- chars = self.stream.charsUntil(("&", "<", "\u0000"))
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
- data + chars})
- return True
-
- def characterReferenceInRcdata(self):
- self.consumeEntity()
- self.state = self.rcdataState
- return True
-
- def rawtextState(self):
- data = self.stream.char()
- if data == "<":
- self.state = self.rawtextLessThanSignState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "\uFFFD"})
- elif data == EOF:
- # Tokenization ends.
- return False
- else:
- chars = self.stream.charsUntil(("<", "\u0000"))
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
- data + chars})
- return True
-
- def scriptDataState(self):
- data = self.stream.char()
- if data == "<":
- self.state = self.scriptDataLessThanSignState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "\uFFFD"})
- elif data == EOF:
- # Tokenization ends.
- return False
- else:
- chars = self.stream.charsUntil(("<", "\u0000"))
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
- data + chars})
- return True
-
- def plaintextState(self):
- data = self.stream.char()
- if data == EOF:
- # Tokenization ends.
- return False
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "\uFFFD"})
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
- data + self.stream.charsUntil("\u0000")})
- return True
-
- def tagOpenState(self):
- data = self.stream.char()
- if data == "!":
- self.state = self.markupDeclarationOpenState
- elif data == "/":
- self.state = self.closeTagOpenState
- elif data in asciiLetters:
- self.currentToken = {"type": tokenTypes["StartTag"],
- "name": data, "data": [],
- "selfClosing": False,
- "selfClosingAcknowledged": False}
- self.state = self.tagNameState
- elif data == ">":
- # XXX In theory it could be something besides a tag name. But
- # do we really care?
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-tag-name-but-got-right-bracket"})
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<>"})
- self.state = self.dataState
- elif data == "?":
- # XXX In theory it could be something besides a tag name. But
- # do we really care?
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-tag-name-but-got-question-mark"})
- self.stream.unget(data)
- self.state = self.bogusCommentState
- else:
- # XXX
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-tag-name"})
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
- self.stream.unget(data)
- self.state = self.dataState
- return True
-
- def closeTagOpenState(self):
- data = self.stream.char()
- if data in asciiLetters:
- self.currentToken = {"type": tokenTypes["EndTag"], "name": data,
- "data": [], "selfClosing": False}
- self.state = self.tagNameState
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-closing-tag-but-got-right-bracket"})
- self.state = self.dataState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-closing-tag-but-got-eof"})
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "</"})
- self.state = self.dataState
- else:
- # XXX data can be _'_...
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-closing-tag-but-got-char",
- "datavars": {"data": data}})
- self.stream.unget(data)
- self.state = self.bogusCommentState
- return True
-
- def tagNameState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- self.state = self.beforeAttributeNameState
- elif data == ">":
- self.emitCurrentToken()
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-tag-name"})
- self.state = self.dataState
- elif data == "/":
- self.state = self.selfClosingStartTagState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["name"] += "\uFFFD"
- else:
- self.currentToken["name"] += data
- # (Don't use charsUntil here, because tag names are
- # very short and it's faster to not do anything fancy)
- return True
-
- def rcdataLessThanSignState(self):
- data = self.stream.char()
- if data == "/":
- self.temporaryBuffer = ""
- self.state = self.rcdataEndTagOpenState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
- self.stream.unget(data)
- self.state = self.rcdataState
- return True
-
- def rcdataEndTagOpenState(self):
- data = self.stream.char()
- if data in asciiLetters:
- self.temporaryBuffer += data
- self.state = self.rcdataEndTagNameState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "</"})
- self.stream.unget(data)
- self.state = self.rcdataState
- return True
-
- def rcdataEndTagNameState(self):
- appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower()
- data = self.stream.char()
- if data in spaceCharacters and appropriate:
- self.currentToken = {"type": tokenTypes["EndTag"],
- "name": self.temporaryBuffer,
- "data": [], "selfClosing": False}
- self.state = self.beforeAttributeNameState
- elif data == "/" and appropriate:
- self.currentToken = {"type": tokenTypes["EndTag"],
- "name": self.temporaryBuffer,
- "data": [], "selfClosing": False}
- self.state = self.selfClosingStartTagState
- elif data == ">" and appropriate:
- self.currentToken = {"type": tokenTypes["EndTag"],
- "name": self.temporaryBuffer,
- "data": [], "selfClosing": False}
- self.emitCurrentToken()
- self.state = self.dataState
- elif data in asciiLetters:
- self.temporaryBuffer += data
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "</" + self.temporaryBuffer})
- self.stream.unget(data)
- self.state = self.rcdataState
- return True
-
- def rawtextLessThanSignState(self):
- data = self.stream.char()
- if data == "/":
- self.temporaryBuffer = ""
- self.state = self.rawtextEndTagOpenState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
- self.stream.unget(data)
- self.state = self.rawtextState
- return True
-
- def rawtextEndTagOpenState(self):
- data = self.stream.char()
- if data in asciiLetters:
- self.temporaryBuffer += data
- self.state = self.rawtextEndTagNameState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "</"})
- self.stream.unget(data)
- self.state = self.rawtextState
- return True
-
- def rawtextEndTagNameState(self):
- appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower()
- data = self.stream.char()
- if data in spaceCharacters and appropriate:
- self.currentToken = {"type": tokenTypes["EndTag"],
- "name": self.temporaryBuffer,
- "data": [], "selfClosing": False}
- self.state = self.beforeAttributeNameState
- elif data == "/" and appropriate:
- self.currentToken = {"type": tokenTypes["EndTag"],
- "name": self.temporaryBuffer,
- "data": [], "selfClosing": False}
- self.state = self.selfClosingStartTagState
- elif data == ">" and appropriate:
- self.currentToken = {"type": tokenTypes["EndTag"],
- "name": self.temporaryBuffer,
- "data": [], "selfClosing": False}
- self.emitCurrentToken()
- self.state = self.dataState
- elif data in asciiLetters:
- self.temporaryBuffer += data
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "</" + self.temporaryBuffer})
- self.stream.unget(data)
- self.state = self.rawtextState
- return True
-
- def scriptDataLessThanSignState(self):
- data = self.stream.char()
- if data == "/":
- self.temporaryBuffer = ""
- self.state = self.scriptDataEndTagOpenState
- elif data == "!":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<!"})
- self.state = self.scriptDataEscapeStartState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
- self.stream.unget(data)
- self.state = self.scriptDataState
- return True
-
- def scriptDataEndTagOpenState(self):
- data = self.stream.char()
- if data in asciiLetters:
- self.temporaryBuffer += data
- self.state = self.scriptDataEndTagNameState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "</"})
- self.stream.unget(data)
- self.state = self.scriptDataState
- return True
-
- def scriptDataEndTagNameState(self):
- appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower()
- data = self.stream.char()
- if data in spaceCharacters and appropriate:
- self.currentToken = {"type": tokenTypes["EndTag"],
- "name": self.temporaryBuffer,
- "data": [], "selfClosing": False}
- self.state = self.beforeAttributeNameState
- elif data == "/" and appropriate:
- self.currentToken = {"type": tokenTypes["EndTag"],
- "name": self.temporaryBuffer,
- "data": [], "selfClosing": False}
- self.state = self.selfClosingStartTagState
- elif data == ">" and appropriate:
- self.currentToken = {"type": tokenTypes["EndTag"],
- "name": self.temporaryBuffer,
- "data": [], "selfClosing": False}
- self.emitCurrentToken()
- self.state = self.dataState
- elif data in asciiLetters:
- self.temporaryBuffer += data
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "</" + self.temporaryBuffer})
- self.stream.unget(data)
- self.state = self.scriptDataState
- return True
-
- def scriptDataEscapeStartState(self):
- data = self.stream.char()
- if data == "-":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
- self.state = self.scriptDataEscapeStartDashState
- else:
- self.stream.unget(data)
- self.state = self.scriptDataState
- return True
-
- def scriptDataEscapeStartDashState(self):
- data = self.stream.char()
- if data == "-":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
- self.state = self.scriptDataEscapedDashDashState
- else:
- self.stream.unget(data)
- self.state = self.scriptDataState
- return True
-
- def scriptDataEscapedState(self):
- data = self.stream.char()
- if data == "-":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
- self.state = self.scriptDataEscapedDashState
- elif data == "<":
- self.state = self.scriptDataEscapedLessThanSignState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "\uFFFD"})
- elif data == EOF:
- self.state = self.dataState
- else:
- chars = self.stream.charsUntil(("<", "-", "\u0000"))
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data":
- data + chars})
- return True
-
- def scriptDataEscapedDashState(self):
- data = self.stream.char()
- if data == "-":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
- self.state = self.scriptDataEscapedDashDashState
- elif data == "<":
- self.state = self.scriptDataEscapedLessThanSignState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "\uFFFD"})
- self.state = self.scriptDataEscapedState
- elif data == EOF:
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
- self.state = self.scriptDataEscapedState
- return True
-
- def scriptDataEscapedDashDashState(self):
- data = self.stream.char()
- if data == "-":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
- elif data == "<":
- self.state = self.scriptDataEscapedLessThanSignState
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ">"})
- self.state = self.scriptDataState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "\uFFFD"})
- self.state = self.scriptDataEscapedState
- elif data == EOF:
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
- self.state = self.scriptDataEscapedState
- return True
-
- def scriptDataEscapedLessThanSignState(self):
- data = self.stream.char()
- if data == "/":
- self.temporaryBuffer = ""
- self.state = self.scriptDataEscapedEndTagOpenState
- elif data in asciiLetters:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<" + data})
- self.temporaryBuffer = data
- self.state = self.scriptDataDoubleEscapeStartState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
- self.stream.unget(data)
- self.state = self.scriptDataEscapedState
- return True
-
- def scriptDataEscapedEndTagOpenState(self):
- data = self.stream.char()
- if data in asciiLetters:
- self.temporaryBuffer = data
- self.state = self.scriptDataEscapedEndTagNameState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "</"})
- self.stream.unget(data)
- self.state = self.scriptDataEscapedState
- return True
-
- def scriptDataEscapedEndTagNameState(self):
- appropriate = self.currentToken and self.currentToken["name"].lower() == self.temporaryBuffer.lower()
- data = self.stream.char()
- if data in spaceCharacters and appropriate:
- self.currentToken = {"type": tokenTypes["EndTag"],
- "name": self.temporaryBuffer,
- "data": [], "selfClosing": False}
- self.state = self.beforeAttributeNameState
- elif data == "/" and appropriate:
- self.currentToken = {"type": tokenTypes["EndTag"],
- "name": self.temporaryBuffer,
- "data": [], "selfClosing": False}
- self.state = self.selfClosingStartTagState
- elif data == ">" and appropriate:
- self.currentToken = {"type": tokenTypes["EndTag"],
- "name": self.temporaryBuffer,
- "data": [], "selfClosing": False}
- self.emitCurrentToken()
- self.state = self.dataState
- elif data in asciiLetters:
- self.temporaryBuffer += data
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "</" + self.temporaryBuffer})
- self.stream.unget(data)
- self.state = self.scriptDataEscapedState
- return True
-
- def scriptDataDoubleEscapeStartState(self):
- data = self.stream.char()
- if data in (spaceCharacters | frozenset(("/", ">"))):
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
- if self.temporaryBuffer.lower() == "script":
- self.state = self.scriptDataDoubleEscapedState
- else:
- self.state = self.scriptDataEscapedState
- elif data in asciiLetters:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
- self.temporaryBuffer += data
- else:
- self.stream.unget(data)
- self.state = self.scriptDataEscapedState
- return True
-
- def scriptDataDoubleEscapedState(self):
- data = self.stream.char()
- if data == "-":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
- self.state = self.scriptDataDoubleEscapedDashState
- elif data == "<":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
- self.state = self.scriptDataDoubleEscapedLessThanSignState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "\uFFFD"})
- elif data == EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-script-in-script"})
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
- return True
-
- def scriptDataDoubleEscapedDashState(self):
- data = self.stream.char()
- if data == "-":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
- self.state = self.scriptDataDoubleEscapedDashDashState
- elif data == "<":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
- self.state = self.scriptDataDoubleEscapedLessThanSignState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "\uFFFD"})
- self.state = self.scriptDataDoubleEscapedState
- elif data == EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-script-in-script"})
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
- self.state = self.scriptDataDoubleEscapedState
- return True
-
- def scriptDataDoubleEscapedDashDashState(self):
- data = self.stream.char()
- if data == "-":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "-"})
- elif data == "<":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "<"})
- self.state = self.scriptDataDoubleEscapedLessThanSignState
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": ">"})
- self.state = self.scriptDataState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": "\uFFFD"})
- self.state = self.scriptDataDoubleEscapedState
- elif data == EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-script-in-script"})
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
- self.state = self.scriptDataDoubleEscapedState
- return True
-
- def scriptDataDoubleEscapedLessThanSignState(self):
- data = self.stream.char()
- if data == "/":
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": "/"})
- self.temporaryBuffer = ""
- self.state = self.scriptDataDoubleEscapeEndState
- else:
- self.stream.unget(data)
- self.state = self.scriptDataDoubleEscapedState
- return True
-
- def scriptDataDoubleEscapeEndState(self):
- data = self.stream.char()
- if data in (spaceCharacters | frozenset(("/", ">"))):
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
- if self.temporaryBuffer.lower() == "script":
- self.state = self.scriptDataEscapedState
- else:
- self.state = self.scriptDataDoubleEscapedState
- elif data in asciiLetters:
- self.tokenQueue.append({"type": tokenTypes["Characters"], "data": data})
- self.temporaryBuffer += data
- else:
- self.stream.unget(data)
- self.state = self.scriptDataDoubleEscapedState
- return True
-
- def beforeAttributeNameState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- self.stream.charsUntil(spaceCharacters, True)
- elif data in asciiLetters:
- self.currentToken["data"].append([data, ""])
- self.state = self.attributeNameState
- elif data == ">":
- self.emitCurrentToken()
- elif data == "/":
- self.state = self.selfClosingStartTagState
- elif data in ("'", '"', "=", "<"):
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "invalid-character-in-attribute-name"})
- self.currentToken["data"].append([data, ""])
- self.state = self.attributeNameState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"].append(["\uFFFD", ""])
- self.state = self.attributeNameState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-attribute-name-but-got-eof"})
- self.state = self.dataState
- else:
- self.currentToken["data"].append([data, ""])
- self.state = self.attributeNameState
- return True
-
- def attributeNameState(self):
- data = self.stream.char()
- leavingThisState = True
- emitToken = False
- if data == "=":
- self.state = self.beforeAttributeValueState
- elif data in asciiLetters:
- self.currentToken["data"][-1][0] += data +\
- self.stream.charsUntil(asciiLetters, True)
- leavingThisState = False
- elif data == ">":
- # XXX If we emit here the attributes are converted to a dict
- # without being checked and when the code below runs we error
- # because data is a dict not a list
- emitToken = True
- elif data in spaceCharacters:
- self.state = self.afterAttributeNameState
- elif data == "/":
- self.state = self.selfClosingStartTagState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"][-1][0] += "\uFFFD"
- leavingThisState = False
- elif data in ("'", '"', "<"):
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data":
- "invalid-character-in-attribute-name"})
- self.currentToken["data"][-1][0] += data
- leavingThisState = False
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "eof-in-attribute-name"})
- self.state = self.dataState
- else:
- self.currentToken["data"][-1][0] += data
- leavingThisState = False
-
- if leavingThisState:
- # Attributes are not dropped at this stage. That happens when the
- # start tag token is emitted so values can still be safely appended
- # to attributes, but we do want to report the parse error in time.
- self.currentToken["data"][-1][0] = (
- self.currentToken["data"][-1][0].translate(asciiUpper2Lower))
- for name, _ in self.currentToken["data"][:-1]:
- if self.currentToken["data"][-1][0] == name:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "duplicate-attribute"})
- break
- # XXX Fix for above XXX
- if emitToken:
- self.emitCurrentToken()
- return True
-
- def afterAttributeNameState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- self.stream.charsUntil(spaceCharacters, True)
- elif data == "=":
- self.state = self.beforeAttributeValueState
- elif data == ">":
- self.emitCurrentToken()
- elif data in asciiLetters:
- self.currentToken["data"].append([data, ""])
- self.state = self.attributeNameState
- elif data == "/":
- self.state = self.selfClosingStartTagState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"].append(["\uFFFD", ""])
- self.state = self.attributeNameState
- elif data in ("'", '"', "<"):
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "invalid-character-after-attribute-name"})
- self.currentToken["data"].append([data, ""])
- self.state = self.attributeNameState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-end-of-tag-but-got-eof"})
- self.state = self.dataState
- else:
- self.currentToken["data"].append([data, ""])
- self.state = self.attributeNameState
- return True
-
- def beforeAttributeValueState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- self.stream.charsUntil(spaceCharacters, True)
- elif data == "\"":
- self.state = self.attributeValueDoubleQuotedState
- elif data == "&":
- self.state = self.attributeValueUnQuotedState
- self.stream.unget(data)
- elif data == "'":
- self.state = self.attributeValueSingleQuotedState
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-attribute-value-but-got-right-bracket"})
- self.emitCurrentToken()
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"][-1][1] += "\uFFFD"
- self.state = self.attributeValueUnQuotedState
- elif data in ("=", "<", "`"):
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "equals-in-unquoted-attribute-value"})
- self.currentToken["data"][-1][1] += data
- self.state = self.attributeValueUnQuotedState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-attribute-value-but-got-eof"})
- self.state = self.dataState
- else:
- self.currentToken["data"][-1][1] += data
- self.state = self.attributeValueUnQuotedState
- return True
-
- def attributeValueDoubleQuotedState(self):
- data = self.stream.char()
- if data == "\"":
- self.state = self.afterAttributeValueState
- elif data == "&":
- self.processEntityInAttribute('"')
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"][-1][1] += "\uFFFD"
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-attribute-value-double-quote"})
- self.state = self.dataState
- else:
- self.currentToken["data"][-1][1] += data +\
- self.stream.charsUntil(("\"", "&", "\u0000"))
- return True
-
- def attributeValueSingleQuotedState(self):
- data = self.stream.char()
- if data == "'":
- self.state = self.afterAttributeValueState
- elif data == "&":
- self.processEntityInAttribute("'")
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"][-1][1] += "\uFFFD"
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-attribute-value-single-quote"})
- self.state = self.dataState
- else:
- self.currentToken["data"][-1][1] += data +\
- self.stream.charsUntil(("'", "&", "\u0000"))
- return True
-
- def attributeValueUnQuotedState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- self.state = self.beforeAttributeNameState
- elif data == "&":
- self.processEntityInAttribute(">")
- elif data == ">":
- self.emitCurrentToken()
- elif data in ('"', "'", "=", "<", "`"):
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-character-in-unquoted-attribute-value"})
- self.currentToken["data"][-1][1] += data
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"][-1][1] += "\uFFFD"
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-attribute-value-no-quotes"})
- self.state = self.dataState
- else:
- self.currentToken["data"][-1][1] += data + self.stream.charsUntil(
- frozenset(("&", ">", '"', "'", "=", "<", "`", "\u0000")) | spaceCharacters)
- return True
-
- def afterAttributeValueState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- self.state = self.beforeAttributeNameState
- elif data == ">":
- self.emitCurrentToken()
- elif data == "/":
- self.state = self.selfClosingStartTagState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-EOF-after-attribute-value"})
- self.stream.unget(data)
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-character-after-attribute-value"})
- self.stream.unget(data)
- self.state = self.beforeAttributeNameState
- return True
-
- def selfClosingStartTagState(self):
- data = self.stream.char()
- if data == ">":
- self.currentToken["selfClosing"] = True
- self.emitCurrentToken()
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data":
- "unexpected-EOF-after-solidus-in-tag"})
- self.stream.unget(data)
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-character-after-solidus-in-tag"})
- self.stream.unget(data)
- self.state = self.beforeAttributeNameState
- return True
-
- def bogusCommentState(self):
- # Make a new comment token and give it as value all the characters
- # until the first > or EOF (charsUntil checks for EOF automatically)
- # and emit it.
- data = self.stream.charsUntil(">")
- data = data.replace("\u0000", "\uFFFD")
- self.tokenQueue.append(
- {"type": tokenTypes["Comment"], "data": data})
-
- # Eat the character directly after the bogus comment which is either a
- # ">" or an EOF.
- self.stream.char()
- self.state = self.dataState
- return True
-
- def markupDeclarationOpenState(self):
- charStack = [self.stream.char()]
- if charStack[-1] == "-":
- charStack.append(self.stream.char())
- if charStack[-1] == "-":
- self.currentToken = {"type": tokenTypes["Comment"], "data": ""}
- self.state = self.commentStartState
- return True
- elif charStack[-1] in ('d', 'D'):
- matched = True
- for expected in (('o', 'O'), ('c', 'C'), ('t', 'T'),
- ('y', 'Y'), ('p', 'P'), ('e', 'E')):
- charStack.append(self.stream.char())
- if charStack[-1] not in expected:
- matched = False
- break
- if matched:
- self.currentToken = {"type": tokenTypes["Doctype"],
- "name": "",
- "publicId": None, "systemId": None,
- "correct": True}
- self.state = self.doctypeState
- return True
- elif (charStack[-1] == "[" and
- self.parser is not None and
- self.parser.tree.openElements and
- self.parser.tree.openElements[-1].namespace != self.parser.tree.defaultNamespace):
- matched = True
- for expected in ["C", "D", "A", "T", "A", "["]:
- charStack.append(self.stream.char())
- if charStack[-1] != expected:
- matched = False
- break
- if matched:
- self.state = self.cdataSectionState
- return True
-
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-dashes-or-doctype"})
-
- while charStack:
- self.stream.unget(charStack.pop())
- self.state = self.bogusCommentState
- return True
-
- def commentStartState(self):
- data = self.stream.char()
- if data == "-":
- self.state = self.commentStartDashState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"] += "\uFFFD"
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "incorrect-comment"})
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-comment"})
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.currentToken["data"] += data
- self.state = self.commentState
- return True
-
- def commentStartDashState(self):
- data = self.stream.char()
- if data == "-":
- self.state = self.commentEndState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"] += "-\uFFFD"
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "incorrect-comment"})
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-comment"})
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.currentToken["data"] += "-" + data
- self.state = self.commentState
- return True
-
- def commentState(self):
- data = self.stream.char()
- if data == "-":
- self.state = self.commentEndDashState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"] += "\uFFFD"
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "eof-in-comment"})
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.currentToken["data"] += data + \
- self.stream.charsUntil(("-", "\u0000"))
- return True
-
- def commentEndDashState(self):
- data = self.stream.char()
- if data == "-":
- self.state = self.commentEndState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"] += "-\uFFFD"
- self.state = self.commentState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-comment-end-dash"})
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.currentToken["data"] += "-" + data
- self.state = self.commentState
- return True
-
- def commentEndState(self):
- data = self.stream.char()
- if data == ">":
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"] += "--\uFFFD"
- self.state = self.commentState
- elif data == "!":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-bang-after-double-dash-in-comment"})
- self.state = self.commentEndBangState
- elif data == "-":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-dash-after-double-dash-in-comment"})
- self.currentToken["data"] += data
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-comment-double-dash"})
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- # XXX
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-char-in-comment"})
- self.currentToken["data"] += "--" + data
- self.state = self.commentState
- return True
-
- def commentEndBangState(self):
- data = self.stream.char()
- if data == ">":
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data == "-":
- self.currentToken["data"] += "--!"
- self.state = self.commentEndDashState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["data"] += "--!\uFFFD"
- self.state = self.commentState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-comment-end-bang-state"})
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.currentToken["data"] += "--!" + data
- self.state = self.commentState
- return True
-
- def doctypeState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- self.state = self.beforeDoctypeNameState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-doctype-name-but-got-eof"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "need-space-after-doctype"})
- self.stream.unget(data)
- self.state = self.beforeDoctypeNameState
- return True
-
- def beforeDoctypeNameState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- pass
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-doctype-name-but-got-right-bracket"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["name"] = "\uFFFD"
- self.state = self.doctypeNameState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-doctype-name-but-got-eof"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.currentToken["name"] = data
- self.state = self.doctypeNameState
- return True
-
- def doctypeNameState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower)
- self.state = self.afterDoctypeNameState
- elif data == ">":
- self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower)
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["name"] += "\uFFFD"
- self.state = self.doctypeNameState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype-name"})
- self.currentToken["correct"] = False
- self.currentToken["name"] = self.currentToken["name"].translate(asciiUpper2Lower)
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.currentToken["name"] += data
- return True
-
- def afterDoctypeNameState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- pass
- elif data == ">":
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data is EOF:
- self.currentToken["correct"] = False
- self.stream.unget(data)
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype"})
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- if data in ("p", "P"):
- matched = True
- for expected in (("u", "U"), ("b", "B"), ("l", "L"),
- ("i", "I"), ("c", "C")):
- data = self.stream.char()
- if data not in expected:
- matched = False
- break
- if matched:
- self.state = self.afterDoctypePublicKeywordState
- return True
- elif data in ("s", "S"):
- matched = True
- for expected in (("y", "Y"), ("s", "S"), ("t", "T"),
- ("e", "E"), ("m", "M")):
- data = self.stream.char()
- if data not in expected:
- matched = False
- break
- if matched:
- self.state = self.afterDoctypeSystemKeywordState
- return True
-
- # All the characters read before the current 'data' will be
- # [a-zA-Z], so they're garbage in the bogus doctype and can be
- # discarded; only the latest character might be '>' or EOF
- # and needs to be ungetted
- self.stream.unget(data)
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "expected-space-or-right-bracket-in-doctype", "datavars":
- {"data": data}})
- self.currentToken["correct"] = False
- self.state = self.bogusDoctypeState
-
- return True
-
- def afterDoctypePublicKeywordState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- self.state = self.beforeDoctypePublicIdentifierState
- elif data in ("'", '"'):
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-char-in-doctype"})
- self.stream.unget(data)
- self.state = self.beforeDoctypePublicIdentifierState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.stream.unget(data)
- self.state = self.beforeDoctypePublicIdentifierState
- return True
-
- def beforeDoctypePublicIdentifierState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- pass
- elif data == "\"":
- self.currentToken["publicId"] = ""
- self.state = self.doctypePublicIdentifierDoubleQuotedState
- elif data == "'":
- self.currentToken["publicId"] = ""
- self.state = self.doctypePublicIdentifierSingleQuotedState
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-end-of-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-char-in-doctype"})
- self.currentToken["correct"] = False
- self.state = self.bogusDoctypeState
- return True
-
- def doctypePublicIdentifierDoubleQuotedState(self):
- data = self.stream.char()
- if data == "\"":
- self.state = self.afterDoctypePublicIdentifierState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["publicId"] += "\uFFFD"
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-end-of-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.currentToken["publicId"] += data
- return True
-
- def doctypePublicIdentifierSingleQuotedState(self):
- data = self.stream.char()
- if data == "'":
- self.state = self.afterDoctypePublicIdentifierState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["publicId"] += "\uFFFD"
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-end-of-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.currentToken["publicId"] += data
- return True
-
- def afterDoctypePublicIdentifierState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- self.state = self.betweenDoctypePublicAndSystemIdentifiersState
- elif data == ">":
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data == '"':
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-char-in-doctype"})
- self.currentToken["systemId"] = ""
- self.state = self.doctypeSystemIdentifierDoubleQuotedState
- elif data == "'":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-char-in-doctype"})
- self.currentToken["systemId"] = ""
- self.state = self.doctypeSystemIdentifierSingleQuotedState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-char-in-doctype"})
- self.currentToken["correct"] = False
- self.state = self.bogusDoctypeState
- return True
-
- def betweenDoctypePublicAndSystemIdentifiersState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- pass
- elif data == ">":
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data == '"':
- self.currentToken["systemId"] = ""
- self.state = self.doctypeSystemIdentifierDoubleQuotedState
- elif data == "'":
- self.currentToken["systemId"] = ""
- self.state = self.doctypeSystemIdentifierSingleQuotedState
- elif data == EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-char-in-doctype"})
- self.currentToken["correct"] = False
- self.state = self.bogusDoctypeState
- return True
-
- def afterDoctypeSystemKeywordState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- self.state = self.beforeDoctypeSystemIdentifierState
- elif data in ("'", '"'):
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-char-in-doctype"})
- self.stream.unget(data)
- self.state = self.beforeDoctypeSystemIdentifierState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.stream.unget(data)
- self.state = self.beforeDoctypeSystemIdentifierState
- return True
-
- def beforeDoctypeSystemIdentifierState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- pass
- elif data == "\"":
- self.currentToken["systemId"] = ""
- self.state = self.doctypeSystemIdentifierDoubleQuotedState
- elif data == "'":
- self.currentToken["systemId"] = ""
- self.state = self.doctypeSystemIdentifierSingleQuotedState
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-char-in-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-char-in-doctype"})
- self.currentToken["correct"] = False
- self.state = self.bogusDoctypeState
- return True
-
- def doctypeSystemIdentifierDoubleQuotedState(self):
- data = self.stream.char()
- if data == "\"":
- self.state = self.afterDoctypeSystemIdentifierState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["systemId"] += "\uFFFD"
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-end-of-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.currentToken["systemId"] += data
- return True
-
- def doctypeSystemIdentifierSingleQuotedState(self):
- data = self.stream.char()
- if data == "'":
- self.state = self.afterDoctypeSystemIdentifierState
- elif data == "\u0000":
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- self.currentToken["systemId"] += "\uFFFD"
- elif data == ">":
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-end-of-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.currentToken["systemId"] += data
- return True
-
- def afterDoctypeSystemIdentifierState(self):
- data = self.stream.char()
- if data in spaceCharacters:
- pass
- elif data == ">":
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data is EOF:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "eof-in-doctype"})
- self.currentToken["correct"] = False
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- self.tokenQueue.append({"type": tokenTypes["ParseError"], "data":
- "unexpected-char-in-doctype"})
- self.state = self.bogusDoctypeState
- return True
-
- def bogusDoctypeState(self):
- data = self.stream.char()
- if data == ">":
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- elif data is EOF:
- # XXX EMIT
- self.stream.unget(data)
- self.tokenQueue.append(self.currentToken)
- self.state = self.dataState
- else:
- pass
- return True
-
- def cdataSectionState(self):
- data = []
- while True:
- data.append(self.stream.charsUntil("]"))
- data.append(self.stream.charsUntil(">"))
- char = self.stream.char()
- if char == EOF:
- break
- else:
- assert char == ">"
- if data[-1][-2:] == "]]":
- data[-1] = data[-1][:-2]
- break
- else:
- data.append(char)
-
- data = "".join(data) # pylint:disable=redefined-variable-type
- # Deal with null here rather than in the parser
- nullCount = data.count("\u0000")
- if nullCount > 0:
- for _ in range(nullCount):
- self.tokenQueue.append({"type": tokenTypes["ParseError"],
- "data": "invalid-codepoint"})
- data = data.replace("\u0000", "\uFFFD")
- if data:
- self.tokenQueue.append({"type": tokenTypes["Characters"],
- "data": data})
- self.state = self.dataState
- return True
diff --git a/src/pip/_vendor/html5lib/_trie/__init__.py b/src/pip/_vendor/html5lib/_trie/__init__.py
deleted file mode 100644
index 07bad5d31..000000000
--- a/src/pip/_vendor/html5lib/_trie/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from .py import Trie
-
-__all__ = ["Trie"]
diff --git a/src/pip/_vendor/html5lib/_trie/_base.py b/src/pip/_vendor/html5lib/_trie/_base.py
deleted file mode 100644
index 6b71975f0..000000000
--- a/src/pip/_vendor/html5lib/_trie/_base.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-try:
- from collections.abc import Mapping
-except ImportError: # Python 2.7
- from collections import Mapping
-
-
-class Trie(Mapping):
- """Abstract base class for tries"""
-
- def keys(self, prefix=None):
- # pylint:disable=arguments-differ
- keys = super(Trie, self).keys()
-
- if prefix is None:
- return set(keys)
-
- return {x for x in keys if x.startswith(prefix)}
-
- def has_keys_with_prefix(self, prefix):
- for key in self.keys():
- if key.startswith(prefix):
- return True
-
- return False
-
- def longest_prefix(self, prefix):
- if prefix in self:
- return prefix
-
- for i in range(1, len(prefix) + 1):
- if prefix[:-i] in self:
- return prefix[:-i]
-
- raise KeyError(prefix)
-
- def longest_prefix_item(self, prefix):
- lprefix = self.longest_prefix(prefix)
- return (lprefix, self[lprefix])
diff --git a/src/pip/_vendor/html5lib/_trie/py.py b/src/pip/_vendor/html5lib/_trie/py.py
deleted file mode 100644
index c178b219d..000000000
--- a/src/pip/_vendor/html5lib/_trie/py.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-from pip._vendor.six import text_type
-
-from bisect import bisect_left
-
-from ._base import Trie as ABCTrie
-
-
-class Trie(ABCTrie):
- def __init__(self, data):
- if not all(isinstance(x, text_type) for x in data.keys()):
- raise TypeError("All keys must be strings")
-
- self._data = data
- self._keys = sorted(data.keys())
- self._cachestr = ""
- self._cachepoints = (0, len(data))
-
- def __contains__(self, key):
- return key in self._data
-
- def __len__(self):
- return len(self._data)
-
- def __iter__(self):
- return iter(self._data)
-
- def __getitem__(self, key):
- return self._data[key]
-
- def keys(self, prefix=None):
- if prefix is None or prefix == "" or not self._keys:
- return set(self._keys)
-
- if prefix.startswith(self._cachestr):
- lo, hi = self._cachepoints
- start = i = bisect_left(self._keys, prefix, lo, hi)
- else:
- start = i = bisect_left(self._keys, prefix)
-
- keys = set()
- if start == len(self._keys):
- return keys
-
- while self._keys[i].startswith(prefix):
- keys.add(self._keys[i])
- i += 1
-
- self._cachestr = prefix
- self._cachepoints = (start, i)
-
- return keys
-
- def has_keys_with_prefix(self, prefix):
- if prefix in self._data:
- return True
-
- if prefix.startswith(self._cachestr):
- lo, hi = self._cachepoints
- i = bisect_left(self._keys, prefix, lo, hi)
- else:
- i = bisect_left(self._keys, prefix)
-
- if i == len(self._keys):
- return False
-
- return self._keys[i].startswith(prefix)
diff --git a/src/pip/_vendor/html5lib/_utils.py b/src/pip/_vendor/html5lib/_utils.py
deleted file mode 100644
index d7c4926af..000000000
--- a/src/pip/_vendor/html5lib/_utils.py
+++ /dev/null
@@ -1,159 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from types import ModuleType
-
-try:
- from collections.abc import Mapping
-except ImportError:
- from collections import Mapping
-
-from pip._vendor.six import text_type, PY3
-
-if PY3:
- import xml.etree.ElementTree as default_etree
-else:
- try:
- import xml.etree.cElementTree as default_etree
- except ImportError:
- import xml.etree.ElementTree as default_etree
-
-
-__all__ = ["default_etree", "MethodDispatcher", "isSurrogatePair",
- "surrogatePairToCodepoint", "moduleFactoryFactory",
- "supports_lone_surrogates"]
-
-
-# Platforms not supporting lone surrogates (\uD800-\uDFFF) should be
-# caught by the below test. In general this would be any platform
-# using UTF-16 as its encoding of unicode strings, such as
-# Jython. This is because UTF-16 itself is based on the use of such
-# surrogates, and there is no mechanism to further escape such
-# escapes.
-try:
- _x = eval('"\\uD800"') # pylint:disable=eval-used
- if not isinstance(_x, text_type):
- # We need this with u"" because of http://bugs.jython.org/issue2039
- _x = eval('u"\\uD800"') # pylint:disable=eval-used
- assert isinstance(_x, text_type)
-except Exception:
- supports_lone_surrogates = False
-else:
- supports_lone_surrogates = True
-
-
-class MethodDispatcher(dict):
- """Dict with 2 special properties:
-
- On initiation, keys that are lists, sets or tuples are converted to
- multiple keys so accessing any one of the items in the original
- list-like object returns the matching value
-
- md = MethodDispatcher({("foo", "bar"):"baz"})
- md["foo"] == "baz"
-
- A default value which can be set through the default attribute.
- """
-
- def __init__(self, items=()):
- _dictEntries = []
- for name, value in items:
- if isinstance(name, (list, tuple, frozenset, set)):
- for item in name:
- _dictEntries.append((item, value))
- else:
- _dictEntries.append((name, value))
- dict.__init__(self, _dictEntries)
- assert len(self) == len(_dictEntries)
- self.default = None
-
- def __getitem__(self, key):
- return dict.get(self, key, self.default)
-
- def __get__(self, instance, owner=None):
- return BoundMethodDispatcher(instance, self)
-
-
-class BoundMethodDispatcher(Mapping):
- """Wraps a MethodDispatcher, binding its return values to `instance`"""
- def __init__(self, instance, dispatcher):
- self.instance = instance
- self.dispatcher = dispatcher
-
- def __getitem__(self, key):
- # see https://docs.python.org/3/reference/datamodel.html#object.__get__
- # on a function, __get__ is used to bind a function to an instance as a bound method
- return self.dispatcher[key].__get__(self.instance)
-
- def get(self, key, default):
- if key in self.dispatcher:
- return self[key]
- else:
- return default
-
- def __iter__(self):
- return iter(self.dispatcher)
-
- def __len__(self):
- return len(self.dispatcher)
-
- def __contains__(self, key):
- return key in self.dispatcher
-
-
-# Some utility functions to deal with weirdness around UCS2 vs UCS4
-# python builds
-
-def isSurrogatePair(data):
- return (len(data) == 2 and
- ord(data[0]) >= 0xD800 and ord(data[0]) <= 0xDBFF and
- ord(data[1]) >= 0xDC00 and ord(data[1]) <= 0xDFFF)
-
-
-def surrogatePairToCodepoint(data):
- char_val = (0x10000 + (ord(data[0]) - 0xD800) * 0x400 +
- (ord(data[1]) - 0xDC00))
- return char_val
-
-# Module Factory Factory (no, this isn't Java, I know)
-# Here to stop this being duplicated all over the place.
-
-
-def moduleFactoryFactory(factory):
- moduleCache = {}
-
- def moduleFactory(baseModule, *args, **kwargs):
- if isinstance(ModuleType.__name__, type("")):
- name = "_%s_factory" % baseModule.__name__
- else:
- name = b"_%s_factory" % baseModule.__name__
-
- kwargs_tuple = tuple(kwargs.items())
-
- try:
- return moduleCache[name][args][kwargs_tuple]
- except KeyError:
- mod = ModuleType(name)
- objs = factory(baseModule, *args, **kwargs)
- mod.__dict__.update(objs)
- if "name" not in moduleCache:
- moduleCache[name] = {}
- if "args" not in moduleCache[name]:
- moduleCache[name][args] = {}
- if "kwargs" not in moduleCache[name][args]:
- moduleCache[name][args][kwargs_tuple] = {}
- moduleCache[name][args][kwargs_tuple] = mod
- return mod
-
- return moduleFactory
-
-
-def memoize(func):
- cache = {}
-
- def wrapped(*args, **kwargs):
- key = (tuple(args), tuple(kwargs.items()))
- if key not in cache:
- cache[key] = func(*args, **kwargs)
- return cache[key]
-
- return wrapped
diff --git a/src/pip/_vendor/html5lib/constants.py b/src/pip/_vendor/html5lib/constants.py
deleted file mode 100644
index fe3e237cd..000000000
--- a/src/pip/_vendor/html5lib/constants.py
+++ /dev/null
@@ -1,2946 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-import string
-
-EOF = None
-
-E = {
- "null-character":
- "Null character in input stream, replaced with U+FFFD.",
- "invalid-codepoint":
- "Invalid codepoint in stream.",
- "incorrectly-placed-solidus":
- "Solidus (/) incorrectly placed in tag.",
- "incorrect-cr-newline-entity":
- "Incorrect CR newline entity, replaced with LF.",
- "illegal-windows-1252-entity":
- "Entity used with illegal number (windows-1252 reference).",
- "cant-convert-numeric-entity":
- "Numeric entity couldn't be converted to character "
- "(codepoint U+%(charAsInt)08x).",
- "illegal-codepoint-for-numeric-entity":
- "Numeric entity represents an illegal codepoint: "
- "U+%(charAsInt)08x.",
- "numeric-entity-without-semicolon":
- "Numeric entity didn't end with ';'.",
- "expected-numeric-entity-but-got-eof":
- "Numeric entity expected. Got end of file instead.",
- "expected-numeric-entity":
- "Numeric entity expected but none found.",
- "named-entity-without-semicolon":
- "Named entity didn't end with ';'.",
- "expected-named-entity":
- "Named entity expected. Got none.",
- "attributes-in-end-tag":
- "End tag contains unexpected attributes.",
- 'self-closing-flag-on-end-tag':
- "End tag contains unexpected self-closing flag.",
- "expected-tag-name-but-got-right-bracket":
- "Expected tag name. Got '>' instead.",
- "expected-tag-name-but-got-question-mark":
- "Expected tag name. Got '?' instead. (HTML doesn't "
- "support processing instructions.)",
- "expected-tag-name":
- "Expected tag name. Got something else instead",
- "expected-closing-tag-but-got-right-bracket":
- "Expected closing tag. Got '>' instead. Ignoring '</>'.",
- "expected-closing-tag-but-got-eof":
- "Expected closing tag. Unexpected end of file.",
- "expected-closing-tag-but-got-char":
- "Expected closing tag. Unexpected character '%(data)s' found.",
- "eof-in-tag-name":
- "Unexpected end of file in the tag name.",
- "expected-attribute-name-but-got-eof":
- "Unexpected end of file. Expected attribute name instead.",
- "eof-in-attribute-name":
- "Unexpected end of file in attribute name.",
- "invalid-character-in-attribute-name":
- "Invalid character in attribute name",
- "duplicate-attribute":
- "Dropped duplicate attribute on tag.",
- "expected-end-of-tag-name-but-got-eof":
- "Unexpected end of file. Expected = or end of tag.",
- "expected-attribute-value-but-got-eof":
- "Unexpected end of file. Expected attribute value.",
- "expected-attribute-value-but-got-right-bracket":
- "Expected attribute value. Got '>' instead.",
- 'equals-in-unquoted-attribute-value':
- "Unexpected = in unquoted attribute",
- 'unexpected-character-in-unquoted-attribute-value':
- "Unexpected character in unquoted attribute",
- "invalid-character-after-attribute-name":
- "Unexpected character after attribute name.",
- "unexpected-character-after-attribute-value":
- "Unexpected character after attribute value.",
- "eof-in-attribute-value-double-quote":
- "Unexpected end of file in attribute value (\").",
- "eof-in-attribute-value-single-quote":
- "Unexpected end of file in attribute value (').",
- "eof-in-attribute-value-no-quotes":
- "Unexpected end of file in attribute value.",
- "unexpected-EOF-after-solidus-in-tag":
- "Unexpected end of file in tag. Expected >",
- "unexpected-character-after-solidus-in-tag":
- "Unexpected character after / in tag. Expected >",
- "expected-dashes-or-doctype":
- "Expected '--' or 'DOCTYPE'. Not found.",
- "unexpected-bang-after-double-dash-in-comment":
- "Unexpected ! after -- in comment",
- "unexpected-space-after-double-dash-in-comment":
- "Unexpected space after -- in comment",
- "incorrect-comment":
- "Incorrect comment.",
- "eof-in-comment":
- "Unexpected end of file in comment.",
- "eof-in-comment-end-dash":
- "Unexpected end of file in comment (-)",
- "unexpected-dash-after-double-dash-in-comment":
- "Unexpected '-' after '--' found in comment.",
- "eof-in-comment-double-dash":
- "Unexpected end of file in comment (--).",
- "eof-in-comment-end-space-state":
- "Unexpected end of file in comment.",
- "eof-in-comment-end-bang-state":
- "Unexpected end of file in comment.",
- "unexpected-char-in-comment":
- "Unexpected character in comment found.",
- "need-space-after-doctype":
- "No space after literal string 'DOCTYPE'.",
- "expected-doctype-name-but-got-right-bracket":
- "Unexpected > character. Expected DOCTYPE name.",
- "expected-doctype-name-but-got-eof":
- "Unexpected end of file. Expected DOCTYPE name.",
- "eof-in-doctype-name":
- "Unexpected end of file in DOCTYPE name.",
- "eof-in-doctype":
- "Unexpected end of file in DOCTYPE.",
- "expected-space-or-right-bracket-in-doctype":
- "Expected space or '>'. Got '%(data)s'",
- "unexpected-end-of-doctype":
- "Unexpected end of DOCTYPE.",
- "unexpected-char-in-doctype":
- "Unexpected character in DOCTYPE.",
- "eof-in-innerhtml":
- "XXX innerHTML EOF",
- "unexpected-doctype":
- "Unexpected DOCTYPE. Ignored.",
- "non-html-root":
- "html needs to be the first start tag.",
- "expected-doctype-but-got-eof":
- "Unexpected End of file. Expected DOCTYPE.",
- "unknown-doctype":
- "Erroneous DOCTYPE.",
- "expected-doctype-but-got-chars":
- "Unexpected non-space characters. Expected DOCTYPE.",
- "expected-doctype-but-got-start-tag":
- "Unexpected start tag (%(name)s). Expected DOCTYPE.",
- "expected-doctype-but-got-end-tag":
- "Unexpected end tag (%(name)s). Expected DOCTYPE.",
- "end-tag-after-implied-root":
- "Unexpected end tag (%(name)s) after the (implied) root element.",
- "expected-named-closing-tag-but-got-eof":
- "Unexpected end of file. Expected end tag (%(name)s).",
- "two-heads-are-not-better-than-one":
- "Unexpected start tag head in existing head. Ignored.",
- "unexpected-end-tag":
- "Unexpected end tag (%(name)s). Ignored.",
- "unexpected-start-tag-out-of-my-head":
- "Unexpected start tag (%(name)s) that can be in head. Moved.",
- "unexpected-start-tag":
- "Unexpected start tag (%(name)s).",
- "missing-end-tag":
- "Missing end tag (%(name)s).",
- "missing-end-tags":
- "Missing end tags (%(name)s).",
- "unexpected-start-tag-implies-end-tag":
- "Unexpected start tag (%(startName)s) "
- "implies end tag (%(endName)s).",
- "unexpected-start-tag-treated-as":
- "Unexpected start tag (%(originalName)s). Treated as %(newName)s.",
- "deprecated-tag":
- "Unexpected start tag %(name)s. Don't use it!",
- "unexpected-start-tag-ignored":
- "Unexpected start tag %(name)s. Ignored.",
- "expected-one-end-tag-but-got-another":
- "Unexpected end tag (%(gotName)s). "
- "Missing end tag (%(expectedName)s).",
- "end-tag-too-early":
- "End tag (%(name)s) seen too early. Expected other end tag.",
- "end-tag-too-early-named":
- "Unexpected end tag (%(gotName)s). Expected end tag (%(expectedName)s).",
- "end-tag-too-early-ignored":
- "End tag (%(name)s) seen too early. Ignored.",
- "adoption-agency-1.1":
- "End tag (%(name)s) violates step 1, "
- "paragraph 1 of the adoption agency algorithm.",
- "adoption-agency-1.2":
- "End tag (%(name)s) violates step 1, "
- "paragraph 2 of the adoption agency algorithm.",
- "adoption-agency-1.3":
- "End tag (%(name)s) violates step 1, "
- "paragraph 3 of the adoption agency algorithm.",
- "adoption-agency-4.4":
- "End tag (%(name)s) violates step 4, "
- "paragraph 4 of the adoption agency algorithm.",
- "unexpected-end-tag-treated-as":
- "Unexpected end tag (%(originalName)s). Treated as %(newName)s.",
- "no-end-tag":
- "This element (%(name)s) has no end tag.",
- "unexpected-implied-end-tag-in-table":
- "Unexpected implied end tag (%(name)s) in the table phase.",
- "unexpected-implied-end-tag-in-table-body":
- "Unexpected implied end tag (%(name)s) in the table body phase.",
- "unexpected-char-implies-table-voodoo":
- "Unexpected non-space characters in "
- "table context caused voodoo mode.",
- "unexpected-hidden-input-in-table":
- "Unexpected input with type hidden in table context.",
- "unexpected-form-in-table":
- "Unexpected form in table context.",
- "unexpected-start-tag-implies-table-voodoo":
- "Unexpected start tag (%(name)s) in "
- "table context caused voodoo mode.",
- "unexpected-end-tag-implies-table-voodoo":
- "Unexpected end tag (%(name)s) in "
- "table context caused voodoo mode.",
- "unexpected-cell-in-table-body":
- "Unexpected table cell start tag (%(name)s) "
- "in the table body phase.",
- "unexpected-cell-end-tag":
- "Got table cell end tag (%(name)s) "
- "while required end tags are missing.",
- "unexpected-end-tag-in-table-body":
- "Unexpected end tag (%(name)s) in the table body phase. Ignored.",
- "unexpected-implied-end-tag-in-table-row":
- "Unexpected implied end tag (%(name)s) in the table row phase.",
- "unexpected-end-tag-in-table-row":
- "Unexpected end tag (%(name)s) in the table row phase. Ignored.",
- "unexpected-select-in-select":
- "Unexpected select start tag in the select phase "
- "treated as select end tag.",
- "unexpected-input-in-select":
- "Unexpected input start tag in the select phase.",
- "unexpected-start-tag-in-select":
- "Unexpected start tag token (%(name)s in the select phase. "
- "Ignored.",
- "unexpected-end-tag-in-select":
- "Unexpected end tag (%(name)s) in the select phase. Ignored.",
- "unexpected-table-element-start-tag-in-select-in-table":
- "Unexpected table element start tag (%(name)s) in the select in table phase.",
- "unexpected-table-element-end-tag-in-select-in-table":
- "Unexpected table element end tag (%(name)s) in the select in table phase.",
- "unexpected-char-after-body":
- "Unexpected non-space characters in the after body phase.",
- "unexpected-start-tag-after-body":
- "Unexpected start tag token (%(name)s)"
- " in the after body phase.",
- "unexpected-end-tag-after-body":
- "Unexpected end tag token (%(name)s)"
- " in the after body phase.",
- "unexpected-char-in-frameset":
- "Unexpected characters in the frameset phase. Characters ignored.",
- "unexpected-start-tag-in-frameset":
- "Unexpected start tag token (%(name)s)"
- " in the frameset phase. Ignored.",
- "unexpected-frameset-in-frameset-innerhtml":
- "Unexpected end tag token (frameset) "
- "in the frameset phase (innerHTML).",
- "unexpected-end-tag-in-frameset":
- "Unexpected end tag token (%(name)s)"
- " in the frameset phase. Ignored.",
- "unexpected-char-after-frameset":
- "Unexpected non-space characters in the "
- "after frameset phase. Ignored.",
- "unexpected-start-tag-after-frameset":
- "Unexpected start tag (%(name)s)"
- " in the after frameset phase. Ignored.",
- "unexpected-end-tag-after-frameset":
- "Unexpected end tag (%(name)s)"
- " in the after frameset phase. Ignored.",
- "unexpected-end-tag-after-body-innerhtml":
- "Unexpected end tag after body(innerHtml)",
- "expected-eof-but-got-char":
- "Unexpected non-space characters. Expected end of file.",
- "expected-eof-but-got-start-tag":
- "Unexpected start tag (%(name)s)"
- ". Expected end of file.",
- "expected-eof-but-got-end-tag":
- "Unexpected end tag (%(name)s)"
- ". Expected end of file.",
- "eof-in-table":
- "Unexpected end of file. Expected table content.",
- "eof-in-select":
- "Unexpected end of file. Expected select content.",
- "eof-in-frameset":
- "Unexpected end of file. Expected frameset content.",
- "eof-in-script-in-script":
- "Unexpected end of file. Expected script content.",
- "eof-in-foreign-lands":
- "Unexpected end of file. Expected foreign content",
- "non-void-element-with-trailing-solidus":
- "Trailing solidus not allowed on element %(name)s",
- "unexpected-html-element-in-foreign-content":
- "Element %(name)s not allowed in a non-html context",
- "unexpected-end-tag-before-html":
- "Unexpected end tag (%(name)s) before html.",
- "unexpected-inhead-noscript-tag":
- "Element %(name)s not allowed in a inhead-noscript context",
- "eof-in-head-noscript":
- "Unexpected end of file. Expected inhead-noscript content",
- "char-in-head-noscript":
- "Unexpected non-space character. Expected inhead-noscript content",
- "XXX-undefined-error":
- "Undefined error (this sucks and should be fixed)",
-}
-
-namespaces = {
- "html": "http://www.w3.org/1999/xhtml",
- "mathml": "http://www.w3.org/1998/Math/MathML",
- "svg": "http://www.w3.org/2000/svg",
- "xlink": "http://www.w3.org/1999/xlink",
- "xml": "http://www.w3.org/XML/1998/namespace",
- "xmlns": "http://www.w3.org/2000/xmlns/"
-}
-
-scopingElements = frozenset([
- (namespaces["html"], "applet"),
- (namespaces["html"], "caption"),
- (namespaces["html"], "html"),
- (namespaces["html"], "marquee"),
- (namespaces["html"], "object"),
- (namespaces["html"], "table"),
- (namespaces["html"], "td"),
- (namespaces["html"], "th"),
- (namespaces["mathml"], "mi"),
- (namespaces["mathml"], "mo"),
- (namespaces["mathml"], "mn"),
- (namespaces["mathml"], "ms"),
- (namespaces["mathml"], "mtext"),
- (namespaces["mathml"], "annotation-xml"),
- (namespaces["svg"], "foreignObject"),
- (namespaces["svg"], "desc"),
- (namespaces["svg"], "title"),
-])
-
-formattingElements = frozenset([
- (namespaces["html"], "a"),
- (namespaces["html"], "b"),
- (namespaces["html"], "big"),
- (namespaces["html"], "code"),
- (namespaces["html"], "em"),
- (namespaces["html"], "font"),
- (namespaces["html"], "i"),
- (namespaces["html"], "nobr"),
- (namespaces["html"], "s"),
- (namespaces["html"], "small"),
- (namespaces["html"], "strike"),
- (namespaces["html"], "strong"),
- (namespaces["html"], "tt"),
- (namespaces["html"], "u")
-])
-
-specialElements = frozenset([
- (namespaces["html"], "address"),
- (namespaces["html"], "applet"),
- (namespaces["html"], "area"),
- (namespaces["html"], "article"),
- (namespaces["html"], "aside"),
- (namespaces["html"], "base"),
- (namespaces["html"], "basefont"),
- (namespaces["html"], "bgsound"),
- (namespaces["html"], "blockquote"),
- (namespaces["html"], "body"),
- (namespaces["html"], "br"),
- (namespaces["html"], "button"),
- (namespaces["html"], "caption"),
- (namespaces["html"], "center"),
- (namespaces["html"], "col"),
- (namespaces["html"], "colgroup"),
- (namespaces["html"], "command"),
- (namespaces["html"], "dd"),
- (namespaces["html"], "details"),
- (namespaces["html"], "dir"),
- (namespaces["html"], "div"),
- (namespaces["html"], "dl"),
- (namespaces["html"], "dt"),
- (namespaces["html"], "embed"),
- (namespaces["html"], "fieldset"),
- (namespaces["html"], "figure"),
- (namespaces["html"], "footer"),
- (namespaces["html"], "form"),
- (namespaces["html"], "frame"),
- (namespaces["html"], "frameset"),
- (namespaces["html"], "h1"),
- (namespaces["html"], "h2"),
- (namespaces["html"], "h3"),
- (namespaces["html"], "h4"),
- (namespaces["html"], "h5"),
- (namespaces["html"], "h6"),
- (namespaces["html"], "head"),
- (namespaces["html"], "header"),
- (namespaces["html"], "hr"),
- (namespaces["html"], "html"),
- (namespaces["html"], "iframe"),
- # Note that image is commented out in the spec as "this isn't an
- # element that can end up on the stack, so it doesn't matter,"
- (namespaces["html"], "image"),
- (namespaces["html"], "img"),
- (namespaces["html"], "input"),
- (namespaces["html"], "isindex"),
- (namespaces["html"], "li"),
- (namespaces["html"], "link"),
- (namespaces["html"], "listing"),
- (namespaces["html"], "marquee"),
- (namespaces["html"], "menu"),
- (namespaces["html"], "meta"),
- (namespaces["html"], "nav"),
- (namespaces["html"], "noembed"),
- (namespaces["html"], "noframes"),
- (namespaces["html"], "noscript"),
- (namespaces["html"], "object"),
- (namespaces["html"], "ol"),
- (namespaces["html"], "p"),
- (namespaces["html"], "param"),
- (namespaces["html"], "plaintext"),
- (namespaces["html"], "pre"),
- (namespaces["html"], "script"),
- (namespaces["html"], "section"),
- (namespaces["html"], "select"),
- (namespaces["html"], "style"),
- (namespaces["html"], "table"),
- (namespaces["html"], "tbody"),
- (namespaces["html"], "td"),
- (namespaces["html"], "textarea"),
- (namespaces["html"], "tfoot"),
- (namespaces["html"], "th"),
- (namespaces["html"], "thead"),
- (namespaces["html"], "title"),
- (namespaces["html"], "tr"),
- (namespaces["html"], "ul"),
- (namespaces["html"], "wbr"),
- (namespaces["html"], "xmp"),
- (namespaces["svg"], "foreignObject")
-])
-
-htmlIntegrationPointElements = frozenset([
- (namespaces["mathml"], "annotation-xml"),
- (namespaces["svg"], "foreignObject"),
- (namespaces["svg"], "desc"),
- (namespaces["svg"], "title")
-])
-
-mathmlTextIntegrationPointElements = frozenset([
- (namespaces["mathml"], "mi"),
- (namespaces["mathml"], "mo"),
- (namespaces["mathml"], "mn"),
- (namespaces["mathml"], "ms"),
- (namespaces["mathml"], "mtext")
-])
-
-adjustSVGAttributes = {
- "attributename": "attributeName",
- "attributetype": "attributeType",
- "basefrequency": "baseFrequency",
- "baseprofile": "baseProfile",
- "calcmode": "calcMode",
- "clippathunits": "clipPathUnits",
- "contentscripttype": "contentScriptType",
- "contentstyletype": "contentStyleType",
- "diffuseconstant": "diffuseConstant",
- "edgemode": "edgeMode",
- "externalresourcesrequired": "externalResourcesRequired",
- "filterres": "filterRes",
- "filterunits": "filterUnits",
- "glyphref": "glyphRef",
- "gradienttransform": "gradientTransform",
- "gradientunits": "gradientUnits",
- "kernelmatrix": "kernelMatrix",
- "kernelunitlength": "kernelUnitLength",
- "keypoints": "keyPoints",
- "keysplines": "keySplines",
- "keytimes": "keyTimes",
- "lengthadjust": "lengthAdjust",
- "limitingconeangle": "limitingConeAngle",
- "markerheight": "markerHeight",
- "markerunits": "markerUnits",
- "markerwidth": "markerWidth",
- "maskcontentunits": "maskContentUnits",
- "maskunits": "maskUnits",
- "numoctaves": "numOctaves",
- "pathlength": "pathLength",
- "patterncontentunits": "patternContentUnits",
- "patterntransform": "patternTransform",
- "patternunits": "patternUnits",
- "pointsatx": "pointsAtX",
- "pointsaty": "pointsAtY",
- "pointsatz": "pointsAtZ",
- "preservealpha": "preserveAlpha",
- "preserveaspectratio": "preserveAspectRatio",
- "primitiveunits": "primitiveUnits",
- "refx": "refX",
- "refy": "refY",
- "repeatcount": "repeatCount",
- "repeatdur": "repeatDur",
- "requiredextensions": "requiredExtensions",
- "requiredfeatures": "requiredFeatures",
- "specularconstant": "specularConstant",
- "specularexponent": "specularExponent",
- "spreadmethod": "spreadMethod",
- "startoffset": "startOffset",
- "stddeviation": "stdDeviation",
- "stitchtiles": "stitchTiles",
- "surfacescale": "surfaceScale",
- "systemlanguage": "systemLanguage",
- "tablevalues": "tableValues",
- "targetx": "targetX",
- "targety": "targetY",
- "textlength": "textLength",
- "viewbox": "viewBox",
- "viewtarget": "viewTarget",
- "xchannelselector": "xChannelSelector",
- "ychannelselector": "yChannelSelector",
- "zoomandpan": "zoomAndPan"
-}
-
-adjustMathMLAttributes = {"definitionurl": "definitionURL"}
-
-adjustForeignAttributes = {
- "xlink:actuate": ("xlink", "actuate", namespaces["xlink"]),
- "xlink:arcrole": ("xlink", "arcrole", namespaces["xlink"]),
- "xlink:href": ("xlink", "href", namespaces["xlink"]),
- "xlink:role": ("xlink", "role", namespaces["xlink"]),
- "xlink:show": ("xlink", "show", namespaces["xlink"]),
- "xlink:title": ("xlink", "title", namespaces["xlink"]),
- "xlink:type": ("xlink", "type", namespaces["xlink"]),
- "xml:base": ("xml", "base", namespaces["xml"]),
- "xml:lang": ("xml", "lang", namespaces["xml"]),
- "xml:space": ("xml", "space", namespaces["xml"]),
- "xmlns": (None, "xmlns", namespaces["xmlns"]),
- "xmlns:xlink": ("xmlns", "xlink", namespaces["xmlns"])
-}
-
-unadjustForeignAttributes = {(ns, local): qname for qname, (prefix, local, ns) in
- adjustForeignAttributes.items()}
-
-spaceCharacters = frozenset([
- "\t",
- "\n",
- "\u000C",
- " ",
- "\r"
-])
-
-tableInsertModeElements = frozenset([
- "table",
- "tbody",
- "tfoot",
- "thead",
- "tr"
-])
-
-asciiLowercase = frozenset(string.ascii_lowercase)
-asciiUppercase = frozenset(string.ascii_uppercase)
-asciiLetters = frozenset(string.ascii_letters)
-digits = frozenset(string.digits)
-hexDigits = frozenset(string.hexdigits)
-
-asciiUpper2Lower = {ord(c): ord(c.lower()) for c in string.ascii_uppercase}
-
-# Heading elements need to be ordered
-headingElements = (
- "h1",
- "h2",
- "h3",
- "h4",
- "h5",
- "h6"
-)
-
-voidElements = frozenset([
- "base",
- "command",
- "event-source",
- "link",
- "meta",
- "hr",
- "br",
- "img",
- "embed",
- "param",
- "area",
- "col",
- "input",
- "source",
- "track"
-])
-
-cdataElements = frozenset(['title', 'textarea'])
-
-rcdataElements = frozenset([
- 'style',
- 'script',
- 'xmp',
- 'iframe',
- 'noembed',
- 'noframes',
- 'noscript'
-])
-
-booleanAttributes = {
- "": frozenset(["irrelevant", "itemscope"]),
- "style": frozenset(["scoped"]),
- "img": frozenset(["ismap"]),
- "audio": frozenset(["autoplay", "controls"]),
- "video": frozenset(["autoplay", "controls"]),
- "script": frozenset(["defer", "async"]),
- "details": frozenset(["open"]),
- "datagrid": frozenset(["multiple", "disabled"]),
- "command": frozenset(["hidden", "disabled", "checked", "default"]),
- "hr": frozenset(["noshade"]),
- "menu": frozenset(["autosubmit"]),
- "fieldset": frozenset(["disabled", "readonly"]),
- "option": frozenset(["disabled", "readonly", "selected"]),
- "optgroup": frozenset(["disabled", "readonly"]),
- "button": frozenset(["disabled", "autofocus"]),
- "input": frozenset(["disabled", "readonly", "required", "autofocus", "checked", "ismap"]),
- "select": frozenset(["disabled", "readonly", "autofocus", "multiple"]),
- "output": frozenset(["disabled", "readonly"]),
- "iframe": frozenset(["seamless"]),
-}
-
-# entitiesWindows1252 has to be _ordered_ and needs to have an index. It
-# therefore can't be a frozenset.
-entitiesWindows1252 = (
- 8364, # 0x80 0x20AC EURO SIGN
- 65533, # 0x81 UNDEFINED
- 8218, # 0x82 0x201A SINGLE LOW-9 QUOTATION MARK
- 402, # 0x83 0x0192 LATIN SMALL LETTER F WITH HOOK
- 8222, # 0x84 0x201E DOUBLE LOW-9 QUOTATION MARK
- 8230, # 0x85 0x2026 HORIZONTAL ELLIPSIS
- 8224, # 0x86 0x2020 DAGGER
- 8225, # 0x87 0x2021 DOUBLE DAGGER
- 710, # 0x88 0x02C6 MODIFIER LETTER CIRCUMFLEX ACCENT
- 8240, # 0x89 0x2030 PER MILLE SIGN
- 352, # 0x8A 0x0160 LATIN CAPITAL LETTER S WITH CARON
- 8249, # 0x8B 0x2039 SINGLE LEFT-POINTING ANGLE QUOTATION MARK
- 338, # 0x8C 0x0152 LATIN CAPITAL LIGATURE OE
- 65533, # 0x8D UNDEFINED
- 381, # 0x8E 0x017D LATIN CAPITAL LETTER Z WITH CARON
- 65533, # 0x8F UNDEFINED
- 65533, # 0x90 UNDEFINED
- 8216, # 0x91 0x2018 LEFT SINGLE QUOTATION MARK
- 8217, # 0x92 0x2019 RIGHT SINGLE QUOTATION MARK
- 8220, # 0x93 0x201C LEFT DOUBLE QUOTATION MARK
- 8221, # 0x94 0x201D RIGHT DOUBLE QUOTATION MARK
- 8226, # 0x95 0x2022 BULLET
- 8211, # 0x96 0x2013 EN DASH
- 8212, # 0x97 0x2014 EM DASH
- 732, # 0x98 0x02DC SMALL TILDE
- 8482, # 0x99 0x2122 TRADE MARK SIGN
- 353, # 0x9A 0x0161 LATIN SMALL LETTER S WITH CARON
- 8250, # 0x9B 0x203A SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
- 339, # 0x9C 0x0153 LATIN SMALL LIGATURE OE
- 65533, # 0x9D UNDEFINED
- 382, # 0x9E 0x017E LATIN SMALL LETTER Z WITH CARON
- 376 # 0x9F 0x0178 LATIN CAPITAL LETTER Y WITH DIAERESIS
-)
-
-xmlEntities = frozenset(['lt;', 'gt;', 'amp;', 'apos;', 'quot;'])
-
-entities = {
- "AElig": "\xc6",
- "AElig;": "\xc6",
- "AMP": "&",
- "AMP;": "&",
- "Aacute": "\xc1",
- "Aacute;": "\xc1",
- "Abreve;": "\u0102",
- "Acirc": "\xc2",
- "Acirc;": "\xc2",
- "Acy;": "\u0410",
- "Afr;": "\U0001d504",
- "Agrave": "\xc0",
- "Agrave;": "\xc0",
- "Alpha;": "\u0391",
- "Amacr;": "\u0100",
- "And;": "\u2a53",
- "Aogon;": "\u0104",
- "Aopf;": "\U0001d538",
- "ApplyFunction;": "\u2061",
- "Aring": "\xc5",
- "Aring;": "\xc5",
- "Ascr;": "\U0001d49c",
- "Assign;": "\u2254",
- "Atilde": "\xc3",
- "Atilde;": "\xc3",
- "Auml": "\xc4",
- "Auml;": "\xc4",
- "Backslash;": "\u2216",
- "Barv;": "\u2ae7",
- "Barwed;": "\u2306",
- "Bcy;": "\u0411",
- "Because;": "\u2235",
- "Bernoullis;": "\u212c",
- "Beta;": "\u0392",
- "Bfr;": "\U0001d505",
- "Bopf;": "\U0001d539",
- "Breve;": "\u02d8",
- "Bscr;": "\u212c",
- "Bumpeq;": "\u224e",
- "CHcy;": "\u0427",
- "COPY": "\xa9",
- "COPY;": "\xa9",
- "Cacute;": "\u0106",
- "Cap;": "\u22d2",
- "CapitalDifferentialD;": "\u2145",
- "Cayleys;": "\u212d",
- "Ccaron;": "\u010c",
- "Ccedil": "\xc7",
- "Ccedil;": "\xc7",
- "Ccirc;": "\u0108",
- "Cconint;": "\u2230",
- "Cdot;": "\u010a",
- "Cedilla;": "\xb8",
- "CenterDot;": "\xb7",
- "Cfr;": "\u212d",
- "Chi;": "\u03a7",
- "CircleDot;": "\u2299",
- "CircleMinus;": "\u2296",
- "CirclePlus;": "\u2295",
- "CircleTimes;": "\u2297",
- "ClockwiseContourIntegral;": "\u2232",
- "CloseCurlyDoubleQuote;": "\u201d",
- "CloseCurlyQuote;": "\u2019",
- "Colon;": "\u2237",
- "Colone;": "\u2a74",
- "Congruent;": "\u2261",
- "Conint;": "\u222f",
- "ContourIntegral;": "\u222e",
- "Copf;": "\u2102",
- "Coproduct;": "\u2210",
- "CounterClockwiseContourIntegral;": "\u2233",
- "Cross;": "\u2a2f",
- "Cscr;": "\U0001d49e",
- "Cup;": "\u22d3",
- "CupCap;": "\u224d",
- "DD;": "\u2145",
- "DDotrahd;": "\u2911",
- "DJcy;": "\u0402",
- "DScy;": "\u0405",
- "DZcy;": "\u040f",
- "Dagger;": "\u2021",
- "Darr;": "\u21a1",
- "Dashv;": "\u2ae4",
- "Dcaron;": "\u010e",
- "Dcy;": "\u0414",
- "Del;": "\u2207",
- "Delta;": "\u0394",
- "Dfr;": "\U0001d507",
- "DiacriticalAcute;": "\xb4",
- "DiacriticalDot;": "\u02d9",
- "DiacriticalDoubleAcute;": "\u02dd",
- "DiacriticalGrave;": "`",
- "DiacriticalTilde;": "\u02dc",
- "Diamond;": "\u22c4",
- "DifferentialD;": "\u2146",
- "Dopf;": "\U0001d53b",
- "Dot;": "\xa8",
- "DotDot;": "\u20dc",
- "DotEqual;": "\u2250",
- "DoubleContourIntegral;": "\u222f",
- "DoubleDot;": "\xa8",
- "DoubleDownArrow;": "\u21d3",
- "DoubleLeftArrow;": "\u21d0",
- "DoubleLeftRightArrow;": "\u21d4",
- "DoubleLeftTee;": "\u2ae4",
- "DoubleLongLeftArrow;": "\u27f8",
- "DoubleLongLeftRightArrow;": "\u27fa",
- "DoubleLongRightArrow;": "\u27f9",
- "DoubleRightArrow;": "\u21d2",
- "DoubleRightTee;": "\u22a8",
- "DoubleUpArrow;": "\u21d1",
- "DoubleUpDownArrow;": "\u21d5",
- "DoubleVerticalBar;": "\u2225",
- "DownArrow;": "\u2193",
- "DownArrowBar;": "\u2913",
- "DownArrowUpArrow;": "\u21f5",
- "DownBreve;": "\u0311",
- "DownLeftRightVector;": "\u2950",
- "DownLeftTeeVector;": "\u295e",
- "DownLeftVector;": "\u21bd",
- "DownLeftVectorBar;": "\u2956",
- "DownRightTeeVector;": "\u295f",
- "DownRightVector;": "\u21c1",
- "DownRightVectorBar;": "\u2957",
- "DownTee;": "\u22a4",
- "DownTeeArrow;": "\u21a7",
- "Downarrow;": "\u21d3",
- "Dscr;": "\U0001d49f",
- "Dstrok;": "\u0110",
- "ENG;": "\u014a",
- "ETH": "\xd0",
- "ETH;": "\xd0",
- "Eacute": "\xc9",
- "Eacute;": "\xc9",
- "Ecaron;": "\u011a",
- "Ecirc": "\xca",
- "Ecirc;": "\xca",
- "Ecy;": "\u042d",
- "Edot;": "\u0116",
- "Efr;": "\U0001d508",
- "Egrave": "\xc8",
- "Egrave;": "\xc8",
- "Element;": "\u2208",
- "Emacr;": "\u0112",
- "EmptySmallSquare;": "\u25fb",
- "EmptyVerySmallSquare;": "\u25ab",
- "Eogon;": "\u0118",
- "Eopf;": "\U0001d53c",
- "Epsilon;": "\u0395",
- "Equal;": "\u2a75",
- "EqualTilde;": "\u2242",
- "Equilibrium;": "\u21cc",
- "Escr;": "\u2130",
- "Esim;": "\u2a73",
- "Eta;": "\u0397",
- "Euml": "\xcb",
- "Euml;": "\xcb",
- "Exists;": "\u2203",
- "ExponentialE;": "\u2147",
- "Fcy;": "\u0424",
- "Ffr;": "\U0001d509",
- "FilledSmallSquare;": "\u25fc",
- "FilledVerySmallSquare;": "\u25aa",
- "Fopf;": "\U0001d53d",
- "ForAll;": "\u2200",
- "Fouriertrf;": "\u2131",
- "Fscr;": "\u2131",
- "GJcy;": "\u0403",
- "GT": ">",
- "GT;": ">",
- "Gamma;": "\u0393",
- "Gammad;": "\u03dc",
- "Gbreve;": "\u011e",
- "Gcedil;": "\u0122",
- "Gcirc;": "\u011c",
- "Gcy;": "\u0413",
- "Gdot;": "\u0120",
- "Gfr;": "\U0001d50a",
- "Gg;": "\u22d9",
- "Gopf;": "\U0001d53e",
- "GreaterEqual;": "\u2265",
- "GreaterEqualLess;": "\u22db",
- "GreaterFullEqual;": "\u2267",
- "GreaterGreater;": "\u2aa2",
- "GreaterLess;": "\u2277",
- "GreaterSlantEqual;": "\u2a7e",
- "GreaterTilde;": "\u2273",
- "Gscr;": "\U0001d4a2",
- "Gt;": "\u226b",
- "HARDcy;": "\u042a",
- "Hacek;": "\u02c7",
- "Hat;": "^",
- "Hcirc;": "\u0124",
- "Hfr;": "\u210c",
- "HilbertSpace;": "\u210b",
- "Hopf;": "\u210d",
- "HorizontalLine;": "\u2500",
- "Hscr;": "\u210b",
- "Hstrok;": "\u0126",
- "HumpDownHump;": "\u224e",
- "HumpEqual;": "\u224f",
- "IEcy;": "\u0415",
- "IJlig;": "\u0132",
- "IOcy;": "\u0401",
- "Iacute": "\xcd",
- "Iacute;": "\xcd",
- "Icirc": "\xce",
- "Icirc;": "\xce",
- "Icy;": "\u0418",
- "Idot;": "\u0130",
- "Ifr;": "\u2111",
- "Igrave": "\xcc",
- "Igrave;": "\xcc",
- "Im;": "\u2111",
- "Imacr;": "\u012a",
- "ImaginaryI;": "\u2148",
- "Implies;": "\u21d2",
- "Int;": "\u222c",
- "Integral;": "\u222b",
- "Intersection;": "\u22c2",
- "InvisibleComma;": "\u2063",
- "InvisibleTimes;": "\u2062",
- "Iogon;": "\u012e",
- "Iopf;": "\U0001d540",
- "Iota;": "\u0399",
- "Iscr;": "\u2110",
- "Itilde;": "\u0128",
- "Iukcy;": "\u0406",
- "Iuml": "\xcf",
- "Iuml;": "\xcf",
- "Jcirc;": "\u0134",
- "Jcy;": "\u0419",
- "Jfr;": "\U0001d50d",
- "Jopf;": "\U0001d541",
- "Jscr;": "\U0001d4a5",
- "Jsercy;": "\u0408",
- "Jukcy;": "\u0404",
- "KHcy;": "\u0425",
- "KJcy;": "\u040c",
- "Kappa;": "\u039a",
- "Kcedil;": "\u0136",
- "Kcy;": "\u041a",
- "Kfr;": "\U0001d50e",
- "Kopf;": "\U0001d542",
- "Kscr;": "\U0001d4a6",
- "LJcy;": "\u0409",
- "LT": "<",
- "LT;": "<",
- "Lacute;": "\u0139",
- "Lambda;": "\u039b",
- "Lang;": "\u27ea",
- "Laplacetrf;": "\u2112",
- "Larr;": "\u219e",
- "Lcaron;": "\u013d",
- "Lcedil;": "\u013b",
- "Lcy;": "\u041b",
- "LeftAngleBracket;": "\u27e8",
- "LeftArrow;": "\u2190",
- "LeftArrowBar;": "\u21e4",
- "LeftArrowRightArrow;": "\u21c6",
- "LeftCeiling;": "\u2308",
- "LeftDoubleBracket;": "\u27e6",
- "LeftDownTeeVector;": "\u2961",
- "LeftDownVector;": "\u21c3",
- "LeftDownVectorBar;": "\u2959",
- "LeftFloor;": "\u230a",
- "LeftRightArrow;": "\u2194",
- "LeftRightVector;": "\u294e",
- "LeftTee;": "\u22a3",
- "LeftTeeArrow;": "\u21a4",
- "LeftTeeVector;": "\u295a",
- "LeftTriangle;": "\u22b2",
- "LeftTriangleBar;": "\u29cf",
- "LeftTriangleEqual;": "\u22b4",
- "LeftUpDownVector;": "\u2951",
- "LeftUpTeeVector;": "\u2960",
- "LeftUpVector;": "\u21bf",
- "LeftUpVectorBar;": "\u2958",
- "LeftVector;": "\u21bc",
- "LeftVectorBar;": "\u2952",
- "Leftarrow;": "\u21d0",
- "Leftrightarrow;": "\u21d4",
- "LessEqualGreater;": "\u22da",
- "LessFullEqual;": "\u2266",
- "LessGreater;": "\u2276",
- "LessLess;": "\u2aa1",
- "LessSlantEqual;": "\u2a7d",
- "LessTilde;": "\u2272",
- "Lfr;": "\U0001d50f",
- "Ll;": "\u22d8",
- "Lleftarrow;": "\u21da",
- "Lmidot;": "\u013f",
- "LongLeftArrow;": "\u27f5",
- "LongLeftRightArrow;": "\u27f7",
- "LongRightArrow;": "\u27f6",
- "Longleftarrow;": "\u27f8",
- "Longleftrightarrow;": "\u27fa",
- "Longrightarrow;": "\u27f9",
- "Lopf;": "\U0001d543",
- "LowerLeftArrow;": "\u2199",
- "LowerRightArrow;": "\u2198",
- "Lscr;": "\u2112",
- "Lsh;": "\u21b0",
- "Lstrok;": "\u0141",
- "Lt;": "\u226a",
- "Map;": "\u2905",
- "Mcy;": "\u041c",
- "MediumSpace;": "\u205f",
- "Mellintrf;": "\u2133",
- "Mfr;": "\U0001d510",
- "MinusPlus;": "\u2213",
- "Mopf;": "\U0001d544",
- "Mscr;": "\u2133",
- "Mu;": "\u039c",
- "NJcy;": "\u040a",
- "Nacute;": "\u0143",
- "Ncaron;": "\u0147",
- "Ncedil;": "\u0145",
- "Ncy;": "\u041d",
- "NegativeMediumSpace;": "\u200b",
- "NegativeThickSpace;": "\u200b",
- "NegativeThinSpace;": "\u200b",
- "NegativeVeryThinSpace;": "\u200b",
- "NestedGreaterGreater;": "\u226b",
- "NestedLessLess;": "\u226a",
- "NewLine;": "\n",
- "Nfr;": "\U0001d511",
- "NoBreak;": "\u2060",
- "NonBreakingSpace;": "\xa0",
- "Nopf;": "\u2115",
- "Not;": "\u2aec",
- "NotCongruent;": "\u2262",
- "NotCupCap;": "\u226d",
- "NotDoubleVerticalBar;": "\u2226",
- "NotElement;": "\u2209",
- "NotEqual;": "\u2260",
- "NotEqualTilde;": "\u2242\u0338",
- "NotExists;": "\u2204",
- "NotGreater;": "\u226f",
- "NotGreaterEqual;": "\u2271",
- "NotGreaterFullEqual;": "\u2267\u0338",
- "NotGreaterGreater;": "\u226b\u0338",
- "NotGreaterLess;": "\u2279",
- "NotGreaterSlantEqual;": "\u2a7e\u0338",
- "NotGreaterTilde;": "\u2275",
- "NotHumpDownHump;": "\u224e\u0338",
- "NotHumpEqual;": "\u224f\u0338",
- "NotLeftTriangle;": "\u22ea",
- "NotLeftTriangleBar;": "\u29cf\u0338",
- "NotLeftTriangleEqual;": "\u22ec",
- "NotLess;": "\u226e",
- "NotLessEqual;": "\u2270",
- "NotLessGreater;": "\u2278",
- "NotLessLess;": "\u226a\u0338",
- "NotLessSlantEqual;": "\u2a7d\u0338",
- "NotLessTilde;": "\u2274",
- "NotNestedGreaterGreater;": "\u2aa2\u0338",
- "NotNestedLessLess;": "\u2aa1\u0338",
- "NotPrecedes;": "\u2280",
- "NotPrecedesEqual;": "\u2aaf\u0338",
- "NotPrecedesSlantEqual;": "\u22e0",
- "NotReverseElement;": "\u220c",
- "NotRightTriangle;": "\u22eb",
- "NotRightTriangleBar;": "\u29d0\u0338",
- "NotRightTriangleEqual;": "\u22ed",
- "NotSquareSubset;": "\u228f\u0338",
- "NotSquareSubsetEqual;": "\u22e2",
- "NotSquareSuperset;": "\u2290\u0338",
- "NotSquareSupersetEqual;": "\u22e3",
- "NotSubset;": "\u2282\u20d2",
- "NotSubsetEqual;": "\u2288",
- "NotSucceeds;": "\u2281",
- "NotSucceedsEqual;": "\u2ab0\u0338",
- "NotSucceedsSlantEqual;": "\u22e1",
- "NotSucceedsTilde;": "\u227f\u0338",
- "NotSuperset;": "\u2283\u20d2",
- "NotSupersetEqual;": "\u2289",
- "NotTilde;": "\u2241",
- "NotTildeEqual;": "\u2244",
- "NotTildeFullEqual;": "\u2247",
- "NotTildeTilde;": "\u2249",
- "NotVerticalBar;": "\u2224",
- "Nscr;": "\U0001d4a9",
- "Ntilde": "\xd1",
- "Ntilde;": "\xd1",
- "Nu;": "\u039d",
- "OElig;": "\u0152",
- "Oacute": "\xd3",
- "Oacute;": "\xd3",
- "Ocirc": "\xd4",
- "Ocirc;": "\xd4",
- "Ocy;": "\u041e",
- "Odblac;": "\u0150",
- "Ofr;": "\U0001d512",
- "Ograve": "\xd2",
- "Ograve;": "\xd2",
- "Omacr;": "\u014c",
- "Omega;": "\u03a9",
- "Omicron;": "\u039f",
- "Oopf;": "\U0001d546",
- "OpenCurlyDoubleQuote;": "\u201c",
- "OpenCurlyQuote;": "\u2018",
- "Or;": "\u2a54",
- "Oscr;": "\U0001d4aa",
- "Oslash": "\xd8",
- "Oslash;": "\xd8",
- "Otilde": "\xd5",
- "Otilde;": "\xd5",
- "Otimes;": "\u2a37",
- "Ouml": "\xd6",
- "Ouml;": "\xd6",
- "OverBar;": "\u203e",
- "OverBrace;": "\u23de",
- "OverBracket;": "\u23b4",
- "OverParenthesis;": "\u23dc",
- "PartialD;": "\u2202",
- "Pcy;": "\u041f",
- "Pfr;": "\U0001d513",
- "Phi;": "\u03a6",
- "Pi;": "\u03a0",
- "PlusMinus;": "\xb1",
- "Poincareplane;": "\u210c",
- "Popf;": "\u2119",
- "Pr;": "\u2abb",
- "Precedes;": "\u227a",
- "PrecedesEqual;": "\u2aaf",
- "PrecedesSlantEqual;": "\u227c",
- "PrecedesTilde;": "\u227e",
- "Prime;": "\u2033",
- "Product;": "\u220f",
- "Proportion;": "\u2237",
- "Proportional;": "\u221d",
- "Pscr;": "\U0001d4ab",
- "Psi;": "\u03a8",
- "QUOT": "\"",
- "QUOT;": "\"",
- "Qfr;": "\U0001d514",
- "Qopf;": "\u211a",
- "Qscr;": "\U0001d4ac",
- "RBarr;": "\u2910",
- "REG": "\xae",
- "REG;": "\xae",
- "Racute;": "\u0154",
- "Rang;": "\u27eb",
- "Rarr;": "\u21a0",
- "Rarrtl;": "\u2916",
- "Rcaron;": "\u0158",
- "Rcedil;": "\u0156",
- "Rcy;": "\u0420",
- "Re;": "\u211c",
- "ReverseElement;": "\u220b",
- "ReverseEquilibrium;": "\u21cb",
- "ReverseUpEquilibrium;": "\u296f",
- "Rfr;": "\u211c",
- "Rho;": "\u03a1",
- "RightAngleBracket;": "\u27e9",
- "RightArrow;": "\u2192",
- "RightArrowBar;": "\u21e5",
- "RightArrowLeftArrow;": "\u21c4",
- "RightCeiling;": "\u2309",
- "RightDoubleBracket;": "\u27e7",
- "RightDownTeeVector;": "\u295d",
- "RightDownVector;": "\u21c2",
- "RightDownVectorBar;": "\u2955",
- "RightFloor;": "\u230b",
- "RightTee;": "\u22a2",
- "RightTeeArrow;": "\u21a6",
- "RightTeeVector;": "\u295b",
- "RightTriangle;": "\u22b3",
- "RightTriangleBar;": "\u29d0",
- "RightTriangleEqual;": "\u22b5",
- "RightUpDownVector;": "\u294f",
- "RightUpTeeVector;": "\u295c",
- "RightUpVector;": "\u21be",
- "RightUpVectorBar;": "\u2954",
- "RightVector;": "\u21c0",
- "RightVectorBar;": "\u2953",
- "Rightarrow;": "\u21d2",
- "Ropf;": "\u211d",
- "RoundImplies;": "\u2970",
- "Rrightarrow;": "\u21db",
- "Rscr;": "\u211b",
- "Rsh;": "\u21b1",
- "RuleDelayed;": "\u29f4",
- "SHCHcy;": "\u0429",
- "SHcy;": "\u0428",
- "SOFTcy;": "\u042c",
- "Sacute;": "\u015a",
- "Sc;": "\u2abc",
- "Scaron;": "\u0160",
- "Scedil;": "\u015e",
- "Scirc;": "\u015c",
- "Scy;": "\u0421",
- "Sfr;": "\U0001d516",
- "ShortDownArrow;": "\u2193",
- "ShortLeftArrow;": "\u2190",
- "ShortRightArrow;": "\u2192",
- "ShortUpArrow;": "\u2191",
- "Sigma;": "\u03a3",
- "SmallCircle;": "\u2218",
- "Sopf;": "\U0001d54a",
- "Sqrt;": "\u221a",
- "Square;": "\u25a1",
- "SquareIntersection;": "\u2293",
- "SquareSubset;": "\u228f",
- "SquareSubsetEqual;": "\u2291",
- "SquareSuperset;": "\u2290",
- "SquareSupersetEqual;": "\u2292",
- "SquareUnion;": "\u2294",
- "Sscr;": "\U0001d4ae",
- "Star;": "\u22c6",
- "Sub;": "\u22d0",
- "Subset;": "\u22d0",
- "SubsetEqual;": "\u2286",
- "Succeeds;": "\u227b",
- "SucceedsEqual;": "\u2ab0",
- "SucceedsSlantEqual;": "\u227d",
- "SucceedsTilde;": "\u227f",
- "SuchThat;": "\u220b",
- "Sum;": "\u2211",
- "Sup;": "\u22d1",
- "Superset;": "\u2283",
- "SupersetEqual;": "\u2287",
- "Supset;": "\u22d1",
- "THORN": "\xde",
- "THORN;": "\xde",
- "TRADE;": "\u2122",
- "TSHcy;": "\u040b",
- "TScy;": "\u0426",
- "Tab;": "\t",
- "Tau;": "\u03a4",
- "Tcaron;": "\u0164",
- "Tcedil;": "\u0162",
- "Tcy;": "\u0422",
- "Tfr;": "\U0001d517",
- "Therefore;": "\u2234",
- "Theta;": "\u0398",
- "ThickSpace;": "\u205f\u200a",
- "ThinSpace;": "\u2009",
- "Tilde;": "\u223c",
- "TildeEqual;": "\u2243",
- "TildeFullEqual;": "\u2245",
- "TildeTilde;": "\u2248",
- "Topf;": "\U0001d54b",
- "TripleDot;": "\u20db",
- "Tscr;": "\U0001d4af",
- "Tstrok;": "\u0166",
- "Uacute": "\xda",
- "Uacute;": "\xda",
- "Uarr;": "\u219f",
- "Uarrocir;": "\u2949",
- "Ubrcy;": "\u040e",
- "Ubreve;": "\u016c",
- "Ucirc": "\xdb",
- "Ucirc;": "\xdb",
- "Ucy;": "\u0423",
- "Udblac;": "\u0170",
- "Ufr;": "\U0001d518",
- "Ugrave": "\xd9",
- "Ugrave;": "\xd9",
- "Umacr;": "\u016a",
- "UnderBar;": "_",
- "UnderBrace;": "\u23df",
- "UnderBracket;": "\u23b5",
- "UnderParenthesis;": "\u23dd",
- "Union;": "\u22c3",
- "UnionPlus;": "\u228e",
- "Uogon;": "\u0172",
- "Uopf;": "\U0001d54c",
- "UpArrow;": "\u2191",
- "UpArrowBar;": "\u2912",
- "UpArrowDownArrow;": "\u21c5",
- "UpDownArrow;": "\u2195",
- "UpEquilibrium;": "\u296e",
- "UpTee;": "\u22a5",
- "UpTeeArrow;": "\u21a5",
- "Uparrow;": "\u21d1",
- "Updownarrow;": "\u21d5",
- "UpperLeftArrow;": "\u2196",
- "UpperRightArrow;": "\u2197",
- "Upsi;": "\u03d2",
- "Upsilon;": "\u03a5",
- "Uring;": "\u016e",
- "Uscr;": "\U0001d4b0",
- "Utilde;": "\u0168",
- "Uuml": "\xdc",
- "Uuml;": "\xdc",
- "VDash;": "\u22ab",
- "Vbar;": "\u2aeb",
- "Vcy;": "\u0412",
- "Vdash;": "\u22a9",
- "Vdashl;": "\u2ae6",
- "Vee;": "\u22c1",
- "Verbar;": "\u2016",
- "Vert;": "\u2016",
- "VerticalBar;": "\u2223",
- "VerticalLine;": "|",
- "VerticalSeparator;": "\u2758",
- "VerticalTilde;": "\u2240",
- "VeryThinSpace;": "\u200a",
- "Vfr;": "\U0001d519",
- "Vopf;": "\U0001d54d",
- "Vscr;": "\U0001d4b1",
- "Vvdash;": "\u22aa",
- "Wcirc;": "\u0174",
- "Wedge;": "\u22c0",
- "Wfr;": "\U0001d51a",
- "Wopf;": "\U0001d54e",
- "Wscr;": "\U0001d4b2",
- "Xfr;": "\U0001d51b",
- "Xi;": "\u039e",
- "Xopf;": "\U0001d54f",
- "Xscr;": "\U0001d4b3",
- "YAcy;": "\u042f",
- "YIcy;": "\u0407",
- "YUcy;": "\u042e",
- "Yacute": "\xdd",
- "Yacute;": "\xdd",
- "Ycirc;": "\u0176",
- "Ycy;": "\u042b",
- "Yfr;": "\U0001d51c",
- "Yopf;": "\U0001d550",
- "Yscr;": "\U0001d4b4",
- "Yuml;": "\u0178",
- "ZHcy;": "\u0416",
- "Zacute;": "\u0179",
- "Zcaron;": "\u017d",
- "Zcy;": "\u0417",
- "Zdot;": "\u017b",
- "ZeroWidthSpace;": "\u200b",
- "Zeta;": "\u0396",
- "Zfr;": "\u2128",
- "Zopf;": "\u2124",
- "Zscr;": "\U0001d4b5",
- "aacute": "\xe1",
- "aacute;": "\xe1",
- "abreve;": "\u0103",
- "ac;": "\u223e",
- "acE;": "\u223e\u0333",
- "acd;": "\u223f",
- "acirc": "\xe2",
- "acirc;": "\xe2",
- "acute": "\xb4",
- "acute;": "\xb4",
- "acy;": "\u0430",
- "aelig": "\xe6",
- "aelig;": "\xe6",
- "af;": "\u2061",
- "afr;": "\U0001d51e",
- "agrave": "\xe0",
- "agrave;": "\xe0",
- "alefsym;": "\u2135",
- "aleph;": "\u2135",
- "alpha;": "\u03b1",
- "amacr;": "\u0101",
- "amalg;": "\u2a3f",
- "amp": "&",
- "amp;": "&",
- "and;": "\u2227",
- "andand;": "\u2a55",
- "andd;": "\u2a5c",
- "andslope;": "\u2a58",
- "andv;": "\u2a5a",
- "ang;": "\u2220",
- "ange;": "\u29a4",
- "angle;": "\u2220",
- "angmsd;": "\u2221",
- "angmsdaa;": "\u29a8",
- "angmsdab;": "\u29a9",
- "angmsdac;": "\u29aa",
- "angmsdad;": "\u29ab",
- "angmsdae;": "\u29ac",
- "angmsdaf;": "\u29ad",
- "angmsdag;": "\u29ae",
- "angmsdah;": "\u29af",
- "angrt;": "\u221f",
- "angrtvb;": "\u22be",
- "angrtvbd;": "\u299d",
- "angsph;": "\u2222",
- "angst;": "\xc5",
- "angzarr;": "\u237c",
- "aogon;": "\u0105",
- "aopf;": "\U0001d552",
- "ap;": "\u2248",
- "apE;": "\u2a70",
- "apacir;": "\u2a6f",
- "ape;": "\u224a",
- "apid;": "\u224b",
- "apos;": "'",
- "approx;": "\u2248",
- "approxeq;": "\u224a",
- "aring": "\xe5",
- "aring;": "\xe5",
- "ascr;": "\U0001d4b6",
- "ast;": "*",
- "asymp;": "\u2248",
- "asympeq;": "\u224d",
- "atilde": "\xe3",
- "atilde;": "\xe3",
- "auml": "\xe4",
- "auml;": "\xe4",
- "awconint;": "\u2233",
- "awint;": "\u2a11",
- "bNot;": "\u2aed",
- "backcong;": "\u224c",
- "backepsilon;": "\u03f6",
- "backprime;": "\u2035",
- "backsim;": "\u223d",
- "backsimeq;": "\u22cd",
- "barvee;": "\u22bd",
- "barwed;": "\u2305",
- "barwedge;": "\u2305",
- "bbrk;": "\u23b5",
- "bbrktbrk;": "\u23b6",
- "bcong;": "\u224c",
- "bcy;": "\u0431",
- "bdquo;": "\u201e",
- "becaus;": "\u2235",
- "because;": "\u2235",
- "bemptyv;": "\u29b0",
- "bepsi;": "\u03f6",
- "bernou;": "\u212c",
- "beta;": "\u03b2",
- "beth;": "\u2136",
- "between;": "\u226c",
- "bfr;": "\U0001d51f",
- "bigcap;": "\u22c2",
- "bigcirc;": "\u25ef",
- "bigcup;": "\u22c3",
- "bigodot;": "\u2a00",
- "bigoplus;": "\u2a01",
- "bigotimes;": "\u2a02",
- "bigsqcup;": "\u2a06",
- "bigstar;": "\u2605",
- "bigtriangledown;": "\u25bd",
- "bigtriangleup;": "\u25b3",
- "biguplus;": "\u2a04",
- "bigvee;": "\u22c1",
- "bigwedge;": "\u22c0",
- "bkarow;": "\u290d",
- "blacklozenge;": "\u29eb",
- "blacksquare;": "\u25aa",
- "blacktriangle;": "\u25b4",
- "blacktriangledown;": "\u25be",
- "blacktriangleleft;": "\u25c2",
- "blacktriangleright;": "\u25b8",
- "blank;": "\u2423",
- "blk12;": "\u2592",
- "blk14;": "\u2591",
- "blk34;": "\u2593",
- "block;": "\u2588",
- "bne;": "=\u20e5",
- "bnequiv;": "\u2261\u20e5",
- "bnot;": "\u2310",
- "bopf;": "\U0001d553",
- "bot;": "\u22a5",
- "bottom;": "\u22a5",
- "bowtie;": "\u22c8",
- "boxDL;": "\u2557",
- "boxDR;": "\u2554",
- "boxDl;": "\u2556",
- "boxDr;": "\u2553",
- "boxH;": "\u2550",
- "boxHD;": "\u2566",
- "boxHU;": "\u2569",
- "boxHd;": "\u2564",
- "boxHu;": "\u2567",
- "boxUL;": "\u255d",
- "boxUR;": "\u255a",
- "boxUl;": "\u255c",
- "boxUr;": "\u2559",
- "boxV;": "\u2551",
- "boxVH;": "\u256c",
- "boxVL;": "\u2563",
- "boxVR;": "\u2560",
- "boxVh;": "\u256b",
- "boxVl;": "\u2562",
- "boxVr;": "\u255f",
- "boxbox;": "\u29c9",
- "boxdL;": "\u2555",
- "boxdR;": "\u2552",
- "boxdl;": "\u2510",
- "boxdr;": "\u250c",
- "boxh;": "\u2500",
- "boxhD;": "\u2565",
- "boxhU;": "\u2568",
- "boxhd;": "\u252c",
- "boxhu;": "\u2534",
- "boxminus;": "\u229f",
- "boxplus;": "\u229e",
- "boxtimes;": "\u22a0",
- "boxuL;": "\u255b",
- "boxuR;": "\u2558",
- "boxul;": "\u2518",
- "boxur;": "\u2514",
- "boxv;": "\u2502",
- "boxvH;": "\u256a",
- "boxvL;": "\u2561",
- "boxvR;": "\u255e",
- "boxvh;": "\u253c",
- "boxvl;": "\u2524",
- "boxvr;": "\u251c",
- "bprime;": "\u2035",
- "breve;": "\u02d8",
- "brvbar": "\xa6",
- "brvbar;": "\xa6",
- "bscr;": "\U0001d4b7",
- "bsemi;": "\u204f",
- "bsim;": "\u223d",
- "bsime;": "\u22cd",
- "bsol;": "\\",
- "bsolb;": "\u29c5",
- "bsolhsub;": "\u27c8",
- "bull;": "\u2022",
- "bullet;": "\u2022",
- "bump;": "\u224e",
- "bumpE;": "\u2aae",
- "bumpe;": "\u224f",
- "bumpeq;": "\u224f",
- "cacute;": "\u0107",
- "cap;": "\u2229",
- "capand;": "\u2a44",
- "capbrcup;": "\u2a49",
- "capcap;": "\u2a4b",
- "capcup;": "\u2a47",
- "capdot;": "\u2a40",
- "caps;": "\u2229\ufe00",
- "caret;": "\u2041",
- "caron;": "\u02c7",
- "ccaps;": "\u2a4d",
- "ccaron;": "\u010d",
- "ccedil": "\xe7",
- "ccedil;": "\xe7",
- "ccirc;": "\u0109",
- "ccups;": "\u2a4c",
- "ccupssm;": "\u2a50",
- "cdot;": "\u010b",
- "cedil": "\xb8",
- "cedil;": "\xb8",
- "cemptyv;": "\u29b2",
- "cent": "\xa2",
- "cent;": "\xa2",
- "centerdot;": "\xb7",
- "cfr;": "\U0001d520",
- "chcy;": "\u0447",
- "check;": "\u2713",
- "checkmark;": "\u2713",
- "chi;": "\u03c7",
- "cir;": "\u25cb",
- "cirE;": "\u29c3",
- "circ;": "\u02c6",
- "circeq;": "\u2257",
- "circlearrowleft;": "\u21ba",
- "circlearrowright;": "\u21bb",
- "circledR;": "\xae",
- "circledS;": "\u24c8",
- "circledast;": "\u229b",
- "circledcirc;": "\u229a",
- "circleddash;": "\u229d",
- "cire;": "\u2257",
- "cirfnint;": "\u2a10",
- "cirmid;": "\u2aef",
- "cirscir;": "\u29c2",
- "clubs;": "\u2663",
- "clubsuit;": "\u2663",
- "colon;": ":",
- "colone;": "\u2254",
- "coloneq;": "\u2254",
- "comma;": ",",
- "commat;": "@",
- "comp;": "\u2201",
- "compfn;": "\u2218",
- "complement;": "\u2201",
- "complexes;": "\u2102",
- "cong;": "\u2245",
- "congdot;": "\u2a6d",
- "conint;": "\u222e",
- "copf;": "\U0001d554",
- "coprod;": "\u2210",
- "copy": "\xa9",
- "copy;": "\xa9",
- "copysr;": "\u2117",
- "crarr;": "\u21b5",
- "cross;": "\u2717",
- "cscr;": "\U0001d4b8",
- "csub;": "\u2acf",
- "csube;": "\u2ad1",
- "csup;": "\u2ad0",
- "csupe;": "\u2ad2",
- "ctdot;": "\u22ef",
- "cudarrl;": "\u2938",
- "cudarrr;": "\u2935",
- "cuepr;": "\u22de",
- "cuesc;": "\u22df",
- "cularr;": "\u21b6",
- "cularrp;": "\u293d",
- "cup;": "\u222a",
- "cupbrcap;": "\u2a48",
- "cupcap;": "\u2a46",
- "cupcup;": "\u2a4a",
- "cupdot;": "\u228d",
- "cupor;": "\u2a45",
- "cups;": "\u222a\ufe00",
- "curarr;": "\u21b7",
- "curarrm;": "\u293c",
- "curlyeqprec;": "\u22de",
- "curlyeqsucc;": "\u22df",
- "curlyvee;": "\u22ce",
- "curlywedge;": "\u22cf",
- "curren": "\xa4",
- "curren;": "\xa4",
- "curvearrowleft;": "\u21b6",
- "curvearrowright;": "\u21b7",
- "cuvee;": "\u22ce",
- "cuwed;": "\u22cf",
- "cwconint;": "\u2232",
- "cwint;": "\u2231",
- "cylcty;": "\u232d",
- "dArr;": "\u21d3",
- "dHar;": "\u2965",
- "dagger;": "\u2020",
- "daleth;": "\u2138",
- "darr;": "\u2193",
- "dash;": "\u2010",
- "dashv;": "\u22a3",
- "dbkarow;": "\u290f",
- "dblac;": "\u02dd",
- "dcaron;": "\u010f",
- "dcy;": "\u0434",
- "dd;": "\u2146",
- "ddagger;": "\u2021",
- "ddarr;": "\u21ca",
- "ddotseq;": "\u2a77",
- "deg": "\xb0",
- "deg;": "\xb0",
- "delta;": "\u03b4",
- "demptyv;": "\u29b1",
- "dfisht;": "\u297f",
- "dfr;": "\U0001d521",
- "dharl;": "\u21c3",
- "dharr;": "\u21c2",
- "diam;": "\u22c4",
- "diamond;": "\u22c4",
- "diamondsuit;": "\u2666",
- "diams;": "\u2666",
- "die;": "\xa8",
- "digamma;": "\u03dd",
- "disin;": "\u22f2",
- "div;": "\xf7",
- "divide": "\xf7",
- "divide;": "\xf7",
- "divideontimes;": "\u22c7",
- "divonx;": "\u22c7",
- "djcy;": "\u0452",
- "dlcorn;": "\u231e",
- "dlcrop;": "\u230d",
- "dollar;": "$",
- "dopf;": "\U0001d555",
- "dot;": "\u02d9",
- "doteq;": "\u2250",
- "doteqdot;": "\u2251",
- "dotminus;": "\u2238",
- "dotplus;": "\u2214",
- "dotsquare;": "\u22a1",
- "doublebarwedge;": "\u2306",
- "downarrow;": "\u2193",
- "downdownarrows;": "\u21ca",
- "downharpoonleft;": "\u21c3",
- "downharpoonright;": "\u21c2",
- "drbkarow;": "\u2910",
- "drcorn;": "\u231f",
- "drcrop;": "\u230c",
- "dscr;": "\U0001d4b9",
- "dscy;": "\u0455",
- "dsol;": "\u29f6",
- "dstrok;": "\u0111",
- "dtdot;": "\u22f1",
- "dtri;": "\u25bf",
- "dtrif;": "\u25be",
- "duarr;": "\u21f5",
- "duhar;": "\u296f",
- "dwangle;": "\u29a6",
- "dzcy;": "\u045f",
- "dzigrarr;": "\u27ff",
- "eDDot;": "\u2a77",
- "eDot;": "\u2251",
- "eacute": "\xe9",
- "eacute;": "\xe9",
- "easter;": "\u2a6e",
- "ecaron;": "\u011b",
- "ecir;": "\u2256",
- "ecirc": "\xea",
- "ecirc;": "\xea",
- "ecolon;": "\u2255",
- "ecy;": "\u044d",
- "edot;": "\u0117",
- "ee;": "\u2147",
- "efDot;": "\u2252",
- "efr;": "\U0001d522",
- "eg;": "\u2a9a",
- "egrave": "\xe8",
- "egrave;": "\xe8",
- "egs;": "\u2a96",
- "egsdot;": "\u2a98",
- "el;": "\u2a99",
- "elinters;": "\u23e7",
- "ell;": "\u2113",
- "els;": "\u2a95",
- "elsdot;": "\u2a97",
- "emacr;": "\u0113",
- "empty;": "\u2205",
- "emptyset;": "\u2205",
- "emptyv;": "\u2205",
- "emsp13;": "\u2004",
- "emsp14;": "\u2005",
- "emsp;": "\u2003",
- "eng;": "\u014b",
- "ensp;": "\u2002",
- "eogon;": "\u0119",
- "eopf;": "\U0001d556",
- "epar;": "\u22d5",
- "eparsl;": "\u29e3",
- "eplus;": "\u2a71",
- "epsi;": "\u03b5",
- "epsilon;": "\u03b5",
- "epsiv;": "\u03f5",
- "eqcirc;": "\u2256",
- "eqcolon;": "\u2255",
- "eqsim;": "\u2242",
- "eqslantgtr;": "\u2a96",
- "eqslantless;": "\u2a95",
- "equals;": "=",
- "equest;": "\u225f",
- "equiv;": "\u2261",
- "equivDD;": "\u2a78",
- "eqvparsl;": "\u29e5",
- "erDot;": "\u2253",
- "erarr;": "\u2971",
- "escr;": "\u212f",
- "esdot;": "\u2250",
- "esim;": "\u2242",
- "eta;": "\u03b7",
- "eth": "\xf0",
- "eth;": "\xf0",
- "euml": "\xeb",
- "euml;": "\xeb",
- "euro;": "\u20ac",
- "excl;": "!",
- "exist;": "\u2203",
- "expectation;": "\u2130",
- "exponentiale;": "\u2147",
- "fallingdotseq;": "\u2252",
- "fcy;": "\u0444",
- "female;": "\u2640",
- "ffilig;": "\ufb03",
- "fflig;": "\ufb00",
- "ffllig;": "\ufb04",
- "ffr;": "\U0001d523",
- "filig;": "\ufb01",
- "fjlig;": "fj",
- "flat;": "\u266d",
- "fllig;": "\ufb02",
- "fltns;": "\u25b1",
- "fnof;": "\u0192",
- "fopf;": "\U0001d557",
- "forall;": "\u2200",
- "fork;": "\u22d4",
- "forkv;": "\u2ad9",
- "fpartint;": "\u2a0d",
- "frac12": "\xbd",
- "frac12;": "\xbd",
- "frac13;": "\u2153",
- "frac14": "\xbc",
- "frac14;": "\xbc",
- "frac15;": "\u2155",
- "frac16;": "\u2159",
- "frac18;": "\u215b",
- "frac23;": "\u2154",
- "frac25;": "\u2156",
- "frac34": "\xbe",
- "frac34;": "\xbe",
- "frac35;": "\u2157",
- "frac38;": "\u215c",
- "frac45;": "\u2158",
- "frac56;": "\u215a",
- "frac58;": "\u215d",
- "frac78;": "\u215e",
- "frasl;": "\u2044",
- "frown;": "\u2322",
- "fscr;": "\U0001d4bb",
- "gE;": "\u2267",
- "gEl;": "\u2a8c",
- "gacute;": "\u01f5",
- "gamma;": "\u03b3",
- "gammad;": "\u03dd",
- "gap;": "\u2a86",
- "gbreve;": "\u011f",
- "gcirc;": "\u011d",
- "gcy;": "\u0433",
- "gdot;": "\u0121",
- "ge;": "\u2265",
- "gel;": "\u22db",
- "geq;": "\u2265",
- "geqq;": "\u2267",
- "geqslant;": "\u2a7e",
- "ges;": "\u2a7e",
- "gescc;": "\u2aa9",
- "gesdot;": "\u2a80",
- "gesdoto;": "\u2a82",
- "gesdotol;": "\u2a84",
- "gesl;": "\u22db\ufe00",
- "gesles;": "\u2a94",
- "gfr;": "\U0001d524",
- "gg;": "\u226b",
- "ggg;": "\u22d9",
- "gimel;": "\u2137",
- "gjcy;": "\u0453",
- "gl;": "\u2277",
- "glE;": "\u2a92",
- "gla;": "\u2aa5",
- "glj;": "\u2aa4",
- "gnE;": "\u2269",
- "gnap;": "\u2a8a",
- "gnapprox;": "\u2a8a",
- "gne;": "\u2a88",
- "gneq;": "\u2a88",
- "gneqq;": "\u2269",
- "gnsim;": "\u22e7",
- "gopf;": "\U0001d558",
- "grave;": "`",
- "gscr;": "\u210a",
- "gsim;": "\u2273",
- "gsime;": "\u2a8e",
- "gsiml;": "\u2a90",
- "gt": ">",
- "gt;": ">",
- "gtcc;": "\u2aa7",
- "gtcir;": "\u2a7a",
- "gtdot;": "\u22d7",
- "gtlPar;": "\u2995",
- "gtquest;": "\u2a7c",
- "gtrapprox;": "\u2a86",
- "gtrarr;": "\u2978",
- "gtrdot;": "\u22d7",
- "gtreqless;": "\u22db",
- "gtreqqless;": "\u2a8c",
- "gtrless;": "\u2277",
- "gtrsim;": "\u2273",
- "gvertneqq;": "\u2269\ufe00",
- "gvnE;": "\u2269\ufe00",
- "hArr;": "\u21d4",
- "hairsp;": "\u200a",
- "half;": "\xbd",
- "hamilt;": "\u210b",
- "hardcy;": "\u044a",
- "harr;": "\u2194",
- "harrcir;": "\u2948",
- "harrw;": "\u21ad",
- "hbar;": "\u210f",
- "hcirc;": "\u0125",
- "hearts;": "\u2665",
- "heartsuit;": "\u2665",
- "hellip;": "\u2026",
- "hercon;": "\u22b9",
- "hfr;": "\U0001d525",
- "hksearow;": "\u2925",
- "hkswarow;": "\u2926",
- "hoarr;": "\u21ff",
- "homtht;": "\u223b",
- "hookleftarrow;": "\u21a9",
- "hookrightarrow;": "\u21aa",
- "hopf;": "\U0001d559",
- "horbar;": "\u2015",
- "hscr;": "\U0001d4bd",
- "hslash;": "\u210f",
- "hstrok;": "\u0127",
- "hybull;": "\u2043",
- "hyphen;": "\u2010",
- "iacute": "\xed",
- "iacute;": "\xed",
- "ic;": "\u2063",
- "icirc": "\xee",
- "icirc;": "\xee",
- "icy;": "\u0438",
- "iecy;": "\u0435",
- "iexcl": "\xa1",
- "iexcl;": "\xa1",
- "iff;": "\u21d4",
- "ifr;": "\U0001d526",
- "igrave": "\xec",
- "igrave;": "\xec",
- "ii;": "\u2148",
- "iiiint;": "\u2a0c",
- "iiint;": "\u222d",
- "iinfin;": "\u29dc",
- "iiota;": "\u2129",
- "ijlig;": "\u0133",
- "imacr;": "\u012b",
- "image;": "\u2111",
- "imagline;": "\u2110",
- "imagpart;": "\u2111",
- "imath;": "\u0131",
- "imof;": "\u22b7",
- "imped;": "\u01b5",
- "in;": "\u2208",
- "incare;": "\u2105",
- "infin;": "\u221e",
- "infintie;": "\u29dd",
- "inodot;": "\u0131",
- "int;": "\u222b",
- "intcal;": "\u22ba",
- "integers;": "\u2124",
- "intercal;": "\u22ba",
- "intlarhk;": "\u2a17",
- "intprod;": "\u2a3c",
- "iocy;": "\u0451",
- "iogon;": "\u012f",
- "iopf;": "\U0001d55a",
- "iota;": "\u03b9",
- "iprod;": "\u2a3c",
- "iquest": "\xbf",
- "iquest;": "\xbf",
- "iscr;": "\U0001d4be",
- "isin;": "\u2208",
- "isinE;": "\u22f9",
- "isindot;": "\u22f5",
- "isins;": "\u22f4",
- "isinsv;": "\u22f3",
- "isinv;": "\u2208",
- "it;": "\u2062",
- "itilde;": "\u0129",
- "iukcy;": "\u0456",
- "iuml": "\xef",
- "iuml;": "\xef",
- "jcirc;": "\u0135",
- "jcy;": "\u0439",
- "jfr;": "\U0001d527",
- "jmath;": "\u0237",
- "jopf;": "\U0001d55b",
- "jscr;": "\U0001d4bf",
- "jsercy;": "\u0458",
- "jukcy;": "\u0454",
- "kappa;": "\u03ba",
- "kappav;": "\u03f0",
- "kcedil;": "\u0137",
- "kcy;": "\u043a",
- "kfr;": "\U0001d528",
- "kgreen;": "\u0138",
- "khcy;": "\u0445",
- "kjcy;": "\u045c",
- "kopf;": "\U0001d55c",
- "kscr;": "\U0001d4c0",
- "lAarr;": "\u21da",
- "lArr;": "\u21d0",
- "lAtail;": "\u291b",
- "lBarr;": "\u290e",
- "lE;": "\u2266",
- "lEg;": "\u2a8b",
- "lHar;": "\u2962",
- "lacute;": "\u013a",
- "laemptyv;": "\u29b4",
- "lagran;": "\u2112",
- "lambda;": "\u03bb",
- "lang;": "\u27e8",
- "langd;": "\u2991",
- "langle;": "\u27e8",
- "lap;": "\u2a85",
- "laquo": "\xab",
- "laquo;": "\xab",
- "larr;": "\u2190",
- "larrb;": "\u21e4",
- "larrbfs;": "\u291f",
- "larrfs;": "\u291d",
- "larrhk;": "\u21a9",
- "larrlp;": "\u21ab",
- "larrpl;": "\u2939",
- "larrsim;": "\u2973",
- "larrtl;": "\u21a2",
- "lat;": "\u2aab",
- "latail;": "\u2919",
- "late;": "\u2aad",
- "lates;": "\u2aad\ufe00",
- "lbarr;": "\u290c",
- "lbbrk;": "\u2772",
- "lbrace;": "{",
- "lbrack;": "[",
- "lbrke;": "\u298b",
- "lbrksld;": "\u298f",
- "lbrkslu;": "\u298d",
- "lcaron;": "\u013e",
- "lcedil;": "\u013c",
- "lceil;": "\u2308",
- "lcub;": "{",
- "lcy;": "\u043b",
- "ldca;": "\u2936",
- "ldquo;": "\u201c",
- "ldquor;": "\u201e",
- "ldrdhar;": "\u2967",
- "ldrushar;": "\u294b",
- "ldsh;": "\u21b2",
- "le;": "\u2264",
- "leftarrow;": "\u2190",
- "leftarrowtail;": "\u21a2",
- "leftharpoondown;": "\u21bd",
- "leftharpoonup;": "\u21bc",
- "leftleftarrows;": "\u21c7",
- "leftrightarrow;": "\u2194",
- "leftrightarrows;": "\u21c6",
- "leftrightharpoons;": "\u21cb",
- "leftrightsquigarrow;": "\u21ad",
- "leftthreetimes;": "\u22cb",
- "leg;": "\u22da",
- "leq;": "\u2264",
- "leqq;": "\u2266",
- "leqslant;": "\u2a7d",
- "les;": "\u2a7d",
- "lescc;": "\u2aa8",
- "lesdot;": "\u2a7f",
- "lesdoto;": "\u2a81",
- "lesdotor;": "\u2a83",
- "lesg;": "\u22da\ufe00",
- "lesges;": "\u2a93",
- "lessapprox;": "\u2a85",
- "lessdot;": "\u22d6",
- "lesseqgtr;": "\u22da",
- "lesseqqgtr;": "\u2a8b",
- "lessgtr;": "\u2276",
- "lesssim;": "\u2272",
- "lfisht;": "\u297c",
- "lfloor;": "\u230a",
- "lfr;": "\U0001d529",
- "lg;": "\u2276",
- "lgE;": "\u2a91",
- "lhard;": "\u21bd",
- "lharu;": "\u21bc",
- "lharul;": "\u296a",
- "lhblk;": "\u2584",
- "ljcy;": "\u0459",
- "ll;": "\u226a",
- "llarr;": "\u21c7",
- "llcorner;": "\u231e",
- "llhard;": "\u296b",
- "lltri;": "\u25fa",
- "lmidot;": "\u0140",
- "lmoust;": "\u23b0",
- "lmoustache;": "\u23b0",
- "lnE;": "\u2268",
- "lnap;": "\u2a89",
- "lnapprox;": "\u2a89",
- "lne;": "\u2a87",
- "lneq;": "\u2a87",
- "lneqq;": "\u2268",
- "lnsim;": "\u22e6",
- "loang;": "\u27ec",
- "loarr;": "\u21fd",
- "lobrk;": "\u27e6",
- "longleftarrow;": "\u27f5",
- "longleftrightarrow;": "\u27f7",
- "longmapsto;": "\u27fc",
- "longrightarrow;": "\u27f6",
- "looparrowleft;": "\u21ab",
- "looparrowright;": "\u21ac",
- "lopar;": "\u2985",
- "lopf;": "\U0001d55d",
- "loplus;": "\u2a2d",
- "lotimes;": "\u2a34",
- "lowast;": "\u2217",
- "lowbar;": "_",
- "loz;": "\u25ca",
- "lozenge;": "\u25ca",
- "lozf;": "\u29eb",
- "lpar;": "(",
- "lparlt;": "\u2993",
- "lrarr;": "\u21c6",
- "lrcorner;": "\u231f",
- "lrhar;": "\u21cb",
- "lrhard;": "\u296d",
- "lrm;": "\u200e",
- "lrtri;": "\u22bf",
- "lsaquo;": "\u2039",
- "lscr;": "\U0001d4c1",
- "lsh;": "\u21b0",
- "lsim;": "\u2272",
- "lsime;": "\u2a8d",
- "lsimg;": "\u2a8f",
- "lsqb;": "[",
- "lsquo;": "\u2018",
- "lsquor;": "\u201a",
- "lstrok;": "\u0142",
- "lt": "<",
- "lt;": "<",
- "ltcc;": "\u2aa6",
- "ltcir;": "\u2a79",
- "ltdot;": "\u22d6",
- "lthree;": "\u22cb",
- "ltimes;": "\u22c9",
- "ltlarr;": "\u2976",
- "ltquest;": "\u2a7b",
- "ltrPar;": "\u2996",
- "ltri;": "\u25c3",
- "ltrie;": "\u22b4",
- "ltrif;": "\u25c2",
- "lurdshar;": "\u294a",
- "luruhar;": "\u2966",
- "lvertneqq;": "\u2268\ufe00",
- "lvnE;": "\u2268\ufe00",
- "mDDot;": "\u223a",
- "macr": "\xaf",
- "macr;": "\xaf",
- "male;": "\u2642",
- "malt;": "\u2720",
- "maltese;": "\u2720",
- "map;": "\u21a6",
- "mapsto;": "\u21a6",
- "mapstodown;": "\u21a7",
- "mapstoleft;": "\u21a4",
- "mapstoup;": "\u21a5",
- "marker;": "\u25ae",
- "mcomma;": "\u2a29",
- "mcy;": "\u043c",
- "mdash;": "\u2014",
- "measuredangle;": "\u2221",
- "mfr;": "\U0001d52a",
- "mho;": "\u2127",
- "micro": "\xb5",
- "micro;": "\xb5",
- "mid;": "\u2223",
- "midast;": "*",
- "midcir;": "\u2af0",
- "middot": "\xb7",
- "middot;": "\xb7",
- "minus;": "\u2212",
- "minusb;": "\u229f",
- "minusd;": "\u2238",
- "minusdu;": "\u2a2a",
- "mlcp;": "\u2adb",
- "mldr;": "\u2026",
- "mnplus;": "\u2213",
- "models;": "\u22a7",
- "mopf;": "\U0001d55e",
- "mp;": "\u2213",
- "mscr;": "\U0001d4c2",
- "mstpos;": "\u223e",
- "mu;": "\u03bc",
- "multimap;": "\u22b8",
- "mumap;": "\u22b8",
- "nGg;": "\u22d9\u0338",
- "nGt;": "\u226b\u20d2",
- "nGtv;": "\u226b\u0338",
- "nLeftarrow;": "\u21cd",
- "nLeftrightarrow;": "\u21ce",
- "nLl;": "\u22d8\u0338",
- "nLt;": "\u226a\u20d2",
- "nLtv;": "\u226a\u0338",
- "nRightarrow;": "\u21cf",
- "nVDash;": "\u22af",
- "nVdash;": "\u22ae",
- "nabla;": "\u2207",
- "nacute;": "\u0144",
- "nang;": "\u2220\u20d2",
- "nap;": "\u2249",
- "napE;": "\u2a70\u0338",
- "napid;": "\u224b\u0338",
- "napos;": "\u0149",
- "napprox;": "\u2249",
- "natur;": "\u266e",
- "natural;": "\u266e",
- "naturals;": "\u2115",
- "nbsp": "\xa0",
- "nbsp;": "\xa0",
- "nbump;": "\u224e\u0338",
- "nbumpe;": "\u224f\u0338",
- "ncap;": "\u2a43",
- "ncaron;": "\u0148",
- "ncedil;": "\u0146",
- "ncong;": "\u2247",
- "ncongdot;": "\u2a6d\u0338",
- "ncup;": "\u2a42",
- "ncy;": "\u043d",
- "ndash;": "\u2013",
- "ne;": "\u2260",
- "neArr;": "\u21d7",
- "nearhk;": "\u2924",
- "nearr;": "\u2197",
- "nearrow;": "\u2197",
- "nedot;": "\u2250\u0338",
- "nequiv;": "\u2262",
- "nesear;": "\u2928",
- "nesim;": "\u2242\u0338",
- "nexist;": "\u2204",
- "nexists;": "\u2204",
- "nfr;": "\U0001d52b",
- "ngE;": "\u2267\u0338",
- "nge;": "\u2271",
- "ngeq;": "\u2271",
- "ngeqq;": "\u2267\u0338",
- "ngeqslant;": "\u2a7e\u0338",
- "nges;": "\u2a7e\u0338",
- "ngsim;": "\u2275",
- "ngt;": "\u226f",
- "ngtr;": "\u226f",
- "nhArr;": "\u21ce",
- "nharr;": "\u21ae",
- "nhpar;": "\u2af2",
- "ni;": "\u220b",
- "nis;": "\u22fc",
- "nisd;": "\u22fa",
- "niv;": "\u220b",
- "njcy;": "\u045a",
- "nlArr;": "\u21cd",
- "nlE;": "\u2266\u0338",
- "nlarr;": "\u219a",
- "nldr;": "\u2025",
- "nle;": "\u2270",
- "nleftarrow;": "\u219a",
- "nleftrightarrow;": "\u21ae",
- "nleq;": "\u2270",
- "nleqq;": "\u2266\u0338",
- "nleqslant;": "\u2a7d\u0338",
- "nles;": "\u2a7d\u0338",
- "nless;": "\u226e",
- "nlsim;": "\u2274",
- "nlt;": "\u226e",
- "nltri;": "\u22ea",
- "nltrie;": "\u22ec",
- "nmid;": "\u2224",
- "nopf;": "\U0001d55f",
- "not": "\xac",
- "not;": "\xac",
- "notin;": "\u2209",
- "notinE;": "\u22f9\u0338",
- "notindot;": "\u22f5\u0338",
- "notinva;": "\u2209",
- "notinvb;": "\u22f7",
- "notinvc;": "\u22f6",
- "notni;": "\u220c",
- "notniva;": "\u220c",
- "notnivb;": "\u22fe",
- "notnivc;": "\u22fd",
- "npar;": "\u2226",
- "nparallel;": "\u2226",
- "nparsl;": "\u2afd\u20e5",
- "npart;": "\u2202\u0338",
- "npolint;": "\u2a14",
- "npr;": "\u2280",
- "nprcue;": "\u22e0",
- "npre;": "\u2aaf\u0338",
- "nprec;": "\u2280",
- "npreceq;": "\u2aaf\u0338",
- "nrArr;": "\u21cf",
- "nrarr;": "\u219b",
- "nrarrc;": "\u2933\u0338",
- "nrarrw;": "\u219d\u0338",
- "nrightarrow;": "\u219b",
- "nrtri;": "\u22eb",
- "nrtrie;": "\u22ed",
- "nsc;": "\u2281",
- "nsccue;": "\u22e1",
- "nsce;": "\u2ab0\u0338",
- "nscr;": "\U0001d4c3",
- "nshortmid;": "\u2224",
- "nshortparallel;": "\u2226",
- "nsim;": "\u2241",
- "nsime;": "\u2244",
- "nsimeq;": "\u2244",
- "nsmid;": "\u2224",
- "nspar;": "\u2226",
- "nsqsube;": "\u22e2",
- "nsqsupe;": "\u22e3",
- "nsub;": "\u2284",
- "nsubE;": "\u2ac5\u0338",
- "nsube;": "\u2288",
- "nsubset;": "\u2282\u20d2",
- "nsubseteq;": "\u2288",
- "nsubseteqq;": "\u2ac5\u0338",
- "nsucc;": "\u2281",
- "nsucceq;": "\u2ab0\u0338",
- "nsup;": "\u2285",
- "nsupE;": "\u2ac6\u0338",
- "nsupe;": "\u2289",
- "nsupset;": "\u2283\u20d2",
- "nsupseteq;": "\u2289",
- "nsupseteqq;": "\u2ac6\u0338",
- "ntgl;": "\u2279",
- "ntilde": "\xf1",
- "ntilde;": "\xf1",
- "ntlg;": "\u2278",
- "ntriangleleft;": "\u22ea",
- "ntrianglelefteq;": "\u22ec",
- "ntriangleright;": "\u22eb",
- "ntrianglerighteq;": "\u22ed",
- "nu;": "\u03bd",
- "num;": "#",
- "numero;": "\u2116",
- "numsp;": "\u2007",
- "nvDash;": "\u22ad",
- "nvHarr;": "\u2904",
- "nvap;": "\u224d\u20d2",
- "nvdash;": "\u22ac",
- "nvge;": "\u2265\u20d2",
- "nvgt;": ">\u20d2",
- "nvinfin;": "\u29de",
- "nvlArr;": "\u2902",
- "nvle;": "\u2264\u20d2",
- "nvlt;": "<\u20d2",
- "nvltrie;": "\u22b4\u20d2",
- "nvrArr;": "\u2903",
- "nvrtrie;": "\u22b5\u20d2",
- "nvsim;": "\u223c\u20d2",
- "nwArr;": "\u21d6",
- "nwarhk;": "\u2923",
- "nwarr;": "\u2196",
- "nwarrow;": "\u2196",
- "nwnear;": "\u2927",
- "oS;": "\u24c8",
- "oacute": "\xf3",
- "oacute;": "\xf3",
- "oast;": "\u229b",
- "ocir;": "\u229a",
- "ocirc": "\xf4",
- "ocirc;": "\xf4",
- "ocy;": "\u043e",
- "odash;": "\u229d",
- "odblac;": "\u0151",
- "odiv;": "\u2a38",
- "odot;": "\u2299",
- "odsold;": "\u29bc",
- "oelig;": "\u0153",
- "ofcir;": "\u29bf",
- "ofr;": "\U0001d52c",
- "ogon;": "\u02db",
- "ograve": "\xf2",
- "ograve;": "\xf2",
- "ogt;": "\u29c1",
- "ohbar;": "\u29b5",
- "ohm;": "\u03a9",
- "oint;": "\u222e",
- "olarr;": "\u21ba",
- "olcir;": "\u29be",
- "olcross;": "\u29bb",
- "oline;": "\u203e",
- "olt;": "\u29c0",
- "omacr;": "\u014d",
- "omega;": "\u03c9",
- "omicron;": "\u03bf",
- "omid;": "\u29b6",
- "ominus;": "\u2296",
- "oopf;": "\U0001d560",
- "opar;": "\u29b7",
- "operp;": "\u29b9",
- "oplus;": "\u2295",
- "or;": "\u2228",
- "orarr;": "\u21bb",
- "ord;": "\u2a5d",
- "order;": "\u2134",
- "orderof;": "\u2134",
- "ordf": "\xaa",
- "ordf;": "\xaa",
- "ordm": "\xba",
- "ordm;": "\xba",
- "origof;": "\u22b6",
- "oror;": "\u2a56",
- "orslope;": "\u2a57",
- "orv;": "\u2a5b",
- "oscr;": "\u2134",
- "oslash": "\xf8",
- "oslash;": "\xf8",
- "osol;": "\u2298",
- "otilde": "\xf5",
- "otilde;": "\xf5",
- "otimes;": "\u2297",
- "otimesas;": "\u2a36",
- "ouml": "\xf6",
- "ouml;": "\xf6",
- "ovbar;": "\u233d",
- "par;": "\u2225",
- "para": "\xb6",
- "para;": "\xb6",
- "parallel;": "\u2225",
- "parsim;": "\u2af3",
- "parsl;": "\u2afd",
- "part;": "\u2202",
- "pcy;": "\u043f",
- "percnt;": "%",
- "period;": ".",
- "permil;": "\u2030",
- "perp;": "\u22a5",
- "pertenk;": "\u2031",
- "pfr;": "\U0001d52d",
- "phi;": "\u03c6",
- "phiv;": "\u03d5",
- "phmmat;": "\u2133",
- "phone;": "\u260e",
- "pi;": "\u03c0",
- "pitchfork;": "\u22d4",
- "piv;": "\u03d6",
- "planck;": "\u210f",
- "planckh;": "\u210e",
- "plankv;": "\u210f",
- "plus;": "+",
- "plusacir;": "\u2a23",
- "plusb;": "\u229e",
- "pluscir;": "\u2a22",
- "plusdo;": "\u2214",
- "plusdu;": "\u2a25",
- "pluse;": "\u2a72",
- "plusmn": "\xb1",
- "plusmn;": "\xb1",
- "plussim;": "\u2a26",
- "plustwo;": "\u2a27",
- "pm;": "\xb1",
- "pointint;": "\u2a15",
- "popf;": "\U0001d561",
- "pound": "\xa3",
- "pound;": "\xa3",
- "pr;": "\u227a",
- "prE;": "\u2ab3",
- "prap;": "\u2ab7",
- "prcue;": "\u227c",
- "pre;": "\u2aaf",
- "prec;": "\u227a",
- "precapprox;": "\u2ab7",
- "preccurlyeq;": "\u227c",
- "preceq;": "\u2aaf",
- "precnapprox;": "\u2ab9",
- "precneqq;": "\u2ab5",
- "precnsim;": "\u22e8",
- "precsim;": "\u227e",
- "prime;": "\u2032",
- "primes;": "\u2119",
- "prnE;": "\u2ab5",
- "prnap;": "\u2ab9",
- "prnsim;": "\u22e8",
- "prod;": "\u220f",
- "profalar;": "\u232e",
- "profline;": "\u2312",
- "profsurf;": "\u2313",
- "prop;": "\u221d",
- "propto;": "\u221d",
- "prsim;": "\u227e",
- "prurel;": "\u22b0",
- "pscr;": "\U0001d4c5",
- "psi;": "\u03c8",
- "puncsp;": "\u2008",
- "qfr;": "\U0001d52e",
- "qint;": "\u2a0c",
- "qopf;": "\U0001d562",
- "qprime;": "\u2057",
- "qscr;": "\U0001d4c6",
- "quaternions;": "\u210d",
- "quatint;": "\u2a16",
- "quest;": "?",
- "questeq;": "\u225f",
- "quot": "\"",
- "quot;": "\"",
- "rAarr;": "\u21db",
- "rArr;": "\u21d2",
- "rAtail;": "\u291c",
- "rBarr;": "\u290f",
- "rHar;": "\u2964",
- "race;": "\u223d\u0331",
- "racute;": "\u0155",
- "radic;": "\u221a",
- "raemptyv;": "\u29b3",
- "rang;": "\u27e9",
- "rangd;": "\u2992",
- "range;": "\u29a5",
- "rangle;": "\u27e9",
- "raquo": "\xbb",
- "raquo;": "\xbb",
- "rarr;": "\u2192",
- "rarrap;": "\u2975",
- "rarrb;": "\u21e5",
- "rarrbfs;": "\u2920",
- "rarrc;": "\u2933",
- "rarrfs;": "\u291e",
- "rarrhk;": "\u21aa",
- "rarrlp;": "\u21ac",
- "rarrpl;": "\u2945",
- "rarrsim;": "\u2974",
- "rarrtl;": "\u21a3",
- "rarrw;": "\u219d",
- "ratail;": "\u291a",
- "ratio;": "\u2236",
- "rationals;": "\u211a",
- "rbarr;": "\u290d",
- "rbbrk;": "\u2773",
- "rbrace;": "}",
- "rbrack;": "]",
- "rbrke;": "\u298c",
- "rbrksld;": "\u298e",
- "rbrkslu;": "\u2990",
- "rcaron;": "\u0159",
- "rcedil;": "\u0157",
- "rceil;": "\u2309",
- "rcub;": "}",
- "rcy;": "\u0440",
- "rdca;": "\u2937",
- "rdldhar;": "\u2969",
- "rdquo;": "\u201d",
- "rdquor;": "\u201d",
- "rdsh;": "\u21b3",
- "real;": "\u211c",
- "realine;": "\u211b",
- "realpart;": "\u211c",
- "reals;": "\u211d",
- "rect;": "\u25ad",
- "reg": "\xae",
- "reg;": "\xae",
- "rfisht;": "\u297d",
- "rfloor;": "\u230b",
- "rfr;": "\U0001d52f",
- "rhard;": "\u21c1",
- "rharu;": "\u21c0",
- "rharul;": "\u296c",
- "rho;": "\u03c1",
- "rhov;": "\u03f1",
- "rightarrow;": "\u2192",
- "rightarrowtail;": "\u21a3",
- "rightharpoondown;": "\u21c1",
- "rightharpoonup;": "\u21c0",
- "rightleftarrows;": "\u21c4",
- "rightleftharpoons;": "\u21cc",
- "rightrightarrows;": "\u21c9",
- "rightsquigarrow;": "\u219d",
- "rightthreetimes;": "\u22cc",
- "ring;": "\u02da",
- "risingdotseq;": "\u2253",
- "rlarr;": "\u21c4",
- "rlhar;": "\u21cc",
- "rlm;": "\u200f",
- "rmoust;": "\u23b1",
- "rmoustache;": "\u23b1",
- "rnmid;": "\u2aee",
- "roang;": "\u27ed",
- "roarr;": "\u21fe",
- "robrk;": "\u27e7",
- "ropar;": "\u2986",
- "ropf;": "\U0001d563",
- "roplus;": "\u2a2e",
- "rotimes;": "\u2a35",
- "rpar;": ")",
- "rpargt;": "\u2994",
- "rppolint;": "\u2a12",
- "rrarr;": "\u21c9",
- "rsaquo;": "\u203a",
- "rscr;": "\U0001d4c7",
- "rsh;": "\u21b1",
- "rsqb;": "]",
- "rsquo;": "\u2019",
- "rsquor;": "\u2019",
- "rthree;": "\u22cc",
- "rtimes;": "\u22ca",
- "rtri;": "\u25b9",
- "rtrie;": "\u22b5",
- "rtrif;": "\u25b8",
- "rtriltri;": "\u29ce",
- "ruluhar;": "\u2968",
- "rx;": "\u211e",
- "sacute;": "\u015b",
- "sbquo;": "\u201a",
- "sc;": "\u227b",
- "scE;": "\u2ab4",
- "scap;": "\u2ab8",
- "scaron;": "\u0161",
- "sccue;": "\u227d",
- "sce;": "\u2ab0",
- "scedil;": "\u015f",
- "scirc;": "\u015d",
- "scnE;": "\u2ab6",
- "scnap;": "\u2aba",
- "scnsim;": "\u22e9",
- "scpolint;": "\u2a13",
- "scsim;": "\u227f",
- "scy;": "\u0441",
- "sdot;": "\u22c5",
- "sdotb;": "\u22a1",
- "sdote;": "\u2a66",
- "seArr;": "\u21d8",
- "searhk;": "\u2925",
- "searr;": "\u2198",
- "searrow;": "\u2198",
- "sect": "\xa7",
- "sect;": "\xa7",
- "semi;": ";",
- "seswar;": "\u2929",
- "setminus;": "\u2216",
- "setmn;": "\u2216",
- "sext;": "\u2736",
- "sfr;": "\U0001d530",
- "sfrown;": "\u2322",
- "sharp;": "\u266f",
- "shchcy;": "\u0449",
- "shcy;": "\u0448",
- "shortmid;": "\u2223",
- "shortparallel;": "\u2225",
- "shy": "\xad",
- "shy;": "\xad",
- "sigma;": "\u03c3",
- "sigmaf;": "\u03c2",
- "sigmav;": "\u03c2",
- "sim;": "\u223c",
- "simdot;": "\u2a6a",
- "sime;": "\u2243",
- "simeq;": "\u2243",
- "simg;": "\u2a9e",
- "simgE;": "\u2aa0",
- "siml;": "\u2a9d",
- "simlE;": "\u2a9f",
- "simne;": "\u2246",
- "simplus;": "\u2a24",
- "simrarr;": "\u2972",
- "slarr;": "\u2190",
- "smallsetminus;": "\u2216",
- "smashp;": "\u2a33",
- "smeparsl;": "\u29e4",
- "smid;": "\u2223",
- "smile;": "\u2323",
- "smt;": "\u2aaa",
- "smte;": "\u2aac",
- "smtes;": "\u2aac\ufe00",
- "softcy;": "\u044c",
- "sol;": "/",
- "solb;": "\u29c4",
- "solbar;": "\u233f",
- "sopf;": "\U0001d564",
- "spades;": "\u2660",
- "spadesuit;": "\u2660",
- "spar;": "\u2225",
- "sqcap;": "\u2293",
- "sqcaps;": "\u2293\ufe00",
- "sqcup;": "\u2294",
- "sqcups;": "\u2294\ufe00",
- "sqsub;": "\u228f",
- "sqsube;": "\u2291",
- "sqsubset;": "\u228f",
- "sqsubseteq;": "\u2291",
- "sqsup;": "\u2290",
- "sqsupe;": "\u2292",
- "sqsupset;": "\u2290",
- "sqsupseteq;": "\u2292",
- "squ;": "\u25a1",
- "square;": "\u25a1",
- "squarf;": "\u25aa",
- "squf;": "\u25aa",
- "srarr;": "\u2192",
- "sscr;": "\U0001d4c8",
- "ssetmn;": "\u2216",
- "ssmile;": "\u2323",
- "sstarf;": "\u22c6",
- "star;": "\u2606",
- "starf;": "\u2605",
- "straightepsilon;": "\u03f5",
- "straightphi;": "\u03d5",
- "strns;": "\xaf",
- "sub;": "\u2282",
- "subE;": "\u2ac5",
- "subdot;": "\u2abd",
- "sube;": "\u2286",
- "subedot;": "\u2ac3",
- "submult;": "\u2ac1",
- "subnE;": "\u2acb",
- "subne;": "\u228a",
- "subplus;": "\u2abf",
- "subrarr;": "\u2979",
- "subset;": "\u2282",
- "subseteq;": "\u2286",
- "subseteqq;": "\u2ac5",
- "subsetneq;": "\u228a",
- "subsetneqq;": "\u2acb",
- "subsim;": "\u2ac7",
- "subsub;": "\u2ad5",
- "subsup;": "\u2ad3",
- "succ;": "\u227b",
- "succapprox;": "\u2ab8",
- "succcurlyeq;": "\u227d",
- "succeq;": "\u2ab0",
- "succnapprox;": "\u2aba",
- "succneqq;": "\u2ab6",
- "succnsim;": "\u22e9",
- "succsim;": "\u227f",
- "sum;": "\u2211",
- "sung;": "\u266a",
- "sup1": "\xb9",
- "sup1;": "\xb9",
- "sup2": "\xb2",
- "sup2;": "\xb2",
- "sup3": "\xb3",
- "sup3;": "\xb3",
- "sup;": "\u2283",
- "supE;": "\u2ac6",
- "supdot;": "\u2abe",
- "supdsub;": "\u2ad8",
- "supe;": "\u2287",
- "supedot;": "\u2ac4",
- "suphsol;": "\u27c9",
- "suphsub;": "\u2ad7",
- "suplarr;": "\u297b",
- "supmult;": "\u2ac2",
- "supnE;": "\u2acc",
- "supne;": "\u228b",
- "supplus;": "\u2ac0",
- "supset;": "\u2283",
- "supseteq;": "\u2287",
- "supseteqq;": "\u2ac6",
- "supsetneq;": "\u228b",
- "supsetneqq;": "\u2acc",
- "supsim;": "\u2ac8",
- "supsub;": "\u2ad4",
- "supsup;": "\u2ad6",
- "swArr;": "\u21d9",
- "swarhk;": "\u2926",
- "swarr;": "\u2199",
- "swarrow;": "\u2199",
- "swnwar;": "\u292a",
- "szlig": "\xdf",
- "szlig;": "\xdf",
- "target;": "\u2316",
- "tau;": "\u03c4",
- "tbrk;": "\u23b4",
- "tcaron;": "\u0165",
- "tcedil;": "\u0163",
- "tcy;": "\u0442",
- "tdot;": "\u20db",
- "telrec;": "\u2315",
- "tfr;": "\U0001d531",
- "there4;": "\u2234",
- "therefore;": "\u2234",
- "theta;": "\u03b8",
- "thetasym;": "\u03d1",
- "thetav;": "\u03d1",
- "thickapprox;": "\u2248",
- "thicksim;": "\u223c",
- "thinsp;": "\u2009",
- "thkap;": "\u2248",
- "thksim;": "\u223c",
- "thorn": "\xfe",
- "thorn;": "\xfe",
- "tilde;": "\u02dc",
- "times": "\xd7",
- "times;": "\xd7",
- "timesb;": "\u22a0",
- "timesbar;": "\u2a31",
- "timesd;": "\u2a30",
- "tint;": "\u222d",
- "toea;": "\u2928",
- "top;": "\u22a4",
- "topbot;": "\u2336",
- "topcir;": "\u2af1",
- "topf;": "\U0001d565",
- "topfork;": "\u2ada",
- "tosa;": "\u2929",
- "tprime;": "\u2034",
- "trade;": "\u2122",
- "triangle;": "\u25b5",
- "triangledown;": "\u25bf",
- "triangleleft;": "\u25c3",
- "trianglelefteq;": "\u22b4",
- "triangleq;": "\u225c",
- "triangleright;": "\u25b9",
- "trianglerighteq;": "\u22b5",
- "tridot;": "\u25ec",
- "trie;": "\u225c",
- "triminus;": "\u2a3a",
- "triplus;": "\u2a39",
- "trisb;": "\u29cd",
- "tritime;": "\u2a3b",
- "trpezium;": "\u23e2",
- "tscr;": "\U0001d4c9",
- "tscy;": "\u0446",
- "tshcy;": "\u045b",
- "tstrok;": "\u0167",
- "twixt;": "\u226c",
- "twoheadleftarrow;": "\u219e",
- "twoheadrightarrow;": "\u21a0",
- "uArr;": "\u21d1",
- "uHar;": "\u2963",
- "uacute": "\xfa",
- "uacute;": "\xfa",
- "uarr;": "\u2191",
- "ubrcy;": "\u045e",
- "ubreve;": "\u016d",
- "ucirc": "\xfb",
- "ucirc;": "\xfb",
- "ucy;": "\u0443",
- "udarr;": "\u21c5",
- "udblac;": "\u0171",
- "udhar;": "\u296e",
- "ufisht;": "\u297e",
- "ufr;": "\U0001d532",
- "ugrave": "\xf9",
- "ugrave;": "\xf9",
- "uharl;": "\u21bf",
- "uharr;": "\u21be",
- "uhblk;": "\u2580",
- "ulcorn;": "\u231c",
- "ulcorner;": "\u231c",
- "ulcrop;": "\u230f",
- "ultri;": "\u25f8",
- "umacr;": "\u016b",
- "uml": "\xa8",
- "uml;": "\xa8",
- "uogon;": "\u0173",
- "uopf;": "\U0001d566",
- "uparrow;": "\u2191",
- "updownarrow;": "\u2195",
- "upharpoonleft;": "\u21bf",
- "upharpoonright;": "\u21be",
- "uplus;": "\u228e",
- "upsi;": "\u03c5",
- "upsih;": "\u03d2",
- "upsilon;": "\u03c5",
- "upuparrows;": "\u21c8",
- "urcorn;": "\u231d",
- "urcorner;": "\u231d",
- "urcrop;": "\u230e",
- "uring;": "\u016f",
- "urtri;": "\u25f9",
- "uscr;": "\U0001d4ca",
- "utdot;": "\u22f0",
- "utilde;": "\u0169",
- "utri;": "\u25b5",
- "utrif;": "\u25b4",
- "uuarr;": "\u21c8",
- "uuml": "\xfc",
- "uuml;": "\xfc",
- "uwangle;": "\u29a7",
- "vArr;": "\u21d5",
- "vBar;": "\u2ae8",
- "vBarv;": "\u2ae9",
- "vDash;": "\u22a8",
- "vangrt;": "\u299c",
- "varepsilon;": "\u03f5",
- "varkappa;": "\u03f0",
- "varnothing;": "\u2205",
- "varphi;": "\u03d5",
- "varpi;": "\u03d6",
- "varpropto;": "\u221d",
- "varr;": "\u2195",
- "varrho;": "\u03f1",
- "varsigma;": "\u03c2",
- "varsubsetneq;": "\u228a\ufe00",
- "varsubsetneqq;": "\u2acb\ufe00",
- "varsupsetneq;": "\u228b\ufe00",
- "varsupsetneqq;": "\u2acc\ufe00",
- "vartheta;": "\u03d1",
- "vartriangleleft;": "\u22b2",
- "vartriangleright;": "\u22b3",
- "vcy;": "\u0432",
- "vdash;": "\u22a2",
- "vee;": "\u2228",
- "veebar;": "\u22bb",
- "veeeq;": "\u225a",
- "vellip;": "\u22ee",
- "verbar;": "|",
- "vert;": "|",
- "vfr;": "\U0001d533",
- "vltri;": "\u22b2",
- "vnsub;": "\u2282\u20d2",
- "vnsup;": "\u2283\u20d2",
- "vopf;": "\U0001d567",
- "vprop;": "\u221d",
- "vrtri;": "\u22b3",
- "vscr;": "\U0001d4cb",
- "vsubnE;": "\u2acb\ufe00",
- "vsubne;": "\u228a\ufe00",
- "vsupnE;": "\u2acc\ufe00",
- "vsupne;": "\u228b\ufe00",
- "vzigzag;": "\u299a",
- "wcirc;": "\u0175",
- "wedbar;": "\u2a5f",
- "wedge;": "\u2227",
- "wedgeq;": "\u2259",
- "weierp;": "\u2118",
- "wfr;": "\U0001d534",
- "wopf;": "\U0001d568",
- "wp;": "\u2118",
- "wr;": "\u2240",
- "wreath;": "\u2240",
- "wscr;": "\U0001d4cc",
- "xcap;": "\u22c2",
- "xcirc;": "\u25ef",
- "xcup;": "\u22c3",
- "xdtri;": "\u25bd",
- "xfr;": "\U0001d535",
- "xhArr;": "\u27fa",
- "xharr;": "\u27f7",
- "xi;": "\u03be",
- "xlArr;": "\u27f8",
- "xlarr;": "\u27f5",
- "xmap;": "\u27fc",
- "xnis;": "\u22fb",
- "xodot;": "\u2a00",
- "xopf;": "\U0001d569",
- "xoplus;": "\u2a01",
- "xotime;": "\u2a02",
- "xrArr;": "\u27f9",
- "xrarr;": "\u27f6",
- "xscr;": "\U0001d4cd",
- "xsqcup;": "\u2a06",
- "xuplus;": "\u2a04",
- "xutri;": "\u25b3",
- "xvee;": "\u22c1",
- "xwedge;": "\u22c0",
- "yacute": "\xfd",
- "yacute;": "\xfd",
- "yacy;": "\u044f",
- "ycirc;": "\u0177",
- "ycy;": "\u044b",
- "yen": "\xa5",
- "yen;": "\xa5",
- "yfr;": "\U0001d536",
- "yicy;": "\u0457",
- "yopf;": "\U0001d56a",
- "yscr;": "\U0001d4ce",
- "yucy;": "\u044e",
- "yuml": "\xff",
- "yuml;": "\xff",
- "zacute;": "\u017a",
- "zcaron;": "\u017e",
- "zcy;": "\u0437",
- "zdot;": "\u017c",
- "zeetrf;": "\u2128",
- "zeta;": "\u03b6",
- "zfr;": "\U0001d537",
- "zhcy;": "\u0436",
- "zigrarr;": "\u21dd",
- "zopf;": "\U0001d56b",
- "zscr;": "\U0001d4cf",
- "zwj;": "\u200d",
- "zwnj;": "\u200c",
-}
-
-replacementCharacters = {
- 0x0: "\uFFFD",
- 0x0d: "\u000D",
- 0x80: "\u20AC",
- 0x81: "\u0081",
- 0x82: "\u201A",
- 0x83: "\u0192",
- 0x84: "\u201E",
- 0x85: "\u2026",
- 0x86: "\u2020",
- 0x87: "\u2021",
- 0x88: "\u02C6",
- 0x89: "\u2030",
- 0x8A: "\u0160",
- 0x8B: "\u2039",
- 0x8C: "\u0152",
- 0x8D: "\u008D",
- 0x8E: "\u017D",
- 0x8F: "\u008F",
- 0x90: "\u0090",
- 0x91: "\u2018",
- 0x92: "\u2019",
- 0x93: "\u201C",
- 0x94: "\u201D",
- 0x95: "\u2022",
- 0x96: "\u2013",
- 0x97: "\u2014",
- 0x98: "\u02DC",
- 0x99: "\u2122",
- 0x9A: "\u0161",
- 0x9B: "\u203A",
- 0x9C: "\u0153",
- 0x9D: "\u009D",
- 0x9E: "\u017E",
- 0x9F: "\u0178",
-}
-
-tokenTypes = {
- "Doctype": 0,
- "Characters": 1,
- "SpaceCharacters": 2,
- "StartTag": 3,
- "EndTag": 4,
- "EmptyTag": 5,
- "Comment": 6,
- "ParseError": 7
-}
-
-tagTokenTypes = frozenset([tokenTypes["StartTag"], tokenTypes["EndTag"],
- tokenTypes["EmptyTag"]])
-
-
-prefixes = {v: k for k, v in namespaces.items()}
-prefixes["http://www.w3.org/1998/Math/MathML"] = "math"
-
-
-class DataLossWarning(UserWarning):
- """Raised when the current tree is unable to represent the input data"""
- pass
-
-
-class _ReparseException(Exception):
- pass
diff --git a/src/pip/_vendor/html5lib/filters/alphabeticalattributes.py b/src/pip/_vendor/html5lib/filters/alphabeticalattributes.py
deleted file mode 100644
index 5ba926e3b..000000000
--- a/src/pip/_vendor/html5lib/filters/alphabeticalattributes.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from . import base
-
-from collections import OrderedDict
-
-
-def _attr_key(attr):
- """Return an appropriate key for an attribute for sorting
-
- Attributes have a namespace that can be either ``None`` or a string. We
- can't compare the two because they're different types, so we convert
- ``None`` to an empty string first.
-
- """
- return (attr[0][0] or ''), attr[0][1]
-
-
-class Filter(base.Filter):
- """Alphabetizes attributes for elements"""
- def __iter__(self):
- for token in base.Filter.__iter__(self):
- if token["type"] in ("StartTag", "EmptyTag"):
- attrs = OrderedDict()
- for name, value in sorted(token["data"].items(),
- key=_attr_key):
- attrs[name] = value
- token["data"] = attrs
- yield token
diff --git a/src/pip/_vendor/html5lib/filters/base.py b/src/pip/_vendor/html5lib/filters/base.py
deleted file mode 100644
index c7dbaed0f..000000000
--- a/src/pip/_vendor/html5lib/filters/base.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-
-class Filter(object):
- def __init__(self, source):
- self.source = source
-
- def __iter__(self):
- return iter(self.source)
-
- def __getattr__(self, name):
- return getattr(self.source, name)
diff --git a/src/pip/_vendor/html5lib/filters/inject_meta_charset.py b/src/pip/_vendor/html5lib/filters/inject_meta_charset.py
deleted file mode 100644
index aefb5c842..000000000
--- a/src/pip/_vendor/html5lib/filters/inject_meta_charset.py
+++ /dev/null
@@ -1,73 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from . import base
-
-
-class Filter(base.Filter):
- """Injects ``<meta charset=ENCODING>`` tag into head of document"""
- def __init__(self, source, encoding):
- """Creates a Filter
-
- :arg source: the source token stream
-
- :arg encoding: the encoding to set
-
- """
- base.Filter.__init__(self, source)
- self.encoding = encoding
-
- def __iter__(self):
- state = "pre_head"
- meta_found = (self.encoding is None)
- pending = []
-
- for token in base.Filter.__iter__(self):
- type = token["type"]
- if type == "StartTag":
- if token["name"].lower() == "head":
- state = "in_head"
-
- elif type == "EmptyTag":
- if token["name"].lower() == "meta":
- # replace charset with actual encoding
- has_http_equiv_content_type = False
- for (namespace, name), value in token["data"].items():
- if namespace is not None:
- continue
- elif name.lower() == 'charset':
- token["data"][(namespace, name)] = self.encoding
- meta_found = True
- break
- elif name == 'http-equiv' and value.lower() == 'content-type':
- has_http_equiv_content_type = True
- else:
- if has_http_equiv_content_type and (None, "content") in token["data"]:
- token["data"][(None, "content")] = 'text/html; charset=%s' % self.encoding
- meta_found = True
-
- elif token["name"].lower() == "head" and not meta_found:
- # insert meta into empty head
- yield {"type": "StartTag", "name": "head",
- "data": token["data"]}
- yield {"type": "EmptyTag", "name": "meta",
- "data": {(None, "charset"): self.encoding}}
- yield {"type": "EndTag", "name": "head"}
- meta_found = True
- continue
-
- elif type == "EndTag":
- if token["name"].lower() == "head" and pending:
- # insert meta into head (if necessary) and flush pending queue
- yield pending.pop(0)
- if not meta_found:
- yield {"type": "EmptyTag", "name": "meta",
- "data": {(None, "charset"): self.encoding}}
- while pending:
- yield pending.pop(0)
- meta_found = True
- state = "post_head"
-
- if state == "in_head":
- pending.append(token)
- else:
- yield token
diff --git a/src/pip/_vendor/html5lib/filters/lint.py b/src/pip/_vendor/html5lib/filters/lint.py
deleted file mode 100644
index fcc07eec5..000000000
--- a/src/pip/_vendor/html5lib/filters/lint.py
+++ /dev/null
@@ -1,93 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from pip._vendor.six import text_type
-
-from . import base
-from ..constants import namespaces, voidElements
-
-from ..constants import spaceCharacters
-spaceCharacters = "".join(spaceCharacters)
-
-
-class Filter(base.Filter):
- """Lints the token stream for errors
-
- If it finds any errors, it'll raise an ``AssertionError``.
-
- """
- def __init__(self, source, require_matching_tags=True):
- """Creates a Filter
-
- :arg source: the source token stream
-
- :arg require_matching_tags: whether or not to require matching tags
-
- """
- super(Filter, self).__init__(source)
- self.require_matching_tags = require_matching_tags
-
- def __iter__(self):
- open_elements = []
- for token in base.Filter.__iter__(self):
- type = token["type"]
- if type in ("StartTag", "EmptyTag"):
- namespace = token["namespace"]
- name = token["name"]
- assert namespace is None or isinstance(namespace, text_type)
- assert namespace != ""
- assert isinstance(name, text_type)
- assert name != ""
- assert isinstance(token["data"], dict)
- if (not namespace or namespace == namespaces["html"]) and name in voidElements:
- assert type == "EmptyTag"
- else:
- assert type == "StartTag"
- if type == "StartTag" and self.require_matching_tags:
- open_elements.append((namespace, name))
- for (namespace, name), value in token["data"].items():
- assert namespace is None or isinstance(namespace, text_type)
- assert namespace != ""
- assert isinstance(name, text_type)
- assert name != ""
- assert isinstance(value, text_type)
-
- elif type == "EndTag":
- namespace = token["namespace"]
- name = token["name"]
- assert namespace is None or isinstance(namespace, text_type)
- assert namespace != ""
- assert isinstance(name, text_type)
- assert name != ""
- if (not namespace or namespace == namespaces["html"]) and name in voidElements:
- assert False, "Void element reported as EndTag token: %(tag)s" % {"tag": name}
- elif self.require_matching_tags:
- start = open_elements.pop()
- assert start == (namespace, name)
-
- elif type == "Comment":
- data = token["data"]
- assert isinstance(data, text_type)
-
- elif type in ("Characters", "SpaceCharacters"):
- data = token["data"]
- assert isinstance(data, text_type)
- assert data != ""
- if type == "SpaceCharacters":
- assert data.strip(spaceCharacters) == ""
-
- elif type == "Doctype":
- name = token["name"]
- assert name is None or isinstance(name, text_type)
- assert token["publicId"] is None or isinstance(name, text_type)
- assert token["systemId"] is None or isinstance(name, text_type)
-
- elif type == "Entity":
- assert isinstance(token["name"], text_type)
-
- elif type == "SerializerError":
- assert isinstance(token["data"], text_type)
-
- else:
- assert False, "Unknown token type: %(type)s" % {"type": type}
-
- yield token
diff --git a/src/pip/_vendor/html5lib/filters/optionaltags.py b/src/pip/_vendor/html5lib/filters/optionaltags.py
deleted file mode 100644
index 4a865012c..000000000
--- a/src/pip/_vendor/html5lib/filters/optionaltags.py
+++ /dev/null
@@ -1,207 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from . import base
-
-
-class Filter(base.Filter):
- """Removes optional tags from the token stream"""
- def slider(self):
- previous1 = previous2 = None
- for token in self.source:
- if previous1 is not None:
- yield previous2, previous1, token
- previous2 = previous1
- previous1 = token
- if previous1 is not None:
- yield previous2, previous1, None
-
- def __iter__(self):
- for previous, token, next in self.slider():
- type = token["type"]
- if type == "StartTag":
- if (token["data"] or
- not self.is_optional_start(token["name"], previous, next)):
- yield token
- elif type == "EndTag":
- if not self.is_optional_end(token["name"], next):
- yield token
- else:
- yield token
-
- def is_optional_start(self, tagname, previous, next):
- type = next and next["type"] or None
- if tagname in 'html':
- # An html element's start tag may be omitted if the first thing
- # inside the html element is not a space character or a comment.
- return type not in ("Comment", "SpaceCharacters")
- elif tagname == 'head':
- # A head element's start tag may be omitted if the first thing
- # inside the head element is an element.
- # XXX: we also omit the start tag if the head element is empty
- if type in ("StartTag", "EmptyTag"):
- return True
- elif type == "EndTag":
- return next["name"] == "head"
- elif tagname == 'body':
- # A body element's start tag may be omitted if the first thing
- # inside the body element is not a space character or a comment,
- # except if the first thing inside the body element is a script
- # or style element and the node immediately preceding the body
- # element is a head element whose end tag has been omitted.
- if type in ("Comment", "SpaceCharacters"):
- return False
- elif type == "StartTag":
- # XXX: we do not look at the preceding event, so we never omit
- # the body element's start tag if it's followed by a script or
- # a style element.
- return next["name"] not in ('script', 'style')
- else:
- return True
- elif tagname == 'colgroup':
- # A colgroup element's start tag may be omitted if the first thing
- # inside the colgroup element is a col element, and if the element
- # is not immediately preceded by another colgroup element whose
- # end tag has been omitted.
- if type in ("StartTag", "EmptyTag"):
- # XXX: we do not look at the preceding event, so instead we never
- # omit the colgroup element's end tag when it is immediately
- # followed by another colgroup element. See is_optional_end.
- return next["name"] == "col"
- else:
- return False
- elif tagname == 'tbody':
- # A tbody element's start tag may be omitted if the first thing
- # inside the tbody element is a tr element, and if the element is
- # not immediately preceded by a tbody, thead, or tfoot element
- # whose end tag has been omitted.
- if type == "StartTag":
- # omit the thead and tfoot elements' end tag when they are
- # immediately followed by a tbody element. See is_optional_end.
- if previous and previous['type'] == 'EndTag' and \
- previous['name'] in ('tbody', 'thead', 'tfoot'):
- return False
- return next["name"] == 'tr'
- else:
- return False
- return False
-
- def is_optional_end(self, tagname, next):
- type = next and next["type"] or None
- if tagname in ('html', 'head', 'body'):
- # An html element's end tag may be omitted if the html element
- # is not immediately followed by a space character or a comment.
- return type not in ("Comment", "SpaceCharacters")
- elif tagname in ('li', 'optgroup', 'tr'):
- # A li element's end tag may be omitted if the li element is
- # immediately followed by another li element or if there is
- # no more content in the parent element.
- # An optgroup element's end tag may be omitted if the optgroup
- # element is immediately followed by another optgroup element,
- # or if there is no more content in the parent element.
- # A tr element's end tag may be omitted if the tr element is
- # immediately followed by another tr element, or if there is
- # no more content in the parent element.
- if type == "StartTag":
- return next["name"] == tagname
- else:
- return type == "EndTag" or type is None
- elif tagname in ('dt', 'dd'):
- # A dt element's end tag may be omitted if the dt element is
- # immediately followed by another dt element or a dd element.
- # A dd element's end tag may be omitted if the dd element is
- # immediately followed by another dd element or a dt element,
- # or if there is no more content in the parent element.
- if type == "StartTag":
- return next["name"] in ('dt', 'dd')
- elif tagname == 'dd':
- return type == "EndTag" or type is None
- else:
- return False
- elif tagname == 'p':
- # A p element's end tag may be omitted if the p element is
- # immediately followed by an address, article, aside,
- # blockquote, datagrid, dialog, dir, div, dl, fieldset,
- # footer, form, h1, h2, h3, h4, h5, h6, header, hr, menu,
- # nav, ol, p, pre, section, table, or ul, element, or if
- # there is no more content in the parent element.
- if type in ("StartTag", "EmptyTag"):
- return next["name"] in ('address', 'article', 'aside',
- 'blockquote', 'datagrid', 'dialog',
- 'dir', 'div', 'dl', 'fieldset', 'footer',
- 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
- 'header', 'hr', 'menu', 'nav', 'ol',
- 'p', 'pre', 'section', 'table', 'ul')
- else:
- return type == "EndTag" or type is None
- elif tagname == 'option':
- # An option element's end tag may be omitted if the option
- # element is immediately followed by another option element,
- # or if it is immediately followed by an <code>optgroup</code>
- # element, or if there is no more content in the parent
- # element.
- if type == "StartTag":
- return next["name"] in ('option', 'optgroup')
- else:
- return type == "EndTag" or type is None
- elif tagname in ('rt', 'rp'):
- # An rt element's end tag may be omitted if the rt element is
- # immediately followed by an rt or rp element, or if there is
- # no more content in the parent element.
- # An rp element's end tag may be omitted if the rp element is
- # immediately followed by an rt or rp element, or if there is
- # no more content in the parent element.
- if type == "StartTag":
- return next["name"] in ('rt', 'rp')
- else:
- return type == "EndTag" or type is None
- elif tagname == 'colgroup':
- # A colgroup element's end tag may be omitted if the colgroup
- # element is not immediately followed by a space character or
- # a comment.
- if type in ("Comment", "SpaceCharacters"):
- return False
- elif type == "StartTag":
- # XXX: we also look for an immediately following colgroup
- # element. See is_optional_start.
- return next["name"] != 'colgroup'
- else:
- return True
- elif tagname in ('thead', 'tbody'):
- # A thead element's end tag may be omitted if the thead element
- # is immediately followed by a tbody or tfoot element.
- # A tbody element's end tag may be omitted if the tbody element
- # is immediately followed by a tbody or tfoot element, or if
- # there is no more content in the parent element.
- # A tfoot element's end tag may be omitted if the tfoot element
- # is immediately followed by a tbody element, or if there is no
- # more content in the parent element.
- # XXX: we never omit the end tag when the following element is
- # a tbody. See is_optional_start.
- if type == "StartTag":
- return next["name"] in ['tbody', 'tfoot']
- elif tagname == 'tbody':
- return type == "EndTag" or type is None
- else:
- return False
- elif tagname == 'tfoot':
- # A tfoot element's end tag may be omitted if the tfoot element
- # is immediately followed by a tbody element, or if there is no
- # more content in the parent element.
- # XXX: we never omit the end tag when the following element is
- # a tbody. See is_optional_start.
- if type == "StartTag":
- return next["name"] == 'tbody'
- else:
- return type == "EndTag" or type is None
- elif tagname in ('td', 'th'):
- # A td element's end tag may be omitted if the td element is
- # immediately followed by a td or th element, or if there is
- # no more content in the parent element.
- # A th element's end tag may be omitted if the th element is
- # immediately followed by a td or th element, or if there is
- # no more content in the parent element.
- if type == "StartTag":
- return next["name"] in ('td', 'th')
- else:
- return type == "EndTag" or type is None
- return False
diff --git a/src/pip/_vendor/html5lib/filters/sanitizer.py b/src/pip/_vendor/html5lib/filters/sanitizer.py
deleted file mode 100644
index aa7431d13..000000000
--- a/src/pip/_vendor/html5lib/filters/sanitizer.py
+++ /dev/null
@@ -1,916 +0,0 @@
-"""Deprecated from html5lib 1.1.
-
-See `here <https://github.com/html5lib/html5lib-python/issues/443>`_ for
-information about its deprecation; `Bleach <https://github.com/mozilla/bleach>`_
-is recommended as a replacement. Please let us know in the aforementioned issue
-if Bleach is unsuitable for your needs.
-
-"""
-from __future__ import absolute_import, division, unicode_literals
-
-import re
-import warnings
-from xml.sax.saxutils import escape, unescape
-
-from pip._vendor.six.moves import urllib_parse as urlparse
-
-from . import base
-from ..constants import namespaces, prefixes
-
-__all__ = ["Filter"]
-
-
-_deprecation_msg = (
- "html5lib's sanitizer is deprecated; see " +
- "https://github.com/html5lib/html5lib-python/issues/443 and please let " +
- "us know if Bleach is unsuitable for your needs"
-)
-
-warnings.warn(_deprecation_msg, DeprecationWarning)
-
-allowed_elements = frozenset((
- (namespaces['html'], 'a'),
- (namespaces['html'], 'abbr'),
- (namespaces['html'], 'acronym'),
- (namespaces['html'], 'address'),
- (namespaces['html'], 'area'),
- (namespaces['html'], 'article'),
- (namespaces['html'], 'aside'),
- (namespaces['html'], 'audio'),
- (namespaces['html'], 'b'),
- (namespaces['html'], 'big'),
- (namespaces['html'], 'blockquote'),
- (namespaces['html'], 'br'),
- (namespaces['html'], 'button'),
- (namespaces['html'], 'canvas'),
- (namespaces['html'], 'caption'),
- (namespaces['html'], 'center'),
- (namespaces['html'], 'cite'),
- (namespaces['html'], 'code'),
- (namespaces['html'], 'col'),
- (namespaces['html'], 'colgroup'),
- (namespaces['html'], 'command'),
- (namespaces['html'], 'datagrid'),
- (namespaces['html'], 'datalist'),
- (namespaces['html'], 'dd'),
- (namespaces['html'], 'del'),
- (namespaces['html'], 'details'),
- (namespaces['html'], 'dfn'),
- (namespaces['html'], 'dialog'),
- (namespaces['html'], 'dir'),
- (namespaces['html'], 'div'),
- (namespaces['html'], 'dl'),
- (namespaces['html'], 'dt'),
- (namespaces['html'], 'em'),
- (namespaces['html'], 'event-source'),
- (namespaces['html'], 'fieldset'),
- (namespaces['html'], 'figcaption'),
- (namespaces['html'], 'figure'),
- (namespaces['html'], 'footer'),
- (namespaces['html'], 'font'),
- (namespaces['html'], 'form'),
- (namespaces['html'], 'header'),
- (namespaces['html'], 'h1'),
- (namespaces['html'], 'h2'),
- (namespaces['html'], 'h3'),
- (namespaces['html'], 'h4'),
- (namespaces['html'], 'h5'),
- (namespaces['html'], 'h6'),
- (namespaces['html'], 'hr'),
- (namespaces['html'], 'i'),
- (namespaces['html'], 'img'),
- (namespaces['html'], 'input'),
- (namespaces['html'], 'ins'),
- (namespaces['html'], 'keygen'),
- (namespaces['html'], 'kbd'),
- (namespaces['html'], 'label'),
- (namespaces['html'], 'legend'),
- (namespaces['html'], 'li'),
- (namespaces['html'], 'm'),
- (namespaces['html'], 'map'),
- (namespaces['html'], 'menu'),
- (namespaces['html'], 'meter'),
- (namespaces['html'], 'multicol'),
- (namespaces['html'], 'nav'),
- (namespaces['html'], 'nextid'),
- (namespaces['html'], 'ol'),
- (namespaces['html'], 'output'),
- (namespaces['html'], 'optgroup'),
- (namespaces['html'], 'option'),
- (namespaces['html'], 'p'),
- (namespaces['html'], 'pre'),
- (namespaces['html'], 'progress'),
- (namespaces['html'], 'q'),
- (namespaces['html'], 's'),
- (namespaces['html'], 'samp'),
- (namespaces['html'], 'section'),
- (namespaces['html'], 'select'),
- (namespaces['html'], 'small'),
- (namespaces['html'], 'sound'),
- (namespaces['html'], 'source'),
- (namespaces['html'], 'spacer'),
- (namespaces['html'], 'span'),
- (namespaces['html'], 'strike'),
- (namespaces['html'], 'strong'),
- (namespaces['html'], 'sub'),
- (namespaces['html'], 'sup'),
- (namespaces['html'], 'table'),
- (namespaces['html'], 'tbody'),
- (namespaces['html'], 'td'),
- (namespaces['html'], 'textarea'),
- (namespaces['html'], 'time'),
- (namespaces['html'], 'tfoot'),
- (namespaces['html'], 'th'),
- (namespaces['html'], 'thead'),
- (namespaces['html'], 'tr'),
- (namespaces['html'], 'tt'),
- (namespaces['html'], 'u'),
- (namespaces['html'], 'ul'),
- (namespaces['html'], 'var'),
- (namespaces['html'], 'video'),
- (namespaces['mathml'], 'maction'),
- (namespaces['mathml'], 'math'),
- (namespaces['mathml'], 'merror'),
- (namespaces['mathml'], 'mfrac'),
- (namespaces['mathml'], 'mi'),
- (namespaces['mathml'], 'mmultiscripts'),
- (namespaces['mathml'], 'mn'),
- (namespaces['mathml'], 'mo'),
- (namespaces['mathml'], 'mover'),
- (namespaces['mathml'], 'mpadded'),
- (namespaces['mathml'], 'mphantom'),
- (namespaces['mathml'], 'mprescripts'),
- (namespaces['mathml'], 'mroot'),
- (namespaces['mathml'], 'mrow'),
- (namespaces['mathml'], 'mspace'),
- (namespaces['mathml'], 'msqrt'),
- (namespaces['mathml'], 'mstyle'),
- (namespaces['mathml'], 'msub'),
- (namespaces['mathml'], 'msubsup'),
- (namespaces['mathml'], 'msup'),
- (namespaces['mathml'], 'mtable'),
- (namespaces['mathml'], 'mtd'),
- (namespaces['mathml'], 'mtext'),
- (namespaces['mathml'], 'mtr'),
- (namespaces['mathml'], 'munder'),
- (namespaces['mathml'], 'munderover'),
- (namespaces['mathml'], 'none'),
- (namespaces['svg'], 'a'),
- (namespaces['svg'], 'animate'),
- (namespaces['svg'], 'animateColor'),
- (namespaces['svg'], 'animateMotion'),
- (namespaces['svg'], 'animateTransform'),
- (namespaces['svg'], 'clipPath'),
- (namespaces['svg'], 'circle'),
- (namespaces['svg'], 'defs'),
- (namespaces['svg'], 'desc'),
- (namespaces['svg'], 'ellipse'),
- (namespaces['svg'], 'font-face'),
- (namespaces['svg'], 'font-face-name'),
- (namespaces['svg'], 'font-face-src'),
- (namespaces['svg'], 'g'),
- (namespaces['svg'], 'glyph'),
- (namespaces['svg'], 'hkern'),
- (namespaces['svg'], 'linearGradient'),
- (namespaces['svg'], 'line'),
- (namespaces['svg'], 'marker'),
- (namespaces['svg'], 'metadata'),
- (namespaces['svg'], 'missing-glyph'),
- (namespaces['svg'], 'mpath'),
- (namespaces['svg'], 'path'),
- (namespaces['svg'], 'polygon'),
- (namespaces['svg'], 'polyline'),
- (namespaces['svg'], 'radialGradient'),
- (namespaces['svg'], 'rect'),
- (namespaces['svg'], 'set'),
- (namespaces['svg'], 'stop'),
- (namespaces['svg'], 'svg'),
- (namespaces['svg'], 'switch'),
- (namespaces['svg'], 'text'),
- (namespaces['svg'], 'title'),
- (namespaces['svg'], 'tspan'),
- (namespaces['svg'], 'use'),
-))
-
-allowed_attributes = frozenset((
- # HTML attributes
- (None, 'abbr'),
- (None, 'accept'),
- (None, 'accept-charset'),
- (None, 'accesskey'),
- (None, 'action'),
- (None, 'align'),
- (None, 'alt'),
- (None, 'autocomplete'),
- (None, 'autofocus'),
- (None, 'axis'),
- (None, 'background'),
- (None, 'balance'),
- (None, 'bgcolor'),
- (None, 'bgproperties'),
- (None, 'border'),
- (None, 'bordercolor'),
- (None, 'bordercolordark'),
- (None, 'bordercolorlight'),
- (None, 'bottompadding'),
- (None, 'cellpadding'),
- (None, 'cellspacing'),
- (None, 'ch'),
- (None, 'challenge'),
- (None, 'char'),
- (None, 'charoff'),
- (None, 'choff'),
- (None, 'charset'),
- (None, 'checked'),
- (None, 'cite'),
- (None, 'class'),
- (None, 'clear'),
- (None, 'color'),
- (None, 'cols'),
- (None, 'colspan'),
- (None, 'compact'),
- (None, 'contenteditable'),
- (None, 'controls'),
- (None, 'coords'),
- (None, 'data'),
- (None, 'datafld'),
- (None, 'datapagesize'),
- (None, 'datasrc'),
- (None, 'datetime'),
- (None, 'default'),
- (None, 'delay'),
- (None, 'dir'),
- (None, 'disabled'),
- (None, 'draggable'),
- (None, 'dynsrc'),
- (None, 'enctype'),
- (None, 'end'),
- (None, 'face'),
- (None, 'for'),
- (None, 'form'),
- (None, 'frame'),
- (None, 'galleryimg'),
- (None, 'gutter'),
- (None, 'headers'),
- (None, 'height'),
- (None, 'hidefocus'),
- (None, 'hidden'),
- (None, 'high'),
- (None, 'href'),
- (None, 'hreflang'),
- (None, 'hspace'),
- (None, 'icon'),
- (None, 'id'),
- (None, 'inputmode'),
- (None, 'ismap'),
- (None, 'keytype'),
- (None, 'label'),
- (None, 'leftspacing'),
- (None, 'lang'),
- (None, 'list'),
- (None, 'longdesc'),
- (None, 'loop'),
- (None, 'loopcount'),
- (None, 'loopend'),
- (None, 'loopstart'),
- (None, 'low'),
- (None, 'lowsrc'),
- (None, 'max'),
- (None, 'maxlength'),
- (None, 'media'),
- (None, 'method'),
- (None, 'min'),
- (None, 'multiple'),
- (None, 'name'),
- (None, 'nohref'),
- (None, 'noshade'),
- (None, 'nowrap'),
- (None, 'open'),
- (None, 'optimum'),
- (None, 'pattern'),
- (None, 'ping'),
- (None, 'point-size'),
- (None, 'poster'),
- (None, 'pqg'),
- (None, 'preload'),
- (None, 'prompt'),
- (None, 'radiogroup'),
- (None, 'readonly'),
- (None, 'rel'),
- (None, 'repeat-max'),
- (None, 'repeat-min'),
- (None, 'replace'),
- (None, 'required'),
- (None, 'rev'),
- (None, 'rightspacing'),
- (None, 'rows'),
- (None, 'rowspan'),
- (None, 'rules'),
- (None, 'scope'),
- (None, 'selected'),
- (None, 'shape'),
- (None, 'size'),
- (None, 'span'),
- (None, 'src'),
- (None, 'start'),
- (None, 'step'),
- (None, 'style'),
- (None, 'summary'),
- (None, 'suppress'),
- (None, 'tabindex'),
- (None, 'target'),
- (None, 'template'),
- (None, 'title'),
- (None, 'toppadding'),
- (None, 'type'),
- (None, 'unselectable'),
- (None, 'usemap'),
- (None, 'urn'),
- (None, 'valign'),
- (None, 'value'),
- (None, 'variable'),
- (None, 'volume'),
- (None, 'vspace'),
- (None, 'vrml'),
- (None, 'width'),
- (None, 'wrap'),
- (namespaces['xml'], 'lang'),
- # MathML attributes
- (None, 'actiontype'),
- (None, 'align'),
- (None, 'columnalign'),
- (None, 'columnalign'),
- (None, 'columnalign'),
- (None, 'columnlines'),
- (None, 'columnspacing'),
- (None, 'columnspan'),
- (None, 'depth'),
- (None, 'display'),
- (None, 'displaystyle'),
- (None, 'equalcolumns'),
- (None, 'equalrows'),
- (None, 'fence'),
- (None, 'fontstyle'),
- (None, 'fontweight'),
- (None, 'frame'),
- (None, 'height'),
- (None, 'linethickness'),
- (None, 'lspace'),
- (None, 'mathbackground'),
- (None, 'mathcolor'),
- (None, 'mathvariant'),
- (None, 'mathvariant'),
- (None, 'maxsize'),
- (None, 'minsize'),
- (None, 'other'),
- (None, 'rowalign'),
- (None, 'rowalign'),
- (None, 'rowalign'),
- (None, 'rowlines'),
- (None, 'rowspacing'),
- (None, 'rowspan'),
- (None, 'rspace'),
- (None, 'scriptlevel'),
- (None, 'selection'),
- (None, 'separator'),
- (None, 'stretchy'),
- (None, 'width'),
- (None, 'width'),
- (namespaces['xlink'], 'href'),
- (namespaces['xlink'], 'show'),
- (namespaces['xlink'], 'type'),
- # SVG attributes
- (None, 'accent-height'),
- (None, 'accumulate'),
- (None, 'additive'),
- (None, 'alphabetic'),
- (None, 'arabic-form'),
- (None, 'ascent'),
- (None, 'attributeName'),
- (None, 'attributeType'),
- (None, 'baseProfile'),
- (None, 'bbox'),
- (None, 'begin'),
- (None, 'by'),
- (None, 'calcMode'),
- (None, 'cap-height'),
- (None, 'class'),
- (None, 'clip-path'),
- (None, 'color'),
- (None, 'color-rendering'),
- (None, 'content'),
- (None, 'cx'),
- (None, 'cy'),
- (None, 'd'),
- (None, 'dx'),
- (None, 'dy'),
- (None, 'descent'),
- (None, 'display'),
- (None, 'dur'),
- (None, 'end'),
- (None, 'fill'),
- (None, 'fill-opacity'),
- (None, 'fill-rule'),
- (None, 'font-family'),
- (None, 'font-size'),
- (None, 'font-stretch'),
- (None, 'font-style'),
- (None, 'font-variant'),
- (None, 'font-weight'),
- (None, 'from'),
- (None, 'fx'),
- (None, 'fy'),
- (None, 'g1'),
- (None, 'g2'),
- (None, 'glyph-name'),
- (None, 'gradientUnits'),
- (None, 'hanging'),
- (None, 'height'),
- (None, 'horiz-adv-x'),
- (None, 'horiz-origin-x'),
- (None, 'id'),
- (None, 'ideographic'),
- (None, 'k'),
- (None, 'keyPoints'),
- (None, 'keySplines'),
- (None, 'keyTimes'),
- (None, 'lang'),
- (None, 'marker-end'),
- (None, 'marker-mid'),
- (None, 'marker-start'),
- (None, 'markerHeight'),
- (None, 'markerUnits'),
- (None, 'markerWidth'),
- (None, 'mathematical'),
- (None, 'max'),
- (None, 'min'),
- (None, 'name'),
- (None, 'offset'),
- (None, 'opacity'),
- (None, 'orient'),
- (None, 'origin'),
- (None, 'overline-position'),
- (None, 'overline-thickness'),
- (None, 'panose-1'),
- (None, 'path'),
- (None, 'pathLength'),
- (None, 'points'),
- (None, 'preserveAspectRatio'),
- (None, 'r'),
- (None, 'refX'),
- (None, 'refY'),
- (None, 'repeatCount'),
- (None, 'repeatDur'),
- (None, 'requiredExtensions'),
- (None, 'requiredFeatures'),
- (None, 'restart'),
- (None, 'rotate'),
- (None, 'rx'),
- (None, 'ry'),
- (None, 'slope'),
- (None, 'stemh'),
- (None, 'stemv'),
- (None, 'stop-color'),
- (None, 'stop-opacity'),
- (None, 'strikethrough-position'),
- (None, 'strikethrough-thickness'),
- (None, 'stroke'),
- (None, 'stroke-dasharray'),
- (None, 'stroke-dashoffset'),
- (None, 'stroke-linecap'),
- (None, 'stroke-linejoin'),
- (None, 'stroke-miterlimit'),
- (None, 'stroke-opacity'),
- (None, 'stroke-width'),
- (None, 'systemLanguage'),
- (None, 'target'),
- (None, 'text-anchor'),
- (None, 'to'),
- (None, 'transform'),
- (None, 'type'),
- (None, 'u1'),
- (None, 'u2'),
- (None, 'underline-position'),
- (None, 'underline-thickness'),
- (None, 'unicode'),
- (None, 'unicode-range'),
- (None, 'units-per-em'),
- (None, 'values'),
- (None, 'version'),
- (None, 'viewBox'),
- (None, 'visibility'),
- (None, 'width'),
- (None, 'widths'),
- (None, 'x'),
- (None, 'x-height'),
- (None, 'x1'),
- (None, 'x2'),
- (namespaces['xlink'], 'actuate'),
- (namespaces['xlink'], 'arcrole'),
- (namespaces['xlink'], 'href'),
- (namespaces['xlink'], 'role'),
- (namespaces['xlink'], 'show'),
- (namespaces['xlink'], 'title'),
- (namespaces['xlink'], 'type'),
- (namespaces['xml'], 'base'),
- (namespaces['xml'], 'lang'),
- (namespaces['xml'], 'space'),
- (None, 'y'),
- (None, 'y1'),
- (None, 'y2'),
- (None, 'zoomAndPan'),
-))
-
-attr_val_is_uri = frozenset((
- (None, 'href'),
- (None, 'src'),
- (None, 'cite'),
- (None, 'action'),
- (None, 'longdesc'),
- (None, 'poster'),
- (None, 'background'),
- (None, 'datasrc'),
- (None, 'dynsrc'),
- (None, 'lowsrc'),
- (None, 'ping'),
- (namespaces['xlink'], 'href'),
- (namespaces['xml'], 'base'),
-))
-
-svg_attr_val_allows_ref = frozenset((
- (None, 'clip-path'),
- (None, 'color-profile'),
- (None, 'cursor'),
- (None, 'fill'),
- (None, 'filter'),
- (None, 'marker'),
- (None, 'marker-start'),
- (None, 'marker-mid'),
- (None, 'marker-end'),
- (None, 'mask'),
- (None, 'stroke'),
-))
-
-svg_allow_local_href = frozenset((
- (None, 'altGlyph'),
- (None, 'animate'),
- (None, 'animateColor'),
- (None, 'animateMotion'),
- (None, 'animateTransform'),
- (None, 'cursor'),
- (None, 'feImage'),
- (None, 'filter'),
- (None, 'linearGradient'),
- (None, 'pattern'),
- (None, 'radialGradient'),
- (None, 'textpath'),
- (None, 'tref'),
- (None, 'set'),
- (None, 'use')
-))
-
-allowed_css_properties = frozenset((
- 'azimuth',
- 'background-color',
- 'border-bottom-color',
- 'border-collapse',
- 'border-color',
- 'border-left-color',
- 'border-right-color',
- 'border-top-color',
- 'clear',
- 'color',
- 'cursor',
- 'direction',
- 'display',
- 'elevation',
- 'float',
- 'font',
- 'font-family',
- 'font-size',
- 'font-style',
- 'font-variant',
- 'font-weight',
- 'height',
- 'letter-spacing',
- 'line-height',
- 'overflow',
- 'pause',
- 'pause-after',
- 'pause-before',
- 'pitch',
- 'pitch-range',
- 'richness',
- 'speak',
- 'speak-header',
- 'speak-numeral',
- 'speak-punctuation',
- 'speech-rate',
- 'stress',
- 'text-align',
- 'text-decoration',
- 'text-indent',
- 'unicode-bidi',
- 'vertical-align',
- 'voice-family',
- 'volume',
- 'white-space',
- 'width',
-))
-
-allowed_css_keywords = frozenset((
- 'auto',
- 'aqua',
- 'black',
- 'block',
- 'blue',
- 'bold',
- 'both',
- 'bottom',
- 'brown',
- 'center',
- 'collapse',
- 'dashed',
- 'dotted',
- 'fuchsia',
- 'gray',
- 'green',
- '!important',
- 'italic',
- 'left',
- 'lime',
- 'maroon',
- 'medium',
- 'none',
- 'navy',
- 'normal',
- 'nowrap',
- 'olive',
- 'pointer',
- 'purple',
- 'red',
- 'right',
- 'solid',
- 'silver',
- 'teal',
- 'top',
- 'transparent',
- 'underline',
- 'white',
- 'yellow',
-))
-
-allowed_svg_properties = frozenset((
- 'fill',
- 'fill-opacity',
- 'fill-rule',
- 'stroke',
- 'stroke-width',
- 'stroke-linecap',
- 'stroke-linejoin',
- 'stroke-opacity',
-))
-
-allowed_protocols = frozenset((
- 'ed2k',
- 'ftp',
- 'http',
- 'https',
- 'irc',
- 'mailto',
- 'news',
- 'gopher',
- 'nntp',
- 'telnet',
- 'webcal',
- 'xmpp',
- 'callto',
- 'feed',
- 'urn',
- 'aim',
- 'rsync',
- 'tag',
- 'ssh',
- 'sftp',
- 'rtsp',
- 'afs',
- 'data',
-))
-
-allowed_content_types = frozenset((
- 'image/png',
- 'image/jpeg',
- 'image/gif',
- 'image/webp',
- 'image/bmp',
- 'text/plain',
-))
-
-
-data_content_type = re.compile(r'''
- ^
- # Match a content type <application>/<type>
- (?P<content_type>[-a-zA-Z0-9.]+/[-a-zA-Z0-9.]+)
- # Match any character set and encoding
- (?:(?:;charset=(?:[-a-zA-Z0-9]+)(?:;(?:base64))?)
- |(?:;(?:base64))?(?:;charset=(?:[-a-zA-Z0-9]+))?)
- # Assume the rest is data
- ,.*
- $
- ''',
- re.VERBOSE)
-
-
-class Filter(base.Filter):
- """Sanitizes token stream of XHTML+MathML+SVG and of inline style attributes"""
- def __init__(self,
- source,
- allowed_elements=allowed_elements,
- allowed_attributes=allowed_attributes,
- allowed_css_properties=allowed_css_properties,
- allowed_css_keywords=allowed_css_keywords,
- allowed_svg_properties=allowed_svg_properties,
- allowed_protocols=allowed_protocols,
- allowed_content_types=allowed_content_types,
- attr_val_is_uri=attr_val_is_uri,
- svg_attr_val_allows_ref=svg_attr_val_allows_ref,
- svg_allow_local_href=svg_allow_local_href):
- """Creates a Filter
-
- :arg allowed_elements: set of elements to allow--everything else will
- be escaped
-
- :arg allowed_attributes: set of attributes to allow in
- elements--everything else will be stripped
-
- :arg allowed_css_properties: set of CSS properties to allow--everything
- else will be stripped
-
- :arg allowed_css_keywords: set of CSS keywords to allow--everything
- else will be stripped
-
- :arg allowed_svg_properties: set of SVG properties to allow--everything
- else will be removed
-
- :arg allowed_protocols: set of allowed protocols for URIs
-
- :arg allowed_content_types: set of allowed content types for ``data`` URIs.
-
- :arg attr_val_is_uri: set of attributes that have URI values--values
- that have a scheme not listed in ``allowed_protocols`` are removed
-
- :arg svg_attr_val_allows_ref: set of SVG attributes that can have
- references
-
- :arg svg_allow_local_href: set of SVG elements that can have local
- hrefs--these are removed
-
- """
- super(Filter, self).__init__(source)
-
- warnings.warn(_deprecation_msg, DeprecationWarning)
-
- self.allowed_elements = allowed_elements
- self.allowed_attributes = allowed_attributes
- self.allowed_css_properties = allowed_css_properties
- self.allowed_css_keywords = allowed_css_keywords
- self.allowed_svg_properties = allowed_svg_properties
- self.allowed_protocols = allowed_protocols
- self.allowed_content_types = allowed_content_types
- self.attr_val_is_uri = attr_val_is_uri
- self.svg_attr_val_allows_ref = svg_attr_val_allows_ref
- self.svg_allow_local_href = svg_allow_local_href
-
- def __iter__(self):
- for token in base.Filter.__iter__(self):
- token = self.sanitize_token(token)
- if token:
- yield token
-
- # Sanitize the +html+, escaping all elements not in ALLOWED_ELEMENTS, and
- # stripping out all attributes not in ALLOWED_ATTRIBUTES. Style attributes
- # are parsed, and a restricted set, specified by ALLOWED_CSS_PROPERTIES and
- # ALLOWED_CSS_KEYWORDS, are allowed through. attributes in ATTR_VAL_IS_URI
- # are scanned, and only URI schemes specified in ALLOWED_PROTOCOLS are
- # allowed.
- #
- # sanitize_html('<script> do_nasty_stuff() </script>')
- # => &lt;script> do_nasty_stuff() &lt;/script>
- # sanitize_html('<a href="javascript: sucker();">Click here for $100</a>')
- # => <a>Click here for $100</a>
- def sanitize_token(self, token):
-
- # accommodate filters which use token_type differently
- token_type = token["type"]
- if token_type in ("StartTag", "EndTag", "EmptyTag"):
- name = token["name"]
- namespace = token["namespace"]
- if ((namespace, name) in self.allowed_elements or
- (namespace is None and
- (namespaces["html"], name) in self.allowed_elements)):
- return self.allowed_token(token)
- else:
- return self.disallowed_token(token)
- elif token_type == "Comment":
- pass
- else:
- return token
-
- def allowed_token(self, token):
- if "data" in token:
- attrs = token["data"]
- attr_names = set(attrs.keys())
-
- # Remove forbidden attributes
- for to_remove in (attr_names - self.allowed_attributes):
- del token["data"][to_remove]
- attr_names.remove(to_remove)
-
- # Remove attributes with disallowed URL values
- for attr in (attr_names & self.attr_val_is_uri):
- assert attr in attrs
- # I don't have a clue where this regexp comes from or why it matches those
- # characters, nor why we call unescape. I just know it's always been here.
- # Should you be worried by this comment in a sanitizer? Yes. On the other hand, all
- # this will do is remove *more* than it otherwise would.
- val_unescaped = re.sub("[`\x00-\x20\x7f-\xa0\\s]+", '',
- unescape(attrs[attr])).lower()
- # remove replacement characters from unescaped characters
- val_unescaped = val_unescaped.replace("\ufffd", "")
- try:
- uri = urlparse.urlparse(val_unescaped)
- except ValueError:
- uri = None
- del attrs[attr]
- if uri and uri.scheme:
- if uri.scheme not in self.allowed_protocols:
- del attrs[attr]
- if uri.scheme == 'data':
- m = data_content_type.match(uri.path)
- if not m:
- del attrs[attr]
- elif m.group('content_type') not in self.allowed_content_types:
- del attrs[attr]
-
- for attr in self.svg_attr_val_allows_ref:
- if attr in attrs:
- attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)',
- ' ',
- unescape(attrs[attr]))
- if (token["name"] in self.svg_allow_local_href and
- (namespaces['xlink'], 'href') in attrs and re.search(r'^\s*[^#\s].*',
- attrs[(namespaces['xlink'], 'href')])):
- del attrs[(namespaces['xlink'], 'href')]
- if (None, 'style') in attrs:
- attrs[(None, 'style')] = self.sanitize_css(attrs[(None, 'style')])
- token["data"] = attrs
- return token
-
- def disallowed_token(self, token):
- token_type = token["type"]
- if token_type == "EndTag":
- token["data"] = "</%s>" % token["name"]
- elif token["data"]:
- assert token_type in ("StartTag", "EmptyTag")
- attrs = []
- for (ns, name), v in token["data"].items():
- attrs.append(' %s="%s"' % (name if ns is None else "%s:%s" % (prefixes[ns], name), escape(v)))
- token["data"] = "<%s%s>" % (token["name"], ''.join(attrs))
- else:
- token["data"] = "<%s>" % token["name"]
- if token.get("selfClosing"):
- token["data"] = token["data"][:-1] + "/>"
-
- token["type"] = "Characters"
-
- del token["name"]
- return token
-
- def sanitize_css(self, style):
- # disallow urls
- style = re.compile(r'url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ', style)
-
- # gauntlet
- if not re.match(r"""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style):
- return ''
- if not re.match(r"^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$", style):
- return ''
-
- clean = []
- for prop, value in re.findall(r"([-\w]+)\s*:\s*([^:;]*)", style):
- if not value:
- continue
- if prop.lower() in self.allowed_css_properties:
- clean.append(prop + ': ' + value + ';')
- elif prop.split('-')[0].lower() in ['background', 'border', 'margin',
- 'padding']:
- for keyword in value.split():
- if keyword not in self.allowed_css_keywords and \
- not re.match(r"^(#[0-9a-fA-F]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$", keyword): # noqa
- break
- else:
- clean.append(prop + ': ' + value + ';')
- elif prop.lower() in self.allowed_svg_properties:
- clean.append(prop + ': ' + value + ';')
-
- return ' '.join(clean)
diff --git a/src/pip/_vendor/html5lib/filters/whitespace.py b/src/pip/_vendor/html5lib/filters/whitespace.py
deleted file mode 100644
index 0d12584b4..000000000
--- a/src/pip/_vendor/html5lib/filters/whitespace.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-import re
-
-from . import base
-from ..constants import rcdataElements, spaceCharacters
-spaceCharacters = "".join(spaceCharacters)
-
-SPACES_REGEX = re.compile("[%s]+" % spaceCharacters)
-
-
-class Filter(base.Filter):
- """Collapses whitespace except in pre, textarea, and script elements"""
- spacePreserveElements = frozenset(["pre", "textarea"] + list(rcdataElements))
-
- def __iter__(self):
- preserve = 0
- for token in base.Filter.__iter__(self):
- type = token["type"]
- if type == "StartTag" \
- and (preserve or token["name"] in self.spacePreserveElements):
- preserve += 1
-
- elif type == "EndTag" and preserve:
- preserve -= 1
-
- elif not preserve and type == "SpaceCharacters" and token["data"]:
- # Test on token["data"] above to not introduce spaces where there were not
- token["data"] = " "
-
- elif not preserve and type == "Characters":
- token["data"] = collapse_spaces(token["data"])
-
- yield token
-
-
-def collapse_spaces(text):
- return SPACES_REGEX.sub(' ', text)
diff --git a/src/pip/_vendor/html5lib/html5parser.py b/src/pip/_vendor/html5lib/html5parser.py
deleted file mode 100644
index d06784f3d..000000000
--- a/src/pip/_vendor/html5lib/html5parser.py
+++ /dev/null
@@ -1,2795 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-from pip._vendor.six import with_metaclass, viewkeys
-
-import types
-
-from . import _inputstream
-from . import _tokenizer
-
-from . import treebuilders
-from .treebuilders.base import Marker
-
-from . import _utils
-from .constants import (
- spaceCharacters, asciiUpper2Lower,
- specialElements, headingElements, cdataElements, rcdataElements,
- tokenTypes, tagTokenTypes,
- namespaces,
- htmlIntegrationPointElements, mathmlTextIntegrationPointElements,
- adjustForeignAttributes as adjustForeignAttributesMap,
- adjustMathMLAttributes, adjustSVGAttributes,
- E,
- _ReparseException
-)
-
-
-def parse(doc, treebuilder="etree", namespaceHTMLElements=True, **kwargs):
- """Parse an HTML document as a string or file-like object into a tree
-
- :arg doc: the document to parse as a string or file-like object
-
- :arg treebuilder: the treebuilder to use when parsing
-
- :arg namespaceHTMLElements: whether or not to namespace HTML elements
-
- :returns: parsed tree
-
- Example:
-
- >>> from html5lib.html5parser import parse
- >>> parse('<html><body><p>This is a doc</p></body></html>')
- <Element u'{http://www.w3.org/1999/xhtml}html' at 0x7feac4909db0>
-
- """
- tb = treebuilders.getTreeBuilder(treebuilder)
- p = HTMLParser(tb, namespaceHTMLElements=namespaceHTMLElements)
- return p.parse(doc, **kwargs)
-
-
-def parseFragment(doc, container="div", treebuilder="etree", namespaceHTMLElements=True, **kwargs):
- """Parse an HTML fragment as a string or file-like object into a tree
-
- :arg doc: the fragment to parse as a string or file-like object
-
- :arg container: the container context to parse the fragment in
-
- :arg treebuilder: the treebuilder to use when parsing
-
- :arg namespaceHTMLElements: whether or not to namespace HTML elements
-
- :returns: parsed tree
-
- Example:
-
- >>> from html5lib.html5libparser import parseFragment
- >>> parseFragment('<b>this is a fragment</b>')
- <Element u'DOCUMENT_FRAGMENT' at 0x7feac484b090>
-
- """
- tb = treebuilders.getTreeBuilder(treebuilder)
- p = HTMLParser(tb, namespaceHTMLElements=namespaceHTMLElements)
- return p.parseFragment(doc, container=container, **kwargs)
-
-
-def method_decorator_metaclass(function):
- class Decorated(type):
- def __new__(meta, classname, bases, classDict):
- for attributeName, attribute in classDict.items():
- if isinstance(attribute, types.FunctionType):
- attribute = function(attribute)
-
- classDict[attributeName] = attribute
- return type.__new__(meta, classname, bases, classDict)
- return Decorated
-
-
-class HTMLParser(object):
- """HTML parser
-
- Generates a tree structure from a stream of (possibly malformed) HTML.
-
- """
-
- def __init__(self, tree=None, strict=False, namespaceHTMLElements=True, debug=False):
- """
- :arg tree: a treebuilder class controlling the type of tree that will be
- returned. Built in treebuilders can be accessed through
- html5lib.treebuilders.getTreeBuilder(treeType)
-
- :arg strict: raise an exception when a parse error is encountered
-
- :arg namespaceHTMLElements: whether or not to namespace HTML elements
-
- :arg debug: whether or not to enable debug mode which logs things
-
- Example:
-
- >>> from html5lib.html5parser import HTMLParser
- >>> parser = HTMLParser() # generates parser with etree builder
- >>> parser = HTMLParser('lxml', strict=True) # generates parser with lxml builder which is strict
-
- """
-
- # Raise an exception on the first error encountered
- self.strict = strict
-
- if tree is None:
- tree = treebuilders.getTreeBuilder("etree")
- self.tree = tree(namespaceHTMLElements)
- self.errors = []
-
- self.phases = {name: cls(self, self.tree) for name, cls in
- getPhases(debug).items()}
-
- def _parse(self, stream, innerHTML=False, container="div", scripting=False, **kwargs):
-
- self.innerHTMLMode = innerHTML
- self.container = container
- self.scripting = scripting
- self.tokenizer = _tokenizer.HTMLTokenizer(stream, parser=self, **kwargs)
- self.reset()
-
- try:
- self.mainLoop()
- except _ReparseException:
- self.reset()
- self.mainLoop()
-
- def reset(self):
- self.tree.reset()
- self.firstStartTag = False
- self.errors = []
- self.log = [] # only used with debug mode
- # "quirks" / "limited quirks" / "no quirks"
- self.compatMode = "no quirks"
-
- if self.innerHTMLMode:
- self.innerHTML = self.container.lower()
-
- if self.innerHTML in cdataElements:
- self.tokenizer.state = self.tokenizer.rcdataState
- elif self.innerHTML in rcdataElements:
- self.tokenizer.state = self.tokenizer.rawtextState
- elif self.innerHTML == 'plaintext':
- self.tokenizer.state = self.tokenizer.plaintextState
- else:
- # state already is data state
- # self.tokenizer.state = self.tokenizer.dataState
- pass
- self.phase = self.phases["beforeHtml"]
- self.phase.insertHtmlElement()
- self.resetInsertionMode()
- else:
- self.innerHTML = False # pylint:disable=redefined-variable-type
- self.phase = self.phases["initial"]
-
- self.lastPhase = None
-
- self.beforeRCDataPhase = None
-
- self.framesetOK = True
-
- @property
- def documentEncoding(self):
- """Name of the character encoding that was used to decode the input stream, or
- :obj:`None` if that is not determined yet
-
- """
- if not hasattr(self, 'tokenizer'):
- return None
- return self.tokenizer.stream.charEncoding[0].name
-
- def isHTMLIntegrationPoint(self, element):
- if (element.name == "annotation-xml" and
- element.namespace == namespaces["mathml"]):
- return ("encoding" in element.attributes and
- element.attributes["encoding"].translate(
- asciiUpper2Lower) in
- ("text/html", "application/xhtml+xml"))
- else:
- return (element.namespace, element.name) in htmlIntegrationPointElements
-
- def isMathMLTextIntegrationPoint(self, element):
- return (element.namespace, element.name) in mathmlTextIntegrationPointElements
-
- def mainLoop(self):
- CharactersToken = tokenTypes["Characters"]
- SpaceCharactersToken = tokenTypes["SpaceCharacters"]
- StartTagToken = tokenTypes["StartTag"]
- EndTagToken = tokenTypes["EndTag"]
- CommentToken = tokenTypes["Comment"]
- DoctypeToken = tokenTypes["Doctype"]
- ParseErrorToken = tokenTypes["ParseError"]
-
- for token in self.tokenizer:
- prev_token = None
- new_token = token
- while new_token is not None:
- prev_token = new_token
- currentNode = self.tree.openElements[-1] if self.tree.openElements else None
- currentNodeNamespace = currentNode.namespace if currentNode else None
- currentNodeName = currentNode.name if currentNode else None
-
- type = new_token["type"]
-
- if type == ParseErrorToken:
- self.parseError(new_token["data"], new_token.get("datavars", {}))
- new_token = None
- else:
- if (len(self.tree.openElements) == 0 or
- currentNodeNamespace == self.tree.defaultNamespace or
- (self.isMathMLTextIntegrationPoint(currentNode) and
- ((type == StartTagToken and
- token["name"] not in frozenset(["mglyph", "malignmark"])) or
- type in (CharactersToken, SpaceCharactersToken))) or
- (currentNodeNamespace == namespaces["mathml"] and
- currentNodeName == "annotation-xml" and
- type == StartTagToken and
- token["name"] == "svg") or
- (self.isHTMLIntegrationPoint(currentNode) and
- type in (StartTagToken, CharactersToken, SpaceCharactersToken))):
- phase = self.phase
- else:
- phase = self.phases["inForeignContent"]
-
- if type == CharactersToken:
- new_token = phase.processCharacters(new_token)
- elif type == SpaceCharactersToken:
- new_token = phase.processSpaceCharacters(new_token)
- elif type == StartTagToken:
- new_token = phase.processStartTag(new_token)
- elif type == EndTagToken:
- new_token = phase.processEndTag(new_token)
- elif type == CommentToken:
- new_token = phase.processComment(new_token)
- elif type == DoctypeToken:
- new_token = phase.processDoctype(new_token)
-
- if (type == StartTagToken and prev_token["selfClosing"] and
- not prev_token["selfClosingAcknowledged"]):
- self.parseError("non-void-element-with-trailing-solidus",
- {"name": prev_token["name"]})
-
- # When the loop finishes it's EOF
- reprocess = True
- phases = []
- while reprocess:
- phases.append(self.phase)
- reprocess = self.phase.processEOF()
- if reprocess:
- assert self.phase not in phases
-
- def parse(self, stream, *args, **kwargs):
- """Parse a HTML document into a well-formed tree
-
- :arg stream: a file-like object or string containing the HTML to be parsed
-
- The optional encoding parameter must be a string that indicates
- the encoding. If specified, that encoding will be used,
- regardless of any BOM or later declaration (such as in a meta
- element).
-
- :arg scripting: treat noscript elements as if JavaScript was turned on
-
- :returns: parsed tree
-
- Example:
-
- >>> from html5lib.html5parser import HTMLParser
- >>> parser = HTMLParser()
- >>> parser.parse('<html><body><p>This is a doc</p></body></html>')
- <Element u'{http://www.w3.org/1999/xhtml}html' at 0x7feac4909db0>
-
- """
- self._parse(stream, False, None, *args, **kwargs)
- return self.tree.getDocument()
-
- def parseFragment(self, stream, *args, **kwargs):
- """Parse a HTML fragment into a well-formed tree fragment
-
- :arg container: name of the element we're setting the innerHTML
- property if set to None, default to 'div'
-
- :arg stream: a file-like object or string containing the HTML to be parsed
-
- The optional encoding parameter must be a string that indicates
- the encoding. If specified, that encoding will be used,
- regardless of any BOM or later declaration (such as in a meta
- element)
-
- :arg scripting: treat noscript elements as if JavaScript was turned on
-
- :returns: parsed tree
-
- Example:
-
- >>> from html5lib.html5libparser import HTMLParser
- >>> parser = HTMLParser()
- >>> parser.parseFragment('<b>this is a fragment</b>')
- <Element u'DOCUMENT_FRAGMENT' at 0x7feac484b090>
-
- """
- self._parse(stream, True, *args, **kwargs)
- return self.tree.getFragment()
-
- def parseError(self, errorcode="XXX-undefined-error", datavars=None):
- # XXX The idea is to make errorcode mandatory.
- if datavars is None:
- datavars = {}
- self.errors.append((self.tokenizer.stream.position(), errorcode, datavars))
- if self.strict:
- raise ParseError(E[errorcode] % datavars)
-
- def adjustMathMLAttributes(self, token):
- adjust_attributes(token, adjustMathMLAttributes)
-
- def adjustSVGAttributes(self, token):
- adjust_attributes(token, adjustSVGAttributes)
-
- def adjustForeignAttributes(self, token):
- adjust_attributes(token, adjustForeignAttributesMap)
-
- def reparseTokenNormal(self, token):
- # pylint:disable=unused-argument
- self.parser.phase()
-
- def resetInsertionMode(self):
- # The name of this method is mostly historical. (It's also used in the
- # specification.)
- last = False
- newModes = {
- "select": "inSelect",
- "td": "inCell",
- "th": "inCell",
- "tr": "inRow",
- "tbody": "inTableBody",
- "thead": "inTableBody",
- "tfoot": "inTableBody",
- "caption": "inCaption",
- "colgroup": "inColumnGroup",
- "table": "inTable",
- "head": "inBody",
- "body": "inBody",
- "frameset": "inFrameset",
- "html": "beforeHead"
- }
- for node in self.tree.openElements[::-1]:
- nodeName = node.name
- new_phase = None
- if node == self.tree.openElements[0]:
- assert self.innerHTML
- last = True
- nodeName = self.innerHTML
- # Check for conditions that should only happen in the innerHTML
- # case
- if nodeName in ("select", "colgroup", "head", "html"):
- assert self.innerHTML
-
- if not last and node.namespace != self.tree.defaultNamespace:
- continue
-
- if nodeName in newModes:
- new_phase = self.phases[newModes[nodeName]]
- break
- elif last:
- new_phase = self.phases["inBody"]
- break
-
- self.phase = new_phase
-
- def parseRCDataRawtext(self, token, contentType):
- # Generic RCDATA/RAWTEXT Parsing algorithm
- assert contentType in ("RAWTEXT", "RCDATA")
-
- self.tree.insertElement(token)
-
- if contentType == "RAWTEXT":
- self.tokenizer.state = self.tokenizer.rawtextState
- else:
- self.tokenizer.state = self.tokenizer.rcdataState
-
- self.originalPhase = self.phase
-
- self.phase = self.phases["text"]
-
-
-@_utils.memoize
-def getPhases(debug):
- def log(function):
- """Logger that records which phase processes each token"""
- type_names = {value: key for key, value in tokenTypes.items()}
-
- def wrapped(self, *args, **kwargs):
- if function.__name__.startswith("process") and len(args) > 0:
- token = args[0]
- info = {"type": type_names[token['type']]}
- if token['type'] in tagTokenTypes:
- info["name"] = token['name']
-
- self.parser.log.append((self.parser.tokenizer.state.__name__,
- self.parser.phase.__class__.__name__,
- self.__class__.__name__,
- function.__name__,
- info))
- return function(self, *args, **kwargs)
- else:
- return function(self, *args, **kwargs)
- return wrapped
-
- def getMetaclass(use_metaclass, metaclass_func):
- if use_metaclass:
- return method_decorator_metaclass(metaclass_func)
- else:
- return type
-
- # pylint:disable=unused-argument
- class Phase(with_metaclass(getMetaclass(debug, log))):
- """Base class for helper object that implements each phase of processing
- """
- __slots__ = ("parser", "tree", "__startTagCache", "__endTagCache")
-
- def __init__(self, parser, tree):
- self.parser = parser
- self.tree = tree
- self.__startTagCache = {}
- self.__endTagCache = {}
-
- def processEOF(self):
- raise NotImplementedError
-
- def processComment(self, token):
- # For most phases the following is correct. Where it's not it will be
- # overridden.
- self.tree.insertComment(token, self.tree.openElements[-1])
-
- def processDoctype(self, token):
- self.parser.parseError("unexpected-doctype")
-
- def processCharacters(self, token):
- self.tree.insertText(token["data"])
-
- def processSpaceCharacters(self, token):
- self.tree.insertText(token["data"])
-
- def processStartTag(self, token):
- # Note the caching is done here rather than BoundMethodDispatcher as doing it there
- # requires a circular reference to the Phase, and this ends up with a significant
- # (CPython 2.7, 3.8) GC cost when parsing many short inputs
- name = token["name"]
- # In Py2, using `in` is quicker in general than try/except KeyError
- # In Py3, `in` is quicker when there are few cache hits (typically short inputs)
- if name in self.__startTagCache:
- func = self.__startTagCache[name]
- else:
- func = self.__startTagCache[name] = self.startTagHandler[name]
- # bound the cache size in case we get loads of unknown tags
- while len(self.__startTagCache) > len(self.startTagHandler) * 1.1:
- # this makes the eviction policy random on Py < 3.7 and FIFO >= 3.7
- self.__startTagCache.pop(next(iter(self.__startTagCache)))
- return func(token)
-
- def startTagHtml(self, token):
- if not self.parser.firstStartTag and token["name"] == "html":
- self.parser.parseError("non-html-root")
- # XXX Need a check here to see if the first start tag token emitted is
- # this token... If it's not, invoke self.parser.parseError().
- for attr, value in token["data"].items():
- if attr not in self.tree.openElements[0].attributes:
- self.tree.openElements[0].attributes[attr] = value
- self.parser.firstStartTag = False
-
- def processEndTag(self, token):
- # Note the caching is done here rather than BoundMethodDispatcher as doing it there
- # requires a circular reference to the Phase, and this ends up with a significant
- # (CPython 2.7, 3.8) GC cost when parsing many short inputs
- name = token["name"]
- # In Py2, using `in` is quicker in general than try/except KeyError
- # In Py3, `in` is quicker when there are few cache hits (typically short inputs)
- if name in self.__endTagCache:
- func = self.__endTagCache[name]
- else:
- func = self.__endTagCache[name] = self.endTagHandler[name]
- # bound the cache size in case we get loads of unknown tags
- while len(self.__endTagCache) > len(self.endTagHandler) * 1.1:
- # this makes the eviction policy random on Py < 3.7 and FIFO >= 3.7
- self.__endTagCache.pop(next(iter(self.__endTagCache)))
- return func(token)
-
- class InitialPhase(Phase):
- __slots__ = tuple()
-
- def processSpaceCharacters(self, token):
- pass
-
- def processComment(self, token):
- self.tree.insertComment(token, self.tree.document)
-
- def processDoctype(self, token):
- name = token["name"]
- publicId = token["publicId"]
- systemId = token["systemId"]
- correct = token["correct"]
-
- if (name != "html" or publicId is not None or
- systemId is not None and systemId != "about:legacy-compat"):
- self.parser.parseError("unknown-doctype")
-
- if publicId is None:
- publicId = ""
-
- self.tree.insertDoctype(token)
-
- if publicId != "":
- publicId = publicId.translate(asciiUpper2Lower)
-
- if (not correct or token["name"] != "html" or
- publicId.startswith(
- ("+//silmaril//dtd html pro v0r11 19970101//",
- "-//advasoft ltd//dtd html 3.0 aswedit + extensions//",
- "-//as//dtd html 3.0 aswedit + extensions//",
- "-//ietf//dtd html 2.0 level 1//",
- "-//ietf//dtd html 2.0 level 2//",
- "-//ietf//dtd html 2.0 strict level 1//",
- "-//ietf//dtd html 2.0 strict level 2//",
- "-//ietf//dtd html 2.0 strict//",
- "-//ietf//dtd html 2.0//",
- "-//ietf//dtd html 2.1e//",
- "-//ietf//dtd html 3.0//",
- "-//ietf//dtd html 3.2 final//",
- "-//ietf//dtd html 3.2//",
- "-//ietf//dtd html 3//",
- "-//ietf//dtd html level 0//",
- "-//ietf//dtd html level 1//",
- "-//ietf//dtd html level 2//",
- "-//ietf//dtd html level 3//",
- "-//ietf//dtd html strict level 0//",
- "-//ietf//dtd html strict level 1//",
- "-//ietf//dtd html strict level 2//",
- "-//ietf//dtd html strict level 3//",
- "-//ietf//dtd html strict//",
- "-//ietf//dtd html//",
- "-//metrius//dtd metrius presentational//",
- "-//microsoft//dtd internet explorer 2.0 html strict//",
- "-//microsoft//dtd internet explorer 2.0 html//",
- "-//microsoft//dtd internet explorer 2.0 tables//",
- "-//microsoft//dtd internet explorer 3.0 html strict//",
- "-//microsoft//dtd internet explorer 3.0 html//",
- "-//microsoft//dtd internet explorer 3.0 tables//",
- "-//netscape comm. corp.//dtd html//",
- "-//netscape comm. corp.//dtd strict html//",
- "-//o'reilly and associates//dtd html 2.0//",
- "-//o'reilly and associates//dtd html extended 1.0//",
- "-//o'reilly and associates//dtd html extended relaxed 1.0//",
- "-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//",
- "-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//",
- "-//spyglass//dtd html 2.0 extended//",
- "-//sq//dtd html 2.0 hotmetal + extensions//",
- "-//sun microsystems corp.//dtd hotjava html//",
- "-//sun microsystems corp.//dtd hotjava strict html//",
- "-//w3c//dtd html 3 1995-03-24//",
- "-//w3c//dtd html 3.2 draft//",
- "-//w3c//dtd html 3.2 final//",
- "-//w3c//dtd html 3.2//",
- "-//w3c//dtd html 3.2s draft//",
- "-//w3c//dtd html 4.0 frameset//",
- "-//w3c//dtd html 4.0 transitional//",
- "-//w3c//dtd html experimental 19960712//",
- "-//w3c//dtd html experimental 970421//",
- "-//w3c//dtd w3 html//",
- "-//w3o//dtd w3 html 3.0//",
- "-//webtechs//dtd mozilla html 2.0//",
- "-//webtechs//dtd mozilla html//")) or
- publicId in ("-//w3o//dtd w3 html strict 3.0//en//",
- "-/w3c/dtd html 4.0 transitional/en",
- "html") or
- publicId.startswith(
- ("-//w3c//dtd html 4.01 frameset//",
- "-//w3c//dtd html 4.01 transitional//")) and
- systemId is None or
- systemId and systemId.lower() == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd"):
- self.parser.compatMode = "quirks"
- elif (publicId.startswith(
- ("-//w3c//dtd xhtml 1.0 frameset//",
- "-//w3c//dtd xhtml 1.0 transitional//")) or
- publicId.startswith(
- ("-//w3c//dtd html 4.01 frameset//",
- "-//w3c//dtd html 4.01 transitional//")) and
- systemId is not None):
- self.parser.compatMode = "limited quirks"
-
- self.parser.phase = self.parser.phases["beforeHtml"]
-
- def anythingElse(self):
- self.parser.compatMode = "quirks"
- self.parser.phase = self.parser.phases["beforeHtml"]
-
- def processCharacters(self, token):
- self.parser.parseError("expected-doctype-but-got-chars")
- self.anythingElse()
- return token
-
- def processStartTag(self, token):
- self.parser.parseError("expected-doctype-but-got-start-tag",
- {"name": token["name"]})
- self.anythingElse()
- return token
-
- def processEndTag(self, token):
- self.parser.parseError("expected-doctype-but-got-end-tag",
- {"name": token["name"]})
- self.anythingElse()
- return token
-
- def processEOF(self):
- self.parser.parseError("expected-doctype-but-got-eof")
- self.anythingElse()
- return True
-
- class BeforeHtmlPhase(Phase):
- __slots__ = tuple()
-
- # helper methods
- def insertHtmlElement(self):
- self.tree.insertRoot(impliedTagToken("html", "StartTag"))
- self.parser.phase = self.parser.phases["beforeHead"]
-
- # other
- def processEOF(self):
- self.insertHtmlElement()
- return True
-
- def processComment(self, token):
- self.tree.insertComment(token, self.tree.document)
-
- def processSpaceCharacters(self, token):
- pass
-
- def processCharacters(self, token):
- self.insertHtmlElement()
- return token
-
- def processStartTag(self, token):
- if token["name"] == "html":
- self.parser.firstStartTag = True
- self.insertHtmlElement()
- return token
-
- def processEndTag(self, token):
- if token["name"] not in ("head", "body", "html", "br"):
- self.parser.parseError("unexpected-end-tag-before-html",
- {"name": token["name"]})
- else:
- self.insertHtmlElement()
- return token
-
- class BeforeHeadPhase(Phase):
- __slots__ = tuple()
-
- def processEOF(self):
- self.startTagHead(impliedTagToken("head", "StartTag"))
- return True
-
- def processSpaceCharacters(self, token):
- pass
-
- def processCharacters(self, token):
- self.startTagHead(impliedTagToken("head", "StartTag"))
- return token
-
- def startTagHtml(self, token):
- return self.parser.phases["inBody"].processStartTag(token)
-
- def startTagHead(self, token):
- self.tree.insertElement(token)
- self.tree.headPointer = self.tree.openElements[-1]
- self.parser.phase = self.parser.phases["inHead"]
-
- def startTagOther(self, token):
- self.startTagHead(impliedTagToken("head", "StartTag"))
- return token
-
- def endTagImplyHead(self, token):
- self.startTagHead(impliedTagToken("head", "StartTag"))
- return token
-
- def endTagOther(self, token):
- self.parser.parseError("end-tag-after-implied-root",
- {"name": token["name"]})
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", startTagHtml),
- ("head", startTagHead)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- (("head", "body", "html", "br"), endTagImplyHead)
- ])
- endTagHandler.default = endTagOther
-
- class InHeadPhase(Phase):
- __slots__ = tuple()
-
- # the real thing
- def processEOF(self):
- self.anythingElse()
- return True
-
- def processCharacters(self, token):
- self.anythingElse()
- return token
-
- def startTagHtml(self, token):
- return self.parser.phases["inBody"].processStartTag(token)
-
- def startTagHead(self, token):
- self.parser.parseError("two-heads-are-not-better-than-one")
-
- def startTagBaseLinkCommand(self, token):
- self.tree.insertElement(token)
- self.tree.openElements.pop()
- token["selfClosingAcknowledged"] = True
-
- def startTagMeta(self, token):
- self.tree.insertElement(token)
- self.tree.openElements.pop()
- token["selfClosingAcknowledged"] = True
-
- attributes = token["data"]
- if self.parser.tokenizer.stream.charEncoding[1] == "tentative":
- if "charset" in attributes:
- self.parser.tokenizer.stream.changeEncoding(attributes["charset"])
- elif ("content" in attributes and
- "http-equiv" in attributes and
- attributes["http-equiv"].lower() == "content-type"):
- # Encoding it as UTF-8 here is a hack, as really we should pass
- # the abstract Unicode string, and just use the
- # ContentAttrParser on that, but using UTF-8 allows all chars
- # to be encoded and as a ASCII-superset works.
- data = _inputstream.EncodingBytes(attributes["content"].encode("utf-8"))
- parser = _inputstream.ContentAttrParser(data)
- codec = parser.parse()
- self.parser.tokenizer.stream.changeEncoding(codec)
-
- def startTagTitle(self, token):
- self.parser.parseRCDataRawtext(token, "RCDATA")
-
- def startTagNoFramesStyle(self, token):
- # Need to decide whether to implement the scripting-disabled case
- self.parser.parseRCDataRawtext(token, "RAWTEXT")
-
- def startTagNoscript(self, token):
- if self.parser.scripting:
- self.parser.parseRCDataRawtext(token, "RAWTEXT")
- else:
- self.tree.insertElement(token)
- self.parser.phase = self.parser.phases["inHeadNoscript"]
-
- def startTagScript(self, token):
- self.tree.insertElement(token)
- self.parser.tokenizer.state = self.parser.tokenizer.scriptDataState
- self.parser.originalPhase = self.parser.phase
- self.parser.phase = self.parser.phases["text"]
-
- def startTagOther(self, token):
- self.anythingElse()
- return token
-
- def endTagHead(self, token):
- node = self.parser.tree.openElements.pop()
- assert node.name == "head", "Expected head got %s" % node.name
- self.parser.phase = self.parser.phases["afterHead"]
-
- def endTagHtmlBodyBr(self, token):
- self.anythingElse()
- return token
-
- def endTagOther(self, token):
- self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
-
- def anythingElse(self):
- self.endTagHead(impliedTagToken("head"))
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", startTagHtml),
- ("title", startTagTitle),
- (("noframes", "style"), startTagNoFramesStyle),
- ("noscript", startTagNoscript),
- ("script", startTagScript),
- (("base", "basefont", "bgsound", "command", "link"),
- startTagBaseLinkCommand),
- ("meta", startTagMeta),
- ("head", startTagHead)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- ("head", endTagHead),
- (("br", "html", "body"), endTagHtmlBodyBr)
- ])
- endTagHandler.default = endTagOther
-
- class InHeadNoscriptPhase(Phase):
- __slots__ = tuple()
-
- def processEOF(self):
- self.parser.parseError("eof-in-head-noscript")
- self.anythingElse()
- return True
-
- def processComment(self, token):
- return self.parser.phases["inHead"].processComment(token)
-
- def processCharacters(self, token):
- self.parser.parseError("char-in-head-noscript")
- self.anythingElse()
- return token
-
- def processSpaceCharacters(self, token):
- return self.parser.phases["inHead"].processSpaceCharacters(token)
-
- def startTagHtml(self, token):
- return self.parser.phases["inBody"].processStartTag(token)
-
- def startTagBaseLinkCommand(self, token):
- return self.parser.phases["inHead"].processStartTag(token)
-
- def startTagHeadNoscript(self, token):
- self.parser.parseError("unexpected-start-tag", {"name": token["name"]})
-
- def startTagOther(self, token):
- self.parser.parseError("unexpected-inhead-noscript-tag", {"name": token["name"]})
- self.anythingElse()
- return token
-
- def endTagNoscript(self, token):
- node = self.parser.tree.openElements.pop()
- assert node.name == "noscript", "Expected noscript got %s" % node.name
- self.parser.phase = self.parser.phases["inHead"]
-
- def endTagBr(self, token):
- self.parser.parseError("unexpected-inhead-noscript-tag", {"name": token["name"]})
- self.anythingElse()
- return token
-
- def endTagOther(self, token):
- self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
-
- def anythingElse(self):
- # Caller must raise parse error first!
- self.endTagNoscript(impliedTagToken("noscript"))
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", startTagHtml),
- (("basefont", "bgsound", "link", "meta", "noframes", "style"), startTagBaseLinkCommand),
- (("head", "noscript"), startTagHeadNoscript),
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- ("noscript", endTagNoscript),
- ("br", endTagBr),
- ])
- endTagHandler.default = endTagOther
-
- class AfterHeadPhase(Phase):
- __slots__ = tuple()
-
- def processEOF(self):
- self.anythingElse()
- return True
-
- def processCharacters(self, token):
- self.anythingElse()
- return token
-
- def startTagHtml(self, token):
- return self.parser.phases["inBody"].processStartTag(token)
-
- def startTagBody(self, token):
- self.parser.framesetOK = False
- self.tree.insertElement(token)
- self.parser.phase = self.parser.phases["inBody"]
-
- def startTagFrameset(self, token):
- self.tree.insertElement(token)
- self.parser.phase = self.parser.phases["inFrameset"]
-
- def startTagFromHead(self, token):
- self.parser.parseError("unexpected-start-tag-out-of-my-head",
- {"name": token["name"]})
- self.tree.openElements.append(self.tree.headPointer)
- self.parser.phases["inHead"].processStartTag(token)
- for node in self.tree.openElements[::-1]:
- if node.name == "head":
- self.tree.openElements.remove(node)
- break
-
- def startTagHead(self, token):
- self.parser.parseError("unexpected-start-tag", {"name": token["name"]})
-
- def startTagOther(self, token):
- self.anythingElse()
- return token
-
- def endTagHtmlBodyBr(self, token):
- self.anythingElse()
- return token
-
- def endTagOther(self, token):
- self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
-
- def anythingElse(self):
- self.tree.insertElement(impliedTagToken("body", "StartTag"))
- self.parser.phase = self.parser.phases["inBody"]
- self.parser.framesetOK = True
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", startTagHtml),
- ("body", startTagBody),
- ("frameset", startTagFrameset),
- (("base", "basefont", "bgsound", "link", "meta", "noframes", "script",
- "style", "title"),
- startTagFromHead),
- ("head", startTagHead)
- ])
- startTagHandler.default = startTagOther
- endTagHandler = _utils.MethodDispatcher([(("body", "html", "br"),
- endTagHtmlBodyBr)])
- endTagHandler.default = endTagOther
-
- class InBodyPhase(Phase):
- # http://www.whatwg.org/specs/web-apps/current-work/#parsing-main-inbody
- # the really-really-really-very crazy mode
- __slots__ = ("processSpaceCharacters",)
-
- def __init__(self, *args, **kwargs):
- super(InBodyPhase, self).__init__(*args, **kwargs)
- # Set this to the default handler
- self.processSpaceCharacters = self.processSpaceCharactersNonPre
-
- def isMatchingFormattingElement(self, node1, node2):
- return (node1.name == node2.name and
- node1.namespace == node2.namespace and
- node1.attributes == node2.attributes)
-
- # helper
- def addFormattingElement(self, token):
- self.tree.insertElement(token)
- element = self.tree.openElements[-1]
-
- matchingElements = []
- for node in self.tree.activeFormattingElements[::-1]:
- if node is Marker:
- break
- elif self.isMatchingFormattingElement(node, element):
- matchingElements.append(node)
-
- assert len(matchingElements) <= 3
- if len(matchingElements) == 3:
- self.tree.activeFormattingElements.remove(matchingElements[-1])
- self.tree.activeFormattingElements.append(element)
-
- # the real deal
- def processEOF(self):
- allowed_elements = frozenset(("dd", "dt", "li", "p", "tbody", "td",
- "tfoot", "th", "thead", "tr", "body",
- "html"))
- for node in self.tree.openElements[::-1]:
- if node.name not in allowed_elements:
- self.parser.parseError("expected-closing-tag-but-got-eof")
- break
- # Stop parsing
-
- def processSpaceCharactersDropNewline(self, token):
- # Sometimes (start of <pre>, <listing>, and <textarea> blocks) we
- # want to drop leading newlines
- data = token["data"]
- self.processSpaceCharacters = self.processSpaceCharactersNonPre
- if (data.startswith("\n") and
- self.tree.openElements[-1].name in ("pre", "listing", "textarea") and
- not self.tree.openElements[-1].hasContent()):
- data = data[1:]
- if data:
- self.tree.reconstructActiveFormattingElements()
- self.tree.insertText(data)
-
- def processCharacters(self, token):
- if token["data"] == "\u0000":
- # The tokenizer should always emit null on its own
- return
- self.tree.reconstructActiveFormattingElements()
- self.tree.insertText(token["data"])
- # This must be bad for performance
- if (self.parser.framesetOK and
- any([char not in spaceCharacters
- for char in token["data"]])):
- self.parser.framesetOK = False
-
- def processSpaceCharactersNonPre(self, token):
- self.tree.reconstructActiveFormattingElements()
- self.tree.insertText(token["data"])
-
- def startTagProcessInHead(self, token):
- return self.parser.phases["inHead"].processStartTag(token)
-
- def startTagBody(self, token):
- self.parser.parseError("unexpected-start-tag", {"name": "body"})
- if (len(self.tree.openElements) == 1 or
- self.tree.openElements[1].name != "body"):
- assert self.parser.innerHTML
- else:
- self.parser.framesetOK = False
- for attr, value in token["data"].items():
- if attr not in self.tree.openElements[1].attributes:
- self.tree.openElements[1].attributes[attr] = value
-
- def startTagFrameset(self, token):
- self.parser.parseError("unexpected-start-tag", {"name": "frameset"})
- if (len(self.tree.openElements) == 1 or self.tree.openElements[1].name != "body"):
- assert self.parser.innerHTML
- elif not self.parser.framesetOK:
- pass
- else:
- if self.tree.openElements[1].parent:
- self.tree.openElements[1].parent.removeChild(self.tree.openElements[1])
- while self.tree.openElements[-1].name != "html":
- self.tree.openElements.pop()
- self.tree.insertElement(token)
- self.parser.phase = self.parser.phases["inFrameset"]
-
- def startTagCloseP(self, token):
- if self.tree.elementInScope("p", variant="button"):
- self.endTagP(impliedTagToken("p"))
- self.tree.insertElement(token)
-
- def startTagPreListing(self, token):
- if self.tree.elementInScope("p", variant="button"):
- self.endTagP(impliedTagToken("p"))
- self.tree.insertElement(token)
- self.parser.framesetOK = False
- self.processSpaceCharacters = self.processSpaceCharactersDropNewline
-
- def startTagForm(self, token):
- if self.tree.formPointer:
- self.parser.parseError("unexpected-start-tag", {"name": "form"})
- else:
- if self.tree.elementInScope("p", variant="button"):
- self.endTagP(impliedTagToken("p"))
- self.tree.insertElement(token)
- self.tree.formPointer = self.tree.openElements[-1]
-
- def startTagListItem(self, token):
- self.parser.framesetOK = False
-
- stopNamesMap = {"li": ["li"],
- "dt": ["dt", "dd"],
- "dd": ["dt", "dd"]}
- stopNames = stopNamesMap[token["name"]]
- for node in reversed(self.tree.openElements):
- if node.name in stopNames:
- self.parser.phase.processEndTag(
- impliedTagToken(node.name, "EndTag"))
- break
- if (node.nameTuple in specialElements and
- node.name not in ("address", "div", "p")):
- break
-
- if self.tree.elementInScope("p", variant="button"):
- self.parser.phase.processEndTag(
- impliedTagToken("p", "EndTag"))
-
- self.tree.insertElement(token)
-
- def startTagPlaintext(self, token):
- if self.tree.elementInScope("p", variant="button"):
- self.endTagP(impliedTagToken("p"))
- self.tree.insertElement(token)
- self.parser.tokenizer.state = self.parser.tokenizer.plaintextState
-
- def startTagHeading(self, token):
- if self.tree.elementInScope("p", variant="button"):
- self.endTagP(impliedTagToken("p"))
- if self.tree.openElements[-1].name in headingElements:
- self.parser.parseError("unexpected-start-tag", {"name": token["name"]})
- self.tree.openElements.pop()
- self.tree.insertElement(token)
-
- def startTagA(self, token):
- afeAElement = self.tree.elementInActiveFormattingElements("a")
- if afeAElement:
- self.parser.parseError("unexpected-start-tag-implies-end-tag",
- {"startName": "a", "endName": "a"})
- self.endTagFormatting(impliedTagToken("a"))
- if afeAElement in self.tree.openElements:
- self.tree.openElements.remove(afeAElement)
- if afeAElement in self.tree.activeFormattingElements:
- self.tree.activeFormattingElements.remove(afeAElement)
- self.tree.reconstructActiveFormattingElements()
- self.addFormattingElement(token)
-
- def startTagFormatting(self, token):
- self.tree.reconstructActiveFormattingElements()
- self.addFormattingElement(token)
-
- def startTagNobr(self, token):
- self.tree.reconstructActiveFormattingElements()
- if self.tree.elementInScope("nobr"):
- self.parser.parseError("unexpected-start-tag-implies-end-tag",
- {"startName": "nobr", "endName": "nobr"})
- self.processEndTag(impliedTagToken("nobr"))
- # XXX Need tests that trigger the following
- self.tree.reconstructActiveFormattingElements()
- self.addFormattingElement(token)
-
- def startTagButton(self, token):
- if self.tree.elementInScope("button"):
- self.parser.parseError("unexpected-start-tag-implies-end-tag",
- {"startName": "button", "endName": "button"})
- self.processEndTag(impliedTagToken("button"))
- return token
- else:
- self.tree.reconstructActiveFormattingElements()
- self.tree.insertElement(token)
- self.parser.framesetOK = False
-
- def startTagAppletMarqueeObject(self, token):
- self.tree.reconstructActiveFormattingElements()
- self.tree.insertElement(token)
- self.tree.activeFormattingElements.append(Marker)
- self.parser.framesetOK = False
-
- def startTagXmp(self, token):
- if self.tree.elementInScope("p", variant="button"):
- self.endTagP(impliedTagToken("p"))
- self.tree.reconstructActiveFormattingElements()
- self.parser.framesetOK = False
- self.parser.parseRCDataRawtext(token, "RAWTEXT")
-
- def startTagTable(self, token):
- if self.parser.compatMode != "quirks":
- if self.tree.elementInScope("p", variant="button"):
- self.processEndTag(impliedTagToken("p"))
- self.tree.insertElement(token)
- self.parser.framesetOK = False
- self.parser.phase = self.parser.phases["inTable"]
-
- def startTagVoidFormatting(self, token):
- self.tree.reconstructActiveFormattingElements()
- self.tree.insertElement(token)
- self.tree.openElements.pop()
- token["selfClosingAcknowledged"] = True
- self.parser.framesetOK = False
-
- def startTagInput(self, token):
- framesetOK = self.parser.framesetOK
- self.startTagVoidFormatting(token)
- if ("type" in token["data"] and
- token["data"]["type"].translate(asciiUpper2Lower) == "hidden"):
- # input type=hidden doesn't change framesetOK
- self.parser.framesetOK = framesetOK
-
- def startTagParamSource(self, token):
- self.tree.insertElement(token)
- self.tree.openElements.pop()
- token["selfClosingAcknowledged"] = True
-
- def startTagHr(self, token):
- if self.tree.elementInScope("p", variant="button"):
- self.endTagP(impliedTagToken("p"))
- self.tree.insertElement(token)
- self.tree.openElements.pop()
- token["selfClosingAcknowledged"] = True
- self.parser.framesetOK = False
-
- def startTagImage(self, token):
- # No really...
- self.parser.parseError("unexpected-start-tag-treated-as",
- {"originalName": "image", "newName": "img"})
- self.processStartTag(impliedTagToken("img", "StartTag",
- attributes=token["data"],
- selfClosing=token["selfClosing"]))
-
- def startTagIsIndex(self, token):
- self.parser.parseError("deprecated-tag", {"name": "isindex"})
- if self.tree.formPointer:
- return
- form_attrs = {}
- if "action" in token["data"]:
- form_attrs["action"] = token["data"]["action"]
- self.processStartTag(impliedTagToken("form", "StartTag",
- attributes=form_attrs))
- self.processStartTag(impliedTagToken("hr", "StartTag"))
- self.processStartTag(impliedTagToken("label", "StartTag"))
- # XXX Localization ...
- if "prompt" in token["data"]:
- prompt = token["data"]["prompt"]
- else:
- prompt = "This is a searchable index. Enter search keywords: "
- self.processCharacters(
- {"type": tokenTypes["Characters"], "data": prompt})
- attributes = token["data"].copy()
- if "action" in attributes:
- del attributes["action"]
- if "prompt" in attributes:
- del attributes["prompt"]
- attributes["name"] = "isindex"
- self.processStartTag(impliedTagToken("input", "StartTag",
- attributes=attributes,
- selfClosing=token["selfClosing"]))
- self.processEndTag(impliedTagToken("label"))
- self.processStartTag(impliedTagToken("hr", "StartTag"))
- self.processEndTag(impliedTagToken("form"))
-
- def startTagTextarea(self, token):
- self.tree.insertElement(token)
- self.parser.tokenizer.state = self.parser.tokenizer.rcdataState
- self.processSpaceCharacters = self.processSpaceCharactersDropNewline
- self.parser.framesetOK = False
-
- def startTagIFrame(self, token):
- self.parser.framesetOK = False
- self.startTagRawtext(token)
-
- def startTagNoscript(self, token):
- if self.parser.scripting:
- self.startTagRawtext(token)
- else:
- self.startTagOther(token)
-
- def startTagRawtext(self, token):
- """iframe, noembed noframes, noscript(if scripting enabled)"""
- self.parser.parseRCDataRawtext(token, "RAWTEXT")
-
- def startTagOpt(self, token):
- if self.tree.openElements[-1].name == "option":
- self.parser.phase.processEndTag(impliedTagToken("option"))
- self.tree.reconstructActiveFormattingElements()
- self.parser.tree.insertElement(token)
-
- def startTagSelect(self, token):
- self.tree.reconstructActiveFormattingElements()
- self.tree.insertElement(token)
- self.parser.framesetOK = False
- if self.parser.phase in (self.parser.phases["inTable"],
- self.parser.phases["inCaption"],
- self.parser.phases["inColumnGroup"],
- self.parser.phases["inTableBody"],
- self.parser.phases["inRow"],
- self.parser.phases["inCell"]):
- self.parser.phase = self.parser.phases["inSelectInTable"]
- else:
- self.parser.phase = self.parser.phases["inSelect"]
-
- def startTagRpRt(self, token):
- if self.tree.elementInScope("ruby"):
- self.tree.generateImpliedEndTags()
- if self.tree.openElements[-1].name != "ruby":
- self.parser.parseError()
- self.tree.insertElement(token)
-
- def startTagMath(self, token):
- self.tree.reconstructActiveFormattingElements()
- self.parser.adjustMathMLAttributes(token)
- self.parser.adjustForeignAttributes(token)
- token["namespace"] = namespaces["mathml"]
- self.tree.insertElement(token)
- # Need to get the parse error right for the case where the token
- # has a namespace not equal to the xmlns attribute
- if token["selfClosing"]:
- self.tree.openElements.pop()
- token["selfClosingAcknowledged"] = True
-
- def startTagSvg(self, token):
- self.tree.reconstructActiveFormattingElements()
- self.parser.adjustSVGAttributes(token)
- self.parser.adjustForeignAttributes(token)
- token["namespace"] = namespaces["svg"]
- self.tree.insertElement(token)
- # Need to get the parse error right for the case where the token
- # has a namespace not equal to the xmlns attribute
- if token["selfClosing"]:
- self.tree.openElements.pop()
- token["selfClosingAcknowledged"] = True
-
- def startTagMisplaced(self, token):
- """ Elements that should be children of other elements that have a
- different insertion mode; here they are ignored
- "caption", "col", "colgroup", "frame", "frameset", "head",
- "option", "optgroup", "tbody", "td", "tfoot", "th", "thead",
- "tr", "noscript"
- """
- self.parser.parseError("unexpected-start-tag-ignored", {"name": token["name"]})
-
- def startTagOther(self, token):
- self.tree.reconstructActiveFormattingElements()
- self.tree.insertElement(token)
-
- def endTagP(self, token):
- if not self.tree.elementInScope("p", variant="button"):
- self.startTagCloseP(impliedTagToken("p", "StartTag"))
- self.parser.parseError("unexpected-end-tag", {"name": "p"})
- self.endTagP(impliedTagToken("p", "EndTag"))
- else:
- self.tree.generateImpliedEndTags("p")
- if self.tree.openElements[-1].name != "p":
- self.parser.parseError("unexpected-end-tag", {"name": "p"})
- node = self.tree.openElements.pop()
- while node.name != "p":
- node = self.tree.openElements.pop()
-
- def endTagBody(self, token):
- if not self.tree.elementInScope("body"):
- self.parser.parseError()
- return
- elif self.tree.openElements[-1].name != "body":
- for node in self.tree.openElements[2:]:
- if node.name not in frozenset(("dd", "dt", "li", "optgroup",
- "option", "p", "rp", "rt",
- "tbody", "td", "tfoot",
- "th", "thead", "tr", "body",
- "html")):
- # Not sure this is the correct name for the parse error
- self.parser.parseError(
- "expected-one-end-tag-but-got-another",
- {"gotName": "body", "expectedName": node.name})
- break
- self.parser.phase = self.parser.phases["afterBody"]
-
- def endTagHtml(self, token):
- # We repeat the test for the body end tag token being ignored here
- if self.tree.elementInScope("body"):
- self.endTagBody(impliedTagToken("body"))
- return token
-
- def endTagBlock(self, token):
- # Put us back in the right whitespace handling mode
- if token["name"] == "pre":
- self.processSpaceCharacters = self.processSpaceCharactersNonPre
- inScope = self.tree.elementInScope(token["name"])
- if inScope:
- self.tree.generateImpliedEndTags()
- if self.tree.openElements[-1].name != token["name"]:
- self.parser.parseError("end-tag-too-early", {"name": token["name"]})
- if inScope:
- node = self.tree.openElements.pop()
- while node.name != token["name"]:
- node = self.tree.openElements.pop()
-
- def endTagForm(self, token):
- node = self.tree.formPointer
- self.tree.formPointer = None
- if node is None or not self.tree.elementInScope(node):
- self.parser.parseError("unexpected-end-tag",
- {"name": "form"})
- else:
- self.tree.generateImpliedEndTags()
- if self.tree.openElements[-1] != node:
- self.parser.parseError("end-tag-too-early-ignored",
- {"name": "form"})
- self.tree.openElements.remove(node)
-
- def endTagListItem(self, token):
- if token["name"] == "li":
- variant = "list"
- else:
- variant = None
- if not self.tree.elementInScope(token["name"], variant=variant):
- self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
- else:
- self.tree.generateImpliedEndTags(exclude=token["name"])
- if self.tree.openElements[-1].name != token["name"]:
- self.parser.parseError(
- "end-tag-too-early",
- {"name": token["name"]})
- node = self.tree.openElements.pop()
- while node.name != token["name"]:
- node = self.tree.openElements.pop()
-
- def endTagHeading(self, token):
- for item in headingElements:
- if self.tree.elementInScope(item):
- self.tree.generateImpliedEndTags()
- break
- if self.tree.openElements[-1].name != token["name"]:
- self.parser.parseError("end-tag-too-early", {"name": token["name"]})
-
- for item in headingElements:
- if self.tree.elementInScope(item):
- item = self.tree.openElements.pop()
- while item.name not in headingElements:
- item = self.tree.openElements.pop()
- break
-
- def endTagFormatting(self, token):
- """The much-feared adoption agency algorithm"""
- # http://svn.whatwg.org/webapps/complete.html#adoptionAgency revision 7867
- # XXX Better parseError messages appreciated.
-
- # Step 1
- outerLoopCounter = 0
-
- # Step 2
- while outerLoopCounter < 8:
-
- # Step 3
- outerLoopCounter += 1
-
- # Step 4:
-
- # Let the formatting element be the last element in
- # the list of active formatting elements that:
- # - is between the end of the list and the last scope
- # marker in the list, if any, or the start of the list
- # otherwise, and
- # - has the same tag name as the token.
- formattingElement = self.tree.elementInActiveFormattingElements(
- token["name"])
- if (not formattingElement or
- (formattingElement in self.tree.openElements and
- not self.tree.elementInScope(formattingElement.name))):
- # If there is no such node, then abort these steps
- # and instead act as described in the "any other
- # end tag" entry below.
- self.endTagOther(token)
- return
-
- # Otherwise, if there is such a node, but that node is
- # not in the stack of open elements, then this is a
- # parse error; remove the element from the list, and
- # abort these steps.
- elif formattingElement not in self.tree.openElements:
- self.parser.parseError("adoption-agency-1.2", {"name": token["name"]})
- self.tree.activeFormattingElements.remove(formattingElement)
- return
-
- # Otherwise, if there is such a node, and that node is
- # also in the stack of open elements, but the element
- # is not in scope, then this is a parse error; ignore
- # the token, and abort these steps.
- elif not self.tree.elementInScope(formattingElement.name):
- self.parser.parseError("adoption-agency-4.4", {"name": token["name"]})
- return
-
- # Otherwise, there is a formatting element and that
- # element is in the stack and is in scope. If the
- # element is not the current node, this is a parse
- # error. In any case, proceed with the algorithm as
- # written in the following steps.
- else:
- if formattingElement != self.tree.openElements[-1]:
- self.parser.parseError("adoption-agency-1.3", {"name": token["name"]})
-
- # Step 5:
-
- # Let the furthest block be the topmost node in the
- # stack of open elements that is lower in the stack
- # than the formatting element, and is an element in
- # the special category. There might not be one.
- afeIndex = self.tree.openElements.index(formattingElement)
- furthestBlock = None
- for element in self.tree.openElements[afeIndex:]:
- if element.nameTuple in specialElements:
- furthestBlock = element
- break
-
- # Step 6:
-
- # If there is no furthest block, then the UA must
- # first pop all the nodes from the bottom of the stack
- # of open elements, from the current node up to and
- # including the formatting element, then remove the
- # formatting element from the list of active
- # formatting elements, and finally abort these steps.
- if furthestBlock is None:
- element = self.tree.openElements.pop()
- while element != formattingElement:
- element = self.tree.openElements.pop()
- self.tree.activeFormattingElements.remove(element)
- return
-
- # Step 7
- commonAncestor = self.tree.openElements[afeIndex - 1]
-
- # Step 8:
- # The bookmark is supposed to help us identify where to reinsert
- # nodes in step 15. We have to ensure that we reinsert nodes after
- # the node before the active formatting element. Note the bookmark
- # can move in step 9.7
- bookmark = self.tree.activeFormattingElements.index(formattingElement)
-
- # Step 9
- lastNode = node = furthestBlock
- innerLoopCounter = 0
-
- index = self.tree.openElements.index(node)
- while innerLoopCounter < 3:
- innerLoopCounter += 1
- # Node is element before node in open elements
- index -= 1
- node = self.tree.openElements[index]
- if node not in self.tree.activeFormattingElements:
- self.tree.openElements.remove(node)
- continue
- # Step 9.6
- if node == formattingElement:
- break
- # Step 9.7
- if lastNode == furthestBlock:
- bookmark = self.tree.activeFormattingElements.index(node) + 1
- # Step 9.8
- clone = node.cloneNode()
- # Replace node with clone
- self.tree.activeFormattingElements[
- self.tree.activeFormattingElements.index(node)] = clone
- self.tree.openElements[
- self.tree.openElements.index(node)] = clone
- node = clone
- # Step 9.9
- # Remove lastNode from its parents, if any
- if lastNode.parent:
- lastNode.parent.removeChild(lastNode)
- node.appendChild(lastNode)
- # Step 9.10
- lastNode = node
-
- # Step 10
- # Foster parent lastNode if commonAncestor is a
- # table, tbody, tfoot, thead, or tr we need to foster
- # parent the lastNode
- if lastNode.parent:
- lastNode.parent.removeChild(lastNode)
-
- if commonAncestor.name in frozenset(("table", "tbody", "tfoot", "thead", "tr")):
- parent, insertBefore = self.tree.getTableMisnestedNodePosition()
- parent.insertBefore(lastNode, insertBefore)
- else:
- commonAncestor.appendChild(lastNode)
-
- # Step 11
- clone = formattingElement.cloneNode()
-
- # Step 12
- furthestBlock.reparentChildren(clone)
-
- # Step 13
- furthestBlock.appendChild(clone)
-
- # Step 14
- self.tree.activeFormattingElements.remove(formattingElement)
- self.tree.activeFormattingElements.insert(bookmark, clone)
-
- # Step 15
- self.tree.openElements.remove(formattingElement)
- self.tree.openElements.insert(
- self.tree.openElements.index(furthestBlock) + 1, clone)
-
- def endTagAppletMarqueeObject(self, token):
- if self.tree.elementInScope(token["name"]):
- self.tree.generateImpliedEndTags()
- if self.tree.openElements[-1].name != token["name"]:
- self.parser.parseError("end-tag-too-early", {"name": token["name"]})
-
- if self.tree.elementInScope(token["name"]):
- element = self.tree.openElements.pop()
- while element.name != token["name"]:
- element = self.tree.openElements.pop()
- self.tree.clearActiveFormattingElements()
-
- def endTagBr(self, token):
- self.parser.parseError("unexpected-end-tag-treated-as",
- {"originalName": "br", "newName": "br element"})
- self.tree.reconstructActiveFormattingElements()
- self.tree.insertElement(impliedTagToken("br", "StartTag"))
- self.tree.openElements.pop()
-
- def endTagOther(self, token):
- for node in self.tree.openElements[::-1]:
- if node.name == token["name"]:
- self.tree.generateImpliedEndTags(exclude=token["name"])
- if self.tree.openElements[-1].name != token["name"]:
- self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
- while self.tree.openElements.pop() != node:
- pass
- break
- else:
- if node.nameTuple in specialElements:
- self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
- break
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", Phase.startTagHtml),
- (("base", "basefont", "bgsound", "command", "link", "meta",
- "script", "style", "title"),
- startTagProcessInHead),
- ("body", startTagBody),
- ("frameset", startTagFrameset),
- (("address", "article", "aside", "blockquote", "center", "details",
- "dir", "div", "dl", "fieldset", "figcaption", "figure",
- "footer", "header", "hgroup", "main", "menu", "nav", "ol", "p",
- "section", "summary", "ul"),
- startTagCloseP),
- (headingElements, startTagHeading),
- (("pre", "listing"), startTagPreListing),
- ("form", startTagForm),
- (("li", "dd", "dt"), startTagListItem),
- ("plaintext", startTagPlaintext),
- ("a", startTagA),
- (("b", "big", "code", "em", "font", "i", "s", "small", "strike",
- "strong", "tt", "u"), startTagFormatting),
- ("nobr", startTagNobr),
- ("button", startTagButton),
- (("applet", "marquee", "object"), startTagAppletMarqueeObject),
- ("xmp", startTagXmp),
- ("table", startTagTable),
- (("area", "br", "embed", "img", "keygen", "wbr"),
- startTagVoidFormatting),
- (("param", "source", "track"), startTagParamSource),
- ("input", startTagInput),
- ("hr", startTagHr),
- ("image", startTagImage),
- ("isindex", startTagIsIndex),
- ("textarea", startTagTextarea),
- ("iframe", startTagIFrame),
- ("noscript", startTagNoscript),
- (("noembed", "noframes"), startTagRawtext),
- ("select", startTagSelect),
- (("rp", "rt"), startTagRpRt),
- (("option", "optgroup"), startTagOpt),
- (("math"), startTagMath),
- (("svg"), startTagSvg),
- (("caption", "col", "colgroup", "frame", "head",
- "tbody", "td", "tfoot", "th", "thead",
- "tr"), startTagMisplaced)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- ("body", endTagBody),
- ("html", endTagHtml),
- (("address", "article", "aside", "blockquote", "button", "center",
- "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure",
- "footer", "header", "hgroup", "listing", "main", "menu", "nav", "ol", "pre",
- "section", "summary", "ul"), endTagBlock),
- ("form", endTagForm),
- ("p", endTagP),
- (("dd", "dt", "li"), endTagListItem),
- (headingElements, endTagHeading),
- (("a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small",
- "strike", "strong", "tt", "u"), endTagFormatting),
- (("applet", "marquee", "object"), endTagAppletMarqueeObject),
- ("br", endTagBr),
- ])
- endTagHandler.default = endTagOther
-
- class TextPhase(Phase):
- __slots__ = tuple()
-
- def processCharacters(self, token):
- self.tree.insertText(token["data"])
-
- def processEOF(self):
- self.parser.parseError("expected-named-closing-tag-but-got-eof",
- {"name": self.tree.openElements[-1].name})
- self.tree.openElements.pop()
- self.parser.phase = self.parser.originalPhase
- return True
-
- def startTagOther(self, token):
- assert False, "Tried to process start tag %s in RCDATA/RAWTEXT mode" % token['name']
-
- def endTagScript(self, token):
- node = self.tree.openElements.pop()
- assert node.name == "script"
- self.parser.phase = self.parser.originalPhase
- # The rest of this method is all stuff that only happens if
- # document.write works
-
- def endTagOther(self, token):
- self.tree.openElements.pop()
- self.parser.phase = self.parser.originalPhase
-
- startTagHandler = _utils.MethodDispatcher([])
- startTagHandler.default = startTagOther
- endTagHandler = _utils.MethodDispatcher([
- ("script", endTagScript)])
- endTagHandler.default = endTagOther
-
- class InTablePhase(Phase):
- # http://www.whatwg.org/specs/web-apps/current-work/#in-table
- __slots__ = tuple()
-
- # helper methods
- def clearStackToTableContext(self):
- # "clear the stack back to a table context"
- while self.tree.openElements[-1].name not in ("table", "html"):
- # self.parser.parseError("unexpected-implied-end-tag-in-table",
- # {"name": self.tree.openElements[-1].name})
- self.tree.openElements.pop()
- # When the current node is <html> it's an innerHTML case
-
- # processing methods
- def processEOF(self):
- if self.tree.openElements[-1].name != "html":
- self.parser.parseError("eof-in-table")
- else:
- assert self.parser.innerHTML
- # Stop parsing
-
- def processSpaceCharacters(self, token):
- originalPhase = self.parser.phase
- self.parser.phase = self.parser.phases["inTableText"]
- self.parser.phase.originalPhase = originalPhase
- self.parser.phase.processSpaceCharacters(token)
-
- def processCharacters(self, token):
- originalPhase = self.parser.phase
- self.parser.phase = self.parser.phases["inTableText"]
- self.parser.phase.originalPhase = originalPhase
- self.parser.phase.processCharacters(token)
-
- def insertText(self, token):
- # If we get here there must be at least one non-whitespace character
- # Do the table magic!
- self.tree.insertFromTable = True
- self.parser.phases["inBody"].processCharacters(token)
- self.tree.insertFromTable = False
-
- def startTagCaption(self, token):
- self.clearStackToTableContext()
- self.tree.activeFormattingElements.append(Marker)
- self.tree.insertElement(token)
- self.parser.phase = self.parser.phases["inCaption"]
-
- def startTagColgroup(self, token):
- self.clearStackToTableContext()
- self.tree.insertElement(token)
- self.parser.phase = self.parser.phases["inColumnGroup"]
-
- def startTagCol(self, token):
- self.startTagColgroup(impliedTagToken("colgroup", "StartTag"))
- return token
-
- def startTagRowGroup(self, token):
- self.clearStackToTableContext()
- self.tree.insertElement(token)
- self.parser.phase = self.parser.phases["inTableBody"]
-
- def startTagImplyTbody(self, token):
- self.startTagRowGroup(impliedTagToken("tbody", "StartTag"))
- return token
-
- def startTagTable(self, token):
- self.parser.parseError("unexpected-start-tag-implies-end-tag",
- {"startName": "table", "endName": "table"})
- self.parser.phase.processEndTag(impliedTagToken("table"))
- if not self.parser.innerHTML:
- return token
-
- def startTagStyleScript(self, token):
- return self.parser.phases["inHead"].processStartTag(token)
-
- def startTagInput(self, token):
- if ("type" in token["data"] and
- token["data"]["type"].translate(asciiUpper2Lower) == "hidden"):
- self.parser.parseError("unexpected-hidden-input-in-table")
- self.tree.insertElement(token)
- # XXX associate with form
- self.tree.openElements.pop()
- else:
- self.startTagOther(token)
-
- def startTagForm(self, token):
- self.parser.parseError("unexpected-form-in-table")
- if self.tree.formPointer is None:
- self.tree.insertElement(token)
- self.tree.formPointer = self.tree.openElements[-1]
- self.tree.openElements.pop()
-
- def startTagOther(self, token):
- self.parser.parseError("unexpected-start-tag-implies-table-voodoo", {"name": token["name"]})
- # Do the table magic!
- self.tree.insertFromTable = True
- self.parser.phases["inBody"].processStartTag(token)
- self.tree.insertFromTable = False
-
- def endTagTable(self, token):
- if self.tree.elementInScope("table", variant="table"):
- self.tree.generateImpliedEndTags()
- if self.tree.openElements[-1].name != "table":
- self.parser.parseError("end-tag-too-early-named",
- {"gotName": "table",
- "expectedName": self.tree.openElements[-1].name})
- while self.tree.openElements[-1].name != "table":
- self.tree.openElements.pop()
- self.tree.openElements.pop()
- self.parser.resetInsertionMode()
- else:
- # innerHTML case
- assert self.parser.innerHTML
- self.parser.parseError()
-
- def endTagIgnore(self, token):
- self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
-
- def endTagOther(self, token):
- self.parser.parseError("unexpected-end-tag-implies-table-voodoo", {"name": token["name"]})
- # Do the table magic!
- self.tree.insertFromTable = True
- self.parser.phases["inBody"].processEndTag(token)
- self.tree.insertFromTable = False
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", Phase.startTagHtml),
- ("caption", startTagCaption),
- ("colgroup", startTagColgroup),
- ("col", startTagCol),
- (("tbody", "tfoot", "thead"), startTagRowGroup),
- (("td", "th", "tr"), startTagImplyTbody),
- ("table", startTagTable),
- (("style", "script"), startTagStyleScript),
- ("input", startTagInput),
- ("form", startTagForm)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- ("table", endTagTable),
- (("body", "caption", "col", "colgroup", "html", "tbody", "td",
- "tfoot", "th", "thead", "tr"), endTagIgnore)
- ])
- endTagHandler.default = endTagOther
-
- class InTableTextPhase(Phase):
- __slots__ = ("originalPhase", "characterTokens")
-
- def __init__(self, *args, **kwargs):
- super(InTableTextPhase, self).__init__(*args, **kwargs)
- self.originalPhase = None
- self.characterTokens = []
-
- def flushCharacters(self):
- data = "".join([item["data"] for item in self.characterTokens])
- if any([item not in spaceCharacters for item in data]):
- token = {"type": tokenTypes["Characters"], "data": data}
- self.parser.phases["inTable"].insertText(token)
- elif data:
- self.tree.insertText(data)
- self.characterTokens = []
-
- def processComment(self, token):
- self.flushCharacters()
- self.parser.phase = self.originalPhase
- return token
-
- def processEOF(self):
- self.flushCharacters()
- self.parser.phase = self.originalPhase
- return True
-
- def processCharacters(self, token):
- if token["data"] == "\u0000":
- return
- self.characterTokens.append(token)
-
- def processSpaceCharacters(self, token):
- # pretty sure we should never reach here
- self.characterTokens.append(token)
- # assert False
-
- def processStartTag(self, token):
- self.flushCharacters()
- self.parser.phase = self.originalPhase
- return token
-
- def processEndTag(self, token):
- self.flushCharacters()
- self.parser.phase = self.originalPhase
- return token
-
- class InCaptionPhase(Phase):
- # http://www.whatwg.org/specs/web-apps/current-work/#in-caption
- __slots__ = tuple()
-
- def ignoreEndTagCaption(self):
- return not self.tree.elementInScope("caption", variant="table")
-
- def processEOF(self):
- self.parser.phases["inBody"].processEOF()
-
- def processCharacters(self, token):
- return self.parser.phases["inBody"].processCharacters(token)
-
- def startTagTableElement(self, token):
- self.parser.parseError()
- # XXX Have to duplicate logic here to find out if the tag is ignored
- ignoreEndTag = self.ignoreEndTagCaption()
- self.parser.phase.processEndTag(impliedTagToken("caption"))
- if not ignoreEndTag:
- return token
-
- def startTagOther(self, token):
- return self.parser.phases["inBody"].processStartTag(token)
-
- def endTagCaption(self, token):
- if not self.ignoreEndTagCaption():
- # AT this code is quite similar to endTagTable in "InTable"
- self.tree.generateImpliedEndTags()
- if self.tree.openElements[-1].name != "caption":
- self.parser.parseError("expected-one-end-tag-but-got-another",
- {"gotName": "caption",
- "expectedName": self.tree.openElements[-1].name})
- while self.tree.openElements[-1].name != "caption":
- self.tree.openElements.pop()
- self.tree.openElements.pop()
- self.tree.clearActiveFormattingElements()
- self.parser.phase = self.parser.phases["inTable"]
- else:
- # innerHTML case
- assert self.parser.innerHTML
- self.parser.parseError()
-
- def endTagTable(self, token):
- self.parser.parseError()
- ignoreEndTag = self.ignoreEndTagCaption()
- self.parser.phase.processEndTag(impliedTagToken("caption"))
- if not ignoreEndTag:
- return token
-
- def endTagIgnore(self, token):
- self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
-
- def endTagOther(self, token):
- return self.parser.phases["inBody"].processEndTag(token)
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", Phase.startTagHtml),
- (("caption", "col", "colgroup", "tbody", "td", "tfoot", "th",
- "thead", "tr"), startTagTableElement)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- ("caption", endTagCaption),
- ("table", endTagTable),
- (("body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th",
- "thead", "tr"), endTagIgnore)
- ])
- endTagHandler.default = endTagOther
-
- class InColumnGroupPhase(Phase):
- # http://www.whatwg.org/specs/web-apps/current-work/#in-column
- __slots__ = tuple()
-
- def ignoreEndTagColgroup(self):
- return self.tree.openElements[-1].name == "html"
-
- def processEOF(self):
- if self.tree.openElements[-1].name == "html":
- assert self.parser.innerHTML
- return
- else:
- ignoreEndTag = self.ignoreEndTagColgroup()
- self.endTagColgroup(impliedTagToken("colgroup"))
- if not ignoreEndTag:
- return True
-
- def processCharacters(self, token):
- ignoreEndTag = self.ignoreEndTagColgroup()
- self.endTagColgroup(impliedTagToken("colgroup"))
- if not ignoreEndTag:
- return token
-
- def startTagCol(self, token):
- self.tree.insertElement(token)
- self.tree.openElements.pop()
- token["selfClosingAcknowledged"] = True
-
- def startTagOther(self, token):
- ignoreEndTag = self.ignoreEndTagColgroup()
- self.endTagColgroup(impliedTagToken("colgroup"))
- if not ignoreEndTag:
- return token
-
- def endTagColgroup(self, token):
- if self.ignoreEndTagColgroup():
- # innerHTML case
- assert self.parser.innerHTML
- self.parser.parseError()
- else:
- self.tree.openElements.pop()
- self.parser.phase = self.parser.phases["inTable"]
-
- def endTagCol(self, token):
- self.parser.parseError("no-end-tag", {"name": "col"})
-
- def endTagOther(self, token):
- ignoreEndTag = self.ignoreEndTagColgroup()
- self.endTagColgroup(impliedTagToken("colgroup"))
- if not ignoreEndTag:
- return token
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", Phase.startTagHtml),
- ("col", startTagCol)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- ("colgroup", endTagColgroup),
- ("col", endTagCol)
- ])
- endTagHandler.default = endTagOther
-
- class InTableBodyPhase(Phase):
- # http://www.whatwg.org/specs/web-apps/current-work/#in-table0
- __slots__ = tuple()
-
- # helper methods
- def clearStackToTableBodyContext(self):
- while self.tree.openElements[-1].name not in ("tbody", "tfoot",
- "thead", "html"):
- # self.parser.parseError("unexpected-implied-end-tag-in-table",
- # {"name": self.tree.openElements[-1].name})
- self.tree.openElements.pop()
- if self.tree.openElements[-1].name == "html":
- assert self.parser.innerHTML
-
- # the rest
- def processEOF(self):
- self.parser.phases["inTable"].processEOF()
-
- def processSpaceCharacters(self, token):
- return self.parser.phases["inTable"].processSpaceCharacters(token)
-
- def processCharacters(self, token):
- return self.parser.phases["inTable"].processCharacters(token)
-
- def startTagTr(self, token):
- self.clearStackToTableBodyContext()
- self.tree.insertElement(token)
- self.parser.phase = self.parser.phases["inRow"]
-
- def startTagTableCell(self, token):
- self.parser.parseError("unexpected-cell-in-table-body",
- {"name": token["name"]})
- self.startTagTr(impliedTagToken("tr", "StartTag"))
- return token
-
- def startTagTableOther(self, token):
- # XXX AT Any ideas on how to share this with endTagTable?
- if (self.tree.elementInScope("tbody", variant="table") or
- self.tree.elementInScope("thead", variant="table") or
- self.tree.elementInScope("tfoot", variant="table")):
- self.clearStackToTableBodyContext()
- self.endTagTableRowGroup(
- impliedTagToken(self.tree.openElements[-1].name))
- return token
- else:
- # innerHTML case
- assert self.parser.innerHTML
- self.parser.parseError()
-
- def startTagOther(self, token):
- return self.parser.phases["inTable"].processStartTag(token)
-
- def endTagTableRowGroup(self, token):
- if self.tree.elementInScope(token["name"], variant="table"):
- self.clearStackToTableBodyContext()
- self.tree.openElements.pop()
- self.parser.phase = self.parser.phases["inTable"]
- else:
- self.parser.parseError("unexpected-end-tag-in-table-body",
- {"name": token["name"]})
-
- def endTagTable(self, token):
- if (self.tree.elementInScope("tbody", variant="table") or
- self.tree.elementInScope("thead", variant="table") or
- self.tree.elementInScope("tfoot", variant="table")):
- self.clearStackToTableBodyContext()
- self.endTagTableRowGroup(
- impliedTagToken(self.tree.openElements[-1].name))
- return token
- else:
- # innerHTML case
- assert self.parser.innerHTML
- self.parser.parseError()
-
- def endTagIgnore(self, token):
- self.parser.parseError("unexpected-end-tag-in-table-body",
- {"name": token["name"]})
-
- def endTagOther(self, token):
- return self.parser.phases["inTable"].processEndTag(token)
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", Phase.startTagHtml),
- ("tr", startTagTr),
- (("td", "th"), startTagTableCell),
- (("caption", "col", "colgroup", "tbody", "tfoot", "thead"),
- startTagTableOther)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- (("tbody", "tfoot", "thead"), endTagTableRowGroup),
- ("table", endTagTable),
- (("body", "caption", "col", "colgroup", "html", "td", "th",
- "tr"), endTagIgnore)
- ])
- endTagHandler.default = endTagOther
-
- class InRowPhase(Phase):
- # http://www.whatwg.org/specs/web-apps/current-work/#in-row
- __slots__ = tuple()
-
- # helper methods (XXX unify this with other table helper methods)
- def clearStackToTableRowContext(self):
- while self.tree.openElements[-1].name not in ("tr", "html"):
- self.parser.parseError("unexpected-implied-end-tag-in-table-row",
- {"name": self.tree.openElements[-1].name})
- self.tree.openElements.pop()
-
- def ignoreEndTagTr(self):
- return not self.tree.elementInScope("tr", variant="table")
-
- # the rest
- def processEOF(self):
- self.parser.phases["inTable"].processEOF()
-
- def processSpaceCharacters(self, token):
- return self.parser.phases["inTable"].processSpaceCharacters(token)
-
- def processCharacters(self, token):
- return self.parser.phases["inTable"].processCharacters(token)
-
- def startTagTableCell(self, token):
- self.clearStackToTableRowContext()
- self.tree.insertElement(token)
- self.parser.phase = self.parser.phases["inCell"]
- self.tree.activeFormattingElements.append(Marker)
-
- def startTagTableOther(self, token):
- ignoreEndTag = self.ignoreEndTagTr()
- self.endTagTr(impliedTagToken("tr"))
- # XXX how are we sure it's always ignored in the innerHTML case?
- if not ignoreEndTag:
- return token
-
- def startTagOther(self, token):
- return self.parser.phases["inTable"].processStartTag(token)
-
- def endTagTr(self, token):
- if not self.ignoreEndTagTr():
- self.clearStackToTableRowContext()
- self.tree.openElements.pop()
- self.parser.phase = self.parser.phases["inTableBody"]
- else:
- # innerHTML case
- assert self.parser.innerHTML
- self.parser.parseError()
-
- def endTagTable(self, token):
- ignoreEndTag = self.ignoreEndTagTr()
- self.endTagTr(impliedTagToken("tr"))
- # Reprocess the current tag if the tr end tag was not ignored
- # XXX how are we sure it's always ignored in the innerHTML case?
- if not ignoreEndTag:
- return token
-
- def endTagTableRowGroup(self, token):
- if self.tree.elementInScope(token["name"], variant="table"):
- self.endTagTr(impliedTagToken("tr"))
- return token
- else:
- self.parser.parseError()
-
- def endTagIgnore(self, token):
- self.parser.parseError("unexpected-end-tag-in-table-row",
- {"name": token["name"]})
-
- def endTagOther(self, token):
- return self.parser.phases["inTable"].processEndTag(token)
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", Phase.startTagHtml),
- (("td", "th"), startTagTableCell),
- (("caption", "col", "colgroup", "tbody", "tfoot", "thead",
- "tr"), startTagTableOther)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- ("tr", endTagTr),
- ("table", endTagTable),
- (("tbody", "tfoot", "thead"), endTagTableRowGroup),
- (("body", "caption", "col", "colgroup", "html", "td", "th"),
- endTagIgnore)
- ])
- endTagHandler.default = endTagOther
-
- class InCellPhase(Phase):
- # http://www.whatwg.org/specs/web-apps/current-work/#in-cell
- __slots__ = tuple()
-
- # helper
- def closeCell(self):
- if self.tree.elementInScope("td", variant="table"):
- self.endTagTableCell(impliedTagToken("td"))
- elif self.tree.elementInScope("th", variant="table"):
- self.endTagTableCell(impliedTagToken("th"))
-
- # the rest
- def processEOF(self):
- self.parser.phases["inBody"].processEOF()
-
- def processCharacters(self, token):
- return self.parser.phases["inBody"].processCharacters(token)
-
- def startTagTableOther(self, token):
- if (self.tree.elementInScope("td", variant="table") or
- self.tree.elementInScope("th", variant="table")):
- self.closeCell()
- return token
- else:
- # innerHTML case
- assert self.parser.innerHTML
- self.parser.parseError()
-
- def startTagOther(self, token):
- return self.parser.phases["inBody"].processStartTag(token)
-
- def endTagTableCell(self, token):
- if self.tree.elementInScope(token["name"], variant="table"):
- self.tree.generateImpliedEndTags(token["name"])
- if self.tree.openElements[-1].name != token["name"]:
- self.parser.parseError("unexpected-cell-end-tag",
- {"name": token["name"]})
- while True:
- node = self.tree.openElements.pop()
- if node.name == token["name"]:
- break
- else:
- self.tree.openElements.pop()
- self.tree.clearActiveFormattingElements()
- self.parser.phase = self.parser.phases["inRow"]
- else:
- self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
-
- def endTagIgnore(self, token):
- self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
-
- def endTagImply(self, token):
- if self.tree.elementInScope(token["name"], variant="table"):
- self.closeCell()
- return token
- else:
- # sometimes innerHTML case
- self.parser.parseError()
-
- def endTagOther(self, token):
- return self.parser.phases["inBody"].processEndTag(token)
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", Phase.startTagHtml),
- (("caption", "col", "colgroup", "tbody", "td", "tfoot", "th",
- "thead", "tr"), startTagTableOther)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- (("td", "th"), endTagTableCell),
- (("body", "caption", "col", "colgroup", "html"), endTagIgnore),
- (("table", "tbody", "tfoot", "thead", "tr"), endTagImply)
- ])
- endTagHandler.default = endTagOther
-
- class InSelectPhase(Phase):
- __slots__ = tuple()
-
- # http://www.whatwg.org/specs/web-apps/current-work/#in-select
- def processEOF(self):
- if self.tree.openElements[-1].name != "html":
- self.parser.parseError("eof-in-select")
- else:
- assert self.parser.innerHTML
-
- def processCharacters(self, token):
- if token["data"] == "\u0000":
- return
- self.tree.insertText(token["data"])
-
- def startTagOption(self, token):
- # We need to imply </option> if <option> is the current node.
- if self.tree.openElements[-1].name == "option":
- self.tree.openElements.pop()
- self.tree.insertElement(token)
-
- def startTagOptgroup(self, token):
- if self.tree.openElements[-1].name == "option":
- self.tree.openElements.pop()
- if self.tree.openElements[-1].name == "optgroup":
- self.tree.openElements.pop()
- self.tree.insertElement(token)
-
- def startTagSelect(self, token):
- self.parser.parseError("unexpected-select-in-select")
- self.endTagSelect(impliedTagToken("select"))
-
- def startTagInput(self, token):
- self.parser.parseError("unexpected-input-in-select")
- if self.tree.elementInScope("select", variant="select"):
- self.endTagSelect(impliedTagToken("select"))
- return token
- else:
- assert self.parser.innerHTML
-
- def startTagScript(self, token):
- return self.parser.phases["inHead"].processStartTag(token)
-
- def startTagOther(self, token):
- self.parser.parseError("unexpected-start-tag-in-select",
- {"name": token["name"]})
-
- def endTagOption(self, token):
- if self.tree.openElements[-1].name == "option":
- self.tree.openElements.pop()
- else:
- self.parser.parseError("unexpected-end-tag-in-select",
- {"name": "option"})
-
- def endTagOptgroup(self, token):
- # </optgroup> implicitly closes <option>
- if (self.tree.openElements[-1].name == "option" and
- self.tree.openElements[-2].name == "optgroup"):
- self.tree.openElements.pop()
- # It also closes </optgroup>
- if self.tree.openElements[-1].name == "optgroup":
- self.tree.openElements.pop()
- # But nothing else
- else:
- self.parser.parseError("unexpected-end-tag-in-select",
- {"name": "optgroup"})
-
- def endTagSelect(self, token):
- if self.tree.elementInScope("select", variant="select"):
- node = self.tree.openElements.pop()
- while node.name != "select":
- node = self.tree.openElements.pop()
- self.parser.resetInsertionMode()
- else:
- # innerHTML case
- assert self.parser.innerHTML
- self.parser.parseError()
-
- def endTagOther(self, token):
- self.parser.parseError("unexpected-end-tag-in-select",
- {"name": token["name"]})
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", Phase.startTagHtml),
- ("option", startTagOption),
- ("optgroup", startTagOptgroup),
- ("select", startTagSelect),
- (("input", "keygen", "textarea"), startTagInput),
- ("script", startTagScript)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- ("option", endTagOption),
- ("optgroup", endTagOptgroup),
- ("select", endTagSelect)
- ])
- endTagHandler.default = endTagOther
-
- class InSelectInTablePhase(Phase):
- __slots__ = tuple()
-
- def processEOF(self):
- self.parser.phases["inSelect"].processEOF()
-
- def processCharacters(self, token):
- return self.parser.phases["inSelect"].processCharacters(token)
-
- def startTagTable(self, token):
- self.parser.parseError("unexpected-table-element-start-tag-in-select-in-table", {"name": token["name"]})
- self.endTagOther(impliedTagToken("select"))
- return token
-
- def startTagOther(self, token):
- return self.parser.phases["inSelect"].processStartTag(token)
-
- def endTagTable(self, token):
- self.parser.parseError("unexpected-table-element-end-tag-in-select-in-table", {"name": token["name"]})
- if self.tree.elementInScope(token["name"], variant="table"):
- self.endTagOther(impliedTagToken("select"))
- return token
-
- def endTagOther(self, token):
- return self.parser.phases["inSelect"].processEndTag(token)
-
- startTagHandler = _utils.MethodDispatcher([
- (("caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"),
- startTagTable)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- (("caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"),
- endTagTable)
- ])
- endTagHandler.default = endTagOther
-
- class InForeignContentPhase(Phase):
- __slots__ = tuple()
-
- breakoutElements = frozenset(["b", "big", "blockquote", "body", "br",
- "center", "code", "dd", "div", "dl", "dt",
- "em", "embed", "h1", "h2", "h3",
- "h4", "h5", "h6", "head", "hr", "i", "img",
- "li", "listing", "menu", "meta", "nobr",
- "ol", "p", "pre", "ruby", "s", "small",
- "span", "strong", "strike", "sub", "sup",
- "table", "tt", "u", "ul", "var"])
-
- def adjustSVGTagNames(self, token):
- replacements = {"altglyph": "altGlyph",
- "altglyphdef": "altGlyphDef",
- "altglyphitem": "altGlyphItem",
- "animatecolor": "animateColor",
- "animatemotion": "animateMotion",
- "animatetransform": "animateTransform",
- "clippath": "clipPath",
- "feblend": "feBlend",
- "fecolormatrix": "feColorMatrix",
- "fecomponenttransfer": "feComponentTransfer",
- "fecomposite": "feComposite",
- "feconvolvematrix": "feConvolveMatrix",
- "fediffuselighting": "feDiffuseLighting",
- "fedisplacementmap": "feDisplacementMap",
- "fedistantlight": "feDistantLight",
- "feflood": "feFlood",
- "fefunca": "feFuncA",
- "fefuncb": "feFuncB",
- "fefuncg": "feFuncG",
- "fefuncr": "feFuncR",
- "fegaussianblur": "feGaussianBlur",
- "feimage": "feImage",
- "femerge": "feMerge",
- "femergenode": "feMergeNode",
- "femorphology": "feMorphology",
- "feoffset": "feOffset",
- "fepointlight": "fePointLight",
- "fespecularlighting": "feSpecularLighting",
- "fespotlight": "feSpotLight",
- "fetile": "feTile",
- "feturbulence": "feTurbulence",
- "foreignobject": "foreignObject",
- "glyphref": "glyphRef",
- "lineargradient": "linearGradient",
- "radialgradient": "radialGradient",
- "textpath": "textPath"}
-
- if token["name"] in replacements:
- token["name"] = replacements[token["name"]]
-
- def processCharacters(self, token):
- if token["data"] == "\u0000":
- token["data"] = "\uFFFD"
- elif (self.parser.framesetOK and
- any(char not in spaceCharacters for char in token["data"])):
- self.parser.framesetOK = False
- Phase.processCharacters(self, token)
-
- def processStartTag(self, token):
- currentNode = self.tree.openElements[-1]
- if (token["name"] in self.breakoutElements or
- (token["name"] == "font" and
- set(token["data"].keys()) & {"color", "face", "size"})):
- self.parser.parseError("unexpected-html-element-in-foreign-content",
- {"name": token["name"]})
- while (self.tree.openElements[-1].namespace !=
- self.tree.defaultNamespace and
- not self.parser.isHTMLIntegrationPoint(self.tree.openElements[-1]) and
- not self.parser.isMathMLTextIntegrationPoint(self.tree.openElements[-1])):
- self.tree.openElements.pop()
- return token
-
- else:
- if currentNode.namespace == namespaces["mathml"]:
- self.parser.adjustMathMLAttributes(token)
- elif currentNode.namespace == namespaces["svg"]:
- self.adjustSVGTagNames(token)
- self.parser.adjustSVGAttributes(token)
- self.parser.adjustForeignAttributes(token)
- token["namespace"] = currentNode.namespace
- self.tree.insertElement(token)
- if token["selfClosing"]:
- self.tree.openElements.pop()
- token["selfClosingAcknowledged"] = True
-
- def processEndTag(self, token):
- nodeIndex = len(self.tree.openElements) - 1
- node = self.tree.openElements[-1]
- if node.name.translate(asciiUpper2Lower) != token["name"]:
- self.parser.parseError("unexpected-end-tag", {"name": token["name"]})
-
- while True:
- if node.name.translate(asciiUpper2Lower) == token["name"]:
- # XXX this isn't in the spec but it seems necessary
- if self.parser.phase == self.parser.phases["inTableText"]:
- self.parser.phase.flushCharacters()
- self.parser.phase = self.parser.phase.originalPhase
- while self.tree.openElements.pop() != node:
- assert self.tree.openElements
- new_token = None
- break
- nodeIndex -= 1
-
- node = self.tree.openElements[nodeIndex]
- if node.namespace != self.tree.defaultNamespace:
- continue
- else:
- new_token = self.parser.phase.processEndTag(token)
- break
- return new_token
-
- class AfterBodyPhase(Phase):
- __slots__ = tuple()
-
- def processEOF(self):
- # Stop parsing
- pass
-
- def processComment(self, token):
- # This is needed because data is to be appended to the <html> element
- # here and not to whatever is currently open.
- self.tree.insertComment(token, self.tree.openElements[0])
-
- def processCharacters(self, token):
- self.parser.parseError("unexpected-char-after-body")
- self.parser.phase = self.parser.phases["inBody"]
- return token
-
- def startTagHtml(self, token):
- return self.parser.phases["inBody"].processStartTag(token)
-
- def startTagOther(self, token):
- self.parser.parseError("unexpected-start-tag-after-body",
- {"name": token["name"]})
- self.parser.phase = self.parser.phases["inBody"]
- return token
-
- def endTagHtml(self, name):
- if self.parser.innerHTML:
- self.parser.parseError("unexpected-end-tag-after-body-innerhtml")
- else:
- self.parser.phase = self.parser.phases["afterAfterBody"]
-
- def endTagOther(self, token):
- self.parser.parseError("unexpected-end-tag-after-body",
- {"name": token["name"]})
- self.parser.phase = self.parser.phases["inBody"]
- return token
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", startTagHtml)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([("html", endTagHtml)])
- endTagHandler.default = endTagOther
-
- class InFramesetPhase(Phase):
- # http://www.whatwg.org/specs/web-apps/current-work/#in-frameset
- __slots__ = tuple()
-
- def processEOF(self):
- if self.tree.openElements[-1].name != "html":
- self.parser.parseError("eof-in-frameset")
- else:
- assert self.parser.innerHTML
-
- def processCharacters(self, token):
- self.parser.parseError("unexpected-char-in-frameset")
-
- def startTagFrameset(self, token):
- self.tree.insertElement(token)
-
- def startTagFrame(self, token):
- self.tree.insertElement(token)
- self.tree.openElements.pop()
-
- def startTagNoframes(self, token):
- return self.parser.phases["inBody"].processStartTag(token)
-
- def startTagOther(self, token):
- self.parser.parseError("unexpected-start-tag-in-frameset",
- {"name": token["name"]})
-
- def endTagFrameset(self, token):
- if self.tree.openElements[-1].name == "html":
- # innerHTML case
- self.parser.parseError("unexpected-frameset-in-frameset-innerhtml")
- else:
- self.tree.openElements.pop()
- if (not self.parser.innerHTML and
- self.tree.openElements[-1].name != "frameset"):
- # If we're not in innerHTML mode and the current node is not a
- # "frameset" element (anymore) then switch.
- self.parser.phase = self.parser.phases["afterFrameset"]
-
- def endTagOther(self, token):
- self.parser.parseError("unexpected-end-tag-in-frameset",
- {"name": token["name"]})
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", Phase.startTagHtml),
- ("frameset", startTagFrameset),
- ("frame", startTagFrame),
- ("noframes", startTagNoframes)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- ("frameset", endTagFrameset)
- ])
- endTagHandler.default = endTagOther
-
- class AfterFramesetPhase(Phase):
- # http://www.whatwg.org/specs/web-apps/current-work/#after3
- __slots__ = tuple()
-
- def processEOF(self):
- # Stop parsing
- pass
-
- def processCharacters(self, token):
- self.parser.parseError("unexpected-char-after-frameset")
-
- def startTagNoframes(self, token):
- return self.parser.phases["inHead"].processStartTag(token)
-
- def startTagOther(self, token):
- self.parser.parseError("unexpected-start-tag-after-frameset",
- {"name": token["name"]})
-
- def endTagHtml(self, token):
- self.parser.phase = self.parser.phases["afterAfterFrameset"]
-
- def endTagOther(self, token):
- self.parser.parseError("unexpected-end-tag-after-frameset",
- {"name": token["name"]})
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", Phase.startTagHtml),
- ("noframes", startTagNoframes)
- ])
- startTagHandler.default = startTagOther
-
- endTagHandler = _utils.MethodDispatcher([
- ("html", endTagHtml)
- ])
- endTagHandler.default = endTagOther
-
- class AfterAfterBodyPhase(Phase):
- __slots__ = tuple()
-
- def processEOF(self):
- pass
-
- def processComment(self, token):
- self.tree.insertComment(token, self.tree.document)
-
- def processSpaceCharacters(self, token):
- return self.parser.phases["inBody"].processSpaceCharacters(token)
-
- def processCharacters(self, token):
- self.parser.parseError("expected-eof-but-got-char")
- self.parser.phase = self.parser.phases["inBody"]
- return token
-
- def startTagHtml(self, token):
- return self.parser.phases["inBody"].processStartTag(token)
-
- def startTagOther(self, token):
- self.parser.parseError("expected-eof-but-got-start-tag",
- {"name": token["name"]})
- self.parser.phase = self.parser.phases["inBody"]
- return token
-
- def processEndTag(self, token):
- self.parser.parseError("expected-eof-but-got-end-tag",
- {"name": token["name"]})
- self.parser.phase = self.parser.phases["inBody"]
- return token
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", startTagHtml)
- ])
- startTagHandler.default = startTagOther
-
- class AfterAfterFramesetPhase(Phase):
- __slots__ = tuple()
-
- def processEOF(self):
- pass
-
- def processComment(self, token):
- self.tree.insertComment(token, self.tree.document)
-
- def processSpaceCharacters(self, token):
- return self.parser.phases["inBody"].processSpaceCharacters(token)
-
- def processCharacters(self, token):
- self.parser.parseError("expected-eof-but-got-char")
-
- def startTagHtml(self, token):
- return self.parser.phases["inBody"].processStartTag(token)
-
- def startTagNoFrames(self, token):
- return self.parser.phases["inHead"].processStartTag(token)
-
- def startTagOther(self, token):
- self.parser.parseError("expected-eof-but-got-start-tag",
- {"name": token["name"]})
-
- def processEndTag(self, token):
- self.parser.parseError("expected-eof-but-got-end-tag",
- {"name": token["name"]})
-
- startTagHandler = _utils.MethodDispatcher([
- ("html", startTagHtml),
- ("noframes", startTagNoFrames)
- ])
- startTagHandler.default = startTagOther
-
- # pylint:enable=unused-argument
-
- return {
- "initial": InitialPhase,
- "beforeHtml": BeforeHtmlPhase,
- "beforeHead": BeforeHeadPhase,
- "inHead": InHeadPhase,
- "inHeadNoscript": InHeadNoscriptPhase,
- "afterHead": AfterHeadPhase,
- "inBody": InBodyPhase,
- "text": TextPhase,
- "inTable": InTablePhase,
- "inTableText": InTableTextPhase,
- "inCaption": InCaptionPhase,
- "inColumnGroup": InColumnGroupPhase,
- "inTableBody": InTableBodyPhase,
- "inRow": InRowPhase,
- "inCell": InCellPhase,
- "inSelect": InSelectPhase,
- "inSelectInTable": InSelectInTablePhase,
- "inForeignContent": InForeignContentPhase,
- "afterBody": AfterBodyPhase,
- "inFrameset": InFramesetPhase,
- "afterFrameset": AfterFramesetPhase,
- "afterAfterBody": AfterAfterBodyPhase,
- "afterAfterFrameset": AfterAfterFramesetPhase,
- # XXX after after frameset
- }
-
-
-def adjust_attributes(token, replacements):
- needs_adjustment = viewkeys(token['data']) & viewkeys(replacements)
- if needs_adjustment:
- token['data'] = type(token['data'])((replacements.get(k, k), v)
- for k, v in token['data'].items())
-
-
-def impliedTagToken(name, type="EndTag", attributes=None,
- selfClosing=False):
- if attributes is None:
- attributes = {}
- return {"type": tokenTypes[type], "name": name, "data": attributes,
- "selfClosing": selfClosing}
-
-
-class ParseError(Exception):
- """Error in parsed document"""
- pass
diff --git a/src/pip/_vendor/html5lib/serializer.py b/src/pip/_vendor/html5lib/serializer.py
deleted file mode 100644
index d5669d8c1..000000000
--- a/src/pip/_vendor/html5lib/serializer.py
+++ /dev/null
@@ -1,409 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-from pip._vendor.six import text_type
-
-import re
-
-from codecs import register_error, xmlcharrefreplace_errors
-
-from .constants import voidElements, booleanAttributes, spaceCharacters
-from .constants import rcdataElements, entities, xmlEntities
-from . import treewalkers, _utils
-from xml.sax.saxutils import escape
-
-_quoteAttributeSpecChars = "".join(spaceCharacters) + "\"'=<>`"
-_quoteAttributeSpec = re.compile("[" + _quoteAttributeSpecChars + "]")
-_quoteAttributeLegacy = re.compile("[" + _quoteAttributeSpecChars +
- "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n"
- "\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15"
- "\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
- "\x20\x2f\x60\xa0\u1680\u180e\u180f\u2000"
- "\u2001\u2002\u2003\u2004\u2005\u2006\u2007"
- "\u2008\u2009\u200a\u2028\u2029\u202f\u205f"
- "\u3000]")
-
-
-_encode_entity_map = {}
-_is_ucs4 = len("\U0010FFFF") == 1
-for k, v in list(entities.items()):
- # skip multi-character entities
- if ((_is_ucs4 and len(v) > 1) or
- (not _is_ucs4 and len(v) > 2)):
- continue
- if v != "&":
- if len(v) == 2:
- v = _utils.surrogatePairToCodepoint(v)
- else:
- v = ord(v)
- if v not in _encode_entity_map or k.islower():
- # prefer &lt; over &LT; and similarly for &amp;, &gt;, etc.
- _encode_entity_map[v] = k
-
-
-def htmlentityreplace_errors(exc):
- if isinstance(exc, (UnicodeEncodeError, UnicodeTranslateError)):
- res = []
- codepoints = []
- skip = False
- for i, c in enumerate(exc.object[exc.start:exc.end]):
- if skip:
- skip = False
- continue
- index = i + exc.start
- if _utils.isSurrogatePair(exc.object[index:min([exc.end, index + 2])]):
- codepoint = _utils.surrogatePairToCodepoint(exc.object[index:index + 2])
- skip = True
- else:
- codepoint = ord(c)
- codepoints.append(codepoint)
- for cp in codepoints:
- e = _encode_entity_map.get(cp)
- if e:
- res.append("&")
- res.append(e)
- if not e.endswith(";"):
- res.append(";")
- else:
- res.append("&#x%s;" % (hex(cp)[2:]))
- return ("".join(res), exc.end)
- else:
- return xmlcharrefreplace_errors(exc)
-
-
-register_error("htmlentityreplace", htmlentityreplace_errors)
-
-
-def serialize(input, tree="etree", encoding=None, **serializer_opts):
- """Serializes the input token stream using the specified treewalker
-
- :arg input: the token stream to serialize
-
- :arg tree: the treewalker to use
-
- :arg encoding: the encoding to use
-
- :arg serializer_opts: any options to pass to the
- :py:class:`html5lib.serializer.HTMLSerializer` that gets created
-
- :returns: the tree serialized as a string
-
- Example:
-
- >>> from html5lib.html5parser import parse
- >>> from html5lib.serializer import serialize
- >>> token_stream = parse('<html><body><p>Hi!</p></body></html>')
- >>> serialize(token_stream, omit_optional_tags=False)
- '<html><head></head><body><p>Hi!</p></body></html>'
-
- """
- # XXX: Should we cache this?
- walker = treewalkers.getTreeWalker(tree)
- s = HTMLSerializer(**serializer_opts)
- return s.render(walker(input), encoding)
-
-
-class HTMLSerializer(object):
-
- # attribute quoting options
- quote_attr_values = "legacy" # be secure by default
- quote_char = '"'
- use_best_quote_char = True
-
- # tag syntax options
- omit_optional_tags = True
- minimize_boolean_attributes = True
- use_trailing_solidus = False
- space_before_trailing_solidus = True
-
- # escaping options
- escape_lt_in_attrs = False
- escape_rcdata = False
- resolve_entities = True
-
- # miscellaneous options
- alphabetical_attributes = False
- inject_meta_charset = True
- strip_whitespace = False
- sanitize = False
-
- options = ("quote_attr_values", "quote_char", "use_best_quote_char",
- "omit_optional_tags", "minimize_boolean_attributes",
- "use_trailing_solidus", "space_before_trailing_solidus",
- "escape_lt_in_attrs", "escape_rcdata", "resolve_entities",
- "alphabetical_attributes", "inject_meta_charset",
- "strip_whitespace", "sanitize")
-
- def __init__(self, **kwargs):
- """Initialize HTMLSerializer
-
- :arg inject_meta_charset: Whether or not to inject the meta charset.
-
- Defaults to ``True``.
-
- :arg quote_attr_values: Whether to quote attribute values that don't
- require quoting per legacy browser behavior (``"legacy"``), when
- required by the standard (``"spec"``), or always (``"always"``).
-
- Defaults to ``"legacy"``.
-
- :arg quote_char: Use given quote character for attribute quoting.
-
- Defaults to ``"`` which will use double quotes unless attribute
- value contains a double quote, in which case single quotes are
- used.
-
- :arg escape_lt_in_attrs: Whether or not to escape ``<`` in attribute
- values.
-
- Defaults to ``False``.
-
- :arg escape_rcdata: Whether to escape characters that need to be
- escaped within normal elements within rcdata elements such as
- style.
-
- Defaults to ``False``.
-
- :arg resolve_entities: Whether to resolve named character entities that
- appear in the source tree. The XML predefined entities &lt; &gt;
- &amp; &quot; &apos; are unaffected by this setting.
-
- Defaults to ``True``.
-
- :arg strip_whitespace: Whether to remove semantically meaningless
- whitespace. (This compresses all whitespace to a single space
- except within ``pre``.)
-
- Defaults to ``False``.
-
- :arg minimize_boolean_attributes: Shortens boolean attributes to give
- just the attribute value, for example::
-
- <input disabled="disabled">
-
- becomes::
-
- <input disabled>
-
- Defaults to ``True``.
-
- :arg use_trailing_solidus: Includes a close-tag slash at the end of the
- start tag of void elements (empty elements whose end tag is
- forbidden). E.g. ``<hr/>``.
-
- Defaults to ``False``.
-
- :arg space_before_trailing_solidus: Places a space immediately before
- the closing slash in a tag using a trailing solidus. E.g.
- ``<hr />``. Requires ``use_trailing_solidus=True``.
-
- Defaults to ``True``.
-
- :arg sanitize: Strip all unsafe or unknown constructs from output.
- See :py:class:`html5lib.filters.sanitizer.Filter`.
-
- Defaults to ``False``.
-
- :arg omit_optional_tags: Omit start/end tags that are optional.
-
- Defaults to ``True``.
-
- :arg alphabetical_attributes: Reorder attributes to be in alphabetical order.
-
- Defaults to ``False``.
-
- """
- unexpected_args = frozenset(kwargs) - frozenset(self.options)
- if len(unexpected_args) > 0:
- raise TypeError("__init__() got an unexpected keyword argument '%s'" % next(iter(unexpected_args)))
- if 'quote_char' in kwargs:
- self.use_best_quote_char = False
- for attr in self.options:
- setattr(self, attr, kwargs.get(attr, getattr(self, attr)))
- self.errors = []
- self.strict = False
-
- def encode(self, string):
- assert(isinstance(string, text_type))
- if self.encoding:
- return string.encode(self.encoding, "htmlentityreplace")
- else:
- return string
-
- def encodeStrict(self, string):
- assert(isinstance(string, text_type))
- if self.encoding:
- return string.encode(self.encoding, "strict")
- else:
- return string
-
- def serialize(self, treewalker, encoding=None):
- # pylint:disable=too-many-nested-blocks
- self.encoding = encoding
- in_cdata = False
- self.errors = []
-
- if encoding and self.inject_meta_charset:
- from .filters.inject_meta_charset import Filter
- treewalker = Filter(treewalker, encoding)
- # Alphabetical attributes is here under the assumption that none of
- # the later filters add or change order of attributes; it needs to be
- # before the sanitizer so escaped elements come out correctly
- if self.alphabetical_attributes:
- from .filters.alphabeticalattributes import Filter
- treewalker = Filter(treewalker)
- # WhitespaceFilter should be used before OptionalTagFilter
- # for maximum efficiently of this latter filter
- if self.strip_whitespace:
- from .filters.whitespace import Filter
- treewalker = Filter(treewalker)
- if self.sanitize:
- from .filters.sanitizer import Filter
- treewalker = Filter(treewalker)
- if self.omit_optional_tags:
- from .filters.optionaltags import Filter
- treewalker = Filter(treewalker)
-
- for token in treewalker:
- type = token["type"]
- if type == "Doctype":
- doctype = "<!DOCTYPE %s" % token["name"]
-
- if token["publicId"]:
- doctype += ' PUBLIC "%s"' % token["publicId"]
- elif token["systemId"]:
- doctype += " SYSTEM"
- if token["systemId"]:
- if token["systemId"].find('"') >= 0:
- if token["systemId"].find("'") >= 0:
- self.serializeError("System identifier contains both single and double quote characters")
- quote_char = "'"
- else:
- quote_char = '"'
- doctype += " %s%s%s" % (quote_char, token["systemId"], quote_char)
-
- doctype += ">"
- yield self.encodeStrict(doctype)
-
- elif type in ("Characters", "SpaceCharacters"):
- if type == "SpaceCharacters" or in_cdata:
- if in_cdata and token["data"].find("</") >= 0:
- self.serializeError("Unexpected </ in CDATA")
- yield self.encode(token["data"])
- else:
- yield self.encode(escape(token["data"]))
-
- elif type in ("StartTag", "EmptyTag"):
- name = token["name"]
- yield self.encodeStrict("<%s" % name)
- if name in rcdataElements and not self.escape_rcdata:
- in_cdata = True
- elif in_cdata:
- self.serializeError("Unexpected child element of a CDATA element")
- for (_, attr_name), attr_value in token["data"].items():
- # TODO: Add namespace support here
- k = attr_name
- v = attr_value
- yield self.encodeStrict(' ')
-
- yield self.encodeStrict(k)
- if not self.minimize_boolean_attributes or \
- (k not in booleanAttributes.get(name, tuple()) and
- k not in booleanAttributes.get("", tuple())):
- yield self.encodeStrict("=")
- if self.quote_attr_values == "always" or len(v) == 0:
- quote_attr = True
- elif self.quote_attr_values == "spec":
- quote_attr = _quoteAttributeSpec.search(v) is not None
- elif self.quote_attr_values == "legacy":
- quote_attr = _quoteAttributeLegacy.search(v) is not None
- else:
- raise ValueError("quote_attr_values must be one of: "
- "'always', 'spec', or 'legacy'")
- v = v.replace("&", "&amp;")
- if self.escape_lt_in_attrs:
- v = v.replace("<", "&lt;")
- if quote_attr:
- quote_char = self.quote_char
- if self.use_best_quote_char:
- if "'" in v and '"' not in v:
- quote_char = '"'
- elif '"' in v and "'" not in v:
- quote_char = "'"
- if quote_char == "'":
- v = v.replace("'", "&#39;")
- else:
- v = v.replace('"', "&quot;")
- yield self.encodeStrict(quote_char)
- yield self.encode(v)
- yield self.encodeStrict(quote_char)
- else:
- yield self.encode(v)
- if name in voidElements and self.use_trailing_solidus:
- if self.space_before_trailing_solidus:
- yield self.encodeStrict(" /")
- else:
- yield self.encodeStrict("/")
- yield self.encode(">")
-
- elif type == "EndTag":
- name = token["name"]
- if name in rcdataElements:
- in_cdata = False
- elif in_cdata:
- self.serializeError("Unexpected child element of a CDATA element")
- yield self.encodeStrict("</%s>" % name)
-
- elif type == "Comment":
- data = token["data"]
- if data.find("--") >= 0:
- self.serializeError("Comment contains --")
- yield self.encodeStrict("<!--%s-->" % token["data"])
-
- elif type == "Entity":
- name = token["name"]
- key = name + ";"
- if key not in entities:
- self.serializeError("Entity %s not recognized" % name)
- if self.resolve_entities and key not in xmlEntities:
- data = entities[key]
- else:
- data = "&%s;" % name
- yield self.encodeStrict(data)
-
- else:
- self.serializeError(token["data"])
-
- def render(self, treewalker, encoding=None):
- """Serializes the stream from the treewalker into a string
-
- :arg treewalker: the treewalker to serialize
-
- :arg encoding: the string encoding to use
-
- :returns: the serialized tree
-
- Example:
-
- >>> from html5lib import parse, getTreeWalker
- >>> from html5lib.serializer import HTMLSerializer
- >>> token_stream = parse('<html><body>Hi!</body></html>')
- >>> walker = getTreeWalker('etree')
- >>> serializer = HTMLSerializer(omit_optional_tags=False)
- >>> serializer.render(walker(token_stream))
- '<html><head></head><body>Hi!</body></html>'
-
- """
- if encoding:
- return b"".join(list(self.serialize(treewalker, encoding)))
- else:
- return "".join(list(self.serialize(treewalker)))
-
- def serializeError(self, data="XXX ERROR MESSAGE NEEDED"):
- # XXX The idea is to make data mandatory.
- self.errors.append(data)
- if self.strict:
- raise SerializeError
-
-
-class SerializeError(Exception):
- """Error in serialized tree"""
- pass
diff --git a/src/pip/_vendor/html5lib/treeadapters/__init__.py b/src/pip/_vendor/html5lib/treeadapters/__init__.py
deleted file mode 100644
index 7ef59590c..000000000
--- a/src/pip/_vendor/html5lib/treeadapters/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Tree adapters let you convert from one tree structure to another
-
-Example:
-
-.. code-block:: python
-
- from pip._vendor import html5lib
- from pip._vendor.html5lib.treeadapters import genshi
-
- doc = '<html><body>Hi!</body></html>'
- treebuilder = html5lib.getTreeBuilder('etree')
- parser = html5lib.HTMLParser(tree=treebuilder)
- tree = parser.parse(doc)
- TreeWalker = html5lib.getTreeWalker('etree')
-
- genshi_tree = genshi.to_genshi(TreeWalker(tree))
-
-"""
-from __future__ import absolute_import, division, unicode_literals
-
-from . import sax
-
-__all__ = ["sax"]
-
-try:
- from . import genshi # noqa
-except ImportError:
- pass
-else:
- __all__.append("genshi")
diff --git a/src/pip/_vendor/html5lib/treeadapters/genshi.py b/src/pip/_vendor/html5lib/treeadapters/genshi.py
deleted file mode 100644
index 61d5fb6ac..000000000
--- a/src/pip/_vendor/html5lib/treeadapters/genshi.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from genshi.core import QName, Attrs
-from genshi.core import START, END, TEXT, COMMENT, DOCTYPE
-
-
-def to_genshi(walker):
- """Convert a tree to a genshi tree
-
- :arg walker: the treewalker to use to walk the tree to convert it
-
- :returns: generator of genshi nodes
-
- """
- text = []
- for token in walker:
- type = token["type"]
- if type in ("Characters", "SpaceCharacters"):
- text.append(token["data"])
- elif text:
- yield TEXT, "".join(text), (None, -1, -1)
- text = []
-
- if type in ("StartTag", "EmptyTag"):
- if token["namespace"]:
- name = "{%s}%s" % (token["namespace"], token["name"])
- else:
- name = token["name"]
- attrs = Attrs([(QName("{%s}%s" % attr if attr[0] is not None else attr[1]), value)
- for attr, value in token["data"].items()])
- yield (START, (QName(name), attrs), (None, -1, -1))
- if type == "EmptyTag":
- type = "EndTag"
-
- if type == "EndTag":
- if token["namespace"]:
- name = "{%s}%s" % (token["namespace"], token["name"])
- else:
- name = token["name"]
-
- yield END, QName(name), (None, -1, -1)
-
- elif type == "Comment":
- yield COMMENT, token["data"], (None, -1, -1)
-
- elif type == "Doctype":
- yield DOCTYPE, (token["name"], token["publicId"],
- token["systemId"]), (None, -1, -1)
-
- else:
- pass # FIXME: What to do?
-
- if text:
- yield TEXT, "".join(text), (None, -1, -1)
diff --git a/src/pip/_vendor/html5lib/treeadapters/sax.py b/src/pip/_vendor/html5lib/treeadapters/sax.py
deleted file mode 100644
index f4ccea5a2..000000000
--- a/src/pip/_vendor/html5lib/treeadapters/sax.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from xml.sax.xmlreader import AttributesNSImpl
-
-from ..constants import adjustForeignAttributes, unadjustForeignAttributes
-
-prefix_mapping = {}
-for prefix, localName, namespace in adjustForeignAttributes.values():
- if prefix is not None:
- prefix_mapping[prefix] = namespace
-
-
-def to_sax(walker, handler):
- """Call SAX-like content handler based on treewalker walker
-
- :arg walker: the treewalker to use to walk the tree to convert it
-
- :arg handler: SAX handler to use
-
- """
- handler.startDocument()
- for prefix, namespace in prefix_mapping.items():
- handler.startPrefixMapping(prefix, namespace)
-
- for token in walker:
- type = token["type"]
- if type == "Doctype":
- continue
- elif type in ("StartTag", "EmptyTag"):
- attrs = AttributesNSImpl(token["data"],
- unadjustForeignAttributes)
- handler.startElementNS((token["namespace"], token["name"]),
- token["name"],
- attrs)
- if type == "EmptyTag":
- handler.endElementNS((token["namespace"], token["name"]),
- token["name"])
- elif type == "EndTag":
- handler.endElementNS((token["namespace"], token["name"]),
- token["name"])
- elif type in ("Characters", "SpaceCharacters"):
- handler.characters(token["data"])
- elif type == "Comment":
- pass
- else:
- assert False, "Unknown token type"
-
- for prefix, namespace in prefix_mapping.items():
- handler.endPrefixMapping(prefix)
- handler.endDocument()
diff --git a/src/pip/_vendor/html5lib/treebuilders/__init__.py b/src/pip/_vendor/html5lib/treebuilders/__init__.py
deleted file mode 100644
index d44447eaf..000000000
--- a/src/pip/_vendor/html5lib/treebuilders/__init__.py
+++ /dev/null
@@ -1,88 +0,0 @@
-"""A collection of modules for building different kinds of trees from HTML
-documents.
-
-To create a treebuilder for a new type of tree, you need to do
-implement several things:
-
-1. A set of classes for various types of elements: Document, Doctype, Comment,
- Element. These must implement the interface of ``base.treebuilders.Node``
- (although comment nodes have a different signature for their constructor,
- see ``treebuilders.etree.Comment``) Textual content may also be implemented
- as another node type, or not, as your tree implementation requires.
-
-2. A treebuilder object (called ``TreeBuilder`` by convention) that inherits
- from ``treebuilders.base.TreeBuilder``. This has 4 required attributes:
-
- * ``documentClass`` - the class to use for the bottommost node of a document
- * ``elementClass`` - the class to use for HTML Elements
- * ``commentClass`` - the class to use for comments
- * ``doctypeClass`` - the class to use for doctypes
-
- It also has one required method:
-
- * ``getDocument`` - Returns the root node of the complete document tree
-
-3. If you wish to run the unit tests, you must also create a ``testSerializer``
- method on your treebuilder which accepts a node and returns a string
- containing Node and its children serialized according to the format used in
- the unittests
-
-"""
-
-from __future__ import absolute_import, division, unicode_literals
-
-from .._utils import default_etree
-
-treeBuilderCache = {}
-
-
-def getTreeBuilder(treeType, implementation=None, **kwargs):
- """Get a TreeBuilder class for various types of trees with built-in support
-
- :arg treeType: the name of the tree type required (case-insensitive). Supported
- values are:
-
- * "dom" - A generic builder for DOM implementations, defaulting to a
- xml.dom.minidom based implementation.
- * "etree" - A generic builder for tree implementations exposing an
- ElementTree-like interface, defaulting to xml.etree.cElementTree if
- available and xml.etree.ElementTree if not.
- * "lxml" - A etree-based builder for lxml.etree, handling limitations
- of lxml's implementation.
-
- :arg implementation: (Currently applies to the "etree" and "dom" tree
- types). A module implementing the tree type e.g. xml.etree.ElementTree
- or xml.etree.cElementTree.
-
- :arg kwargs: Any additional options to pass to the TreeBuilder when
- creating it.
-
- Example:
-
- >>> from html5lib.treebuilders import getTreeBuilder
- >>> builder = getTreeBuilder('etree')
-
- """
-
- treeType = treeType.lower()
- if treeType not in treeBuilderCache:
- if treeType == "dom":
- from . import dom
- # Come up with a sane default (pref. from the stdlib)
- if implementation is None:
- from xml.dom import minidom
- implementation = minidom
- # NEVER cache here, caching is done in the dom submodule
- return dom.getDomModule(implementation, **kwargs).TreeBuilder
- elif treeType == "lxml":
- from . import etree_lxml
- treeBuilderCache[treeType] = etree_lxml.TreeBuilder
- elif treeType == "etree":
- from . import etree
- if implementation is None:
- implementation = default_etree
- # NEVER cache here, caching is done in the etree submodule
- return etree.getETreeModule(implementation, **kwargs).TreeBuilder
- else:
- raise ValueError("""Unrecognised treebuilder "%s" """ % treeType)
- return treeBuilderCache.get(treeType)
diff --git a/src/pip/_vendor/html5lib/treebuilders/base.py b/src/pip/_vendor/html5lib/treebuilders/base.py
deleted file mode 100644
index 965fce29d..000000000
--- a/src/pip/_vendor/html5lib/treebuilders/base.py
+++ /dev/null
@@ -1,417 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-from pip._vendor.six import text_type
-
-from ..constants import scopingElements, tableInsertModeElements, namespaces
-
-# The scope markers are inserted when entering object elements,
-# marquees, table cells, and table captions, and are used to prevent formatting
-# from "leaking" into tables, object elements, and marquees.
-Marker = None
-
-listElementsMap = {
- None: (frozenset(scopingElements), False),
- "button": (frozenset(scopingElements | {(namespaces["html"], "button")}), False),
- "list": (frozenset(scopingElements | {(namespaces["html"], "ol"),
- (namespaces["html"], "ul")}), False),
- "table": (frozenset([(namespaces["html"], "html"),
- (namespaces["html"], "table")]), False),
- "select": (frozenset([(namespaces["html"], "optgroup"),
- (namespaces["html"], "option")]), True)
-}
-
-
-class Node(object):
- """Represents an item in the tree"""
- def __init__(self, name):
- """Creates a Node
-
- :arg name: The tag name associated with the node
-
- """
- # The tag name associated with the node
- self.name = name
- # The parent of the current node (or None for the document node)
- self.parent = None
- # The value of the current node (applies to text nodes and comments)
- self.value = None
- # A dict holding name -> value pairs for attributes of the node
- self.attributes = {}
- # A list of child nodes of the current node. This must include all
- # elements but not necessarily other node types.
- self.childNodes = []
- # A list of miscellaneous flags that can be set on the node.
- self._flags = []
-
- def __str__(self):
- attributesStr = " ".join(["%s=\"%s\"" % (name, value)
- for name, value in
- self.attributes.items()])
- if attributesStr:
- return "<%s %s>" % (self.name, attributesStr)
- else:
- return "<%s>" % (self.name)
-
- def __repr__(self):
- return "<%s>" % (self.name)
-
- def appendChild(self, node):
- """Insert node as a child of the current node
-
- :arg node: the node to insert
-
- """
- raise NotImplementedError
-
- def insertText(self, data, insertBefore=None):
- """Insert data as text in the current node, positioned before the
- start of node insertBefore or to the end of the node's text.
-
- :arg data: the data to insert
-
- :arg insertBefore: True if you want to insert the text before the node
- and False if you want to insert it after the node
-
- """
- raise NotImplementedError
-
- def insertBefore(self, node, refNode):
- """Insert node as a child of the current node, before refNode in the
- list of child nodes. Raises ValueError if refNode is not a child of
- the current node
-
- :arg node: the node to insert
-
- :arg refNode: the child node to insert the node before
-
- """
- raise NotImplementedError
-
- def removeChild(self, node):
- """Remove node from the children of the current node
-
- :arg node: the child node to remove
-
- """
- raise NotImplementedError
-
- def reparentChildren(self, newParent):
- """Move all the children of the current node to newParent.
- This is needed so that trees that don't store text as nodes move the
- text in the correct way
-
- :arg newParent: the node to move all this node's children to
-
- """
- # XXX - should this method be made more general?
- for child in self.childNodes:
- newParent.appendChild(child)
- self.childNodes = []
-
- def cloneNode(self):
- """Return a shallow copy of the current node i.e. a node with the same
- name and attributes but with no parent or child nodes
- """
- raise NotImplementedError
-
- def hasContent(self):
- """Return true if the node has children or text, false otherwise
- """
- raise NotImplementedError
-
-
-class ActiveFormattingElements(list):
- def append(self, node):
- equalCount = 0
- if node != Marker:
- for element in self[::-1]:
- if element == Marker:
- break
- if self.nodesEqual(element, node):
- equalCount += 1
- if equalCount == 3:
- self.remove(element)
- break
- list.append(self, node)
-
- def nodesEqual(self, node1, node2):
- if not node1.nameTuple == node2.nameTuple:
- return False
-
- if not node1.attributes == node2.attributes:
- return False
-
- return True
-
-
-class TreeBuilder(object):
- """Base treebuilder implementation
-
- * documentClass - the class to use for the bottommost node of a document
- * elementClass - the class to use for HTML Elements
- * commentClass - the class to use for comments
- * doctypeClass - the class to use for doctypes
-
- """
- # pylint:disable=not-callable
-
- # Document class
- documentClass = None
-
- # The class to use for creating a node
- elementClass = None
-
- # The class to use for creating comments
- commentClass = None
-
- # The class to use for creating doctypes
- doctypeClass = None
-
- # Fragment class
- fragmentClass = None
-
- def __init__(self, namespaceHTMLElements):
- """Create a TreeBuilder
-
- :arg namespaceHTMLElements: whether or not to namespace HTML elements
-
- """
- if namespaceHTMLElements:
- self.defaultNamespace = "http://www.w3.org/1999/xhtml"
- else:
- self.defaultNamespace = None
- self.reset()
-
- def reset(self):
- self.openElements = []
- self.activeFormattingElements = ActiveFormattingElements()
-
- # XXX - rename these to headElement, formElement
- self.headPointer = None
- self.formPointer = None
-
- self.insertFromTable = False
-
- self.document = self.documentClass()
-
- def elementInScope(self, target, variant=None):
-
- # If we pass a node in we match that. if we pass a string
- # match any node with that name
- exactNode = hasattr(target, "nameTuple")
- if not exactNode:
- if isinstance(target, text_type):
- target = (namespaces["html"], target)
- assert isinstance(target, tuple)
-
- listElements, invert = listElementsMap[variant]
-
- for node in reversed(self.openElements):
- if exactNode and node == target:
- return True
- elif not exactNode and node.nameTuple == target:
- return True
- elif (invert ^ (node.nameTuple in listElements)):
- return False
-
- assert False # We should never reach this point
-
- def reconstructActiveFormattingElements(self):
- # Within this algorithm the order of steps described in the
- # specification is not quite the same as the order of steps in the
- # code. It should still do the same though.
-
- # Step 1: stop the algorithm when there's nothing to do.
- if not self.activeFormattingElements:
- return
-
- # Step 2 and step 3: we start with the last element. So i is -1.
- i = len(self.activeFormattingElements) - 1
- entry = self.activeFormattingElements[i]
- if entry == Marker or entry in self.openElements:
- return
-
- # Step 6
- while entry != Marker and entry not in self.openElements:
- if i == 0:
- # This will be reset to 0 below
- i = -1
- break
- i -= 1
- # Step 5: let entry be one earlier in the list.
- entry = self.activeFormattingElements[i]
-
- while True:
- # Step 7
- i += 1
-
- # Step 8
- entry = self.activeFormattingElements[i]
- clone = entry.cloneNode() # Mainly to get a new copy of the attributes
-
- # Step 9
- element = self.insertElement({"type": "StartTag",
- "name": clone.name,
- "namespace": clone.namespace,
- "data": clone.attributes})
-
- # Step 10
- self.activeFormattingElements[i] = element
-
- # Step 11
- if element == self.activeFormattingElements[-1]:
- break
-
- def clearActiveFormattingElements(self):
- entry = self.activeFormattingElements.pop()
- while self.activeFormattingElements and entry != Marker:
- entry = self.activeFormattingElements.pop()
-
- def elementInActiveFormattingElements(self, name):
- """Check if an element exists between the end of the active
- formatting elements and the last marker. If it does, return it, else
- return false"""
-
- for item in self.activeFormattingElements[::-1]:
- # Check for Marker first because if it's a Marker it doesn't have a
- # name attribute.
- if item == Marker:
- break
- elif item.name == name:
- return item
- return False
-
- def insertRoot(self, token):
- element = self.createElement(token)
- self.openElements.append(element)
- self.document.appendChild(element)
-
- def insertDoctype(self, token):
- name = token["name"]
- publicId = token["publicId"]
- systemId = token["systemId"]
-
- doctype = self.doctypeClass(name, publicId, systemId)
- self.document.appendChild(doctype)
-
- def insertComment(self, token, parent=None):
- if parent is None:
- parent = self.openElements[-1]
- parent.appendChild(self.commentClass(token["data"]))
-
- def createElement(self, token):
- """Create an element but don't insert it anywhere"""
- name = token["name"]
- namespace = token.get("namespace", self.defaultNamespace)
- element = self.elementClass(name, namespace)
- element.attributes = token["data"]
- return element
-
- def _getInsertFromTable(self):
- return self._insertFromTable
-
- def _setInsertFromTable(self, value):
- """Switch the function used to insert an element from the
- normal one to the misnested table one and back again"""
- self._insertFromTable = value
- if value:
- self.insertElement = self.insertElementTable
- else:
- self.insertElement = self.insertElementNormal
-
- insertFromTable = property(_getInsertFromTable, _setInsertFromTable)
-
- def insertElementNormal(self, token):
- name = token["name"]
- assert isinstance(name, text_type), "Element %s not unicode" % name
- namespace = token.get("namespace", self.defaultNamespace)
- element = self.elementClass(name, namespace)
- element.attributes = token["data"]
- self.openElements[-1].appendChild(element)
- self.openElements.append(element)
- return element
-
- def insertElementTable(self, token):
- """Create an element and insert it into the tree"""
- element = self.createElement(token)
- if self.openElements[-1].name not in tableInsertModeElements:
- return self.insertElementNormal(token)
- else:
- # We should be in the InTable mode. This means we want to do
- # special magic element rearranging
- parent, insertBefore = self.getTableMisnestedNodePosition()
- if insertBefore is None:
- parent.appendChild(element)
- else:
- parent.insertBefore(element, insertBefore)
- self.openElements.append(element)
- return element
-
- def insertText(self, data, parent=None):
- """Insert text data."""
- if parent is None:
- parent = self.openElements[-1]
-
- if (not self.insertFromTable or (self.insertFromTable and
- self.openElements[-1].name
- not in tableInsertModeElements)):
- parent.insertText(data)
- else:
- # We should be in the InTable mode. This means we want to do
- # special magic element rearranging
- parent, insertBefore = self.getTableMisnestedNodePosition()
- parent.insertText(data, insertBefore)
-
- def getTableMisnestedNodePosition(self):
- """Get the foster parent element, and sibling to insert before
- (or None) when inserting a misnested table node"""
- # The foster parent element is the one which comes before the most
- # recently opened table element
- # XXX - this is really inelegant
- lastTable = None
- fosterParent = None
- insertBefore = None
- for elm in self.openElements[::-1]:
- if elm.name == "table":
- lastTable = elm
- break
- if lastTable:
- # XXX - we should really check that this parent is actually a
- # node here
- if lastTable.parent:
- fosterParent = lastTable.parent
- insertBefore = lastTable
- else:
- fosterParent = self.openElements[
- self.openElements.index(lastTable) - 1]
- else:
- fosterParent = self.openElements[0]
- return fosterParent, insertBefore
-
- def generateImpliedEndTags(self, exclude=None):
- name = self.openElements[-1].name
- # XXX td, th and tr are not actually needed
- if (name in frozenset(("dd", "dt", "li", "option", "optgroup", "p", "rp", "rt")) and
- name != exclude):
- self.openElements.pop()
- # XXX This is not entirely what the specification says. We should
- # investigate it more closely.
- self.generateImpliedEndTags(exclude)
-
- def getDocument(self):
- """Return the final tree"""
- return self.document
-
- def getFragment(self):
- """Return the final fragment"""
- # assert self.innerHTML
- fragment = self.fragmentClass()
- self.openElements[0].reparentChildren(fragment)
- return fragment
-
- def testSerializer(self, node):
- """Serialize the subtree of node in the format required by unit tests
-
- :arg node: the node from which to start serializing
-
- """
- raise NotImplementedError
diff --git a/src/pip/_vendor/html5lib/treebuilders/dom.py b/src/pip/_vendor/html5lib/treebuilders/dom.py
deleted file mode 100644
index d8b530046..000000000
--- a/src/pip/_vendor/html5lib/treebuilders/dom.py
+++ /dev/null
@@ -1,239 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-
-try:
- from collections.abc import MutableMapping
-except ImportError: # Python 2.7
- from collections import MutableMapping
-from xml.dom import minidom, Node
-import weakref
-
-from . import base
-from .. import constants
-from ..constants import namespaces
-from .._utils import moduleFactoryFactory
-
-
-def getDomBuilder(DomImplementation):
- Dom = DomImplementation
-
- class AttrList(MutableMapping):
- def __init__(self, element):
- self.element = element
-
- def __iter__(self):
- return iter(self.element.attributes.keys())
-
- def __setitem__(self, name, value):
- if isinstance(name, tuple):
- raise NotImplementedError
- else:
- attr = self.element.ownerDocument.createAttribute(name)
- attr.value = value
- self.element.attributes[name] = attr
-
- def __len__(self):
- return len(self.element.attributes)
-
- def items(self):
- return list(self.element.attributes.items())
-
- def values(self):
- return list(self.element.attributes.values())
-
- def __getitem__(self, name):
- if isinstance(name, tuple):
- raise NotImplementedError
- else:
- return self.element.attributes[name].value
-
- def __delitem__(self, name):
- if isinstance(name, tuple):
- raise NotImplementedError
- else:
- del self.element.attributes[name]
-
- class NodeBuilder(base.Node):
- def __init__(self, element):
- base.Node.__init__(self, element.nodeName)
- self.element = element
-
- namespace = property(lambda self: hasattr(self.element, "namespaceURI") and
- self.element.namespaceURI or None)
-
- def appendChild(self, node):
- node.parent = self
- self.element.appendChild(node.element)
-
- def insertText(self, data, insertBefore=None):
- text = self.element.ownerDocument.createTextNode(data)
- if insertBefore:
- self.element.insertBefore(text, insertBefore.element)
- else:
- self.element.appendChild(text)
-
- def insertBefore(self, node, refNode):
- self.element.insertBefore(node.element, refNode.element)
- node.parent = self
-
- def removeChild(self, node):
- if node.element.parentNode == self.element:
- self.element.removeChild(node.element)
- node.parent = None
-
- def reparentChildren(self, newParent):
- while self.element.hasChildNodes():
- child = self.element.firstChild
- self.element.removeChild(child)
- newParent.element.appendChild(child)
- self.childNodes = []
-
- def getAttributes(self):
- return AttrList(self.element)
-
- def setAttributes(self, attributes):
- if attributes:
- for name, value in list(attributes.items()):
- if isinstance(name, tuple):
- if name[0] is not None:
- qualifiedName = (name[0] + ":" + name[1])
- else:
- qualifiedName = name[1]
- self.element.setAttributeNS(name[2], qualifiedName,
- value)
- else:
- self.element.setAttribute(
- name, value)
- attributes = property(getAttributes, setAttributes)
-
- def cloneNode(self):
- return NodeBuilder(self.element.cloneNode(False))
-
- def hasContent(self):
- return self.element.hasChildNodes()
-
- def getNameTuple(self):
- if self.namespace is None:
- return namespaces["html"], self.name
- else:
- return self.namespace, self.name
-
- nameTuple = property(getNameTuple)
-
- class TreeBuilder(base.TreeBuilder): # pylint:disable=unused-variable
- def documentClass(self):
- self.dom = Dom.getDOMImplementation().createDocument(None, None, None)
- return weakref.proxy(self)
-
- def insertDoctype(self, token):
- name = token["name"]
- publicId = token["publicId"]
- systemId = token["systemId"]
-
- domimpl = Dom.getDOMImplementation()
- doctype = domimpl.createDocumentType(name, publicId, systemId)
- self.document.appendChild(NodeBuilder(doctype))
- if Dom == minidom:
- doctype.ownerDocument = self.dom
-
- def elementClass(self, name, namespace=None):
- if namespace is None and self.defaultNamespace is None:
- node = self.dom.createElement(name)
- else:
- node = self.dom.createElementNS(namespace, name)
-
- return NodeBuilder(node)
-
- def commentClass(self, data):
- return NodeBuilder(self.dom.createComment(data))
-
- def fragmentClass(self):
- return NodeBuilder(self.dom.createDocumentFragment())
-
- def appendChild(self, node):
- self.dom.appendChild(node.element)
-
- def testSerializer(self, element):
- return testSerializer(element)
-
- def getDocument(self):
- return self.dom
-
- def getFragment(self):
- return base.TreeBuilder.getFragment(self).element
-
- def insertText(self, data, parent=None):
- data = data
- if parent != self:
- base.TreeBuilder.insertText(self, data, parent)
- else:
- # HACK: allow text nodes as children of the document node
- if hasattr(self.dom, '_child_node_types'):
- # pylint:disable=protected-access
- if Node.TEXT_NODE not in self.dom._child_node_types:
- self.dom._child_node_types = list(self.dom._child_node_types)
- self.dom._child_node_types.append(Node.TEXT_NODE)
- self.dom.appendChild(self.dom.createTextNode(data))
-
- implementation = DomImplementation
- name = None
-
- def testSerializer(element):
- element.normalize()
- rv = []
-
- def serializeElement(element, indent=0):
- if element.nodeType == Node.DOCUMENT_TYPE_NODE:
- if element.name:
- if element.publicId or element.systemId:
- publicId = element.publicId or ""
- systemId = element.systemId or ""
- rv.append("""|%s<!DOCTYPE %s "%s" "%s">""" %
- (' ' * indent, element.name, publicId, systemId))
- else:
- rv.append("|%s<!DOCTYPE %s>" % (' ' * indent, element.name))
- else:
- rv.append("|%s<!DOCTYPE >" % (' ' * indent,))
- elif element.nodeType == Node.DOCUMENT_NODE:
- rv.append("#document")
- elif element.nodeType == Node.DOCUMENT_FRAGMENT_NODE:
- rv.append("#document-fragment")
- elif element.nodeType == Node.COMMENT_NODE:
- rv.append("|%s<!-- %s -->" % (' ' * indent, element.nodeValue))
- elif element.nodeType == Node.TEXT_NODE:
- rv.append("|%s\"%s\"" % (' ' * indent, element.nodeValue))
- else:
- if (hasattr(element, "namespaceURI") and
- element.namespaceURI is not None):
- name = "%s %s" % (constants.prefixes[element.namespaceURI],
- element.nodeName)
- else:
- name = element.nodeName
- rv.append("|%s<%s>" % (' ' * indent, name))
- if element.hasAttributes():
- attributes = []
- for i in range(len(element.attributes)):
- attr = element.attributes.item(i)
- name = attr.nodeName
- value = attr.value
- ns = attr.namespaceURI
- if ns:
- name = "%s %s" % (constants.prefixes[ns], attr.localName)
- else:
- name = attr.nodeName
- attributes.append((name, value))
-
- for name, value in sorted(attributes):
- rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value))
- indent += 2
- for child in element.childNodes:
- serializeElement(child, indent)
- serializeElement(element, 0)
-
- return "\n".join(rv)
-
- return locals()
-
-
-# The actual means to get a module!
-getDomModule = moduleFactoryFactory(getDomBuilder)
diff --git a/src/pip/_vendor/html5lib/treebuilders/etree.py b/src/pip/_vendor/html5lib/treebuilders/etree.py
deleted file mode 100644
index ea92dc301..000000000
--- a/src/pip/_vendor/html5lib/treebuilders/etree.py
+++ /dev/null
@@ -1,343 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-# pylint:disable=protected-access
-
-from pip._vendor.six import text_type
-
-import re
-
-from copy import copy
-
-from . import base
-from .. import _ihatexml
-from .. import constants
-from ..constants import namespaces
-from .._utils import moduleFactoryFactory
-
-tag_regexp = re.compile("{([^}]*)}(.*)")
-
-
-def getETreeBuilder(ElementTreeImplementation, fullTree=False):
- ElementTree = ElementTreeImplementation
- ElementTreeCommentType = ElementTree.Comment("asd").tag
-
- class Element(base.Node):
- def __init__(self, name, namespace=None):
- self._name = name
- self._namespace = namespace
- self._element = ElementTree.Element(self._getETreeTag(name,
- namespace))
- if namespace is None:
- self.nameTuple = namespaces["html"], self._name
- else:
- self.nameTuple = self._namespace, self._name
- self.parent = None
- self._childNodes = []
- self._flags = []
-
- def _getETreeTag(self, name, namespace):
- if namespace is None:
- etree_tag = name
- else:
- etree_tag = "{%s}%s" % (namespace, name)
- return etree_tag
-
- def _setName(self, name):
- self._name = name
- self._element.tag = self._getETreeTag(self._name, self._namespace)
-
- def _getName(self):
- return self._name
-
- name = property(_getName, _setName)
-
- def _setNamespace(self, namespace):
- self._namespace = namespace
- self._element.tag = self._getETreeTag(self._name, self._namespace)
-
- def _getNamespace(self):
- return self._namespace
-
- namespace = property(_getNamespace, _setNamespace)
-
- def _getAttributes(self):
- return self._element.attrib
-
- def _setAttributes(self, attributes):
- el_attrib = self._element.attrib
- el_attrib.clear()
- if attributes:
- # calling .items _always_ allocates, and the above truthy check is cheaper than the
- # allocation on average
- for key, value in attributes.items():
- if isinstance(key, tuple):
- name = "{%s}%s" % (key[2], key[1])
- else:
- name = key
- el_attrib[name] = value
-
- attributes = property(_getAttributes, _setAttributes)
-
- def _getChildNodes(self):
- return self._childNodes
-
- def _setChildNodes(self, value):
- del self._element[:]
- self._childNodes = []
- for element in value:
- self.insertChild(element)
-
- childNodes = property(_getChildNodes, _setChildNodes)
-
- def hasContent(self):
- """Return true if the node has children or text"""
- return bool(self._element.text or len(self._element))
-
- def appendChild(self, node):
- self._childNodes.append(node)
- self._element.append(node._element)
- node.parent = self
-
- def insertBefore(self, node, refNode):
- index = list(self._element).index(refNode._element)
- self._element.insert(index, node._element)
- node.parent = self
-
- def removeChild(self, node):
- self._childNodes.remove(node)
- self._element.remove(node._element)
- node.parent = None
-
- def insertText(self, data, insertBefore=None):
- if not(len(self._element)):
- if not self._element.text:
- self._element.text = ""
- self._element.text += data
- elif insertBefore is None:
- # Insert the text as the tail of the last child element
- if not self._element[-1].tail:
- self._element[-1].tail = ""
- self._element[-1].tail += data
- else:
- # Insert the text before the specified node
- children = list(self._element)
- index = children.index(insertBefore._element)
- if index > 0:
- if not self._element[index - 1].tail:
- self._element[index - 1].tail = ""
- self._element[index - 1].tail += data
- else:
- if not self._element.text:
- self._element.text = ""
- self._element.text += data
-
- def cloneNode(self):
- element = type(self)(self.name, self.namespace)
- if self._element.attrib:
- element._element.attrib = copy(self._element.attrib)
- return element
-
- def reparentChildren(self, newParent):
- if newParent.childNodes:
- newParent.childNodes[-1]._element.tail += self._element.text
- else:
- if not newParent._element.text:
- newParent._element.text = ""
- if self._element.text is not None:
- newParent._element.text += self._element.text
- self._element.text = ""
- base.Node.reparentChildren(self, newParent)
-
- class Comment(Element):
- def __init__(self, data):
- # Use the superclass constructor to set all properties on the
- # wrapper element
- self._element = ElementTree.Comment(data)
- self.parent = None
- self._childNodes = []
- self._flags = []
-
- def _getData(self):
- return self._element.text
-
- def _setData(self, value):
- self._element.text = value
-
- data = property(_getData, _setData)
-
- class DocumentType(Element):
- def __init__(self, name, publicId, systemId):
- Element.__init__(self, "<!DOCTYPE>")
- self._element.text = name
- self.publicId = publicId
- self.systemId = systemId
-
- def _getPublicId(self):
- return self._element.get("publicId", "")
-
- def _setPublicId(self, value):
- if value is not None:
- self._element.set("publicId", value)
-
- publicId = property(_getPublicId, _setPublicId)
-
- def _getSystemId(self):
- return self._element.get("systemId", "")
-
- def _setSystemId(self, value):
- if value is not None:
- self._element.set("systemId", value)
-
- systemId = property(_getSystemId, _setSystemId)
-
- class Document(Element):
- def __init__(self):
- Element.__init__(self, "DOCUMENT_ROOT")
-
- class DocumentFragment(Element):
- def __init__(self):
- Element.__init__(self, "DOCUMENT_FRAGMENT")
-
- def testSerializer(element):
- rv = []
-
- def serializeElement(element, indent=0):
- if not(hasattr(element, "tag")):
- element = element.getroot()
- if element.tag == "<!DOCTYPE>":
- if element.get("publicId") or element.get("systemId"):
- publicId = element.get("publicId") or ""
- systemId = element.get("systemId") or ""
- rv.append("""<!DOCTYPE %s "%s" "%s">""" %
- (element.text, publicId, systemId))
- else:
- rv.append("<!DOCTYPE %s>" % (element.text,))
- elif element.tag == "DOCUMENT_ROOT":
- rv.append("#document")
- if element.text is not None:
- rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text))
- if element.tail is not None:
- raise TypeError("Document node cannot have tail")
- if hasattr(element, "attrib") and len(element.attrib):
- raise TypeError("Document node cannot have attributes")
- elif element.tag == ElementTreeCommentType:
- rv.append("|%s<!-- %s -->" % (' ' * indent, element.text))
- else:
- assert isinstance(element.tag, text_type), \
- "Expected unicode, got %s, %s" % (type(element.tag), element.tag)
- nsmatch = tag_regexp.match(element.tag)
-
- if nsmatch is None:
- name = element.tag
- else:
- ns, name = nsmatch.groups()
- prefix = constants.prefixes[ns]
- name = "%s %s" % (prefix, name)
- rv.append("|%s<%s>" % (' ' * indent, name))
-
- if hasattr(element, "attrib"):
- attributes = []
- for name, value in element.attrib.items():
- nsmatch = tag_regexp.match(name)
- if nsmatch is not None:
- ns, name = nsmatch.groups()
- prefix = constants.prefixes[ns]
- attr_string = "%s %s" % (prefix, name)
- else:
- attr_string = name
- attributes.append((attr_string, value))
-
- for name, value in sorted(attributes):
- rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value))
- if element.text:
- rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text))
- indent += 2
- for child in element:
- serializeElement(child, indent)
- if element.tail:
- rv.append("|%s\"%s\"" % (' ' * (indent - 2), element.tail))
- serializeElement(element, 0)
-
- return "\n".join(rv)
-
- def tostring(element): # pylint:disable=unused-variable
- """Serialize an element and its child nodes to a string"""
- rv = []
- filter = _ihatexml.InfosetFilter()
-
- def serializeElement(element):
- if isinstance(element, ElementTree.ElementTree):
- element = element.getroot()
-
- if element.tag == "<!DOCTYPE>":
- if element.get("publicId") or element.get("systemId"):
- publicId = element.get("publicId") or ""
- systemId = element.get("systemId") or ""
- rv.append("""<!DOCTYPE %s PUBLIC "%s" "%s">""" %
- (element.text, publicId, systemId))
- else:
- rv.append("<!DOCTYPE %s>" % (element.text,))
- elif element.tag == "DOCUMENT_ROOT":
- if element.text is not None:
- rv.append(element.text)
- if element.tail is not None:
- raise TypeError("Document node cannot have tail")
- if hasattr(element, "attrib") and len(element.attrib):
- raise TypeError("Document node cannot have attributes")
-
- for child in element:
- serializeElement(child)
-
- elif element.tag == ElementTreeCommentType:
- rv.append("<!--%s-->" % (element.text,))
- else:
- # This is assumed to be an ordinary element
- if not element.attrib:
- rv.append("<%s>" % (filter.fromXmlName(element.tag),))
- else:
- attr = " ".join(["%s=\"%s\"" % (
- filter.fromXmlName(name), value)
- for name, value in element.attrib.items()])
- rv.append("<%s %s>" % (element.tag, attr))
- if element.text:
- rv.append(element.text)
-
- for child in element:
- serializeElement(child)
-
- rv.append("</%s>" % (element.tag,))
-
- if element.tail:
- rv.append(element.tail)
-
- serializeElement(element)
-
- return "".join(rv)
-
- class TreeBuilder(base.TreeBuilder): # pylint:disable=unused-variable
- documentClass = Document
- doctypeClass = DocumentType
- elementClass = Element
- commentClass = Comment
- fragmentClass = DocumentFragment
- implementation = ElementTreeImplementation
-
- def testSerializer(self, element):
- return testSerializer(element)
-
- def getDocument(self):
- if fullTree:
- return self.document._element
- else:
- if self.defaultNamespace is not None:
- return self.document._element.find(
- "{%s}html" % self.defaultNamespace)
- else:
- return self.document._element.find("html")
-
- def getFragment(self):
- return base.TreeBuilder.getFragment(self)._element
-
- return locals()
-
-
-getETreeModule = moduleFactoryFactory(getETreeBuilder)
diff --git a/src/pip/_vendor/html5lib/treebuilders/etree_lxml.py b/src/pip/_vendor/html5lib/treebuilders/etree_lxml.py
deleted file mode 100644
index f037759f4..000000000
--- a/src/pip/_vendor/html5lib/treebuilders/etree_lxml.py
+++ /dev/null
@@ -1,392 +0,0 @@
-"""Module for supporting the lxml.etree library. The idea here is to use as much
-of the native library as possible, without using fragile hacks like custom element
-names that break between releases. The downside of this is that we cannot represent
-all possible trees; specifically the following are known to cause problems:
-
-Text or comments as siblings of the root element
-Docypes with no name
-
-When any of these things occur, we emit a DataLossWarning
-"""
-
-from __future__ import absolute_import, division, unicode_literals
-# pylint:disable=protected-access
-
-import warnings
-import re
-import sys
-
-try:
- from collections.abc import MutableMapping
-except ImportError:
- from collections import MutableMapping
-
-from . import base
-from ..constants import DataLossWarning
-from .. import constants
-from . import etree as etree_builders
-from .. import _ihatexml
-
-import lxml.etree as etree
-from pip._vendor.six import PY3, binary_type
-
-
-fullTree = True
-tag_regexp = re.compile("{([^}]*)}(.*)")
-
-comment_type = etree.Comment("asd").tag
-
-
-class DocumentType(object):
- def __init__(self, name, publicId, systemId):
- self.name = name
- self.publicId = publicId
- self.systemId = systemId
-
-
-class Document(object):
- def __init__(self):
- self._elementTree = None
- self._childNodes = []
-
- def appendChild(self, element):
- last = self._elementTree.getroot()
- for last in self._elementTree.getroot().itersiblings():
- pass
-
- last.addnext(element._element)
-
- def _getChildNodes(self):
- return self._childNodes
-
- childNodes = property(_getChildNodes)
-
-
-def testSerializer(element):
- rv = []
- infosetFilter = _ihatexml.InfosetFilter(preventDoubleDashComments=True)
-
- def serializeElement(element, indent=0):
- if not hasattr(element, "tag"):
- if hasattr(element, "getroot"):
- # Full tree case
- rv.append("#document")
- if element.docinfo.internalDTD:
- if not (element.docinfo.public_id or
- element.docinfo.system_url):
- dtd_str = "<!DOCTYPE %s>" % element.docinfo.root_name
- else:
- dtd_str = """<!DOCTYPE %s "%s" "%s">""" % (
- element.docinfo.root_name,
- element.docinfo.public_id,
- element.docinfo.system_url)
- rv.append("|%s%s" % (' ' * (indent + 2), dtd_str))
- next_element = element.getroot()
- while next_element.getprevious() is not None:
- next_element = next_element.getprevious()
- while next_element is not None:
- serializeElement(next_element, indent + 2)
- next_element = next_element.getnext()
- elif isinstance(element, str) or isinstance(element, bytes):
- # Text in a fragment
- assert isinstance(element, str) or sys.version_info[0] == 2
- rv.append("|%s\"%s\"" % (' ' * indent, element))
- else:
- # Fragment case
- rv.append("#document-fragment")
- for next_element in element:
- serializeElement(next_element, indent + 2)
- elif element.tag == comment_type:
- rv.append("|%s<!-- %s -->" % (' ' * indent, element.text))
- if hasattr(element, "tail") and element.tail:
- rv.append("|%s\"%s\"" % (' ' * indent, element.tail))
- else:
- assert isinstance(element, etree._Element)
- nsmatch = etree_builders.tag_regexp.match(element.tag)
- if nsmatch is not None:
- ns = nsmatch.group(1)
- tag = nsmatch.group(2)
- prefix = constants.prefixes[ns]
- rv.append("|%s<%s %s>" % (' ' * indent, prefix,
- infosetFilter.fromXmlName(tag)))
- else:
- rv.append("|%s<%s>" % (' ' * indent,
- infosetFilter.fromXmlName(element.tag)))
-
- if hasattr(element, "attrib"):
- attributes = []
- for name, value in element.attrib.items():
- nsmatch = tag_regexp.match(name)
- if nsmatch is not None:
- ns, name = nsmatch.groups()
- name = infosetFilter.fromXmlName(name)
- prefix = constants.prefixes[ns]
- attr_string = "%s %s" % (prefix, name)
- else:
- attr_string = infosetFilter.fromXmlName(name)
- attributes.append((attr_string, value))
-
- for name, value in sorted(attributes):
- rv.append('|%s%s="%s"' % (' ' * (indent + 2), name, value))
-
- if element.text:
- rv.append("|%s\"%s\"" % (' ' * (indent + 2), element.text))
- indent += 2
- for child in element:
- serializeElement(child, indent)
- if hasattr(element, "tail") and element.tail:
- rv.append("|%s\"%s\"" % (' ' * (indent - 2), element.tail))
- serializeElement(element, 0)
-
- return "\n".join(rv)
-
-
-def tostring(element):
- """Serialize an element and its child nodes to a string"""
- rv = []
-
- def serializeElement(element):
- if not hasattr(element, "tag"):
- if element.docinfo.internalDTD:
- if element.docinfo.doctype:
- dtd_str = element.docinfo.doctype
- else:
- dtd_str = "<!DOCTYPE %s>" % element.docinfo.root_name
- rv.append(dtd_str)
- serializeElement(element.getroot())
-
- elif element.tag == comment_type:
- rv.append("<!--%s-->" % (element.text,))
-
- else:
- # This is assumed to be an ordinary element
- if not element.attrib:
- rv.append("<%s>" % (element.tag,))
- else:
- attr = " ".join(["%s=\"%s\"" % (name, value)
- for name, value in element.attrib.items()])
- rv.append("<%s %s>" % (element.tag, attr))
- if element.text:
- rv.append(element.text)
-
- for child in element:
- serializeElement(child)
-
- rv.append("</%s>" % (element.tag,))
-
- if hasattr(element, "tail") and element.tail:
- rv.append(element.tail)
-
- serializeElement(element)
-
- return "".join(rv)
-
-
-class TreeBuilder(base.TreeBuilder):
- documentClass = Document
- doctypeClass = DocumentType
- elementClass = None
- commentClass = None
- fragmentClass = Document
- implementation = etree
-
- def __init__(self, namespaceHTMLElements, fullTree=False):
- builder = etree_builders.getETreeModule(etree, fullTree=fullTree)
- infosetFilter = self.infosetFilter = _ihatexml.InfosetFilter(preventDoubleDashComments=True)
- self.namespaceHTMLElements = namespaceHTMLElements
-
- class Attributes(MutableMapping):
- def __init__(self, element):
- self._element = element
-
- def _coerceKey(self, key):
- if isinstance(key, tuple):
- name = "{%s}%s" % (key[2], infosetFilter.coerceAttribute(key[1]))
- else:
- name = infosetFilter.coerceAttribute(key)
- return name
-
- def __getitem__(self, key):
- value = self._element._element.attrib[self._coerceKey(key)]
- if not PY3 and isinstance(value, binary_type):
- value = value.decode("ascii")
- return value
-
- def __setitem__(self, key, value):
- self._element._element.attrib[self._coerceKey(key)] = value
-
- def __delitem__(self, key):
- del self._element._element.attrib[self._coerceKey(key)]
-
- def __iter__(self):
- return iter(self._element._element.attrib)
-
- def __len__(self):
- return len(self._element._element.attrib)
-
- def clear(self):
- return self._element._element.attrib.clear()
-
- class Element(builder.Element):
- def __init__(self, name, namespace):
- name = infosetFilter.coerceElement(name)
- builder.Element.__init__(self, name, namespace=namespace)
- self._attributes = Attributes(self)
-
- def _setName(self, name):
- self._name = infosetFilter.coerceElement(name)
- self._element.tag = self._getETreeTag(
- self._name, self._namespace)
-
- def _getName(self):
- return infosetFilter.fromXmlName(self._name)
-
- name = property(_getName, _setName)
-
- def _getAttributes(self):
- return self._attributes
-
- def _setAttributes(self, value):
- attributes = self.attributes
- attributes.clear()
- attributes.update(value)
-
- attributes = property(_getAttributes, _setAttributes)
-
- def insertText(self, data, insertBefore=None):
- data = infosetFilter.coerceCharacters(data)
- builder.Element.insertText(self, data, insertBefore)
-
- def cloneNode(self):
- element = type(self)(self.name, self.namespace)
- if self._element.attrib:
- element._element.attrib.update(self._element.attrib)
- return element
-
- class Comment(builder.Comment):
- def __init__(self, data):
- data = infosetFilter.coerceComment(data)
- builder.Comment.__init__(self, data)
-
- def _setData(self, data):
- data = infosetFilter.coerceComment(data)
- self._element.text = data
-
- def _getData(self):
- return self._element.text
-
- data = property(_getData, _setData)
-
- self.elementClass = Element
- self.commentClass = Comment
- # self.fragmentClass = builder.DocumentFragment
- base.TreeBuilder.__init__(self, namespaceHTMLElements)
-
- def reset(self):
- base.TreeBuilder.reset(self)
- self.insertComment = self.insertCommentInitial
- self.initial_comments = []
- self.doctype = None
-
- def testSerializer(self, element):
- return testSerializer(element)
-
- def getDocument(self):
- if fullTree:
- return self.document._elementTree
- else:
- return self.document._elementTree.getroot()
-
- def getFragment(self):
- fragment = []
- element = self.openElements[0]._element
- if element.text:
- fragment.append(element.text)
- fragment.extend(list(element))
- if element.tail:
- fragment.append(element.tail)
- return fragment
-
- def insertDoctype(self, token):
- name = token["name"]
- publicId = token["publicId"]
- systemId = token["systemId"]
-
- if not name:
- warnings.warn("lxml cannot represent empty doctype", DataLossWarning)
- self.doctype = None
- else:
- coercedName = self.infosetFilter.coerceElement(name)
- if coercedName != name:
- warnings.warn("lxml cannot represent non-xml doctype", DataLossWarning)
-
- doctype = self.doctypeClass(coercedName, publicId, systemId)
- self.doctype = doctype
-
- def insertCommentInitial(self, data, parent=None):
- assert parent is None or parent is self.document
- assert self.document._elementTree is None
- self.initial_comments.append(data)
-
- def insertCommentMain(self, data, parent=None):
- if (parent == self.document and
- self.document._elementTree.getroot()[-1].tag == comment_type):
- warnings.warn("lxml cannot represent adjacent comments beyond the root elements", DataLossWarning)
- super(TreeBuilder, self).insertComment(data, parent)
-
- def insertRoot(self, token):
- # Because of the way libxml2 works, it doesn't seem to be possible to
- # alter information like the doctype after the tree has been parsed.
- # Therefore we need to use the built-in parser to create our initial
- # tree, after which we can add elements like normal
- docStr = ""
- if self.doctype:
- assert self.doctype.name
- docStr += "<!DOCTYPE %s" % self.doctype.name
- if (self.doctype.publicId is not None or
- self.doctype.systemId is not None):
- docStr += (' PUBLIC "%s" ' %
- (self.infosetFilter.coercePubid(self.doctype.publicId or "")))
- if self.doctype.systemId:
- sysid = self.doctype.systemId
- if sysid.find("'") >= 0 and sysid.find('"') >= 0:
- warnings.warn("DOCTYPE system cannot contain single and double quotes", DataLossWarning)
- sysid = sysid.replace("'", 'U00027')
- if sysid.find("'") >= 0:
- docStr += '"%s"' % sysid
- else:
- docStr += "'%s'" % sysid
- else:
- docStr += "''"
- docStr += ">"
- if self.doctype.name != token["name"]:
- warnings.warn("lxml cannot represent doctype with a different name to the root element", DataLossWarning)
- docStr += "<THIS_SHOULD_NEVER_APPEAR_PUBLICLY/>"
- root = etree.fromstring(docStr)
-
- # Append the initial comments:
- for comment_token in self.initial_comments:
- comment = self.commentClass(comment_token["data"])
- root.addprevious(comment._element)
-
- # Create the root document and add the ElementTree to it
- self.document = self.documentClass()
- self.document._elementTree = root.getroottree()
-
- # Give the root element the right name
- name = token["name"]
- namespace = token.get("namespace", self.defaultNamespace)
- if namespace is None:
- etree_tag = name
- else:
- etree_tag = "{%s}%s" % (namespace, name)
- root.tag = etree_tag
-
- # Add the root element to the internal child/open data structures
- root_element = self.elementClass(name, namespace)
- root_element._element = root
- self.document._childNodes.append(root_element)
- self.openElements.append(root_element)
-
- # Reset to the default insert comment function
- self.insertComment = self.insertCommentMain
diff --git a/src/pip/_vendor/html5lib/treewalkers/__init__.py b/src/pip/_vendor/html5lib/treewalkers/__init__.py
deleted file mode 100644
index b2d3aac31..000000000
--- a/src/pip/_vendor/html5lib/treewalkers/__init__.py
+++ /dev/null
@@ -1,154 +0,0 @@
-"""A collection of modules for iterating through different kinds of
-tree, generating tokens identical to those produced by the tokenizer
-module.
-
-To create a tree walker for a new type of tree, you need to
-implement a tree walker object (called TreeWalker by convention) that
-implements a 'serialize' method which takes a tree as sole argument and
-returns an iterator which generates tokens.
-"""
-
-from __future__ import absolute_import, division, unicode_literals
-
-from .. import constants
-from .._utils import default_etree
-
-__all__ = ["getTreeWalker", "pprint"]
-
-treeWalkerCache = {}
-
-
-def getTreeWalker(treeType, implementation=None, **kwargs):
- """Get a TreeWalker class for various types of tree with built-in support
-
- :arg str treeType: the name of the tree type required (case-insensitive).
- Supported values are:
-
- * "dom": The xml.dom.minidom DOM implementation
- * "etree": A generic walker for tree implementations exposing an
- elementtree-like interface (known to work with ElementTree,
- cElementTree and lxml.etree).
- * "lxml": Optimized walker for lxml.etree
- * "genshi": a Genshi stream
-
- :arg implementation: A module implementing the tree type e.g.
- xml.etree.ElementTree or cElementTree (Currently applies to the "etree"
- tree type only).
-
- :arg kwargs: keyword arguments passed to the etree walker--for other
- walkers, this has no effect
-
- :returns: a TreeWalker class
-
- """
-
- treeType = treeType.lower()
- if treeType not in treeWalkerCache:
- if treeType == "dom":
- from . import dom
- treeWalkerCache[treeType] = dom.TreeWalker
- elif treeType == "genshi":
- from . import genshi
- treeWalkerCache[treeType] = genshi.TreeWalker
- elif treeType == "lxml":
- from . import etree_lxml
- treeWalkerCache[treeType] = etree_lxml.TreeWalker
- elif treeType == "etree":
- from . import etree
- if implementation is None:
- implementation = default_etree
- # XXX: NEVER cache here, caching is done in the etree submodule
- return etree.getETreeModule(implementation, **kwargs).TreeWalker
- return treeWalkerCache.get(treeType)
-
-
-def concatenateCharacterTokens(tokens):
- pendingCharacters = []
- for token in tokens:
- type = token["type"]
- if type in ("Characters", "SpaceCharacters"):
- pendingCharacters.append(token["data"])
- else:
- if pendingCharacters:
- yield {"type": "Characters", "data": "".join(pendingCharacters)}
- pendingCharacters = []
- yield token
- if pendingCharacters:
- yield {"type": "Characters", "data": "".join(pendingCharacters)}
-
-
-def pprint(walker):
- """Pretty printer for tree walkers
-
- Takes a TreeWalker instance and pretty prints the output of walking the tree.
-
- :arg walker: a TreeWalker instance
-
- """
- output = []
- indent = 0
- for token in concatenateCharacterTokens(walker):
- type = token["type"]
- if type in ("StartTag", "EmptyTag"):
- # tag name
- if token["namespace"] and token["namespace"] != constants.namespaces["html"]:
- if token["namespace"] in constants.prefixes:
- ns = constants.prefixes[token["namespace"]]
- else:
- ns = token["namespace"]
- name = "%s %s" % (ns, token["name"])
- else:
- name = token["name"]
- output.append("%s<%s>" % (" " * indent, name))
- indent += 2
- # attributes (sorted for consistent ordering)
- attrs = token["data"]
- for (namespace, localname), value in sorted(attrs.items()):
- if namespace:
- if namespace in constants.prefixes:
- ns = constants.prefixes[namespace]
- else:
- ns = namespace
- name = "%s %s" % (ns, localname)
- else:
- name = localname
- output.append("%s%s=\"%s\"" % (" " * indent, name, value))
- # self-closing
- if type == "EmptyTag":
- indent -= 2
-
- elif type == "EndTag":
- indent -= 2
-
- elif type == "Comment":
- output.append("%s<!-- %s -->" % (" " * indent, token["data"]))
-
- elif type == "Doctype":
- if token["name"]:
- if token["publicId"]:
- output.append("""%s<!DOCTYPE %s "%s" "%s">""" %
- (" " * indent,
- token["name"],
- token["publicId"],
- token["systemId"] if token["systemId"] else ""))
- elif token["systemId"]:
- output.append("""%s<!DOCTYPE %s "" "%s">""" %
- (" " * indent,
- token["name"],
- token["systemId"]))
- else:
- output.append("%s<!DOCTYPE %s>" % (" " * indent,
- token["name"]))
- else:
- output.append("%s<!DOCTYPE >" % (" " * indent,))
-
- elif type == "Characters":
- output.append("%s\"%s\"" % (" " * indent, token["data"]))
-
- elif type == "SpaceCharacters":
- assert False, "concatenateCharacterTokens should have got rid of all Space tokens"
-
- else:
- raise ValueError("Unknown token type, %s" % type)
-
- return "\n".join(output)
diff --git a/src/pip/_vendor/html5lib/treewalkers/base.py b/src/pip/_vendor/html5lib/treewalkers/base.py
deleted file mode 100644
index 80c474c4e..000000000
--- a/src/pip/_vendor/html5lib/treewalkers/base.py
+++ /dev/null
@@ -1,252 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from xml.dom import Node
-from ..constants import namespaces, voidElements, spaceCharacters
-
-__all__ = ["DOCUMENT", "DOCTYPE", "TEXT", "ELEMENT", "COMMENT", "ENTITY", "UNKNOWN",
- "TreeWalker", "NonRecursiveTreeWalker"]
-
-DOCUMENT = Node.DOCUMENT_NODE
-DOCTYPE = Node.DOCUMENT_TYPE_NODE
-TEXT = Node.TEXT_NODE
-ELEMENT = Node.ELEMENT_NODE
-COMMENT = Node.COMMENT_NODE
-ENTITY = Node.ENTITY_NODE
-UNKNOWN = "<#UNKNOWN#>"
-
-spaceCharacters = "".join(spaceCharacters)
-
-
-class TreeWalker(object):
- """Walks a tree yielding tokens
-
- Tokens are dicts that all have a ``type`` field specifying the type of the
- token.
-
- """
- def __init__(self, tree):
- """Creates a TreeWalker
-
- :arg tree: the tree to walk
-
- """
- self.tree = tree
-
- def __iter__(self):
- raise NotImplementedError
-
- def error(self, msg):
- """Generates an error token with the given message
-
- :arg msg: the error message
-
- :returns: SerializeError token
-
- """
- return {"type": "SerializeError", "data": msg}
-
- def emptyTag(self, namespace, name, attrs, hasChildren=False):
- """Generates an EmptyTag token
-
- :arg namespace: the namespace of the token--can be ``None``
-
- :arg name: the name of the element
-
- :arg attrs: the attributes of the element as a dict
-
- :arg hasChildren: whether or not to yield a SerializationError because
- this tag shouldn't have children
-
- :returns: EmptyTag token
-
- """
- yield {"type": "EmptyTag", "name": name,
- "namespace": namespace,
- "data": attrs}
- if hasChildren:
- yield self.error("Void element has children")
-
- def startTag(self, namespace, name, attrs):
- """Generates a StartTag token
-
- :arg namespace: the namespace of the token--can be ``None``
-
- :arg name: the name of the element
-
- :arg attrs: the attributes of the element as a dict
-
- :returns: StartTag token
-
- """
- return {"type": "StartTag",
- "name": name,
- "namespace": namespace,
- "data": attrs}
-
- def endTag(self, namespace, name):
- """Generates an EndTag token
-
- :arg namespace: the namespace of the token--can be ``None``
-
- :arg name: the name of the element
-
- :returns: EndTag token
-
- """
- return {"type": "EndTag",
- "name": name,
- "namespace": namespace}
-
- def text(self, data):
- """Generates SpaceCharacters and Characters tokens
-
- Depending on what's in the data, this generates one or more
- ``SpaceCharacters`` and ``Characters`` tokens.
-
- For example:
-
- >>> from html5lib.treewalkers.base import TreeWalker
- >>> # Give it an empty tree just so it instantiates
- >>> walker = TreeWalker([])
- >>> list(walker.text(''))
- []
- >>> list(walker.text(' '))
- [{u'data': ' ', u'type': u'SpaceCharacters'}]
- >>> list(walker.text(' abc ')) # doctest: +NORMALIZE_WHITESPACE
- [{u'data': ' ', u'type': u'SpaceCharacters'},
- {u'data': u'abc', u'type': u'Characters'},
- {u'data': u' ', u'type': u'SpaceCharacters'}]
-
- :arg data: the text data
-
- :returns: one or more ``SpaceCharacters`` and ``Characters`` tokens
-
- """
- data = data
- middle = data.lstrip(spaceCharacters)
- left = data[:len(data) - len(middle)]
- if left:
- yield {"type": "SpaceCharacters", "data": left}
- data = middle
- middle = data.rstrip(spaceCharacters)
- right = data[len(middle):]
- if middle:
- yield {"type": "Characters", "data": middle}
- if right:
- yield {"type": "SpaceCharacters", "data": right}
-
- def comment(self, data):
- """Generates a Comment token
-
- :arg data: the comment
-
- :returns: Comment token
-
- """
- return {"type": "Comment", "data": data}
-
- def doctype(self, name, publicId=None, systemId=None):
- """Generates a Doctype token
-
- :arg name:
-
- :arg publicId:
-
- :arg systemId:
-
- :returns: the Doctype token
-
- """
- return {"type": "Doctype",
- "name": name,
- "publicId": publicId,
- "systemId": systemId}
-
- def entity(self, name):
- """Generates an Entity token
-
- :arg name: the entity name
-
- :returns: an Entity token
-
- """
- return {"type": "Entity", "name": name}
-
- def unknown(self, nodeType):
- """Handles unknown node types"""
- return self.error("Unknown node type: " + nodeType)
-
-
-class NonRecursiveTreeWalker(TreeWalker):
- def getNodeDetails(self, node):
- raise NotImplementedError
-
- def getFirstChild(self, node):
- raise NotImplementedError
-
- def getNextSibling(self, node):
- raise NotImplementedError
-
- def getParentNode(self, node):
- raise NotImplementedError
-
- def __iter__(self):
- currentNode = self.tree
- while currentNode is not None:
- details = self.getNodeDetails(currentNode)
- type, details = details[0], details[1:]
- hasChildren = False
-
- if type == DOCTYPE:
- yield self.doctype(*details)
-
- elif type == TEXT:
- for token in self.text(*details):
- yield token
-
- elif type == ELEMENT:
- namespace, name, attributes, hasChildren = details
- if (not namespace or namespace == namespaces["html"]) and name in voidElements:
- for token in self.emptyTag(namespace, name, attributes,
- hasChildren):
- yield token
- hasChildren = False
- else:
- yield self.startTag(namespace, name, attributes)
-
- elif type == COMMENT:
- yield self.comment(details[0])
-
- elif type == ENTITY:
- yield self.entity(details[0])
-
- elif type == DOCUMENT:
- hasChildren = True
-
- else:
- yield self.unknown(details[0])
-
- if hasChildren:
- firstChild = self.getFirstChild(currentNode)
- else:
- firstChild = None
-
- if firstChild is not None:
- currentNode = firstChild
- else:
- while currentNode is not None:
- details = self.getNodeDetails(currentNode)
- type, details = details[0], details[1:]
- if type == ELEMENT:
- namespace, name, attributes, hasChildren = details
- if (namespace and namespace != namespaces["html"]) or name not in voidElements:
- yield self.endTag(namespace, name)
- if self.tree is currentNode:
- currentNode = None
- break
- nextSibling = self.getNextSibling(currentNode)
- if nextSibling is not None:
- currentNode = nextSibling
- break
- else:
- currentNode = self.getParentNode(currentNode)
diff --git a/src/pip/_vendor/html5lib/treewalkers/dom.py b/src/pip/_vendor/html5lib/treewalkers/dom.py
deleted file mode 100644
index b0c89b001..000000000
--- a/src/pip/_vendor/html5lib/treewalkers/dom.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from xml.dom import Node
-
-from . import base
-
-
-class TreeWalker(base.NonRecursiveTreeWalker):
- def getNodeDetails(self, node):
- if node.nodeType == Node.DOCUMENT_TYPE_NODE:
- return base.DOCTYPE, node.name, node.publicId, node.systemId
-
- elif node.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
- return base.TEXT, node.nodeValue
-
- elif node.nodeType == Node.ELEMENT_NODE:
- attrs = {}
- for attr in list(node.attributes.keys()):
- attr = node.getAttributeNode(attr)
- if attr.namespaceURI:
- attrs[(attr.namespaceURI, attr.localName)] = attr.value
- else:
- attrs[(None, attr.name)] = attr.value
- return (base.ELEMENT, node.namespaceURI, node.nodeName,
- attrs, node.hasChildNodes())
-
- elif node.nodeType == Node.COMMENT_NODE:
- return base.COMMENT, node.nodeValue
-
- elif node.nodeType in (Node.DOCUMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE):
- return (base.DOCUMENT,)
-
- else:
- return base.UNKNOWN, node.nodeType
-
- def getFirstChild(self, node):
- return node.firstChild
-
- def getNextSibling(self, node):
- return node.nextSibling
-
- def getParentNode(self, node):
- return node.parentNode
diff --git a/src/pip/_vendor/html5lib/treewalkers/etree.py b/src/pip/_vendor/html5lib/treewalkers/etree.py
deleted file mode 100644
index 837b27ec4..000000000
--- a/src/pip/_vendor/html5lib/treewalkers/etree.py
+++ /dev/null
@@ -1,131 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from collections import OrderedDict
-import re
-
-from pip._vendor.six import string_types
-
-from . import base
-from .._utils import moduleFactoryFactory
-
-tag_regexp = re.compile("{([^}]*)}(.*)")
-
-
-def getETreeBuilder(ElementTreeImplementation):
- ElementTree = ElementTreeImplementation
- ElementTreeCommentType = ElementTree.Comment("asd").tag
-
- class TreeWalker(base.NonRecursiveTreeWalker): # pylint:disable=unused-variable
- """Given the particular ElementTree representation, this implementation,
- to avoid using recursion, returns "nodes" as tuples with the following
- content:
-
- 1. The current element
-
- 2. The index of the element relative to its parent
-
- 3. A stack of ancestor elements
-
- 4. A flag "text", "tail" or None to indicate if the current node is a
- text node; either the text or tail of the current element (1)
- """
- def getNodeDetails(self, node):
- if isinstance(node, tuple): # It might be the root Element
- elt, _, _, flag = node
- if flag in ("text", "tail"):
- return base.TEXT, getattr(elt, flag)
- else:
- node = elt
-
- if not(hasattr(node, "tag")):
- node = node.getroot()
-
- if node.tag in ("DOCUMENT_ROOT", "DOCUMENT_FRAGMENT"):
- return (base.DOCUMENT,)
-
- elif node.tag == "<!DOCTYPE>":
- return (base.DOCTYPE, node.text,
- node.get("publicId"), node.get("systemId"))
-
- elif node.tag == ElementTreeCommentType:
- return base.COMMENT, node.text
-
- else:
- assert isinstance(node.tag, string_types), type(node.tag)
- # This is assumed to be an ordinary element
- match = tag_regexp.match(node.tag)
- if match:
- namespace, tag = match.groups()
- else:
- namespace = None
- tag = node.tag
- attrs = OrderedDict()
- for name, value in list(node.attrib.items()):
- match = tag_regexp.match(name)
- if match:
- attrs[(match.group(1), match.group(2))] = value
- else:
- attrs[(None, name)] = value
- return (base.ELEMENT, namespace, tag,
- attrs, len(node) or node.text)
-
- def getFirstChild(self, node):
- if isinstance(node, tuple):
- element, key, parents, flag = node
- else:
- element, key, parents, flag = node, None, [], None
-
- if flag in ("text", "tail"):
- return None
- else:
- if element.text:
- return element, key, parents, "text"
- elif len(element):
- parents.append(element)
- return element[0], 0, parents, None
- else:
- return None
-
- def getNextSibling(self, node):
- if isinstance(node, tuple):
- element, key, parents, flag = node
- else:
- return None
-
- if flag == "text":
- if len(element):
- parents.append(element)
- return element[0], 0, parents, None
- else:
- return None
- else:
- if element.tail and flag != "tail":
- return element, key, parents, "tail"
- elif key < len(parents[-1]) - 1:
- return parents[-1][key + 1], key + 1, parents, None
- else:
- return None
-
- def getParentNode(self, node):
- if isinstance(node, tuple):
- element, key, parents, flag = node
- else:
- return None
-
- if flag == "text":
- if not parents:
- return element
- else:
- return element, key, parents, None
- else:
- parent = parents.pop()
- if not parents:
- return parent
- else:
- assert list(parents[-1]).count(parent) == 1
- return parent, list(parents[-1]).index(parent), parents, None
-
- return locals()
-
-
-getETreeModule = moduleFactoryFactory(getETreeBuilder)
diff --git a/src/pip/_vendor/html5lib/treewalkers/etree_lxml.py b/src/pip/_vendor/html5lib/treewalkers/etree_lxml.py
deleted file mode 100644
index c56af390f..000000000
--- a/src/pip/_vendor/html5lib/treewalkers/etree_lxml.py
+++ /dev/null
@@ -1,215 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-from pip._vendor.six import text_type
-
-from collections import OrderedDict
-
-from lxml import etree
-from ..treebuilders.etree import tag_regexp
-
-from . import base
-
-from .. import _ihatexml
-
-
-def ensure_str(s):
- if s is None:
- return None
- elif isinstance(s, text_type):
- return s
- else:
- return s.decode("ascii", "strict")
-
-
-class Root(object):
- def __init__(self, et):
- self.elementtree = et
- self.children = []
-
- try:
- if et.docinfo.internalDTD:
- self.children.append(Doctype(self,
- ensure_str(et.docinfo.root_name),
- ensure_str(et.docinfo.public_id),
- ensure_str(et.docinfo.system_url)))
- except AttributeError:
- pass
-
- try:
- node = et.getroot()
- except AttributeError:
- node = et
-
- while node.getprevious() is not None:
- node = node.getprevious()
- while node is not None:
- self.children.append(node)
- node = node.getnext()
-
- self.text = None
- self.tail = None
-
- def __getitem__(self, key):
- return self.children[key]
-
- def getnext(self):
- return None
-
- def __len__(self):
- return 1
-
-
-class Doctype(object):
- def __init__(self, root_node, name, public_id, system_id):
- self.root_node = root_node
- self.name = name
- self.public_id = public_id
- self.system_id = system_id
-
- self.text = None
- self.tail = None
-
- def getnext(self):
- return self.root_node.children[1]
-
-
-class FragmentRoot(Root):
- def __init__(self, children):
- self.children = [FragmentWrapper(self, child) for child in children]
- self.text = self.tail = None
-
- def getnext(self):
- return None
-
-
-class FragmentWrapper(object):
- def __init__(self, fragment_root, obj):
- self.root_node = fragment_root
- self.obj = obj
- if hasattr(self.obj, 'text'):
- self.text = ensure_str(self.obj.text)
- else:
- self.text = None
- if hasattr(self.obj, 'tail'):
- self.tail = ensure_str(self.obj.tail)
- else:
- self.tail = None
-
- def __getattr__(self, name):
- return getattr(self.obj, name)
-
- def getnext(self):
- siblings = self.root_node.children
- idx = siblings.index(self)
- if idx < len(siblings) - 1:
- return siblings[idx + 1]
- else:
- return None
-
- def __getitem__(self, key):
- return self.obj[key]
-
- def __bool__(self):
- return bool(self.obj)
-
- def getparent(self):
- return None
-
- def __str__(self):
- return str(self.obj)
-
- def __unicode__(self):
- return str(self.obj)
-
- def __len__(self):
- return len(self.obj)
-
-
-class TreeWalker(base.NonRecursiveTreeWalker):
- def __init__(self, tree):
- # pylint:disable=redefined-variable-type
- if isinstance(tree, list):
- self.fragmentChildren = set(tree)
- tree = FragmentRoot(tree)
- else:
- self.fragmentChildren = set()
- tree = Root(tree)
- base.NonRecursiveTreeWalker.__init__(self, tree)
- self.filter = _ihatexml.InfosetFilter()
-
- def getNodeDetails(self, node):
- if isinstance(node, tuple): # Text node
- node, key = node
- assert key in ("text", "tail"), "Text nodes are text or tail, found %s" % key
- return base.TEXT, ensure_str(getattr(node, key))
-
- elif isinstance(node, Root):
- return (base.DOCUMENT,)
-
- elif isinstance(node, Doctype):
- return base.DOCTYPE, node.name, node.public_id, node.system_id
-
- elif isinstance(node, FragmentWrapper) and not hasattr(node, "tag"):
- return base.TEXT, ensure_str(node.obj)
-
- elif node.tag == etree.Comment:
- return base.COMMENT, ensure_str(node.text)
-
- elif node.tag == etree.Entity:
- return base.ENTITY, ensure_str(node.text)[1:-1] # strip &;
-
- else:
- # This is assumed to be an ordinary element
- match = tag_regexp.match(ensure_str(node.tag))
- if match:
- namespace, tag = match.groups()
- else:
- namespace = None
- tag = ensure_str(node.tag)
- attrs = OrderedDict()
- for name, value in list(node.attrib.items()):
- name = ensure_str(name)
- value = ensure_str(value)
- match = tag_regexp.match(name)
- if match:
- attrs[(match.group(1), match.group(2))] = value
- else:
- attrs[(None, name)] = value
- return (base.ELEMENT, namespace, self.filter.fromXmlName(tag),
- attrs, len(node) > 0 or node.text)
-
- def getFirstChild(self, node):
- assert not isinstance(node, tuple), "Text nodes have no children"
-
- assert len(node) or node.text, "Node has no children"
- if node.text:
- return (node, "text")
- else:
- return node[0]
-
- def getNextSibling(self, node):
- if isinstance(node, tuple): # Text node
- node, key = node
- assert key in ("text", "tail"), "Text nodes are text or tail, found %s" % key
- if key == "text":
- # XXX: we cannot use a "bool(node) and node[0] or None" construct here
- # because node[0] might evaluate to False if it has no child element
- if len(node):
- return node[0]
- else:
- return None
- else: # tail
- return node.getnext()
-
- return (node, "tail") if node.tail else node.getnext()
-
- def getParentNode(self, node):
- if isinstance(node, tuple): # Text node
- node, key = node
- assert key in ("text", "tail"), "Text nodes are text or tail, found %s" % key
- if key == "text":
- return node
- # else: fallback to "normal" processing
- elif node in self.fragmentChildren:
- return None
-
- return node.getparent()
diff --git a/src/pip/_vendor/html5lib/treewalkers/genshi.py b/src/pip/_vendor/html5lib/treewalkers/genshi.py
deleted file mode 100644
index 7483be27d..000000000
--- a/src/pip/_vendor/html5lib/treewalkers/genshi.py
+++ /dev/null
@@ -1,69 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-from genshi.core import QName
-from genshi.core import START, END, XML_NAMESPACE, DOCTYPE, TEXT
-from genshi.core import START_NS, END_NS, START_CDATA, END_CDATA, PI, COMMENT
-
-from . import base
-
-from ..constants import voidElements, namespaces
-
-
-class TreeWalker(base.TreeWalker):
- def __iter__(self):
- # Buffer the events so we can pass in the following one
- previous = None
- for event in self.tree:
- if previous is not None:
- for token in self.tokens(previous, event):
- yield token
- previous = event
-
- # Don't forget the final event!
- if previous is not None:
- for token in self.tokens(previous, None):
- yield token
-
- def tokens(self, event, next):
- kind, data, _ = event
- if kind == START:
- tag, attribs = data
- name = tag.localname
- namespace = tag.namespace
- converted_attribs = {}
- for k, v in attribs:
- if isinstance(k, QName):
- converted_attribs[(k.namespace, k.localname)] = v
- else:
- converted_attribs[(None, k)] = v
-
- if namespace == namespaces["html"] and name in voidElements:
- for token in self.emptyTag(namespace, name, converted_attribs,
- not next or next[0] != END or
- next[1] != tag):
- yield token
- else:
- yield self.startTag(namespace, name, converted_attribs)
-
- elif kind == END:
- name = data.localname
- namespace = data.namespace
- if namespace != namespaces["html"] or name not in voidElements:
- yield self.endTag(namespace, name)
-
- elif kind == COMMENT:
- yield self.comment(data)
-
- elif kind == TEXT:
- for token in self.text(data):
- yield token
-
- elif kind == DOCTYPE:
- yield self.doctype(*data)
-
- elif kind in (XML_NAMESPACE, DOCTYPE, START_NS, END_NS,
- START_CDATA, END_CDATA, PI):
- pass
-
- else:
- yield self.unknown(kind)
diff --git a/src/pip/_vendor/idna.pyi b/src/pip/_vendor/idna.pyi
deleted file mode 100644
index 7410d72fe..000000000
--- a/src/pip/_vendor/idna.pyi
+++ /dev/null
@@ -1 +0,0 @@
-from idna import * \ No newline at end of file
diff --git a/src/pip/_vendor/idna/codec.py b/src/pip/_vendor/idna/codec.py
index 080f22a3b..1ca9ba62c 100644
--- a/src/pip/_vendor/idna/codec.py
+++ b/src/pip/_vendor/idna/codec.py
@@ -7,8 +7,7 @@ _unicode_dots_re = re.compile('[\u002e\u3002\uff0e\uff61]')
class Codec(codecs.Codec):
- def encode(self, data, errors='strict'):
- # type: (str, str) -> Tuple[bytes, int]
+ def encode(self, data: str, errors: str = 'strict') -> Tuple[bytes, int]:
if errors != 'strict':
raise IDNAError('Unsupported error handling \"{}\"'.format(errors))
@@ -17,8 +16,7 @@ class Codec(codecs.Codec):
return encode(data), len(data)
- def decode(self, data, errors='strict'):
- # type: (bytes, str) -> Tuple[str, int]
+ def decode(self, data: bytes, errors: str = 'strict') -> Tuple[str, int]:
if errors != 'strict':
raise IDNAError('Unsupported error handling \"{}\"'.format(errors))
@@ -28,8 +26,7 @@ class Codec(codecs.Codec):
return decode(data), len(data)
class IncrementalEncoder(codecs.BufferedIncrementalEncoder):
- def _buffer_encode(self, data, errors, final): # type: ignore
- # type: (str, str, bool) -> Tuple[str, int]
+ def _buffer_encode(self, data: str, errors: str, final: bool) -> Tuple[str, int]: # type: ignore
if errors != 'strict':
raise IDNAError('Unsupported error handling \"{}\"'.format(errors))
@@ -62,8 +59,7 @@ class IncrementalEncoder(codecs.BufferedIncrementalEncoder):
return result_str, size
class IncrementalDecoder(codecs.BufferedIncrementalDecoder):
- def _buffer_decode(self, data, errors, final): # type: ignore
- # type: (str, str, bool) -> Tuple[str, int]
+ def _buffer_decode(self, data: str, errors: str, final: bool) -> Tuple[str, int]: # type: ignore
if errors != 'strict':
raise IDNAError('Unsupported error handling \"{}\"'.format(errors))
@@ -103,8 +99,7 @@ class StreamReader(Codec, codecs.StreamReader):
pass
-def getregentry():
- # type: () -> codecs.CodecInfo
+def getregentry() -> codecs.CodecInfo:
# Compatibility as a search_function for codecs.register()
return codecs.CodecInfo(
name='idna',
diff --git a/src/pip/_vendor/idna/compat.py b/src/pip/_vendor/idna/compat.py
index dc896c766..786e6bda6 100644
--- a/src/pip/_vendor/idna/compat.py
+++ b/src/pip/_vendor/idna/compat.py
@@ -2,15 +2,12 @@ from .core import *
from .codec import *
from typing import Any, Union
-def ToASCII(label):
- # type: (str) -> bytes
+def ToASCII(label: str) -> bytes:
return encode(label)
-def ToUnicode(label):
- # type: (Union[bytes, bytearray]) -> str
+def ToUnicode(label: Union[bytes, bytearray]) -> str:
return decode(label)
-def nameprep(s):
- # type: (Any) -> None
+def nameprep(s: Any) -> None:
raise NotImplementedError('IDNA 2008 does not utilise nameprep protocol')
diff --git a/src/pip/_vendor/idna/core.py b/src/pip/_vendor/idna/core.py
index d6051297d..55ab96788 100644
--- a/src/pip/_vendor/idna/core.py
+++ b/src/pip/_vendor/idna/core.py
@@ -29,43 +29,36 @@ class InvalidCodepointContext(IDNAError):
pass
-def _combining_class(cp):
- # type: (int) -> int
+def _combining_class(cp: int) -> int:
v = unicodedata.combining(chr(cp))
if v == 0:
if not unicodedata.name(chr(cp)):
raise ValueError('Unknown character in unicodedata')
return v
-def _is_script(cp, script):
- # type: (str, str) -> bool
+def _is_script(cp: str, script: str) -> bool:
return intranges_contain(ord(cp), idnadata.scripts[script])
-def _punycode(s):
- # type: (str) -> bytes
+def _punycode(s: str) -> bytes:
return s.encode('punycode')
-def _unot(s):
- # type: (int) -> str
+def _unot(s: int) -> str:
return 'U+{:04X}'.format(s)
-def valid_label_length(label):
- # type: (Union[bytes, str]) -> bool
+def valid_label_length(label: Union[bytes, str]) -> bool:
if len(label) > 63:
return False
return True
-def valid_string_length(label, trailing_dot):
- # type: (Union[bytes, str], bool) -> bool
+def valid_string_length(label: Union[bytes, str], trailing_dot: bool) -> bool:
if len(label) > (254 if trailing_dot else 253):
return False
return True
-def check_bidi(label, check_ltr=False):
- # type: (str, bool) -> bool
+def check_bidi(label: str, check_ltr: bool = False) -> bool:
# Bidi rules should only be applied if string contains RTL characters
bidi_label = False
for (idx, cp) in enumerate(label, 1):
@@ -124,15 +117,13 @@ def check_bidi(label, check_ltr=False):
return True
-def check_initial_combiner(label):
- # type: (str) -> bool
+def check_initial_combiner(label: str) -> bool:
if unicodedata.category(label[0])[0] == 'M':
raise IDNAError('Label begins with an illegal combining character')
return True
-def check_hyphen_ok(label):
- # type: (str) -> bool
+def check_hyphen_ok(label: str) -> bool:
if label[2:4] == '--':
raise IDNAError('Label has disallowed hyphens in 3rd and 4th position')
if label[0] == '-' or label[-1] == '-':
@@ -140,14 +131,12 @@ def check_hyphen_ok(label):
return True
-def check_nfc(label):
- # type: (str) -> None
+def check_nfc(label: str) -> None:
if unicodedata.normalize('NFC', label) != label:
raise IDNAError('Label must be in Normalization Form C')
-def valid_contextj(label, pos):
- # type: (str, int) -> bool
+def valid_contextj(label: str, pos: int) -> bool:
cp_value = ord(label[pos])
if cp_value == 0x200c:
@@ -190,8 +179,7 @@ def valid_contextj(label, pos):
return False
-def valid_contexto(label, pos, exception=False):
- # type: (str, int, bool) -> bool
+def valid_contexto(label: str, pos: int, exception: bool = False) -> bool:
cp_value = ord(label[pos])
if cp_value == 0x00b7:
@@ -233,8 +221,7 @@ def valid_contexto(label, pos, exception=False):
return False
-def check_label(label):
- # type: (Union[str, bytes, bytearray]) -> None
+def check_label(label: Union[str, bytes, bytearray]) -> None:
if isinstance(label, (bytes, bytearray)):
label = label.decode('utf-8')
if len(label) == 0:
@@ -265,8 +252,7 @@ def check_label(label):
check_bidi(label)
-def alabel(label):
- # type: (str) -> bytes
+def alabel(label: str) -> bytes:
try:
label_bytes = label.encode('ascii')
ulabel(label_bytes)
@@ -290,8 +276,7 @@ def alabel(label):
return label_bytes
-def ulabel(label):
- # type: (Union[str, bytes, bytearray]) -> str
+def ulabel(label: Union[str, bytes, bytearray]) -> str:
if not isinstance(label, (bytes, bytearray)):
try:
label_bytes = label.encode('ascii')
@@ -312,13 +297,15 @@ def ulabel(label):
check_label(label_bytes)
return label_bytes.decode('ascii')
- label = label_bytes.decode('punycode')
+ try:
+ label = label_bytes.decode('punycode')
+ except UnicodeError:
+ raise IDNAError('Invalid A-label')
check_label(label)
return label
-def uts46_remap(domain, std3_rules=True, transitional=False):
- # type: (str, bool, bool) -> str
+def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False) -> str:
"""Re-map the characters in the string according to UTS46 processing."""
from .uts46data import uts46data
output = ''
@@ -350,8 +337,7 @@ def uts46_remap(domain, std3_rules=True, transitional=False):
return unicodedata.normalize('NFC', output)
-def encode(s, strict=False, uts46=False, std3_rules=False, transitional=False):
- # type: (Union[str, bytes, bytearray], bool, bool, bool, bool) -> bytes
+def encode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool = False, std3_rules: bool = False, transitional: bool = False) -> bytes:
if isinstance(s, (bytes, bytearray)):
s = s.decode('ascii')
if uts46:
@@ -381,10 +367,12 @@ def encode(s, strict=False, uts46=False, std3_rules=False, transitional=False):
return s
-def decode(s, strict=False, uts46=False, std3_rules=False):
- # type: (Union[str, bytes, bytearray], bool, bool, bool) -> str
- if isinstance(s, (bytes, bytearray)):
- s = s.decode('ascii')
+def decode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool = False, std3_rules: bool = False) -> str:
+ try:
+ if isinstance(s, (bytes, bytearray)):
+ s = s.decode('ascii')
+ except UnicodeDecodeError:
+ raise IDNAError('Invalid ASCII in A-label')
if uts46:
s = uts46_remap(s, std3_rules, False)
trailing_dot = False
diff --git a/src/pip/_vendor/idna/idnadata.py b/src/pip/_vendor/idna/idnadata.py
index b86a3e06e..1b5805d15 100644
--- a/src/pip/_vendor/idna/idnadata.py
+++ b/src/pip/_vendor/idna/idnadata.py
@@ -1,6 +1,6 @@
# This file is automatically generated by tools/idna-data
-__version__ = '13.0.0'
+__version__ = '14.0.0'
scripts = {
'Greek': (
0x37000000374,
@@ -49,12 +49,13 @@ scripts = {
0x30210000302a,
0x30380000303c,
0x340000004dc0,
- 0x4e0000009ffd,
+ 0x4e000000a000,
0xf9000000fa6e,
0xfa700000fada,
+ 0x16fe200016fe4,
0x16ff000016ff2,
- 0x200000002a6de,
- 0x2a7000002b735,
+ 0x200000002a6e0,
+ 0x2a7000002b739,
0x2b7400002b81e,
0x2b8200002cea2,
0x2ceb00002ebe1,
@@ -75,7 +76,7 @@ scripts = {
'Hiragana': (
0x304100003097,
0x309d000030a0,
- 0x1b0010001b11f,
+ 0x1b0010001b120,
0x1b1500001b153,
0x1f2000001f201,
),
@@ -87,7 +88,11 @@ scripts = {
0x330000003358,
0xff660000ff70,
0xff710000ff9e,
+ 0x1aff00001aff4,
+ 0x1aff50001affc,
+ 0x1affd0001afff,
0x1b0000001b001,
+ 0x1b1200001b123,
0x1b1640001b168,
),
}
@@ -405,6 +410,39 @@ joining_types = {
0x868: 68,
0x869: 82,
0x86a: 82,
+ 0x870: 82,
+ 0x871: 82,
+ 0x872: 82,
+ 0x873: 82,
+ 0x874: 82,
+ 0x875: 82,
+ 0x876: 82,
+ 0x877: 82,
+ 0x878: 82,
+ 0x879: 82,
+ 0x87a: 82,
+ 0x87b: 82,
+ 0x87c: 82,
+ 0x87d: 82,
+ 0x87e: 82,
+ 0x87f: 82,
+ 0x880: 82,
+ 0x881: 82,
+ 0x882: 82,
+ 0x883: 67,
+ 0x884: 67,
+ 0x885: 67,
+ 0x886: 68,
+ 0x887: 85,
+ 0x888: 85,
+ 0x889: 68,
+ 0x88a: 68,
+ 0x88b: 68,
+ 0x88c: 68,
+ 0x88d: 68,
+ 0x88e: 82,
+ 0x890: 85,
+ 0x891: 85,
0x8a0: 68,
0x8a1: 68,
0x8a2: 68,
@@ -426,6 +464,7 @@ joining_types = {
0x8b2: 82,
0x8b3: 68,
0x8b4: 68,
+ 0x8b5: 68,
0x8b6: 68,
0x8b7: 68,
0x8b8: 68,
@@ -444,6 +483,7 @@ joining_types = {
0x8c5: 68,
0x8c6: 68,
0x8c7: 68,
+ 0x8c8: 68,
0x8e2: 85,
0x1806: 85,
0x1807: 68,
@@ -768,6 +808,24 @@ joining_types = {
0x10f52: 68,
0x10f53: 68,
0x10f54: 82,
+ 0x10f70: 68,
+ 0x10f71: 68,
+ 0x10f72: 68,
+ 0x10f73: 68,
+ 0x10f74: 82,
+ 0x10f75: 82,
+ 0x10f76: 68,
+ 0x10f77: 68,
+ 0x10f78: 68,
+ 0x10f79: 68,
+ 0x10f7a: 68,
+ 0x10f7b: 68,
+ 0x10f7c: 68,
+ 0x10f7d: 68,
+ 0x10f7e: 68,
+ 0x10f7f: 68,
+ 0x10f80: 68,
+ 0x10f81: 68,
0x10fb0: 68,
0x10fb1: 85,
0x10fb2: 68,
@@ -1168,9 +1226,9 @@ codepoint_classes = {
0x8000000082e,
0x8400000085c,
0x8600000086b,
- 0x8a0000008b5,
- 0x8b6000008c8,
- 0x8d3000008e2,
+ 0x87000000888,
+ 0x8890000088f,
+ 0x898000008e2,
0x8e300000958,
0x96000000964,
0x96600000970,
@@ -1252,11 +1310,12 @@ codepoint_classes = {
0xc0e00000c11,
0xc1200000c29,
0xc2a00000c3a,
- 0xc3d00000c45,
+ 0xc3c00000c45,
0xc4600000c49,
0xc4a00000c4e,
0xc5500000c57,
0xc5800000c5b,
+ 0xc5d00000c5e,
0xc6000000c64,
0xc6600000c70,
0xc8000000c84,
@@ -1269,7 +1328,7 @@ codepoint_classes = {
0xcc600000cc9,
0xcca00000cce,
0xcd500000cd7,
- 0xcde00000cdf,
+ 0xcdd00000cdf,
0xce000000ce4,
0xce600000cf0,
0xcf100000cf3,
@@ -1366,9 +1425,8 @@ codepoint_classes = {
0x16810000169b,
0x16a0000016eb,
0x16f1000016f9,
- 0x17000000170d,
- 0x170e00001715,
- 0x172000001735,
+ 0x170000001716,
+ 0x171f00001735,
0x174000001754,
0x17600000176d,
0x176e00001771,
@@ -1397,8 +1455,8 @@ codepoint_classes = {
0x1a9000001a9a,
0x1aa700001aa8,
0x1ab000001abe,
- 0x1abf00001ac1,
- 0x1b0000001b4c,
+ 0x1abf00001acf,
+ 0x1b0000001b4d,
0x1b5000001b5a,
0x1b6b00001b74,
0x1b8000001bf4,
@@ -1413,8 +1471,7 @@ codepoint_classes = {
0x1d4e00001d4f,
0x1d6b00001d78,
0x1d7900001d9b,
- 0x1dc000001dfa,
- 0x1dfb00001e00,
+ 0x1dc000001e00,
0x1e0100001e02,
0x1e0300001e04,
0x1e0500001e06,
@@ -1563,7 +1620,7 @@ codepoint_classes = {
0x1ff600001ff7,
0x214e0000214f,
0x218400002185,
- 0x2c3000002c5f,
+ 0x2c3000002c60,
0x2c6100002c62,
0x2c6500002c67,
0x2c6800002c69,
@@ -1652,8 +1709,7 @@ codepoint_classes = {
0x31a0000031c0,
0x31f000003200,
0x340000004dc0,
- 0x4e0000009ffd,
- 0xa0000000a48d,
+ 0x4e000000a48d,
0xa4d00000a4fe,
0xa5000000a60d,
0xa6100000a62c,
@@ -1766,9 +1822,16 @@ codepoint_classes = {
0xa7bb0000a7bc,
0xa7bd0000a7be,
0xa7bf0000a7c0,
+ 0xa7c10000a7c2,
0xa7c30000a7c4,
0xa7c80000a7c9,
0xa7ca0000a7cb,
+ 0xa7d10000a7d2,
+ 0xa7d30000a7d4,
+ 0xa7d50000a7d6,
+ 0xa7d70000a7d8,
+ 0xa7d90000a7da,
+ 0xa7f20000a7f5,
0xa7f60000a7f8,
0xa7fa0000a828,
0xa82c0000a82d,
@@ -1834,9 +1897,16 @@ codepoint_classes = {
0x104d8000104fc,
0x1050000010528,
0x1053000010564,
+ 0x10597000105a2,
+ 0x105a3000105b2,
+ 0x105b3000105ba,
+ 0x105bb000105bd,
0x1060000010737,
0x1074000010756,
0x1076000010768,
+ 0x1078000010786,
+ 0x10787000107b1,
+ 0x107b2000107bb,
0x1080000010806,
0x1080800010809,
0x1080a00010836,
@@ -1876,11 +1946,13 @@ codepoint_classes = {
0x10f0000010f1d,
0x10f2700010f28,
0x10f3000010f51,
+ 0x10f7000010f86,
0x10fb000010fc5,
0x10fe000010ff7,
0x1100000011047,
- 0x1106600011070,
+ 0x1106600011076,
0x1107f000110bb,
+ 0x110c2000110c3,
0x110d0000110e9,
0x110f0000110fa,
0x1110000011135,
@@ -1934,6 +2006,7 @@ codepoint_classes = {
0x117000001171b,
0x1171d0001172c,
0x117300001173a,
+ 0x1174000011747,
0x118000001183b,
0x118c0000118ea,
0x118ff00011907,
@@ -1952,7 +2025,7 @@ codepoint_classes = {
0x11a4700011a48,
0x11a5000011a9a,
0x11a9d00011a9e,
- 0x11ac000011af9,
+ 0x11ab000011af9,
0x11c0000011c09,
0x11c0a00011c37,
0x11c3800011c41,
@@ -1977,11 +2050,14 @@ codepoint_classes = {
0x11fb000011fb1,
0x120000001239a,
0x1248000012544,
+ 0x12f9000012ff1,
0x130000001342f,
0x1440000014647,
0x1680000016a39,
0x16a4000016a5f,
0x16a6000016a6a,
+ 0x16a7000016abf,
+ 0x16ac000016aca,
0x16ad000016aee,
0x16af000016af5,
0x16b0000016b37,
@@ -1999,7 +2075,10 @@ codepoint_classes = {
0x17000000187f8,
0x1880000018cd6,
0x18d0000018d09,
- 0x1b0000001b11f,
+ 0x1aff00001aff4,
+ 0x1aff50001affc,
+ 0x1affd0001afff,
+ 0x1b0000001b123,
0x1b1500001b153,
0x1b1640001b168,
0x1b1700001b2fc,
@@ -2008,12 +2087,15 @@ codepoint_classes = {
0x1bc800001bc89,
0x1bc900001bc9a,
0x1bc9d0001bc9f,
+ 0x1cf000001cf2e,
+ 0x1cf300001cf47,
0x1da000001da37,
0x1da3b0001da6d,
0x1da750001da76,
0x1da840001da85,
0x1da9b0001daa0,
0x1daa10001dab0,
+ 0x1df000001df1f,
0x1e0000001e007,
0x1e0080001e019,
0x1e01b0001e022,
@@ -2023,14 +2105,19 @@ codepoint_classes = {
0x1e1300001e13e,
0x1e1400001e14a,
0x1e14e0001e14f,
+ 0x1e2900001e2af,
0x1e2c00001e2fa,
+ 0x1e7e00001e7e7,
+ 0x1e7e80001e7ec,
+ 0x1e7ed0001e7ef,
+ 0x1e7f00001e7ff,
0x1e8000001e8c5,
0x1e8d00001e8d7,
0x1e9220001e94c,
0x1e9500001e95a,
0x1fbf00001fbfa,
- 0x200000002a6de,
- 0x2a7000002b735,
+ 0x200000002a6e0,
+ 0x2a7000002b739,
0x2b7400002b81e,
0x2b8200002cea2,
0x2ceb00002ebe1,
diff --git a/src/pip/_vendor/idna/intranges.py b/src/pip/_vendor/idna/intranges.py
index ee364a904..6a43b0475 100644
--- a/src/pip/_vendor/idna/intranges.py
+++ b/src/pip/_vendor/idna/intranges.py
@@ -8,8 +8,7 @@ in the original list?" in time O(log(# runs)).
import bisect
from typing import List, Tuple
-def intranges_from_list(list_):
- # type: (List[int]) -> Tuple[int, ...]
+def intranges_from_list(list_: List[int]) -> Tuple[int, ...]:
"""Represent a list of integers as a sequence of ranges:
((start_0, end_0), (start_1, end_1), ...), such that the original
integers are exactly those x such that start_i <= x < end_i for some i.
@@ -30,17 +29,14 @@ def intranges_from_list(list_):
return tuple(ranges)
-def _encode_range(start, end):
- # type: (int, int) -> int
+def _encode_range(start: int, end: int) -> int:
return (start << 32) | end
-def _decode_range(r):
- # type: (int) -> Tuple[int, int]
+def _decode_range(r: int) -> Tuple[int, int]:
return (r >> 32), (r & ((1 << 32) - 1))
-def intranges_contain(int_, ranges):
- # type: (int, Tuple[int, ...]) -> bool
+def intranges_contain(int_: int, ranges: Tuple[int, ...]) -> bool:
"""Determine if `int_` falls into one of the ranges in `ranges`."""
tuple_ = _encode_range(int_, 0)
pos = bisect.bisect_left(ranges, tuple_)
diff --git a/src/pip/_vendor/idna/package_data.py b/src/pip/_vendor/idna/package_data.py
index e096d1d52..f5ea87c12 100644
--- a/src/pip/_vendor/idna/package_data.py
+++ b/src/pip/_vendor/idna/package_data.py
@@ -1,2 +1,2 @@
-__version__ = '3.2'
+__version__ = '3.3'
diff --git a/src/pip/_vendor/idna/uts46data.py b/src/pip/_vendor/idna/uts46data.py
index f382ce389..8f65705ee 100644
--- a/src/pip/_vendor/idna/uts46data.py
+++ b/src/pip/_vendor/idna/uts46data.py
@@ -1,13 +1,14 @@
# This file is automatically generated by tools/idna-data
+# vim: set fileencoding=utf-8 :
from typing import List, Tuple, Union
+
"""IDNA Mapping Table from UTS46."""
-__version__ = '13.0.0'
-def _seg_0():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+__version__ = '14.0.0'
+def _seg_0() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0x0, '3'),
(0x1, '3'),
@@ -111,8 +112,7 @@ def _seg_0():
(0x63, 'V'),
]
-def _seg_1():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_1() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0x64, 'V'),
(0x65, 'V'),
@@ -216,8 +216,7 @@ def _seg_1():
(0xC7, 'M', 'ç'),
]
-def _seg_2():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_2() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0xC8, 'M', 'è'),
(0xC9, 'M', 'é'),
@@ -321,8 +320,7 @@ def _seg_2():
(0x12B, 'V'),
]
-def _seg_3():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_3() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0x12C, 'M', 'Ä­'),
(0x12D, 'V'),
@@ -426,8 +424,7 @@ def _seg_3():
(0x193, 'M', 'É '),
]
-def _seg_4():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_4() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0x194, 'M', 'É£'),
(0x195, 'V'),
@@ -531,8 +528,7 @@ def _seg_4():
(0x20C, 'M', 'È'),
]
-def _seg_5():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_5() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0x20D, 'V'),
(0x20E, 'M', 'È'),
@@ -636,8 +632,7 @@ def _seg_5():
(0x377, 'V'),
]
-def _seg_6():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_6() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0x378, 'X'),
(0x37A, '3', ' ι'),
@@ -741,8 +736,7 @@ def _seg_6():
(0x402, 'M', 'Ñ’'),
]
-def _seg_7():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_7() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0x403, 'M', 'Ñ“'),
(0x404, 'M', 'Ñ”'),
@@ -846,8 +840,7 @@ def _seg_7():
(0x49D, 'V'),
]
-def _seg_8():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_8() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0x49E, 'M', 'ÒŸ'),
(0x49F, 'V'),
@@ -951,8 +944,7 @@ def _seg_8():
(0x502, 'M', 'Ôƒ'),
]
-def _seg_9():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_9() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0x503, 'V'),
(0x504, 'M', 'Ô…'),
@@ -1053,11 +1045,10 @@ def _seg_9():
(0x5F5, 'X'),
(0x606, 'V'),
(0x61C, 'X'),
- (0x61E, 'V'),
+ (0x61D, 'V'),
]
-def _seg_10():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_10() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0x675, 'M', 'اٴ'),
(0x676, 'M', 'وٴ'),
@@ -1083,11 +1074,9 @@ def _seg_10():
(0x85F, 'X'),
(0x860, 'V'),
(0x86B, 'X'),
- (0x8A0, 'V'),
- (0x8B5, 'X'),
- (0x8B6, 'V'),
- (0x8C8, 'X'),
- (0x8D3, 'V'),
+ (0x870, 'V'),
+ (0x88F, 'X'),
+ (0x898, 'V'),
(0x8E2, 'X'),
(0x8E3, 'V'),
(0x958, 'M', 'क़'),
@@ -1159,13 +1148,12 @@ def _seg_10():
(0xA59, 'M', 'ਖ਼'),
(0xA5A, 'M', 'ਗ਼'),
(0xA5B, 'M', 'ਜ਼'),
+ (0xA5C, 'V'),
+ (0xA5D, 'X'),
]
-def _seg_11():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_11() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
- (0xA5C, 'V'),
- (0xA5D, 'X'),
(0xA5E, 'M', 'ਫ਼'),
(0xA5F, 'X'),
(0xA66, 'V'),
@@ -1264,15 +1252,14 @@ def _seg_11():
(0xC0E, 'V'),
(0xC11, 'X'),
(0xC12, 'V'),
+ (0xC29, 'X'),
+ (0xC2A, 'V'),
]
-def _seg_12():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_12() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
- (0xC29, 'X'),
- (0xC2A, 'V'),
(0xC3A, 'X'),
- (0xC3D, 'V'),
+ (0xC3C, 'V'),
(0xC45, 'X'),
(0xC46, 'V'),
(0xC49, 'X'),
@@ -1282,6 +1269,8 @@ def _seg_12():
(0xC57, 'X'),
(0xC58, 'V'),
(0xC5B, 'X'),
+ (0xC5D, 'V'),
+ (0xC5E, 'X'),
(0xC60, 'V'),
(0xC64, 'X'),
(0xC66, 'V'),
@@ -1304,7 +1293,7 @@ def _seg_12():
(0xCCE, 'X'),
(0xCD5, 'V'),
(0xCD7, 'X'),
- (0xCDE, 'V'),
+ (0xCDD, 'V'),
(0xCDF, 'X'),
(0xCE0, 'V'),
(0xCE4, 'X'),
@@ -1371,8 +1360,7 @@ def _seg_12():
(0xEB4, 'V'),
]
-def _seg_13():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_13() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0xEBE, 'X'),
(0xEC0, 'V'),
@@ -1476,8 +1464,7 @@ def _seg_13():
(0x1312, 'V'),
]
-def _seg_14():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_14() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
(0x1316, 'X'),
(0x1318, 'V'),
@@ -1502,10 +1489,8 @@ def _seg_14():
(0x16A0, 'V'),
(0x16F9, 'X'),
(0x1700, 'V'),
- (0x170D, 'X'),
- (0x170E, 'V'),
- (0x1715, 'X'),
- (0x1720, 'V'),
+ (0x1716, 'X'),
+ (0x171F, 'V'),
(0x1737, 'X'),
(0x1740, 'V'),
(0x1754, 'X'),
@@ -1528,6 +1513,7 @@ def _seg_14():
(0x1807, 'V'),
(0x180B, 'I'),
(0x180E, 'X'),
+ (0x180F, 'I'),
(0x1810, 'V'),
(0x181A, 'X'),
(0x1820, 'V'),
@@ -1567,11 +1553,11 @@ def _seg_14():
(0x1AA0, 'V'),
(0x1AAE, 'X'),
(0x1AB0, 'V'),
- (0x1AC1, 'X'),
+ (0x1ACF, 'X'),
(0x1B00, 'V'),
- (0x1B4C, 'X'),
+ (0x1B4D, 'X'),
(0x1B50, 'V'),
- (0x1B7D, 'X'),
+ (0x1B7F, 'X'),
(0x1B80, 'V'),
(0x1BF4, 'X'),
(0x1BFC, 'V'),
@@ -1579,12 +1565,11 @@ def _seg_14():
(0x1C3B, 'V'),
(0x1C4A, 'X'),
(0x1C4D, 'V'),
+ (0x1C80, 'M', 'в'),
]
-def _seg_15():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_15() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
- (0x1C80, 'M', 'в'),
(0x1C81, 'M', 'д'),
(0x1C82, 'M', 'о'),
(0x1C83, 'M', 'Ñ'),
@@ -1684,12 +1669,11 @@ def _seg_15():
(0x1D50, 'M', 'm'),
(0x1D51, 'M', 'Å‹'),
(0x1D52, 'M', 'o'),
+ (0x1D53, 'M', 'É”'),
]
-def _seg_16():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
+def _seg_16() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
return [
- (0x1D53, 'M', 'É”'),
(0x1D54, 'M', 'á´–'),
(0x1D55, 'M', 'á´—'),
(0x1D56, 'M', 'p'),
@@ -1754,8 +1738,6 @@ def _seg_16():
(0x1DBE, 'M', 'Ê’'),
(0x1DBF, 'M', 'θ'),
(0x1DC0, 'V'),
- (0x1DFA, 'X'),
- (0x1DFB, 'V'),
(0x1E00, 'M', 'á¸'),
(0x1E01, 'V'),
(0x1E02, 'M', 'ḃ'),
@@ -1789,14 +1771,13 @@ def _seg_16():
(0x1E1E, 'M', 'ḟ'),
(0x1E1F, 'V'),
(0x1E20, 'M', 'ḡ'),
- ]
-
-def _seg_17():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1E21, 'V'),
(0x1E22, 'M', 'ḣ'),
(0x1E23, 'V'),
+ ]
+
+def _seg_17() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1E24, 'M', 'ḥ'),
(0x1E25, 'V'),
(0x1E26, 'M', 'ḧ'),
@@ -1894,14 +1875,13 @@ def _seg_17():
(0x1E82, 'M', 'ẃ'),
(0x1E83, 'V'),
(0x1E84, 'M', 'ẅ'),
- ]
-
-def _seg_18():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1E85, 'V'),
(0x1E86, 'M', 'ẇ'),
(0x1E87, 'V'),
+ ]
+
+def _seg_18() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1E88, 'M', 'ẉ'),
(0x1E89, 'V'),
(0x1E8A, 'M', 'ẋ'),
@@ -1999,14 +1979,13 @@ def _seg_18():
(0x1EEB, 'V'),
(0x1EEC, 'M', 'á»­'),
(0x1EED, 'V'),
- ]
-
-def _seg_19():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1EEE, 'M', 'ữ'),
(0x1EEF, 'V'),
(0x1EF0, 'M', 'á»±'),
+ ]
+
+def _seg_19() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1EF1, 'V'),
(0x1EF2, 'M', 'ỳ'),
(0x1EF3, 'V'),
@@ -2104,14 +2083,13 @@ def _seg_19():
(0x1F82, 'M', 'ἂι'),
(0x1F83, 'M', 'ἃι'),
(0x1F84, 'M', 'ἄι'),
- ]
-
-def _seg_20():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1F85, 'M', 'ἅι'),
(0x1F86, 'M', 'ἆι'),
(0x1F87, 'M', 'ἇι'),
+ ]
+
+def _seg_20() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1F88, 'M', 'ἀι'),
(0x1F89, 'M', 'á¼Î¹'),
(0x1F8A, 'M', 'ἂι'),
@@ -2209,14 +2187,13 @@ def _seg_20():
(0x1FF0, 'X'),
(0x1FF2, 'M', 'ὼι'),
(0x1FF3, 'M', 'ωι'),
- ]
-
-def _seg_21():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1FF4, 'M', 'ώι'),
(0x1FF5, 'X'),
(0x1FF6, 'V'),
+ ]
+
+def _seg_21() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1FF7, 'M', 'ῶι'),
(0x1FF8, 'M', 'ὸ'),
(0x1FF9, 'M', 'ό'),
@@ -2309,19 +2286,18 @@ def _seg_21():
(0x20A0, 'V'),
(0x20A8, 'M', 'rs'),
(0x20A9, 'V'),
- (0x20C0, 'X'),
+ (0x20C1, 'X'),
(0x20D0, 'V'),
(0x20F1, 'X'),
(0x2100, '3', 'a/c'),
(0x2101, '3', 'a/s'),
- ]
-
-def _seg_22():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x2102, 'M', 'c'),
(0x2103, 'M', '°c'),
(0x2104, 'V'),
+ ]
+
+def _seg_22() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x2105, '3', 'c/o'),
(0x2106, '3', 'c/u'),
(0x2107, 'M', 'É›'),
@@ -2419,14 +2395,13 @@ def _seg_22():
(0x2177, 'M', 'viii'),
(0x2178, 'M', 'ix'),
(0x2179, 'M', 'x'),
- ]
-
-def _seg_23():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x217A, 'M', 'xi'),
(0x217B, 'M', 'xii'),
(0x217C, 'M', 'l'),
+ ]
+
+def _seg_23() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x217D, 'M', 'c'),
(0x217E, 'M', 'd'),
(0x217F, 'M', 'm'),
@@ -2524,14 +2499,13 @@ def _seg_23():
(0x24B7, 'M', 'b'),
(0x24B8, 'M', 'c'),
(0x24B9, 'M', 'd'),
- ]
-
-def _seg_24():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x24BA, 'M', 'e'),
(0x24BB, 'M', 'f'),
(0x24BC, 'M', 'g'),
+ ]
+
+def _seg_24() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x24BD, 'M', 'h'),
(0x24BE, 'M', 'i'),
(0x24BF, 'M', 'j'),
@@ -2629,23 +2603,21 @@ def _seg_24():
(0x2C23, 'M', 'ⱓ'),
(0x2C24, 'M', 'â±”'),
(0x2C25, 'M', 'ⱕ'),
- ]
-
-def _seg_25():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x2C26, 'M', 'â±–'),
(0x2C27, 'M', 'â±—'),
(0x2C28, 'M', 'ⱘ'),
+ ]
+
+def _seg_25() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x2C29, 'M', 'â±™'),
(0x2C2A, 'M', 'ⱚ'),
(0x2C2B, 'M', 'â±›'),
(0x2C2C, 'M', 'ⱜ'),
(0x2C2D, 'M', 'â±'),
(0x2C2E, 'M', 'ⱞ'),
- (0x2C2F, 'X'),
+ (0x2C2F, 'M', 'ⱟ'),
(0x2C30, 'V'),
- (0x2C5F, 'X'),
(0x2C60, 'M', 'ⱡ'),
(0x2C61, 'V'),
(0x2C62, 'M', 'É«'),
@@ -2734,15 +2706,14 @@ def _seg_25():
(0x2CBC, 'M', 'â²½'),
(0x2CBD, 'V'),
(0x2CBE, 'M', 'ⲿ'),
- ]
-
-def _seg_26():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x2CBF, 'V'),
(0x2CC0, 'M', 'â³'),
(0x2CC1, 'V'),
(0x2CC2, 'M', 'ⳃ'),
+ ]
+
+def _seg_26() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x2CC3, 'V'),
(0x2CC4, 'M', 'â³…'),
(0x2CC5, 'V'),
@@ -2813,7 +2784,7 @@ def _seg_26():
(0x2DD8, 'V'),
(0x2DDF, 'X'),
(0x2DE0, 'V'),
- (0x2E53, 'X'),
+ (0x2E5E, 'X'),
(0x2E80, 'V'),
(0x2E9A, 'X'),
(0x2E9B, 'V'),
@@ -2839,15 +2810,14 @@ def _seg_26():
(0x2F0F, 'M', '几'),
(0x2F10, 'M', '凵'),
(0x2F11, 'M', '刀'),
- ]
-
-def _seg_27():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x2F12, 'M', '力'),
(0x2F13, 'M', '勹'),
(0x2F14, 'M', '匕'),
(0x2F15, 'M', '匚'),
+ ]
+
+def _seg_27() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x2F16, 'M', '匸'),
(0x2F17, 'M', 'å'),
(0x2F18, 'M', 'åœ'),
@@ -2944,15 +2914,14 @@ def _seg_27():
(0x2F73, 'M', 'ç©´'),
(0x2F74, 'M', 'ç«‹'),
(0x2F75, 'M', '竹'),
- ]
-
-def _seg_28():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x2F76, 'M', 'ç±³'),
(0x2F77, 'M', '糸'),
(0x2F78, 'M', '缶'),
(0x2F79, 'M', '网'),
+ ]
+
+def _seg_28() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x2F7A, 'M', '羊'),
(0x2F7B, 'M', 'ç¾½'),
(0x2F7C, 'M', 'è€'),
@@ -3049,15 +3018,14 @@ def _seg_28():
(0x3000, '3', ' '),
(0x3001, 'V'),
(0x3002, 'M', '.'),
- ]
-
-def _seg_29():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x3003, 'V'),
(0x3036, 'M', '〒'),
(0x3037, 'V'),
(0x3038, 'M', 'å'),
+ ]
+
+def _seg_29() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x3039, 'M', 'å„'),
(0x303A, 'M', 'å…'),
(0x303B, 'V'),
@@ -3154,15 +3122,14 @@ def _seg_29():
(0x317E, 'M', 'ᄶ'),
(0x317F, 'M', 'á…€'),
(0x3180, 'M', 'á…‡'),
- ]
-
-def _seg_30():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x3181, 'M', 'ᅌ'),
(0x3182, 'M', 'ᇱ'),
(0x3183, 'M', 'ᇲ'),
(0x3184, 'M', 'á…—'),
+ ]
+
+def _seg_30() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x3185, 'M', 'á…˜'),
(0x3186, 'M', 'á…™'),
(0x3187, 'M', 'ᆄ'),
@@ -3259,15 +3226,14 @@ def _seg_30():
(0x3240, '3', '(祭)'),
(0x3241, '3', '(休)'),
(0x3242, '3', '(自)'),
- ]
-
-def _seg_31():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x3243, '3', '(至)'),
(0x3244, 'M', 'å•'),
(0x3245, 'M', 'å¹¼'),
(0x3246, 'M', 'æ–‡'),
+ ]
+
+def _seg_31() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x3247, 'M', 'ç®'),
(0x3248, 'V'),
(0x3250, 'M', 'pte'),
@@ -3364,15 +3330,14 @@ def _seg_31():
(0x32AB, 'M', 'å­¦'),
(0x32AC, 'M', '監'),
(0x32AD, 'M', 'ä¼'),
- ]
-
-def _seg_32():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x32AE, 'M', '資'),
(0x32AF, 'M', 'å”'),
(0x32B0, 'M', '夜'),
(0x32B1, 'M', '36'),
+ ]
+
+def _seg_32() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x32B2, 'M', '37'),
(0x32B3, 'M', '38'),
(0x32B4, 'M', '39'),
@@ -3469,15 +3434,14 @@ def _seg_32():
(0x330F, 'M', 'ガンマ'),
(0x3310, 'M', 'ギガ'),
(0x3311, 'M', 'ギニー'),
- ]
-
-def _seg_33():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x3312, 'M', 'キュリー'),
(0x3313, 'M', 'ギルダー'),
(0x3314, 'M', 'キロ'),
(0x3315, 'M', 'キログラム'),
+ ]
+
+def _seg_33() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x3316, 'M', 'キロメートル'),
(0x3317, 'M', 'キロワット'),
(0x3318, 'M', 'グラム'),
@@ -3574,15 +3538,14 @@ def _seg_33():
(0x3373, 'M', 'au'),
(0x3374, 'M', 'bar'),
(0x3375, 'M', 'ov'),
- ]
-
-def _seg_34():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x3376, 'M', 'pc'),
(0x3377, 'M', 'dm'),
(0x3378, 'M', 'dm2'),
(0x3379, 'M', 'dm3'),
+ ]
+
+def _seg_34() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x337A, 'M', 'iu'),
(0x337B, 'M', 'å¹³æˆ'),
(0x337C, 'M', '昭和'),
@@ -3679,15 +3642,14 @@ def _seg_34():
(0x33D7, 'M', 'ph'),
(0x33D8, 'X'),
(0x33D9, 'M', 'ppm'),
- ]
-
-def _seg_35():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x33DA, 'M', 'pr'),
(0x33DB, 'M', 'sr'),
(0x33DC, 'M', 'sv'),
(0x33DD, 'M', 'wb'),
+ ]
+
+def _seg_35() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x33DE, 'M', 'v∕m'),
(0x33DF, 'M', 'a∕m'),
(0x33E0, 'M', '1æ—¥'),
@@ -3723,8 +3685,6 @@ def _seg_35():
(0x33FE, 'M', '31æ—¥'),
(0x33FF, 'M', 'gal'),
(0x3400, 'V'),
- (0x9FFD, 'X'),
- (0xA000, 'V'),
(0xA48D, 'X'),
(0xA490, 'V'),
(0xA4C7, 'X'),
@@ -3784,17 +3744,16 @@ def _seg_35():
(0xA685, 'V'),
(0xA686, 'M', 'ꚇ'),
(0xA687, 'V'),
- ]
-
-def _seg_36():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xA688, 'M', 'ꚉ'),
(0xA689, 'V'),
(0xA68A, 'M', 'êš‹'),
(0xA68B, 'V'),
(0xA68C, 'M', 'êš'),
(0xA68D, 'V'),
+ ]
+
+def _seg_36() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xA68E, 'M', 'êš'),
(0xA68F, 'V'),
(0xA690, 'M', 'êš‘'),
@@ -3889,17 +3848,16 @@ def _seg_36():
(0xA76C, 'M', 'ê­'),
(0xA76D, 'V'),
(0xA76E, 'M', 'ê¯'),
- ]
-
-def _seg_37():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xA76F, 'V'),
(0xA770, 'M', 'ê¯'),
(0xA771, 'V'),
(0xA779, 'M', 'êº'),
(0xA77A, 'V'),
(0xA77B, 'M', 'ê¼'),
+ ]
+
+def _seg_37() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xA77C, 'V'),
(0xA77D, 'M', 'áµ¹'),
(0xA77E, 'M', 'ê¿'),
@@ -3962,7 +3920,8 @@ def _seg_37():
(0xA7BD, 'V'),
(0xA7BE, 'M', 'êž¿'),
(0xA7BF, 'V'),
- (0xA7C0, 'X'),
+ (0xA7C0, 'M', 'êŸ'),
+ (0xA7C1, 'V'),
(0xA7C2, 'M', 'ꟃ'),
(0xA7C3, 'V'),
(0xA7C4, 'M', 'êž”'),
@@ -3973,6 +3932,20 @@ def _seg_37():
(0xA7C9, 'M', 'ꟊ'),
(0xA7CA, 'V'),
(0xA7CB, 'X'),
+ (0xA7D0, 'M', 'ꟑ'),
+ (0xA7D1, 'V'),
+ (0xA7D2, 'X'),
+ (0xA7D3, 'V'),
+ (0xA7D4, 'X'),
+ (0xA7D5, 'V'),
+ (0xA7D6, 'M', 'ꟗ'),
+ (0xA7D7, 'V'),
+ (0xA7D8, 'M', 'ꟙ'),
+ (0xA7D9, 'V'),
+ (0xA7DA, 'X'),
+ (0xA7F2, 'M', 'c'),
+ (0xA7F3, 'M', 'f'),
+ (0xA7F4, 'M', 'q'),
(0xA7F5, 'M', 'ꟶ'),
(0xA7F6, 'V'),
(0xA7F8, 'M', 'ħ'),
@@ -3985,6 +3958,10 @@ def _seg_37():
(0xA878, 'X'),
(0xA880, 'V'),
(0xA8C6, 'X'),
+ ]
+
+def _seg_38() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xA8CE, 'V'),
(0xA8DA, 'X'),
(0xA8E0, 'V'),
@@ -3994,11 +3971,6 @@ def _seg_37():
(0xA980, 'V'),
(0xA9CE, 'X'),
(0xA9CF, 'V'),
- ]
-
-def _seg_38():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xA9DA, 'X'),
(0xA9DE, 'V'),
(0xA9FF, 'X'),
@@ -4090,6 +4062,10 @@ def _seg_38():
(0xABA8, 'M', 'á˜'),
(0xABA9, 'M', 'á™'),
(0xABAA, 'M', 'áš'),
+ ]
+
+def _seg_39() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xABAB, 'M', 'á›'),
(0xABAC, 'M', 'áœ'),
(0xABAD, 'M', 'á'),
@@ -4099,11 +4075,6 @@ def _seg_38():
(0xABB1, 'M', 'á¡'),
(0xABB2, 'M', 'á¢'),
(0xABB3, 'M', 'á£'),
- ]
-
-def _seg_39():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xABB4, 'M', 'á¤'),
(0xABB5, 'M', 'á¥'),
(0xABB6, 'M', 'á¦'),
@@ -4195,6 +4166,10 @@ def _seg_39():
(0xF943, 'M', '弄'),
(0xF944, 'M', 'ç± '),
(0xF945, 'M', 'è¾'),
+ ]
+
+def _seg_40() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xF946, 'M', '牢'),
(0xF947, 'M', '磊'),
(0xF948, 'M', '賂'),
@@ -4204,11 +4179,6 @@ def _seg_39():
(0xF94C, 'M', '樓'),
(0xF94D, 'M', 'æ·š'),
(0xF94E, 'M', 'æ¼'),
- ]
-
-def _seg_40():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xF94F, 'M', 'ç´¯'),
(0xF950, 'M', '縷'),
(0xF951, 'M', '陋'),
@@ -4300,6 +4270,10 @@ def _seg_40():
(0xF9A7, 'M', 'çµ'),
(0xF9A8, 'M', '令'),
(0xF9A9, 'M', '囹'),
+ ]
+
+def _seg_41() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xF9AA, 'M', '寧'),
(0xF9AB, 'M', '嶺'),
(0xF9AC, 'M', '怜'),
@@ -4309,11 +4283,6 @@ def _seg_40():
(0xF9B0, 'M', 'è†'),
(0xF9B1, 'M', '鈴'),
(0xF9B2, 'M', '零'),
- ]
-
-def _seg_41():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xF9B3, 'M', 'éˆ'),
(0xF9B4, 'M', 'é ˜'),
(0xF9B5, 'M', '例'),
@@ -4405,6 +4374,10 @@ def _seg_41():
(0xFA0B, 'M', '廓'),
(0xFA0C, 'M', 'å…€'),
(0xFA0D, 'M', 'å—€'),
+ ]
+
+def _seg_42() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xFA0E, 'V'),
(0xFA10, 'M', 'å¡š'),
(0xFA11, 'V'),
@@ -4414,11 +4387,6 @@ def _seg_41():
(0xFA16, 'M', '猪'),
(0xFA17, 'M', '益'),
(0xFA18, 'M', '礼'),
- ]
-
-def _seg_42():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xFA19, 'M', '神'),
(0xFA1A, 'M', '祥'),
(0xFA1B, 'M', 'ç¦'),
@@ -4510,6 +4478,10 @@ def _seg_42():
(0xFA76, 'M', '勇'),
(0xFA77, 'M', '勺'),
(0xFA78, 'M', 'å–'),
+ ]
+
+def _seg_43() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xFA79, 'M', 'å••'),
(0xFA7A, 'M', 'å–™'),
(0xFA7B, 'M', 'å—¢'),
@@ -4519,11 +4491,6 @@ def _seg_42():
(0xFA7F, 'M', '奔'),
(0xFA80, 'M', 'å©¢'),
(0xFA81, 'M', '嬨'),
- ]
-
-def _seg_43():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xFA82, 'M', 'å»’'),
(0xFA83, 'M', 'å»™'),
(0xFA84, 'M', '彩'),
@@ -4615,6 +4582,10 @@ def _seg_43():
(0xFADA, 'X'),
(0xFB00, 'M', 'ff'),
(0xFB01, 'M', 'fi'),
+ ]
+
+def _seg_44() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xFB02, 'M', 'fl'),
(0xFB03, 'M', 'ffi'),
(0xFB04, 'M', 'ffl'),
@@ -4624,11 +4595,6 @@ def _seg_43():
(0xFB14, 'M', 'Õ´Õ¥'),
(0xFB15, 'M', 'Õ´Õ«'),
(0xFB16, 'M', 'Õ¾Õ¶'),
- ]
-
-def _seg_44():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xFB17, 'M', 'Õ´Õ­'),
(0xFB18, 'X'),
(0xFB1D, 'M', '×™Ö´'),
@@ -4713,13 +4679,17 @@ def _seg_44():
(0xFBAE, 'M', 'Û’'),
(0xFBB0, 'M', 'Û“'),
(0xFBB2, 'V'),
- (0xFBC2, 'X'),
+ (0xFBC3, 'X'),
(0xFBD3, 'M', 'Ú­'),
(0xFBD7, 'M', 'Û‡'),
(0xFBD9, 'M', 'Û†'),
(0xFBDB, 'M', 'Ûˆ'),
(0xFBDD, 'M', 'Û‡Ù´'),
(0xFBDE, 'M', 'Û‹'),
+ ]
+
+def _seg_45() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xFBE0, 'M', 'Û…'),
(0xFBE2, 'M', 'Û‰'),
(0xFBE4, 'M', 'Û'),
@@ -4729,11 +4699,6 @@ def _seg_44():
(0xFBEE, 'M', 'ئو'),
(0xFBF0, 'M', 'ئۇ'),
(0xFBF2, 'M', 'ئۆ'),
- ]
-
-def _seg_45():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xFBF4, 'M', 'ئۈ'),
(0xFBF6, 'M', 'ئÛ'),
(0xFBF9, 'M', 'ئى'),
@@ -4825,6 +4790,10 @@ def _seg_45():
(0xFC54, 'M', 'هي'),
(0xFC55, 'M', 'يج'),
(0xFC56, 'M', 'يح'),
+ ]
+
+def _seg_46() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xFC57, 'M', 'يخ'),
(0xFC58, 'M', 'يم'),
(0xFC59, 'M', 'يى'),
@@ -4834,11 +4803,6 @@ def _seg_45():
(0xFC5D, 'M', 'ىٰ'),
(0xFC5E, '3', ' ٌّ'),
(0xFC5F, '3', ' ÙÙ‘'),
- ]
-
-def _seg_46():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xFC60, '3', ' ÙŽÙ‘'),
(0xFC61, '3', ' ÙÙ‘'),
(0xFC62, '3', ' ÙÙ‘'),
@@ -4930,6 +4894,10 @@ def _seg_46():
(0xFCB8, 'M', 'طح'),
(0xFCB9, 'M', 'ظم'),
(0xFCBA, 'M', 'عج'),
+ ]
+
+def _seg_47() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xFCBB, 'M', 'عم'),
(0xFCBC, 'M', 'غج'),
(0xFCBD, 'M', 'غم'),
@@ -4939,11 +4907,6 @@ def _seg_46():
(0xFCC1, 'M', 'ÙÙ…'),
(0xFCC2, 'M', 'قح'),
(0xFCC3, 'M', 'قم'),
- ]
-
-def _seg_47():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xFCC4, 'M', 'كج'),
(0xFCC5, 'M', 'كح'),
(0xFCC6, 'M', 'كخ'),
@@ -5035,6 +4998,10 @@ def _seg_47():
(0xFD1C, 'M', 'حي'),
(0xFD1D, 'M', 'جى'),
(0xFD1E, 'M', 'جي'),
+ ]
+
+def _seg_48() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xFD1F, 'M', 'خى'),
(0xFD20, 'M', 'خي'),
(0xFD21, 'M', 'صى'),
@@ -5044,11 +5011,6 @@ def _seg_47():
(0xFD25, 'M', 'شج'),
(0xFD26, 'M', 'شح'),
(0xFD27, 'M', 'شخ'),
- ]
-
-def _seg_48():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xFD28, 'M', 'شم'),
(0xFD29, 'M', 'شر'),
(0xFD2A, 'M', 'سر'),
@@ -5071,7 +5033,6 @@ def _seg_48():
(0xFD3B, 'M', 'ظم'),
(0xFD3C, 'M', 'اً'),
(0xFD3E, 'V'),
- (0xFD40, 'X'),
(0xFD50, 'M', 'تجم'),
(0xFD51, 'M', 'تحج'),
(0xFD53, 'M', 'تحم'),
@@ -5141,6 +5102,10 @@ def _seg_48():
(0xFDA4, 'M', 'تمى'),
(0xFDA5, 'M', 'جمي'),
(0xFDA6, 'M', 'جحى'),
+ ]
+
+def _seg_49() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xFDA7, 'M', 'جمى'),
(0xFDA8, 'M', 'سخى'),
(0xFDA9, 'M', 'صحي'),
@@ -5149,11 +5114,6 @@ def _seg_48():
(0xFDAC, 'M', 'لجي'),
(0xFDAD, 'M', 'لمي'),
(0xFDAE, 'M', 'يحي'),
- ]
-
-def _seg_49():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xFDAF, 'M', 'يجي'),
(0xFDB0, 'M', 'يمي'),
(0xFDB1, 'M', 'ممي'),
@@ -5180,6 +5140,8 @@ def _seg_49():
(0xFDC6, 'M', 'سخي'),
(0xFDC7, 'M', 'نجي'),
(0xFDC8, 'X'),
+ (0xFDCF, 'V'),
+ (0xFDD0, 'X'),
(0xFDF0, 'M', 'صلے'),
(0xFDF1, 'M', 'قلے'),
(0xFDF2, 'M', 'الله'),
@@ -5194,7 +5156,6 @@ def _seg_49():
(0xFDFB, '3', 'جل جلاله'),
(0xFDFC, 'M', 'ریال'),
(0xFDFD, 'V'),
- (0xFDFE, 'X'),
(0xFE00, 'I'),
(0xFE10, '3', ','),
(0xFE11, 'M', 'ã€'),
@@ -5245,6 +5206,10 @@ def _seg_49():
(0xFE5B, '3', '{'),
(0xFE5C, '3', '}'),
(0xFE5D, 'M', '〔'),
+ ]
+
+def _seg_50() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xFE5E, 'M', '〕'),
(0xFE5F, '3', '#'),
(0xFE60, '3', '&'),
@@ -5254,11 +5219,6 @@ def _seg_49():
(0xFE64, '3', '<'),
(0xFE65, '3', '>'),
(0xFE66, '3', '='),
- ]
-
-def _seg_50():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xFE67, 'X'),
(0xFE68, '3', '\\'),
(0xFE69, '3', '$'),
@@ -5350,6 +5310,10 @@ def _seg_50():
(0xFF18, 'M', '8'),
(0xFF19, 'M', '9'),
(0xFF1A, '3', ':'),
+ ]
+
+def _seg_51() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xFF1B, '3', ';'),
(0xFF1C, '3', '<'),
(0xFF1D, '3', '='),
@@ -5359,11 +5323,6 @@ def _seg_50():
(0xFF21, 'M', 'a'),
(0xFF22, 'M', 'b'),
(0xFF23, 'M', 'c'),
- ]
-
-def _seg_51():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xFF24, 'M', 'd'),
(0xFF25, 'M', 'e'),
(0xFF26, 'M', 'f'),
@@ -5455,6 +5414,10 @@ def _seg_51():
(0xFF7C, 'M', 'ã‚·'),
(0xFF7D, 'M', 'ス'),
(0xFF7E, 'M', 'ã‚»'),
+ ]
+
+def _seg_52() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xFF7F, 'M', 'ソ'),
(0xFF80, 'M', 'ã‚¿'),
(0xFF81, 'M', 'ãƒ'),
@@ -5464,11 +5427,6 @@ def _seg_51():
(0xFF85, 'M', 'ナ'),
(0xFF86, 'M', 'ニ'),
(0xFF87, 'M', 'ヌ'),
- ]
-
-def _seg_52():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0xFF88, 'M', 'ãƒ'),
(0xFF89, 'M', 'ノ'),
(0xFF8A, 'M', 'ãƒ'),
@@ -5560,6 +5518,10 @@ def _seg_52():
(0xFFE7, 'X'),
(0xFFE8, 'M', '│'),
(0xFFE9, 'M', 'â†'),
+ ]
+
+def _seg_53() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0xFFEA, 'M', '↑'),
(0xFFEB, 'M', '→'),
(0xFFEC, 'M', '↓'),
@@ -5569,11 +5531,6 @@ def _seg_52():
(0x10000, 'V'),
(0x1000C, 'X'),
(0x1000D, 'V'),
- ]
-
-def _seg_53():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x10027, 'X'),
(0x10028, 'V'),
(0x1003B, 'X'),
@@ -5665,6 +5622,10 @@ def _seg_53():
(0x104B3, 'M', 'ð“›'),
(0x104B4, 'M', 'ð“œ'),
(0x104B5, 'M', 'ð“'),
+ ]
+
+def _seg_54() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x104B6, 'M', 'ð“ž'),
(0x104B7, 'M', 'ð“Ÿ'),
(0x104B8, 'M', 'ð“ '),
@@ -5674,11 +5635,6 @@ def _seg_53():
(0x104BC, 'M', 'ð“¤'),
(0x104BD, 'M', 'ð“¥'),
(0x104BE, 'M', 'ð“¦'),
- ]
-
-def _seg_54():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x104BF, 'M', 'ð“§'),
(0x104C0, 'M', 'ð“¨'),
(0x104C1, 'M', 'ð“©'),
@@ -5708,13 +5664,123 @@ def _seg_54():
(0x10530, 'V'),
(0x10564, 'X'),
(0x1056F, 'V'),
- (0x10570, 'X'),
+ (0x10570, 'M', 'ð–—'),
+ (0x10571, 'M', 'ð–˜'),
+ (0x10572, 'M', 'ð–™'),
+ (0x10573, 'M', 'ð–š'),
+ (0x10574, 'M', 'ð–›'),
+ (0x10575, 'M', 'ð–œ'),
+ (0x10576, 'M', 'ð–'),
+ (0x10577, 'M', 'ð–ž'),
+ (0x10578, 'M', 'ð–Ÿ'),
+ (0x10579, 'M', 'ð– '),
+ (0x1057A, 'M', 'ð–¡'),
+ (0x1057B, 'X'),
+ (0x1057C, 'M', 'ð–£'),
+ (0x1057D, 'M', 'ð–¤'),
+ (0x1057E, 'M', 'ð–¥'),
+ (0x1057F, 'M', 'ð–¦'),
+ (0x10580, 'M', 'ð–§'),
+ (0x10581, 'M', 'ð–¨'),
+ (0x10582, 'M', 'ð–©'),
+ (0x10583, 'M', 'ð–ª'),
+ (0x10584, 'M', 'ð–«'),
+ (0x10585, 'M', 'ð–¬'),
+ (0x10586, 'M', 'ð–­'),
+ (0x10587, 'M', 'ð–®'),
+ (0x10588, 'M', 'ð–¯'),
+ (0x10589, 'M', 'ð–°'),
+ (0x1058A, 'M', 'ð–±'),
+ (0x1058B, 'X'),
+ (0x1058C, 'M', 'ð–³'),
+ (0x1058D, 'M', 'ð–´'),
+ (0x1058E, 'M', 'ð–µ'),
+ (0x1058F, 'M', 'ð–¶'),
+ (0x10590, 'M', 'ð–·'),
+ (0x10591, 'M', 'ð–¸'),
+ (0x10592, 'M', 'ð–¹'),
+ (0x10593, 'X'),
+ (0x10594, 'M', 'ð–»'),
+ (0x10595, 'M', 'ð–¼'),
+ (0x10596, 'X'),
+ (0x10597, 'V'),
+ (0x105A2, 'X'),
+ (0x105A3, 'V'),
+ (0x105B2, 'X'),
+ (0x105B3, 'V'),
+ (0x105BA, 'X'),
+ (0x105BB, 'V'),
+ (0x105BD, 'X'),
(0x10600, 'V'),
(0x10737, 'X'),
(0x10740, 'V'),
(0x10756, 'X'),
(0x10760, 'V'),
(0x10768, 'X'),
+ (0x10780, 'V'),
+ (0x10781, 'M', 'Ë'),
+ (0x10782, 'M', 'Ë‘'),
+ (0x10783, 'M', 'æ'),
+ (0x10784, 'M', 'Ê™'),
+ (0x10785, 'M', 'É“'),
+ (0x10786, 'X'),
+ (0x10787, 'M', 'Ê£'),
+ (0x10788, 'M', 'ê­¦'),
+ ]
+
+def _seg_55() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
+ (0x10789, 'M', 'Ê¥'),
+ (0x1078A, 'M', 'ʤ'),
+ (0x1078B, 'M', 'É–'),
+ (0x1078C, 'M', 'É—'),
+ (0x1078D, 'M', 'ᶑ'),
+ (0x1078E, 'M', 'ɘ'),
+ (0x1078F, 'M', 'Éž'),
+ (0x10790, 'M', 'Ê©'),
+ (0x10791, 'M', 'ɤ'),
+ (0x10792, 'M', 'É¢'),
+ (0x10793, 'M', 'É '),
+ (0x10794, 'M', 'Ê›'),
+ (0x10795, 'M', 'ħ'),
+ (0x10796, 'M', 'ʜ'),
+ (0x10797, 'M', 'ɧ'),
+ (0x10798, 'M', 'Ê„'),
+ (0x10799, 'M', 'ʪ'),
+ (0x1079A, 'M', 'Ê«'),
+ (0x1079B, 'M', 'ɬ'),
+ (0x1079C, 'M', 'ð¼„'),
+ (0x1079D, 'M', 'ꞎ'),
+ (0x1079E, 'M', 'É®'),
+ (0x1079F, 'M', 'ð¼…'),
+ (0x107A0, 'M', 'ÊŽ'),
+ (0x107A1, 'M', 'ð¼†'),
+ (0x107A2, 'M', 'ø'),
+ (0x107A3, 'M', 'ɶ'),
+ (0x107A4, 'M', 'É·'),
+ (0x107A5, 'M', 'q'),
+ (0x107A6, 'M', 'ɺ'),
+ (0x107A7, 'M', 'ð¼ˆ'),
+ (0x107A8, 'M', 'ɽ'),
+ (0x107A9, 'M', 'ɾ'),
+ (0x107AA, 'M', 'Ê€'),
+ (0x107AB, 'M', 'ʨ'),
+ (0x107AC, 'M', 'ʦ'),
+ (0x107AD, 'M', 'ê­§'),
+ (0x107AE, 'M', 'ʧ'),
+ (0x107AF, 'M', 'ʈ'),
+ (0x107B0, 'M', 'â±±'),
+ (0x107B1, 'X'),
+ (0x107B2, 'M', 'Ê'),
+ (0x107B3, 'M', 'Ê¡'),
+ (0x107B4, 'M', 'Ê¢'),
+ (0x107B5, 'M', 'ʘ'),
+ (0x107B6, 'M', 'Ç€'),
+ (0x107B7, 'M', 'Ç'),
+ (0x107B8, 'M', 'Ç‚'),
+ (0x107B9, 'M', 'ð¼Š'),
+ (0x107BA, 'M', 'ð¼ž'),
+ (0x107BB, 'X'),
(0x10800, 'V'),
(0x10806, 'X'),
(0x10808, 'V'),
@@ -5764,6 +5830,10 @@ def _seg_54():
(0x10A60, 'V'),
(0x10AA0, 'X'),
(0x10AC0, 'V'),
+ ]
+
+def _seg_56() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x10AE7, 'X'),
(0x10AEB, 'V'),
(0x10AF7, 'X'),
@@ -5779,11 +5849,6 @@ def _seg_54():
(0x10B9D, 'X'),
(0x10BA9, 'V'),
(0x10BB0, 'X'),
- ]
-
-def _seg_55():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x10C00, 'V'),
(0x10C49, 'X'),
(0x10C80, 'M', 'ð³€'),
@@ -5856,6 +5921,8 @@ def _seg_55():
(0x10F28, 'X'),
(0x10F30, 'V'),
(0x10F5A, 'X'),
+ (0x10F70, 'V'),
+ (0x10F8A, 'X'),
(0x10FB0, 'V'),
(0x10FCC, 'X'),
(0x10FE0, 'V'),
@@ -5863,11 +5930,15 @@ def _seg_55():
(0x11000, 'V'),
(0x1104E, 'X'),
(0x11052, 'V'),
- (0x11070, 'X'),
+ (0x11076, 'X'),
(0x1107F, 'V'),
(0x110BD, 'X'),
(0x110BE, 'V'),
- (0x110C2, 'X'),
+ ]
+
+def _seg_57() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
+ (0x110C3, 'X'),
(0x110D0, 'V'),
(0x110E9, 'X'),
(0x110F0, 'V'),
@@ -5884,11 +5955,6 @@ def _seg_55():
(0x111F5, 'X'),
(0x11200, 'V'),
(0x11212, 'X'),
- ]
-
-def _seg_56():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x11213, 'V'),
(0x1123F, 'X'),
(0x11280, 'V'),
@@ -5954,7 +6020,7 @@ def _seg_56():
(0x11660, 'V'),
(0x1166D, 'X'),
(0x11680, 'V'),
- (0x116B9, 'X'),
+ (0x116BA, 'X'),
(0x116C0, 'V'),
(0x116CA, 'X'),
(0x11700, 'V'),
@@ -5962,7 +6028,7 @@ def _seg_56():
(0x1171D, 'V'),
(0x1172C, 'X'),
(0x11730, 'V'),
- (0x11740, 'X'),
+ (0x11747, 'X'),
(0x11800, 'V'),
(0x1183C, 'X'),
(0x118A0, 'M', 'ð‘£€'),
@@ -5972,6 +6038,10 @@ def _seg_56():
(0x118A4, 'M', '𑣄'),
(0x118A5, 'M', 'ð‘£…'),
(0x118A6, 'M', '𑣆'),
+ ]
+
+def _seg_58() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x118A7, 'M', '𑣇'),
(0x118A8, 'M', '𑣈'),
(0x118A9, 'M', '𑣉'),
@@ -5989,11 +6059,6 @@ def _seg_56():
(0x118B5, 'M', '𑣕'),
(0x118B6, 'M', 'ð‘£–'),
(0x118B7, 'M', 'ð‘£—'),
- ]
-
-def _seg_57():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x118B8, 'M', '𑣘'),
(0x118B9, 'M', 'ð‘£™'),
(0x118BA, 'M', '𑣚'),
@@ -6030,7 +6095,7 @@ def _seg_57():
(0x11A48, 'X'),
(0x11A50, 'V'),
(0x11AA3, 'X'),
- (0x11AC0, 'V'),
+ (0x11AB0, 'V'),
(0x11AF9, 'X'),
(0x11C00, 'V'),
(0x11C09, 'X'),
@@ -6077,6 +6142,10 @@ def _seg_57():
(0x11FB0, 'V'),
(0x11FB1, 'X'),
(0x11FC0, 'V'),
+ ]
+
+def _seg_59() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x11FF2, 'X'),
(0x11FFF, 'V'),
(0x1239A, 'X'),
@@ -6086,6 +6155,8 @@ def _seg_57():
(0x12475, 'X'),
(0x12480, 'V'),
(0x12544, 'X'),
+ (0x12F90, 'V'),
+ (0x12FF3, 'X'),
(0x13000, 'V'),
(0x1342F, 'X'),
(0x14400, 'V'),
@@ -6094,15 +6165,12 @@ def _seg_57():
(0x16A39, 'X'),
(0x16A40, 'V'),
(0x16A5F, 'X'),
- ]
-
-def _seg_58():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x16A60, 'V'),
(0x16A6A, 'X'),
(0x16A6E, 'V'),
- (0x16A70, 'X'),
+ (0x16ABF, 'X'),
+ (0x16AC0, 'V'),
+ (0x16ACA, 'X'),
(0x16AD0, 'V'),
(0x16AEE, 'X'),
(0x16AF0, 'V'),
@@ -6167,11 +6235,21 @@ def _seg_58():
(0x18CD6, 'X'),
(0x18D00, 'V'),
(0x18D09, 'X'),
+ (0x1AFF0, 'V'),
+ (0x1AFF4, 'X'),
+ (0x1AFF5, 'V'),
+ (0x1AFFC, 'X'),
+ (0x1AFFD, 'V'),
+ (0x1AFFF, 'X'),
(0x1B000, 'V'),
- (0x1B11F, 'X'),
+ (0x1B123, 'X'),
(0x1B150, 'V'),
(0x1B153, 'X'),
(0x1B164, 'V'),
+ ]
+
+def _seg_60() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1B168, 'X'),
(0x1B170, 'V'),
(0x1B2FC, 'X'),
@@ -6186,6 +6264,12 @@ def _seg_58():
(0x1BC9C, 'V'),
(0x1BCA0, 'I'),
(0x1BCA4, 'X'),
+ (0x1CF00, 'V'),
+ (0x1CF2E, 'X'),
+ (0x1CF30, 'V'),
+ (0x1CF47, 'X'),
+ (0x1CF50, 'V'),
+ (0x1CFC4, 'X'),
(0x1D000, 'V'),
(0x1D0F6, 'X'),
(0x1D100, 'V'),
@@ -6199,11 +6283,6 @@ def _seg_58():
(0x1D163, 'M', 'ð…˜ð…¥ð…±'),
(0x1D164, 'M', 'ð…˜ð…¥ð…²'),
(0x1D165, 'V'),
- ]
-
-def _seg_59():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1D173, 'X'),
(0x1D17B, 'V'),
(0x1D1BB, 'M', 'ð†¹ð…¥'),
@@ -6213,7 +6292,7 @@ def _seg_59():
(0x1D1BF, 'M', 'ð†¹ð…¥ð…¯'),
(0x1D1C0, 'M', 'ð†ºð…¥ð…¯'),
(0x1D1C1, 'V'),
- (0x1D1E9, 'X'),
+ (0x1D1EB, 'X'),
(0x1D200, 'V'),
(0x1D246, 'X'),
(0x1D2E0, 'V'),
@@ -6271,6 +6350,10 @@ def _seg_59():
(0x1D42E, 'M', 'u'),
(0x1D42F, 'M', 'v'),
(0x1D430, 'M', 'w'),
+ ]
+
+def _seg_61() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1D431, 'M', 'x'),
(0x1D432, 'M', 'y'),
(0x1D433, 'M', 'z'),
@@ -6304,11 +6387,6 @@ def _seg_59():
(0x1D44F, 'M', 'b'),
(0x1D450, 'M', 'c'),
(0x1D451, 'M', 'd'),
- ]
-
-def _seg_60():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1D452, 'M', 'e'),
(0x1D453, 'M', 'f'),
(0x1D454, 'M', 'g'),
@@ -6376,6 +6454,10 @@ def _seg_60():
(0x1D492, 'M', 'q'),
(0x1D493, 'M', 'r'),
(0x1D494, 'M', 's'),
+ ]
+
+def _seg_62() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1D495, 'M', 't'),
(0x1D496, 'M', 'u'),
(0x1D497, 'M', 'v'),
@@ -6409,11 +6491,6 @@ def _seg_60():
(0x1D4B6, 'M', 'a'),
(0x1D4B7, 'M', 'b'),
(0x1D4B8, 'M', 'c'),
- ]
-
-def _seg_61():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1D4B9, 'M', 'd'),
(0x1D4BA, 'X'),
(0x1D4BB, 'M', 'f'),
@@ -6481,6 +6558,10 @@ def _seg_61():
(0x1D4F9, 'M', 'p'),
(0x1D4FA, 'M', 'q'),
(0x1D4FB, 'M', 'r'),
+ ]
+
+def _seg_63() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1D4FC, 'M', 's'),
(0x1D4FD, 'M', 't'),
(0x1D4FE, 'M', 'u'),
@@ -6514,11 +6595,6 @@ def _seg_61():
(0x1D51B, 'M', 'x'),
(0x1D51C, 'M', 'y'),
(0x1D51D, 'X'),
- ]
-
-def _seg_62():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1D51E, 'M', 'a'),
(0x1D51F, 'M', 'b'),
(0x1D520, 'M', 'c'),
@@ -6586,6 +6662,10 @@ def _seg_62():
(0x1D560, 'M', 'o'),
(0x1D561, 'M', 'p'),
(0x1D562, 'M', 'q'),
+ ]
+
+def _seg_64() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1D563, 'M', 'r'),
(0x1D564, 'M', 's'),
(0x1D565, 'M', 't'),
@@ -6619,11 +6699,6 @@ def _seg_62():
(0x1D581, 'M', 'v'),
(0x1D582, 'M', 'w'),
(0x1D583, 'M', 'x'),
- ]
-
-def _seg_63():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1D584, 'M', 'y'),
(0x1D585, 'M', 'z'),
(0x1D586, 'M', 'a'),
@@ -6691,6 +6766,10 @@ def _seg_63():
(0x1D5C4, 'M', 'k'),
(0x1D5C5, 'M', 'l'),
(0x1D5C6, 'M', 'm'),
+ ]
+
+def _seg_65() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1D5C7, 'M', 'n'),
(0x1D5C8, 'M', 'o'),
(0x1D5C9, 'M', 'p'),
@@ -6724,11 +6803,6 @@ def _seg_63():
(0x1D5E5, 'M', 'r'),
(0x1D5E6, 'M', 's'),
(0x1D5E7, 'M', 't'),
- ]
-
-def _seg_64():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1D5E8, 'M', 'u'),
(0x1D5E9, 'M', 'v'),
(0x1D5EA, 'M', 'w'),
@@ -6796,6 +6870,10 @@ def _seg_64():
(0x1D628, 'M', 'g'),
(0x1D629, 'M', 'h'),
(0x1D62A, 'M', 'i'),
+ ]
+
+def _seg_66() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1D62B, 'M', 'j'),
(0x1D62C, 'M', 'k'),
(0x1D62D, 'M', 'l'),
@@ -6829,11 +6907,6 @@ def _seg_64():
(0x1D649, 'M', 'n'),
(0x1D64A, 'M', 'o'),
(0x1D64B, 'M', 'p'),
- ]
-
-def _seg_65():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1D64C, 'M', 'q'),
(0x1D64D, 'M', 'r'),
(0x1D64E, 'M', 's'),
@@ -6901,6 +6974,10 @@ def _seg_65():
(0x1D68C, 'M', 'c'),
(0x1D68D, 'M', 'd'),
(0x1D68E, 'M', 'e'),
+ ]
+
+def _seg_67() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1D68F, 'M', 'f'),
(0x1D690, 'M', 'g'),
(0x1D691, 'M', 'h'),
@@ -6934,11 +7011,6 @@ def _seg_65():
(0x1D6AE, 'M', 'η'),
(0x1D6AF, 'M', 'θ'),
(0x1D6B0, 'M', 'ι'),
- ]
-
-def _seg_66():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1D6B1, 'M', 'κ'),
(0x1D6B2, 'M', 'λ'),
(0x1D6B3, 'M', 'μ'),
@@ -7006,6 +7078,10 @@ def _seg_66():
(0x1D6F2, 'M', 'Ï'),
(0x1D6F3, 'M', 'θ'),
(0x1D6F4, 'M', 'σ'),
+ ]
+
+def _seg_68() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1D6F5, 'M', 'Ï„'),
(0x1D6F6, 'M', 'Ï…'),
(0x1D6F7, 'M', 'φ'),
@@ -7039,11 +7115,6 @@ def _seg_66():
(0x1D714, 'M', 'ω'),
(0x1D715, 'M', '∂'),
(0x1D716, 'M', 'ε'),
- ]
-
-def _seg_67():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1D717, 'M', 'θ'),
(0x1D718, 'M', 'κ'),
(0x1D719, 'M', 'φ'),
@@ -7111,6 +7182,10 @@ def _seg_67():
(0x1D758, 'M', 'γ'),
(0x1D759, 'M', 'δ'),
(0x1D75A, 'M', 'ε'),
+ ]
+
+def _seg_69() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1D75B, 'M', 'ζ'),
(0x1D75C, 'M', 'η'),
(0x1D75D, 'M', 'θ'),
@@ -7144,11 +7219,6 @@ def _seg_67():
(0x1D779, 'M', 'κ'),
(0x1D77A, 'M', 'λ'),
(0x1D77B, 'M', 'μ'),
- ]
-
-def _seg_68():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1D77C, 'M', 'ν'),
(0x1D77D, 'M', 'ξ'),
(0x1D77E, 'M', 'ο'),
@@ -7216,6 +7286,10 @@ def _seg_68():
(0x1D7BE, 'M', 'Ï…'),
(0x1D7BF, 'M', 'φ'),
(0x1D7C0, 'M', 'χ'),
+ ]
+
+def _seg_70() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1D7C1, 'M', 'ψ'),
(0x1D7C2, 'M', 'ω'),
(0x1D7C3, 'M', '∂'),
@@ -7249,11 +7323,6 @@ def _seg_68():
(0x1D7E1, 'M', '9'),
(0x1D7E2, 'M', '0'),
(0x1D7E3, 'M', '1'),
- ]
-
-def _seg_69():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1D7E4, 'M', '2'),
(0x1D7E5, 'M', '3'),
(0x1D7E6, 'M', '4'),
@@ -7288,6 +7357,8 @@ def _seg_69():
(0x1DAA0, 'X'),
(0x1DAA1, 'V'),
(0x1DAB0, 'X'),
+ (0x1DF00, 'V'),
+ (0x1DF1F, 'X'),
(0x1E000, 'V'),
(0x1E007, 'X'),
(0x1E008, 'V'),
@@ -7306,10 +7377,24 @@ def _seg_69():
(0x1E14A, 'X'),
(0x1E14E, 'V'),
(0x1E150, 'X'),
+ (0x1E290, 'V'),
+ (0x1E2AF, 'X'),
(0x1E2C0, 'V'),
(0x1E2FA, 'X'),
(0x1E2FF, 'V'),
(0x1E300, 'X'),
+ (0x1E7E0, 'V'),
+ (0x1E7E7, 'X'),
+ (0x1E7E8, 'V'),
+ (0x1E7EC, 'X'),
+ (0x1E7ED, 'V'),
+ (0x1E7EF, 'X'),
+ (0x1E7F0, 'V'),
+ ]
+
+def _seg_71() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
+ (0x1E7FF, 'X'),
(0x1E800, 'V'),
(0x1E8C5, 'X'),
(0x1E8C7, 'V'),
@@ -7354,11 +7439,6 @@ def _seg_69():
(0x1E95A, 'X'),
(0x1E95E, 'V'),
(0x1E960, 'X'),
- ]
-
-def _seg_70():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1EC71, 'V'),
(0x1ECB5, 'X'),
(0x1ED01, 'V'),
@@ -7414,6 +7494,10 @@ def _seg_70():
(0x1EE31, 'M', 'ص'),
(0x1EE32, 'M', 'Ù‚'),
(0x1EE33, 'X'),
+ ]
+
+def _seg_72() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1EE34, 'M', 'Ø´'),
(0x1EE35, 'M', 'ت'),
(0x1EE36, 'M', 'Ø«'),
@@ -7459,11 +7543,6 @@ def _seg_70():
(0x1EE68, 'M', 'Ø·'),
(0x1EE69, 'M', 'ÙŠ'),
(0x1EE6A, 'M', 'Ùƒ'),
- ]
-
-def _seg_71():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1EE6B, 'X'),
(0x1EE6C, 'M', 'Ù…'),
(0x1EE6D, 'M', 'Ù†'),
@@ -7519,6 +7598,10 @@ def _seg_71():
(0x1EEA3, 'M', 'د'),
(0x1EEA4, 'X'),
(0x1EEA5, 'M', 'Ùˆ'),
+ ]
+
+def _seg_73() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1EEA6, 'M', 'ز'),
(0x1EEA7, 'M', 'Ø­'),
(0x1EEA8, 'M', 'Ø·'),
@@ -7564,11 +7647,6 @@ def _seg_71():
(0x1F106, '3', '5,'),
(0x1F107, '3', '6,'),
(0x1F108, '3', '7,'),
- ]
-
-def _seg_72():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1F109, '3', '8,'),
(0x1F10A, '3', '9,'),
(0x1F10B, 'V'),
@@ -7624,6 +7702,10 @@ def _seg_72():
(0x1F141, 'M', 'r'),
(0x1F142, 'M', 's'),
(0x1F143, 'M', 't'),
+ ]
+
+def _seg_74() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1F144, 'M', 'u'),
(0x1F145, 'M', 'v'),
(0x1F146, 'M', 'w'),
@@ -7669,11 +7751,6 @@ def _seg_72():
(0x1F221, 'M', '終'),
(0x1F222, 'M', '生'),
(0x1F223, 'M', '販'),
- ]
-
-def _seg_73():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1F224, 'M', '声'),
(0x1F225, 'M', 'å¹'),
(0x1F226, 'M', 'æ¼”'),
@@ -7716,7 +7793,7 @@ def _seg_73():
(0x1F266, 'X'),
(0x1F300, 'V'),
(0x1F6D8, 'X'),
- (0x1F6E0, 'V'),
+ (0x1F6DD, 'V'),
(0x1F6ED, 'X'),
(0x1F6F0, 'V'),
(0x1F6FD, 'X'),
@@ -7726,7 +7803,13 @@ def _seg_73():
(0x1F7D9, 'X'),
(0x1F7E0, 'V'),
(0x1F7EC, 'X'),
+ (0x1F7F0, 'V'),
+ (0x1F7F1, 'X'),
(0x1F800, 'V'),
+ ]
+
+def _seg_75() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x1F80C, 'X'),
(0x1F810, 'V'),
(0x1F848, 'X'),
@@ -7739,27 +7822,27 @@ def _seg_73():
(0x1F8B0, 'V'),
(0x1F8B2, 'X'),
(0x1F900, 'V'),
- (0x1F979, 'X'),
- (0x1F97A, 'V'),
- (0x1F9CC, 'X'),
- (0x1F9CD, 'V'),
(0x1FA54, 'X'),
(0x1FA60, 'V'),
(0x1FA6E, 'X'),
(0x1FA70, 'V'),
(0x1FA75, 'X'),
(0x1FA78, 'V'),
- (0x1FA7B, 'X'),
+ (0x1FA7D, 'X'),
(0x1FA80, 'V'),
(0x1FA87, 'X'),
(0x1FA90, 'V'),
- (0x1FAA9, 'X'),
+ (0x1FAAD, 'X'),
(0x1FAB0, 'V'),
- (0x1FAB7, 'X'),
+ (0x1FABB, 'X'),
(0x1FAC0, 'V'),
- (0x1FAC3, 'X'),
+ (0x1FAC6, 'X'),
(0x1FAD0, 'V'),
- (0x1FAD7, 'X'),
+ (0x1FADA, 'X'),
+ (0x1FAE0, 'V'),
+ (0x1FAE8, 'X'),
+ (0x1FAF0, 'V'),
+ (0x1FAF7, 'X'),
(0x1FB00, 'V'),
(0x1FB93, 'X'),
(0x1FB94, 'V'),
@@ -7774,16 +7857,11 @@ def _seg_73():
(0x1FBF7, 'M', '7'),
(0x1FBF8, 'M', '8'),
(0x1FBF9, 'M', '9'),
- ]
-
-def _seg_74():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x1FBFA, 'X'),
(0x20000, 'V'),
- (0x2A6DE, 'X'),
+ (0x2A6E0, 'X'),
(0x2A700, 'V'),
- (0x2B735, 'X'),
+ (0x2B739, 'X'),
(0x2B740, 'V'),
(0x2B81E, 'X'),
(0x2B820, 'V'),
@@ -7832,6 +7910,10 @@ def _seg_74():
(0x2F827, 'M', '勤'),
(0x2F828, 'M', '勺'),
(0x2F829, 'M', '包'),
+ ]
+
+def _seg_76() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x2F82A, 'M', '匆'),
(0x2F82B, 'M', '北'),
(0x2F82C, 'M', 'å‰'),
@@ -7879,11 +7961,6 @@ def _seg_74():
(0x2F859, 'M', '𡓤'),
(0x2F85A, 'M', '売'),
(0x2F85B, 'M', '壷'),
- ]
-
-def _seg_75():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x2F85C, 'M', '夆'),
(0x2F85D, 'M', '多'),
(0x2F85E, 'M', '夢'),
@@ -7937,6 +8014,10 @@ def _seg_75():
(0x2F88F, 'M', '𪎒'),
(0x2F890, 'M', '廾'),
(0x2F891, 'M', '𢌱'),
+ ]
+
+def _seg_77() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x2F893, 'M', 'èˆ'),
(0x2F894, 'M', 'å¼¢'),
(0x2F896, 'M', '㣇'),
@@ -7984,11 +8065,6 @@ def _seg_75():
(0x2F8C0, 'M', 'æ…'),
(0x2F8C1, 'M', '掩'),
(0x2F8C2, 'M', '㨮'),
- ]
-
-def _seg_76():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x2F8C3, 'M', 'æ‘©'),
(0x2F8C4, 'M', '摾'),
(0x2F8C5, 'M', 'æ’'),
@@ -8042,6 +8118,10 @@ def _seg_76():
(0x2F8F5, 'M', '殺'),
(0x2F8F6, 'M', 'æ®»'),
(0x2F8F7, 'M', 'ð£ª'),
+ ]
+
+def _seg_78() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x2F8F8, 'M', 'ð¡´‹'),
(0x2F8F9, 'M', '𣫺'),
(0x2F8FA, 'M', '汎'),
@@ -8089,11 +8169,6 @@ def _seg_76():
(0x2F924, 'M', '犀'),
(0x2F925, 'M', '犕'),
(0x2F926, 'M', '𤜵'),
- ]
-
-def _seg_77():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x2F927, 'M', '𤠔'),
(0x2F928, 'M', 'çº'),
(0x2F929, 'M', '王'),
@@ -8147,6 +8222,10 @@ def _seg_77():
(0x2F95B, 'M', 'ç©'),
(0x2F95C, 'M', '𥥼'),
(0x2F95D, 'M', '𥪧'),
+ ]
+
+def _seg_79() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x2F95F, 'X'),
(0x2F960, 'M', '䈂'),
(0x2F961, 'M', '𥮫'),
@@ -8194,11 +8273,6 @@ def _seg_77():
(0x2F98B, 'M', 'èˆ'),
(0x2F98C, 'M', '舄'),
(0x2F98D, 'M', '辞'),
- ]
-
-def _seg_78():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x2F98E, 'M', 'ä‘«'),
(0x2F98F, 'M', '芑'),
(0x2F990, 'M', '芋'),
@@ -8252,6 +8326,10 @@ def _seg_78():
(0x2F9C0, 'M', '蟡'),
(0x2F9C1, 'M', 'è '),
(0x2F9C2, 'M', 'ä—¹'),
+ ]
+
+def _seg_80() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]:
+ return [
(0x2F9C3, 'M', 'è¡ '),
(0x2F9C4, 'M', 'è¡£'),
(0x2F9C5, 'M', '𧙧'),
@@ -8299,11 +8377,6 @@ def _seg_78():
(0x2F9EF, 'M', '䦕'),
(0x2F9F0, 'M', 'é–·'),
(0x2F9F1, 'M', '𨵷'),
- ]
-
-def _seg_79():
- # type: () -> List[Union[Tuple[int, str], Tuple[int, str, str]]]
- return [
(0x2F9F2, 'M', '䧦'),
(0x2F9F3, 'M', '雃'),
(0x2F9F4, 'M', '嶲'),
@@ -8435,4 +8508,5 @@ uts46data = tuple(
+ _seg_77()
+ _seg_78()
+ _seg_79()
+ + _seg_80()
) # type: Tuple[Union[Tuple[int, str], Tuple[int, str, str]], ...]
diff --git a/src/pip/_vendor/msgpack/__init__.py b/src/pip/_vendor/msgpack/__init__.py
index d6705e22b..507102189 100644
--- a/src/pip/_vendor/msgpack/__init__.py
+++ b/src/pip/_vendor/msgpack/__init__.py
@@ -1,5 +1,4 @@
# coding: utf-8
-from ._version import version
from .exceptions import *
from .ext import ExtType, Timestamp
@@ -7,6 +6,10 @@ import os
import sys
+version = (1, 0, 4)
+__version__ = "1.0.4"
+
+
if os.environ.get("MSGPACK_PUREPYTHON") or sys.version_info[0] == 2:
from .fallback import Packer, unpackb, Unpacker
else:
diff --git a/src/pip/_vendor/msgpack/_version.py b/src/pip/_vendor/msgpack/_version.py
deleted file mode 100644
index 1c83c8ed3..000000000
--- a/src/pip/_vendor/msgpack/_version.py
+++ /dev/null
@@ -1 +0,0 @@
-version = (1, 0, 2)
diff --git a/src/pip/_vendor/msgpack/ext.py b/src/pip/_vendor/msgpack/ext.py
index 4eb9dd65a..25544c555 100644
--- a/src/pip/_vendor/msgpack/ext.py
+++ b/src/pip/_vendor/msgpack/ext.py
@@ -59,7 +59,7 @@ class Timestamp(object):
raise TypeError("seconds must be an interger")
if not isinstance(nanoseconds, int_types):
raise TypeError("nanoseconds must be an integer")
- if not (0 <= nanoseconds < 10 ** 9):
+ if not (0 <= nanoseconds < 10**9):
raise ValueError(
"nanoseconds must be a non-negative integer less than 999999999."
)
@@ -143,7 +143,7 @@ class Timestamp(object):
:type unix_float: int or float.
"""
seconds = int(unix_sec // 1)
- nanoseconds = int((unix_sec % 1) * 10 ** 9)
+ nanoseconds = int((unix_sec % 1) * 10**9)
return Timestamp(seconds, nanoseconds)
def to_unix(self):
@@ -161,7 +161,7 @@ class Timestamp(object):
:param int unix_ns: Posix timestamp in nanoseconds.
:rtype: Timestamp
"""
- return Timestamp(*divmod(unix_ns, 10 ** 9))
+ return Timestamp(*divmod(unix_ns, 10**9))
def to_unix_nano(self):
"""Get the timestamp as a unixtime in nanoseconds.
@@ -169,7 +169,7 @@ class Timestamp(object):
:returns: posix timestamp in nanoseconds
:rtype: int
"""
- return self.seconds * 10 ** 9 + self.nanoseconds
+ return self.seconds * 10**9 + self.nanoseconds
def to_datetime(self):
"""Get the timestamp as a UTC datetime.
diff --git a/src/pip/_vendor/msgpack/fallback.py b/src/pip/_vendor/msgpack/fallback.py
index 0bfa94eac..f560c7b55 100644
--- a/src/pip/_vendor/msgpack/fallback.py
+++ b/src/pip/_vendor/msgpack/fallback.py
@@ -1,5 +1,4 @@
"""Fallback pure Python implementation of msgpack"""
-
from datetime import datetime as _DateTime
import sys
import struct
@@ -12,7 +11,6 @@ if PY2:
def dict_iteritems(d):
return d.iteritems()
-
else:
int_types = int
unicode = str
@@ -33,7 +31,6 @@ if sys.version_info < (3, 5):
and e.args[0].startswith("maximum recursion depth exceeded")
)
-
else:
def _is_recursionerror(e):
@@ -69,7 +66,6 @@ if hasattr(sys, "pypy_version_info"):
def getvalue(self):
return self.builder.build()
-
else:
USING_STRINGBUILDER = False
from io import BytesIO as StringIO
@@ -144,10 +140,41 @@ if sys.version_info < (2, 7, 6):
"""Explicit type cast for legacy struct.unpack_from"""
return struct.unpack_from(f, bytes(b), o)
-
else:
_unpack_from = struct.unpack_from
+_NO_FORMAT_USED = ""
+_MSGPACK_HEADERS = {
+ 0xC4: (1, _NO_FORMAT_USED, TYPE_BIN),
+ 0xC5: (2, ">H", TYPE_BIN),
+ 0xC6: (4, ">I", TYPE_BIN),
+ 0xC7: (2, "Bb", TYPE_EXT),
+ 0xC8: (3, ">Hb", TYPE_EXT),
+ 0xC9: (5, ">Ib", TYPE_EXT),
+ 0xCA: (4, ">f"),
+ 0xCB: (8, ">d"),
+ 0xCC: (1, _NO_FORMAT_USED),
+ 0xCD: (2, ">H"),
+ 0xCE: (4, ">I"),
+ 0xCF: (8, ">Q"),
+ 0xD0: (1, "b"),
+ 0xD1: (2, ">h"),
+ 0xD2: (4, ">i"),
+ 0xD3: (8, ">q"),
+ 0xD4: (1, "b1s", TYPE_EXT),
+ 0xD5: (2, "b2s", TYPE_EXT),
+ 0xD6: (4, "b4s", TYPE_EXT),
+ 0xD7: (8, "b8s", TYPE_EXT),
+ 0xD8: (16, "b16s", TYPE_EXT),
+ 0xD9: (1, _NO_FORMAT_USED, TYPE_RAW),
+ 0xDA: (2, ">H", TYPE_RAW),
+ 0xDB: (4, ">I", TYPE_RAW),
+ 0xDC: (2, ">H", TYPE_ARRAY),
+ 0xDD: (4, ">I", TYPE_ARRAY),
+ 0xDE: (2, ">H", TYPE_MAP),
+ 0xDF: (4, ">I", TYPE_MAP),
+}
+
class Unpacker(object):
"""Streaming unpacker.
@@ -229,7 +256,7 @@ class Unpacker(object):
Example of streaming deserialize from socket::
- unpacker = Unpacker(max_buffer_size)
+ unpacker = Unpacker()
while True:
buf = sock.recv(1024**2)
if not buf:
@@ -291,7 +318,7 @@ class Unpacker(object):
self._buf_checkpoint = 0
if not max_buffer_size:
- max_buffer_size = 2 ** 31 - 1
+ max_buffer_size = 2**31 - 1
if max_str_len == -1:
max_str_len = max_buffer_size
if max_bin_len == -1:
@@ -354,7 +381,7 @@ class Unpacker(object):
self._buffer.extend(view)
def _consume(self):
- """ Gets rid of the used parts of the buffer. """
+ """Gets rid of the used parts of the buffer."""
self._stream_offset += self._buff_i - self._buf_checkpoint
self._buf_checkpoint = self._buff_i
@@ -396,6 +423,8 @@ class Unpacker(object):
# Read from file
remain_bytes = -remain_bytes
+ if remain_bytes + len(self._buffer) > self._max_buffer_size:
+ raise BufferFull
while remain_bytes > 0:
to_read_bytes = max(self._read_size, remain_bytes)
read_data = self.file_like.read(to_read_bytes)
@@ -409,7 +438,7 @@ class Unpacker(object):
self._buff_i = 0 # rollback
raise OutOfData
- def _read_header(self, execute=EX_CONSTRUCT):
+ def _read_header(self):
typ = TYPE_IMMEDIATE
n = 0
obj = None
@@ -424,205 +453,95 @@ class Unpacker(object):
n = b & 0b00011111
typ = TYPE_RAW
if n > self._max_str_len:
- raise ValueError("%s exceeds max_str_len(%s)", n, self._max_str_len)
+ raise ValueError("%s exceeds max_str_len(%s)" % (n, self._max_str_len))
obj = self._read(n)
elif b & 0b11110000 == 0b10010000:
n = b & 0b00001111
typ = TYPE_ARRAY
if n > self._max_array_len:
- raise ValueError("%s exceeds max_array_len(%s)", n, self._max_array_len)
+ raise ValueError(
+ "%s exceeds max_array_len(%s)" % (n, self._max_array_len)
+ )
elif b & 0b11110000 == 0b10000000:
n = b & 0b00001111
typ = TYPE_MAP
if n > self._max_map_len:
- raise ValueError("%s exceeds max_map_len(%s)", n, self._max_map_len)
+ raise ValueError("%s exceeds max_map_len(%s)" % (n, self._max_map_len))
elif b == 0xC0:
obj = None
elif b == 0xC2:
obj = False
elif b == 0xC3:
obj = True
- elif b == 0xC4:
- typ = TYPE_BIN
- self._reserve(1)
- n = self._buffer[self._buff_i]
- self._buff_i += 1
- if n > self._max_bin_len:
- raise ValueError("%s exceeds max_bin_len(%s)" % (n, self._max_bin_len))
- obj = self._read(n)
- elif b == 0xC5:
- typ = TYPE_BIN
- self._reserve(2)
- n = _unpack_from(">H", self._buffer, self._buff_i)[0]
- self._buff_i += 2
- if n > self._max_bin_len:
- raise ValueError("%s exceeds max_bin_len(%s)" % (n, self._max_bin_len))
- obj = self._read(n)
- elif b == 0xC6:
- typ = TYPE_BIN
- self._reserve(4)
- n = _unpack_from(">I", self._buffer, self._buff_i)[0]
- self._buff_i += 4
+ elif 0xC4 <= b <= 0xC6:
+ size, fmt, typ = _MSGPACK_HEADERS[b]
+ self._reserve(size)
+ if len(fmt) > 0:
+ n = _unpack_from(fmt, self._buffer, self._buff_i)[0]
+ else:
+ n = self._buffer[self._buff_i]
+ self._buff_i += size
if n > self._max_bin_len:
raise ValueError("%s exceeds max_bin_len(%s)" % (n, self._max_bin_len))
obj = self._read(n)
- elif b == 0xC7: # ext 8
- typ = TYPE_EXT
- self._reserve(2)
- L, n = _unpack_from("Bb", self._buffer, self._buff_i)
- self._buff_i += 2
+ elif 0xC7 <= b <= 0xC9:
+ size, fmt, typ = _MSGPACK_HEADERS[b]
+ self._reserve(size)
+ L, n = _unpack_from(fmt, self._buffer, self._buff_i)
+ self._buff_i += size
if L > self._max_ext_len:
raise ValueError("%s exceeds max_ext_len(%s)" % (L, self._max_ext_len))
obj = self._read(L)
- elif b == 0xC8: # ext 16
- typ = TYPE_EXT
- self._reserve(3)
- L, n = _unpack_from(">Hb", self._buffer, self._buff_i)
- self._buff_i += 3
- if L > self._max_ext_len:
- raise ValueError("%s exceeds max_ext_len(%s)" % (L, self._max_ext_len))
- obj = self._read(L)
- elif b == 0xC9: # ext 32
- typ = TYPE_EXT
- self._reserve(5)
- L, n = _unpack_from(">Ib", self._buffer, self._buff_i)
- self._buff_i += 5
- if L > self._max_ext_len:
- raise ValueError("%s exceeds max_ext_len(%s)" % (L, self._max_ext_len))
- obj = self._read(L)
- elif b == 0xCA:
- self._reserve(4)
- obj = _unpack_from(">f", self._buffer, self._buff_i)[0]
- self._buff_i += 4
- elif b == 0xCB:
- self._reserve(8)
- obj = _unpack_from(">d", self._buffer, self._buff_i)[0]
- self._buff_i += 8
- elif b == 0xCC:
- self._reserve(1)
- obj = self._buffer[self._buff_i]
- self._buff_i += 1
- elif b == 0xCD:
- self._reserve(2)
- obj = _unpack_from(">H", self._buffer, self._buff_i)[0]
- self._buff_i += 2
- elif b == 0xCE:
- self._reserve(4)
- obj = _unpack_from(">I", self._buffer, self._buff_i)[0]
- self._buff_i += 4
- elif b == 0xCF:
- self._reserve(8)
- obj = _unpack_from(">Q", self._buffer, self._buff_i)[0]
- self._buff_i += 8
- elif b == 0xD0:
- self._reserve(1)
- obj = _unpack_from("b", self._buffer, self._buff_i)[0]
- self._buff_i += 1
- elif b == 0xD1:
- self._reserve(2)
- obj = _unpack_from(">h", self._buffer, self._buff_i)[0]
- self._buff_i += 2
- elif b == 0xD2:
- self._reserve(4)
- obj = _unpack_from(">i", self._buffer, self._buff_i)[0]
- self._buff_i += 4
- elif b == 0xD3:
- self._reserve(8)
- obj = _unpack_from(">q", self._buffer, self._buff_i)[0]
- self._buff_i += 8
- elif b == 0xD4: # fixext 1
- typ = TYPE_EXT
- if self._max_ext_len < 1:
- raise ValueError("%s exceeds max_ext_len(%s)" % (1, self._max_ext_len))
- self._reserve(2)
- n, obj = _unpack_from("b1s", self._buffer, self._buff_i)
- self._buff_i += 2
- elif b == 0xD5: # fixext 2
- typ = TYPE_EXT
- if self._max_ext_len < 2:
- raise ValueError("%s exceeds max_ext_len(%s)" % (2, self._max_ext_len))
- self._reserve(3)
- n, obj = _unpack_from("b2s", self._buffer, self._buff_i)
- self._buff_i += 3
- elif b == 0xD6: # fixext 4
- typ = TYPE_EXT
- if self._max_ext_len < 4:
- raise ValueError("%s exceeds max_ext_len(%s)" % (4, self._max_ext_len))
- self._reserve(5)
- n, obj = _unpack_from("b4s", self._buffer, self._buff_i)
- self._buff_i += 5
- elif b == 0xD7: # fixext 8
- typ = TYPE_EXT
- if self._max_ext_len < 8:
- raise ValueError("%s exceeds max_ext_len(%s)" % (8, self._max_ext_len))
- self._reserve(9)
- n, obj = _unpack_from("b8s", self._buffer, self._buff_i)
- self._buff_i += 9
- elif b == 0xD8: # fixext 16
- typ = TYPE_EXT
- if self._max_ext_len < 16:
- raise ValueError("%s exceeds max_ext_len(%s)" % (16, self._max_ext_len))
- self._reserve(17)
- n, obj = _unpack_from("b16s", self._buffer, self._buff_i)
- self._buff_i += 17
- elif b == 0xD9:
- typ = TYPE_RAW
- self._reserve(1)
- n = self._buffer[self._buff_i]
- self._buff_i += 1
- if n > self._max_str_len:
- raise ValueError("%s exceeds max_str_len(%s)", n, self._max_str_len)
- obj = self._read(n)
- elif b == 0xDA:
- typ = TYPE_RAW
- self._reserve(2)
- (n,) = _unpack_from(">H", self._buffer, self._buff_i)
- self._buff_i += 2
- if n > self._max_str_len:
- raise ValueError("%s exceeds max_str_len(%s)", n, self._max_str_len)
- obj = self._read(n)
- elif b == 0xDB:
- typ = TYPE_RAW
- self._reserve(4)
- (n,) = _unpack_from(">I", self._buffer, self._buff_i)
- self._buff_i += 4
+ elif 0xCA <= b <= 0xD3:
+ size, fmt = _MSGPACK_HEADERS[b]
+ self._reserve(size)
+ if len(fmt) > 0:
+ obj = _unpack_from(fmt, self._buffer, self._buff_i)[0]
+ else:
+ obj = self._buffer[self._buff_i]
+ self._buff_i += size
+ elif 0xD4 <= b <= 0xD8:
+ size, fmt, typ = _MSGPACK_HEADERS[b]
+ if self._max_ext_len < size:
+ raise ValueError(
+ "%s exceeds max_ext_len(%s)" % (size, self._max_ext_len)
+ )
+ self._reserve(size + 1)
+ n, obj = _unpack_from(fmt, self._buffer, self._buff_i)
+ self._buff_i += size + 1
+ elif 0xD9 <= b <= 0xDB:
+ size, fmt, typ = _MSGPACK_HEADERS[b]
+ self._reserve(size)
+ if len(fmt) > 0:
+ (n,) = _unpack_from(fmt, self._buffer, self._buff_i)
+ else:
+ n = self._buffer[self._buff_i]
+ self._buff_i += size
if n > self._max_str_len:
- raise ValueError("%s exceeds max_str_len(%s)", n, self._max_str_len)
+ raise ValueError("%s exceeds max_str_len(%s)" % (n, self._max_str_len))
obj = self._read(n)
- elif b == 0xDC:
- typ = TYPE_ARRAY
- self._reserve(2)
- (n,) = _unpack_from(">H", self._buffer, self._buff_i)
- self._buff_i += 2
+ elif 0xDC <= b <= 0xDD:
+ size, fmt, typ = _MSGPACK_HEADERS[b]
+ self._reserve(size)
+ (n,) = _unpack_from(fmt, self._buffer, self._buff_i)
+ self._buff_i += size
if n > self._max_array_len:
- raise ValueError("%s exceeds max_array_len(%s)", n, self._max_array_len)
- elif b == 0xDD:
- typ = TYPE_ARRAY
- self._reserve(4)
- (n,) = _unpack_from(">I", self._buffer, self._buff_i)
- self._buff_i += 4
- if n > self._max_array_len:
- raise ValueError("%s exceeds max_array_len(%s)", n, self._max_array_len)
- elif b == 0xDE:
- self._reserve(2)
- (n,) = _unpack_from(">H", self._buffer, self._buff_i)
- self._buff_i += 2
- if n > self._max_map_len:
- raise ValueError("%s exceeds max_map_len(%s)", n, self._max_map_len)
- typ = TYPE_MAP
- elif b == 0xDF:
- self._reserve(4)
- (n,) = _unpack_from(">I", self._buffer, self._buff_i)
- self._buff_i += 4
+ raise ValueError(
+ "%s exceeds max_array_len(%s)" % (n, self._max_array_len)
+ )
+ elif 0xDE <= b <= 0xDF:
+ size, fmt, typ = _MSGPACK_HEADERS[b]
+ self._reserve(size)
+ (n,) = _unpack_from(fmt, self._buffer, self._buff_i)
+ self._buff_i += size
if n > self._max_map_len:
- raise ValueError("%s exceeds max_map_len(%s)", n, self._max_map_len)
- typ = TYPE_MAP
+ raise ValueError("%s exceeds max_map_len(%s)" % (n, self._max_map_len))
else:
raise FormatError("Unknown header: 0x%x" % b)
return typ, n, obj
def _unpack(self, execute=EX_CONSTRUCT):
- typ, n, obj = self._read_header(execute)
+ typ, n, obj = self._read_header()
if execute == EX_READ_ARRAY_HEADER:
if typ != TYPE_ARRAY:
@@ -883,20 +802,20 @@ class Packer(object):
raise OverflowError("Integer value out of range")
if check(obj, (bytes, bytearray)):
n = len(obj)
- if n >= 2 ** 32:
+ if n >= 2**32:
raise ValueError("%s is too large" % type(obj).__name__)
self._pack_bin_header(n)
return self._buffer.write(obj)
if check(obj, unicode):
obj = obj.encode("utf-8", self._unicode_errors)
n = len(obj)
- if n >= 2 ** 32:
+ if n >= 2**32:
raise ValueError("String is too large")
self._pack_raw_header(n)
return self._buffer.write(obj)
if check(obj, memoryview):
n = len(obj) * obj.itemsize
- if n >= 2 ** 32:
+ if n >= 2**32:
raise ValueError("Memoryview is too large")
self._pack_bin_header(n)
return self._buffer.write(obj)
@@ -953,6 +872,10 @@ class Packer(object):
obj = self._default(obj)
default_used = 1
continue
+
+ if self._datetime and check(obj, _DateTime):
+ raise ValueError("Cannot serialize %r where tzinfo=None" % (obj,))
+
raise TypeError("Cannot serialize %r" % (obj,))
def pack(self, obj):
@@ -974,7 +897,7 @@ class Packer(object):
return ret
def pack_array_header(self, n):
- if n >= 2 ** 32:
+ if n >= 2**32:
raise ValueError
self._pack_array_header(n)
if self._autoreset:
@@ -983,7 +906,7 @@ class Packer(object):
return ret
def pack_map_header(self, n):
- if n >= 2 ** 32:
+ if n >= 2**32:
raise ValueError
self._pack_map_header(n)
if self._autoreset:
diff --git a/src/pip/_vendor/packaging.pyi b/src/pip/_vendor/packaging.pyi
deleted file mode 100644
index 3458a3d63..000000000
--- a/src/pip/_vendor/packaging.pyi
+++ /dev/null
@@ -1 +0,0 @@
-from packaging import * \ No newline at end of file
diff --git a/src/pip/_vendor/packaging/__about__.py b/src/pip/_vendor/packaging/__about__.py
index e70d692c8..3551bc2d2 100644
--- a/src/pip/_vendor/packaging/__about__.py
+++ b/src/pip/_vendor/packaging/__about__.py
@@ -17,7 +17,7 @@ __title__ = "packaging"
__summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging"
-__version__ = "21.0"
+__version__ = "21.3"
__author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io"
diff --git a/src/pip/_vendor/packaging/_musllinux.py b/src/pip/_vendor/packaging/_musllinux.py
index 85450fafa..8ac3059ba 100644
--- a/src/pip/_vendor/packaging/_musllinux.py
+++ b/src/pip/_vendor/packaging/_musllinux.py
@@ -98,7 +98,7 @@ def _get_musl_version(executable: str) -> Optional[_MuslVersion]:
with contextlib.ExitStack() as stack:
try:
f = stack.enter_context(open(executable, "rb"))
- except IOError:
+ except OSError:
return None
ld = _parse_ld_musl_from_elf(f)
if not ld:
diff --git a/src/pip/_vendor/packaging/_structures.py b/src/pip/_vendor/packaging/_structures.py
index 951549753..90a6465f9 100644
--- a/src/pip/_vendor/packaging/_structures.py
+++ b/src/pip/_vendor/packaging/_structures.py
@@ -19,9 +19,6 @@ class InfinityType:
def __eq__(self, other: object) -> bool:
return isinstance(other, self.__class__)
- def __ne__(self, other: object) -> bool:
- return not isinstance(other, self.__class__)
-
def __gt__(self, other: object) -> bool:
return True
@@ -51,9 +48,6 @@ class NegativeInfinityType:
def __eq__(self, other: object) -> bool:
return isinstance(other, self.__class__)
- def __ne__(self, other: object) -> bool:
- return not isinstance(other, self.__class__)
-
def __gt__(self, other: object) -> bool:
return False
diff --git a/src/pip/_vendor/packaging/specifiers.py b/src/pip/_vendor/packaging/specifiers.py
index ce66bd4ad..0e218a6f9 100644
--- a/src/pip/_vendor/packaging/specifiers.py
+++ b/src/pip/_vendor/packaging/specifiers.py
@@ -57,13 +57,6 @@ class BaseSpecifier(metaclass=abc.ABCMeta):
objects are equal.
"""
- @abc.abstractmethod
- def __ne__(self, other: object) -> bool:
- """
- Returns a boolean representing whether or not the two Specifier like
- objects are not equal.
- """
-
@abc.abstractproperty
def prereleases(self) -> Optional[bool]:
"""
@@ -119,7 +112,7 @@ class _IndividualSpecifier(BaseSpecifier):
else ""
)
- return "<{}({!r}{})>".format(self.__class__.__name__, str(self), pre)
+ return f"<{self.__class__.__name__}({str(self)!r}{pre})>"
def __str__(self) -> str:
return "{}{}".format(*self._spec)
@@ -142,17 +135,6 @@ class _IndividualSpecifier(BaseSpecifier):
return self._canonical_spec == other._canonical_spec
- def __ne__(self, other: object) -> bool:
- if isinstance(other, str):
- try:
- other = self.__class__(str(other))
- except InvalidSpecifier:
- return NotImplemented
- elif not isinstance(other, self.__class__):
- return NotImplemented
-
- return self._spec != other._spec
-
def _get_operator(self, op: str) -> CallableOperator:
operator_callable: CallableOperator = getattr(
self, f"_compare_{self._operators[op]}"
@@ -667,7 +649,7 @@ class SpecifierSet(BaseSpecifier):
else ""
)
- return "<SpecifierSet({!r}{})>".format(str(self), pre)
+ return f"<SpecifierSet({str(self)!r}{pre})>"
def __str__(self) -> str:
return ",".join(sorted(str(s) for s in self._specs))
@@ -706,14 +688,6 @@ class SpecifierSet(BaseSpecifier):
return self._specs == other._specs
- def __ne__(self, other: object) -> bool:
- if isinstance(other, (str, _IndividualSpecifier)):
- other = SpecifierSet(str(other))
- elif not isinstance(other, SpecifierSet):
- return NotImplemented
-
- return self._specs != other._specs
-
def __len__(self) -> int:
return len(self._specs)
diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py
index 82a47cdae..9a3d25a71 100644
--- a/src/pip/_vendor/packaging/tags.py
+++ b/src/pip/_vendor/packaging/tags.py
@@ -90,7 +90,7 @@ class Tag:
return f"{self._interpreter}-{self._abi}-{self._platform}"
def __repr__(self) -> str:
- return "<{self} @ {self_id}>".format(self=self, self_id=id(self))
+ return f"<{self} @ {id(self)}>"
def parse_tag(tag: str) -> FrozenSet[Tag]:
@@ -192,7 +192,7 @@ def cpython_tags(
if not python_version:
python_version = sys.version_info[:2]
- interpreter = "cp{}".format(_version_nodot(python_version[:2]))
+ interpreter = f"cp{_version_nodot(python_version[:2])}"
if abis is None:
if len(python_version) > 1:
@@ -207,7 +207,7 @@ def cpython_tags(
except ValueError:
pass
- platforms = list(platforms or _platform_tags())
+ platforms = list(platforms or platform_tags())
for abi in abis:
for platform_ in platforms:
yield Tag(interpreter, abi, platform_)
@@ -251,7 +251,7 @@ def generic_tags(
interpreter = "".join([interp_name, interp_version])
if abis is None:
abis = _generic_abi()
- platforms = list(platforms or _platform_tags())
+ platforms = list(platforms or platform_tags())
abis = list(abis)
if "none" not in abis:
abis.append("none")
@@ -268,11 +268,11 @@ def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]:
all previous versions of that major version.
"""
if len(py_version) > 1:
- yield "py{version}".format(version=_version_nodot(py_version[:2]))
- yield "py{major}".format(major=py_version[0])
+ yield f"py{_version_nodot(py_version[:2])}"
+ yield f"py{py_version[0]}"
if len(py_version) > 1:
for minor in range(py_version[1] - 1, -1, -1):
- yield "py{version}".format(version=_version_nodot((py_version[0], minor)))
+ yield f"py{_version_nodot((py_version[0], minor))}"
def compatible_tags(
@@ -290,7 +290,7 @@ def compatible_tags(
"""
if not python_version:
python_version = sys.version_info[:2]
- platforms = list(platforms or _platform_tags())
+ platforms = list(platforms or platform_tags())
for version in _py_interpreter_range(python_version):
for platform_ in platforms:
yield Tag(version, "none", platform_)
@@ -431,7 +431,7 @@ def _generic_platforms() -> Iterator[str]:
yield _normalize_string(sysconfig.get_platform())
-def _platform_tags() -> Iterator[str]:
+def platform_tags() -> Iterator[str]:
"""
Provides the platform tags for this installation.
"""
@@ -481,4 +481,7 @@ def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
else:
yield from generic_tags()
- yield from compatible_tags()
+ if interp_name == "pp":
+ yield from compatible_tags(interpreter="pp3")
+ else:
+ yield from compatible_tags()
diff --git a/src/pip/_vendor/pep517/__init__.py b/src/pip/_vendor/pep517/__init__.py
index f064d60c8..2b6b88567 100644
--- a/src/pip/_vendor/pep517/__init__.py
+++ b/src/pip/_vendor/pep517/__init__.py
@@ -1,6 +1,6 @@
"""Wrappers to build Python packages using PEP 517 hooks
"""
-__version__ = '0.11.0'
+__version__ = '0.12.0'
from .wrappers import * # noqa: F401, F403
diff --git a/src/pip/_vendor/pep517/build.py b/src/pip/_vendor/pep517/build.py
index 3b7521453..bc463b2ba 100644
--- a/src/pip/_vendor/pep517/build.py
+++ b/src/pip/_vendor/pep517/build.py
@@ -31,7 +31,7 @@ def load_system(source_dir):
Load the build system from a source dir (pyproject.toml).
"""
pyproject = os.path.join(source_dir, 'pyproject.toml')
- with io.open(pyproject, encoding="utf-8") as f:
+ with io.open(pyproject, 'rb') as f:
pyproject_data = toml_load(f)
return pyproject_data['build-system']
diff --git a/src/pip/_vendor/pep517/check.py b/src/pip/_vendor/pep517/check.py
index 719be0403..bf3c72264 100644
--- a/src/pip/_vendor/pep517/check.py
+++ b/src/pip/_vendor/pep517/check.py
@@ -142,7 +142,7 @@ def check(source_dir):
return False
try:
- with io.open(pyproject, encoding="utf-8") as f:
+ with io.open(pyproject, 'rb') as f:
pyproject_data = toml_load(f)
# Ensure the mandatory data can be loaded
buildsys = pyproject_data['build-system']
diff --git a/src/pip/_vendor/pep517/compat.py b/src/pip/_vendor/pep517/compat.py
index d5636645b..730ef5ffa 100644
--- a/src/pip/_vendor/pep517/compat.py
+++ b/src/pip/_vendor/pep517/compat.py
@@ -1,4 +1,5 @@
"""Python 2/3 compatibility"""
+import io
import json
import sys
@@ -35,7 +36,15 @@ except NameError:
if sys.version_info < (3, 6):
- from toml import load as toml_load # noqa: F401
+ from toml import load as _toml_load # noqa: F401
+
+ def toml_load(f):
+ w = io.TextIOWrapper(f, encoding="utf8", newline="")
+ try:
+ return _toml_load(w)
+ finally:
+ w.detach()
+
from toml import TomlDecodeError as TOMLDecodeError # noqa: F401
else:
from pip._vendor.tomli import load as toml_load # noqa: F401
diff --git a/src/pip/_vendor/pep517/envbuild.py b/src/pip/_vendor/pep517/envbuild.py
index 7c2344bf3..fe8873c64 100644
--- a/src/pip/_vendor/pep517/envbuild.py
+++ b/src/pip/_vendor/pep517/envbuild.py
@@ -19,7 +19,7 @@ log = logging.getLogger(__name__)
def _load_pyproject(source_dir):
with io.open(
os.path.join(source_dir, 'pyproject.toml'),
- encoding="utf-8",
+ 'rb',
) as f:
pyproject_data = toml_load(f)
buildsys = pyproject_data['build-system']
diff --git a/src/pip/_vendor/pep517/in_process/_in_process.py b/src/pip/_vendor/pep517/in_process/_in_process.py
index c7f5f0577..954a4ab05 100644
--- a/src/pip/_vendor/pep517/in_process/_in_process.py
+++ b/src/pip/_vendor/pep517/in_process/_in_process.py
@@ -103,6 +103,19 @@ def _build_backend():
return obj
+def _supported_features():
+ """Return the list of options features supported by the backend.
+
+ Returns a list of strings.
+ The only possible value is 'build_editable'.
+ """
+ backend = _build_backend()
+ features = []
+ if hasattr(backend, "build_editable"):
+ features.append("build_editable")
+ return features
+
+
def get_requires_for_build_wheel(config_settings):
"""Invoke the optional get_requires_for_build_wheel hook
@@ -312,6 +325,7 @@ HOOK_NAMES = {
'build_editable',
'get_requires_for_build_sdist',
'build_sdist',
+ '_supported_features',
}
diff --git a/src/pip/_vendor/pep517/wrappers.py b/src/pip/_vendor/pep517/wrappers.py
index 52da22e82..e031ed708 100644
--- a/src/pip/_vendor/pep517/wrappers.py
+++ b/src/pip/_vendor/pep517/wrappers.py
@@ -154,6 +154,10 @@ class Pep517HookCaller(object):
finally:
self._subprocess_runner = prev
+ def _supported_features(self):
+ """Return the list of optional features supported by the backend."""
+ return self._call_hook('_supported_features', {})
+
def get_requires_for_build_wheel(self, config_settings=None):
"""Identify packages required for building a wheel
diff --git a/src/pip/_vendor/pkg_resources/__init__.py b/src/pip/_vendor/pkg_resources/__init__.py
index a457ff27e..4cd562cf9 100644
--- a/src/pip/_vendor/pkg_resources/__init__.py
+++ b/src/pip/_vendor/pkg_resources/__init__.py
@@ -77,7 +77,7 @@ except ImportError:
importlib_machinery = None
from . import py31compat
-from pip._vendor import appdirs
+from pip._vendor import platformdirs
from pip._vendor import packaging
__import__('pip._vendor.packaging.version')
__import__('pip._vendor.packaging.specifiers')
@@ -1310,7 +1310,7 @@ def get_default_cache():
"""
return (
os.environ.get('PYTHON_EGG_CACHE')
- or appdirs.user_cache_dir(appname='Python-Eggs')
+ or platformdirs.user_cache_dir(appname='Python-Eggs')
)
diff --git a/src/pip/_vendor/appdirs.LICENSE.txt b/src/pip/_vendor/platformdirs/LICENSE.txt
index 107c61405..f0bbd69f0 100644
--- a/src/pip/_vendor/appdirs.LICENSE.txt
+++ b/src/pip/_vendor/platformdirs/LICENSE.txt
@@ -20,4 +20,3 @@ 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.
-
diff --git a/src/pip/_vendor/platformdirs/__init__.py b/src/pip/_vendor/platformdirs/__init__.py
new file mode 100644
index 000000000..9d513dcf1
--- /dev/null
+++ b/src/pip/_vendor/platformdirs/__init__.py
@@ -0,0 +1,340 @@
+"""
+Utilities for determining application-specific dirs. See <https://github.com/platformdirs/platformdirs> for details and
+usage.
+"""
+from __future__ import annotations
+
+import os
+import sys
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from pip._vendor.typing_extensions import Literal # pragma: no cover
+
+from .api import PlatformDirsABC
+from .version import __version__, __version_info__
+
+
+def _set_platform_dir_class() -> type[PlatformDirsABC]:
+ if sys.platform == "win32":
+ from pip._vendor.platformdirs.windows import Windows as Result
+ elif sys.platform == "darwin":
+ from pip._vendor.platformdirs.macos import MacOS as Result
+ else:
+ from pip._vendor.platformdirs.unix import Unix as Result
+
+ if os.getenv("ANDROID_DATA") == "/data" and os.getenv("ANDROID_ROOT") == "/system":
+
+ if os.getenv("SHELL") is not None:
+ return Result
+
+ from pip._vendor.platformdirs.android import _android_folder
+
+ if _android_folder() is not None:
+ from pip._vendor.platformdirs.android import Android
+
+ return Android # return to avoid redefinition of result
+
+ return Result
+
+
+PlatformDirs = _set_platform_dir_class() #: Currently active platform
+AppDirs = PlatformDirs #: Backwards compatibility with appdirs
+
+
+def user_data_dir(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ roaming: bool = False,
+) -> str:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param roaming: See `roaming <platformdirs.api.PlatformDirsABC.version>`.
+ :returns: data directory tied to the user
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, roaming=roaming).user_data_dir
+
+
+def site_data_dir(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ multipath: bool = False,
+) -> str:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param multipath: See `roaming <platformdirs.api.PlatformDirsABC.multipath>`.
+ :returns: data directory shared by users
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, multipath=multipath).site_data_dir
+
+
+def user_config_dir(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ roaming: bool = False,
+) -> str:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param roaming: See `roaming <platformdirs.api.PlatformDirsABC.version>`.
+ :returns: config directory tied to the user
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, roaming=roaming).user_config_dir
+
+
+def site_config_dir(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ multipath: bool = False,
+) -> str:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param multipath: See `roaming <platformdirs.api.PlatformDirsABC.multipath>`.
+ :returns: config directory shared by the users
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, multipath=multipath).site_config_dir
+
+
+def user_cache_dir(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ opinion: bool = True,
+) -> str:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param opinion: See `roaming <platformdirs.api.PlatformDirsABC.opinion>`.
+ :returns: cache directory tied to the user
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, opinion=opinion).user_cache_dir
+
+
+def user_state_dir(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ roaming: bool = False,
+) -> str:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param roaming: See `roaming <platformdirs.api.PlatformDirsABC.version>`.
+ :returns: state directory tied to the user
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, roaming=roaming).user_state_dir
+
+
+def user_log_dir(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ opinion: bool = True,
+) -> str:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param opinion: See `roaming <platformdirs.api.PlatformDirsABC.opinion>`.
+ :returns: log directory tied to the user
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, opinion=opinion).user_log_dir
+
+
+def user_documents_dir() -> str:
+ """
+ :returns: documents directory tied to the user
+ """
+ return PlatformDirs().user_documents_dir
+
+
+def user_runtime_dir(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ opinion: bool = True,
+) -> str:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param opinion: See `opinion <platformdirs.api.PlatformDirsABC.opinion>`.
+ :returns: runtime directory tied to the user
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, opinion=opinion).user_runtime_dir
+
+
+def user_data_path(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ roaming: bool = False,
+) -> Path:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param roaming: See `roaming <platformdirs.api.PlatformDirsABC.version>`.
+ :returns: data path tied to the user
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, roaming=roaming).user_data_path
+
+
+def site_data_path(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ multipath: bool = False,
+) -> Path:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param multipath: See `multipath <platformdirs.api.PlatformDirsABC.multipath>`.
+ :returns: data path shared by users
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, multipath=multipath).site_data_path
+
+
+def user_config_path(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ roaming: bool = False,
+) -> Path:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param roaming: See `roaming <platformdirs.api.PlatformDirsABC.version>`.
+ :returns: config path tied to the user
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, roaming=roaming).user_config_path
+
+
+def site_config_path(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ multipath: bool = False,
+) -> Path:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param multipath: See `roaming <platformdirs.api.PlatformDirsABC.multipath>`.
+ :returns: config path shared by the users
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, multipath=multipath).site_config_path
+
+
+def user_cache_path(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ opinion: bool = True,
+) -> Path:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param opinion: See `roaming <platformdirs.api.PlatformDirsABC.opinion>`.
+ :returns: cache path tied to the user
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, opinion=opinion).user_cache_path
+
+
+def user_state_path(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ roaming: bool = False,
+) -> Path:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param roaming: See `roaming <platformdirs.api.PlatformDirsABC.version>`.
+ :returns: state path tied to the user
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, roaming=roaming).user_state_path
+
+
+def user_log_path(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ opinion: bool = True,
+) -> Path:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param opinion: See `roaming <platformdirs.api.PlatformDirsABC.opinion>`.
+ :returns: log path tied to the user
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, opinion=opinion).user_log_path
+
+
+def user_documents_path() -> Path:
+ """
+ :returns: documents path tied to the user
+ """
+ return PlatformDirs().user_documents_path
+
+
+def user_runtime_path(
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ opinion: bool = True,
+) -> Path:
+ """
+ :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+ :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+ :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+ :param opinion: See `opinion <platformdirs.api.PlatformDirsABC.opinion>`.
+ :returns: runtime path tied to the user
+ """
+ return PlatformDirs(appname=appname, appauthor=appauthor, version=version, opinion=opinion).user_runtime_path
+
+
+__all__ = [
+ "__version__",
+ "__version_info__",
+ "PlatformDirs",
+ "AppDirs",
+ "PlatformDirsABC",
+ "user_data_dir",
+ "user_config_dir",
+ "user_cache_dir",
+ "user_state_dir",
+ "user_log_dir",
+ "user_documents_dir",
+ "user_runtime_dir",
+ "site_data_dir",
+ "site_config_dir",
+ "user_data_path",
+ "user_config_path",
+ "user_cache_path",
+ "user_state_path",
+ "user_log_path",
+ "user_documents_path",
+ "user_runtime_path",
+ "site_data_path",
+ "site_config_path",
+]
diff --git a/src/pip/_vendor/platformdirs/__main__.py b/src/pip/_vendor/platformdirs/__main__.py
new file mode 100644
index 000000000..9c54bfb43
--- /dev/null
+++ b/src/pip/_vendor/platformdirs/__main__.py
@@ -0,0 +1,46 @@
+from __future__ import annotations
+
+from pip._vendor.platformdirs import PlatformDirs, __version__
+
+PROPS = (
+ "user_data_dir",
+ "user_config_dir",
+ "user_cache_dir",
+ "user_state_dir",
+ "user_log_dir",
+ "user_documents_dir",
+ "user_runtime_dir",
+ "site_data_dir",
+ "site_config_dir",
+)
+
+
+def main() -> None:
+ app_name = "MyApp"
+ app_author = "MyCompany"
+
+ print(f"-- platformdirs {__version__} --")
+
+ print("-- app dirs (with optional 'version')")
+ dirs = PlatformDirs(app_name, app_author, version="1.0")
+ for prop in PROPS:
+ print(f"{prop}: {getattr(dirs, prop)}")
+
+ print("\n-- app dirs (without optional 'version')")
+ dirs = PlatformDirs(app_name, app_author)
+ for prop in PROPS:
+ print(f"{prop}: {getattr(dirs, prop)}")
+
+ print("\n-- app dirs (without optional 'appauthor')")
+ dirs = PlatformDirs(app_name)
+ for prop in PROPS:
+ print(f"{prop}: {getattr(dirs, prop)}")
+
+ print("\n-- app dirs (with disabled 'appauthor')")
+ dirs = PlatformDirs(app_name, appauthor=False)
+ for prop in PROPS:
+ print(f"{prop}: {getattr(dirs, prop)}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pip/_vendor/platformdirs/android.py b/src/pip/_vendor/platformdirs/android.py
new file mode 100644
index 000000000..eda809351
--- /dev/null
+++ b/src/pip/_vendor/platformdirs/android.py
@@ -0,0 +1,120 @@
+from __future__ import annotations
+
+import os
+import re
+import sys
+from functools import lru_cache
+from typing import cast
+
+from .api import PlatformDirsABC
+
+
+class Android(PlatformDirsABC):
+ """
+ Follows the guidance `from here <https://android.stackexchange.com/a/216132>`_. Makes use of the
+ `appname <platformdirs.api.PlatformDirsABC.appname>` and
+ `version <platformdirs.api.PlatformDirsABC.version>`.
+ """
+
+ @property
+ def user_data_dir(self) -> str:
+ """:return: data directory tied to the user, e.g. ``/data/user/<userid>/<packagename>/files/<AppName>``"""
+ return self._append_app_name_and_version(cast(str, _android_folder()), "files")
+
+ @property
+ def site_data_dir(self) -> str:
+ """:return: data directory shared by users, same as `user_data_dir`"""
+ return self.user_data_dir
+
+ @property
+ def user_config_dir(self) -> str:
+ """
+ :return: config directory tied to the user, e.g. ``/data/user/<userid>/<packagename>/shared_prefs/<AppName>``
+ """
+ return self._append_app_name_and_version(cast(str, _android_folder()), "shared_prefs")
+
+ @property
+ def site_config_dir(self) -> str:
+ """:return: config directory shared by the users, same as `user_config_dir`"""
+ return self.user_config_dir
+
+ @property
+ def user_cache_dir(self) -> str:
+ """:return: cache directory tied to the user, e.g. e.g. ``/data/user/<userid>/<packagename>/cache/<AppName>``"""
+ return self._append_app_name_and_version(cast(str, _android_folder()), "cache")
+
+ @property
+ def user_state_dir(self) -> str:
+ """:return: state directory tied to the user, same as `user_data_dir`"""
+ return self.user_data_dir
+
+ @property
+ def user_log_dir(self) -> str:
+ """
+ :return: log directory tied to the user, same as `user_cache_dir` if not opinionated else ``log`` in it,
+ e.g. ``/data/user/<userid>/<packagename>/cache/<AppName>/log``
+ """
+ path = self.user_cache_dir
+ if self.opinion:
+ path = os.path.join(path, "log")
+ return path
+
+ @property
+ def user_documents_dir(self) -> str:
+ """
+ :return: documents directory tied to the user e.g. ``/storage/emulated/0/Documents``
+ """
+ return _android_documents_folder()
+
+ @property
+ def user_runtime_dir(self) -> str:
+ """
+ :return: runtime directory tied to the user, same as `user_cache_dir` if not opinionated else ``tmp`` in it,
+ e.g. ``/data/user/<userid>/<packagename>/cache/<AppName>/tmp``
+ """
+ path = self.user_cache_dir
+ if self.opinion:
+ path = os.path.join(path, "tmp")
+ return path
+
+
+@lru_cache(maxsize=1)
+def _android_folder() -> str | None:
+ """:return: base folder for the Android OS or None if cannot be found"""
+ try:
+ # First try to get path to android app via pyjnius
+ from jnius import autoclass
+
+ Context = autoclass("android.content.Context") # noqa: N806
+ result: str | None = Context.getFilesDir().getParentFile().getAbsolutePath()
+ except Exception:
+ # if fails find an android folder looking path on the sys.path
+ pattern = re.compile(r"/data/(data|user/\d+)/(.+)/files")
+ for path in sys.path:
+ if pattern.match(path):
+ result = path.split("/files")[0]
+ break
+ else:
+ result = None
+ return result
+
+
+@lru_cache(maxsize=1)
+def _android_documents_folder() -> str:
+ """:return: documents folder for the Android OS"""
+ # Get directories with pyjnius
+ try:
+ from jnius import autoclass
+
+ Context = autoclass("android.content.Context") # noqa: N806
+ Environment = autoclass("android.os.Environment") # noqa: N806
+ documents_dir: str = Context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath()
+ except Exception:
+ documents_dir = "/storage/emulated/0/Documents"
+
+ return documents_dir
+
+
+__all__ = [
+ "Android",
+]
diff --git a/src/pip/_vendor/platformdirs/api.py b/src/pip/_vendor/platformdirs/api.py
new file mode 100644
index 000000000..6f6e2c2c6
--- /dev/null
+++ b/src/pip/_vendor/platformdirs/api.py
@@ -0,0 +1,156 @@
+from __future__ import annotations
+
+import os
+import sys
+from abc import ABC, abstractmethod
+from pathlib import Path
+
+if sys.version_info >= (3, 8): # pragma: no branch
+ from typing import Literal # pragma: no cover
+
+
+class PlatformDirsABC(ABC):
+ """
+ Abstract base class for platform directories.
+ """
+
+ def __init__(
+ self,
+ appname: str | None = None,
+ appauthor: str | None | Literal[False] = None,
+ version: str | None = None,
+ roaming: bool = False,
+ multipath: bool = False,
+ opinion: bool = True,
+ ):
+ """
+ Create a new platform directory.
+
+ :param appname: See `appname`.
+ :param appauthor: See `appauthor`.
+ :param version: See `version`.
+ :param roaming: See `roaming`.
+ :param multipath: See `multipath`.
+ :param opinion: See `opinion`.
+ """
+ self.appname = appname #: The name of application.
+ self.appauthor = appauthor
+ """
+ The name of the app author or distributing body for this application. Typically, it is the owning company name.
+ Defaults to `appname`. You may pass ``False`` to disable it.
+ """
+ self.version = version
+ """
+ An optional version path element to append to the path. You might want to use this if you want multiple versions
+ of your app to be able to run independently. If used, this would typically be ``<major>.<minor>``.
+ """
+ self.roaming = roaming
+ """
+ Whether to use the roaming appdata directory on Windows. That means that for users on a Windows network setup
+ for roaming profiles, this user data will be synced on login (see
+ `here <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>`_).
+ """
+ self.multipath = multipath
+ """
+ An optional parameter only applicable to Unix/Linux which indicates that the entire list of data dirs should be
+ returned. By default, the first item would only be returned.
+ """
+ self.opinion = opinion #: A flag to indicating to use opinionated values.
+
+ def _append_app_name_and_version(self, *base: str) -> str:
+ params = list(base[1:])
+ if self.appname:
+ params.append(self.appname)
+ if self.version:
+ params.append(self.version)
+ return os.path.join(base[0], *params)
+
+ @property
+ @abstractmethod
+ def user_data_dir(self) -> str:
+ """:return: data directory tied to the user"""
+
+ @property
+ @abstractmethod
+ def site_data_dir(self) -> str:
+ """:return: data directory shared by users"""
+
+ @property
+ @abstractmethod
+ def user_config_dir(self) -> str:
+ """:return: config directory tied to the user"""
+
+ @property
+ @abstractmethod
+ def site_config_dir(self) -> str:
+ """:return: config directory shared by the users"""
+
+ @property
+ @abstractmethod
+ def user_cache_dir(self) -> str:
+ """:return: cache directory tied to the user"""
+
+ @property
+ @abstractmethod
+ def user_state_dir(self) -> str:
+ """:return: state directory tied to the user"""
+
+ @property
+ @abstractmethod
+ def user_log_dir(self) -> str:
+ """:return: log directory tied to the user"""
+
+ @property
+ @abstractmethod
+ def user_documents_dir(self) -> str:
+ """:return: documents directory tied to the user"""
+
+ @property
+ @abstractmethod
+ def user_runtime_dir(self) -> str:
+ """:return: runtime directory tied to the user"""
+
+ @property
+ def user_data_path(self) -> Path:
+ """:return: data path tied to the user"""
+ return Path(self.user_data_dir)
+
+ @property
+ def site_data_path(self) -> Path:
+ """:return: data path shared by users"""
+ return Path(self.site_data_dir)
+
+ @property
+ def user_config_path(self) -> Path:
+ """:return: config path tied to the user"""
+ return Path(self.user_config_dir)
+
+ @property
+ def site_config_path(self) -> Path:
+ """:return: config path shared by the users"""
+ return Path(self.site_config_dir)
+
+ @property
+ def user_cache_path(self) -> Path:
+ """:return: cache path tied to the user"""
+ return Path(self.user_cache_dir)
+
+ @property
+ def user_state_path(self) -> Path:
+ """:return: state path tied to the user"""
+ return Path(self.user_state_dir)
+
+ @property
+ def user_log_path(self) -> Path:
+ """:return: log path tied to the user"""
+ return Path(self.user_log_dir)
+
+ @property
+ def user_documents_path(self) -> Path:
+ """:return: documents path tied to the user"""
+ return Path(self.user_documents_dir)
+
+ @property
+ def user_runtime_path(self) -> Path:
+ """:return: runtime path tied to the user"""
+ return Path(self.user_runtime_dir)
diff --git a/src/pip/_vendor/platformdirs/macos.py b/src/pip/_vendor/platformdirs/macos.py
new file mode 100644
index 000000000..a01337c77
--- /dev/null
+++ b/src/pip/_vendor/platformdirs/macos.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+import os
+
+from .api import PlatformDirsABC
+
+
+class MacOS(PlatformDirsABC):
+ """
+ Platform directories for the macOS operating system. Follows the guidance from `Apple documentation
+ <https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/MacOSXDirectories/MacOSXDirectories.html>`_.
+ Makes use of the `appname <platformdirs.api.PlatformDirsABC.appname>` and
+ `version <platformdirs.api.PlatformDirsABC.version>`.
+ """
+
+ @property
+ def user_data_dir(self) -> str:
+ """:return: data directory tied to the user, e.g. ``~/Library/Application Support/$appname/$version``"""
+ return self._append_app_name_and_version(os.path.expanduser("~/Library/Application Support/"))
+
+ @property
+ def site_data_dir(self) -> str:
+ """:return: data directory shared by users, e.g. ``/Library/Application Support/$appname/$version``"""
+ return self._append_app_name_and_version("/Library/Application Support")
+
+ @property
+ def user_config_dir(self) -> str:
+ """:return: config directory tied to the user, e.g. ``~/Library/Preferences/$appname/$version``"""
+ return self._append_app_name_and_version(os.path.expanduser("~/Library/Preferences/"))
+
+ @property
+ def site_config_dir(self) -> str:
+ """:return: config directory shared by the users, e.g. ``/Library/Preferences/$appname``"""
+ return self._append_app_name_and_version("/Library/Preferences")
+
+ @property
+ def user_cache_dir(self) -> str:
+ """:return: cache directory tied to the user, e.g. ``~/Library/Caches/$appname/$version``"""
+ return self._append_app_name_and_version(os.path.expanduser("~/Library/Caches"))
+
+ @property
+ def user_state_dir(self) -> str:
+ """:return: state directory tied to the user, same as `user_data_dir`"""
+ return self.user_data_dir
+
+ @property
+ def user_log_dir(self) -> str:
+ """:return: log directory tied to the user, e.g. ``~/Library/Logs/$appname/$version``"""
+ return self._append_app_name_and_version(os.path.expanduser("~/Library/Logs"))
+
+ @property
+ def user_documents_dir(self) -> str:
+ """:return: documents directory tied to the user, e.g. ``~/Documents``"""
+ return os.path.expanduser("~/Documents")
+
+ @property
+ def user_runtime_dir(self) -> str:
+ """:return: runtime directory tied to the user, e.g. ``~/Library/Caches/TemporaryItems/$appname/$version``"""
+ return self._append_app_name_and_version(os.path.expanduser("~/Library/Caches/TemporaryItems"))
+
+
+__all__ = [
+ "MacOS",
+]
diff --git a/src/pip/_vendor/platformdirs/py.typed b/src/pip/_vendor/platformdirs/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pip/_vendor/platformdirs/py.typed
diff --git a/src/pip/_vendor/platformdirs/unix.py b/src/pip/_vendor/platformdirs/unix.py
new file mode 100644
index 000000000..2fbd4d4f3
--- /dev/null
+++ b/src/pip/_vendor/platformdirs/unix.py
@@ -0,0 +1,181 @@
+from __future__ import annotations
+
+import os
+import sys
+from configparser import ConfigParser
+from pathlib import Path
+
+from .api import PlatformDirsABC
+
+if sys.platform.startswith("linux"): # pragma: no branch # no op check, only to please the type checker
+ from os import getuid
+else:
+
+ def getuid() -> int:
+ raise RuntimeError("should only be used on Linux")
+
+
+class Unix(PlatformDirsABC):
+ """
+ On Unix/Linux, we follow the
+ `XDG Basedir Spec <https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html>`_. The spec allows
+ overriding directories with environment variables. The examples show are the default values, alongside the name of
+ the environment variable that overrides them. Makes use of the
+ `appname <platformdirs.api.PlatformDirsABC.appname>`,
+ `version <platformdirs.api.PlatformDirsABC.version>`,
+ `multipath <platformdirs.api.PlatformDirsABC.multipath>`,
+ `opinion <platformdirs.api.PlatformDirsABC.opinion>`.
+ """
+
+ @property
+ def user_data_dir(self) -> str:
+ """
+ :return: data directory tied to the user, e.g. ``~/.local/share/$appname/$version`` or
+ ``$XDG_DATA_HOME/$appname/$version``
+ """
+ path = os.environ.get("XDG_DATA_HOME", "")
+ if not path.strip():
+ path = os.path.expanduser("~/.local/share")
+ return self._append_app_name_and_version(path)
+
+ @property
+ def site_data_dir(self) -> str:
+ """
+ :return: data directories shared by users (if `multipath <platformdirs.api.PlatformDirsABC.multipath>` is
+ enabled and ``XDG_DATA_DIR`` is set and a multi path the response is also a multi path separated by the OS
+ path separator), e.g. ``/usr/local/share/$appname/$version`` or ``/usr/share/$appname/$version``
+ """
+ # XDG default for $XDG_DATA_DIRS; only first, if multipath is False
+ path = os.environ.get("XDG_DATA_DIRS", "")
+ if not path.strip():
+ path = f"/usr/local/share{os.pathsep}/usr/share"
+ return self._with_multi_path(path)
+
+ def _with_multi_path(self, path: str) -> str:
+ path_list = path.split(os.pathsep)
+ if not self.multipath:
+ path_list = path_list[0:1]
+ path_list = [self._append_app_name_and_version(os.path.expanduser(p)) for p in path_list]
+ return os.pathsep.join(path_list)
+
+ @property
+ def user_config_dir(self) -> str:
+ """
+ :return: config directory tied to the user, e.g. ``~/.config/$appname/$version`` or
+ ``$XDG_CONFIG_HOME/$appname/$version``
+ """
+ path = os.environ.get("XDG_CONFIG_HOME", "")
+ if not path.strip():
+ path = os.path.expanduser("~/.config")
+ return self._append_app_name_and_version(path)
+
+ @property
+ def site_config_dir(self) -> str:
+ """
+ :return: config directories shared by users (if `multipath <platformdirs.api.PlatformDirsABC.multipath>`
+ is enabled and ``XDG_DATA_DIR`` is set and a multi path the response is also a multi path separated by the OS
+ path separator), e.g. ``/etc/xdg/$appname/$version``
+ """
+ # XDG default for $XDG_CONFIG_DIRS only first, if multipath is False
+ path = os.environ.get("XDG_CONFIG_DIRS", "")
+ if not path.strip():
+ path = "/etc/xdg"
+ return self._with_multi_path(path)
+
+ @property
+ def user_cache_dir(self) -> str:
+ """
+ :return: cache directory tied to the user, e.g. ``~/.cache/$appname/$version`` or
+ ``~/$XDG_CACHE_HOME/$appname/$version``
+ """
+ path = os.environ.get("XDG_CACHE_HOME", "")
+ if not path.strip():
+ path = os.path.expanduser("~/.cache")
+ return self._append_app_name_and_version(path)
+
+ @property
+ def user_state_dir(self) -> str:
+ """
+ :return: state directory tied to the user, e.g. ``~/.local/state/$appname/$version`` or
+ ``$XDG_STATE_HOME/$appname/$version``
+ """
+ path = os.environ.get("XDG_STATE_HOME", "")
+ if not path.strip():
+ path = os.path.expanduser("~/.local/state")
+ return self._append_app_name_and_version(path)
+
+ @property
+ def user_log_dir(self) -> str:
+ """
+ :return: log directory tied to the user, same as `user_data_dir` if not opinionated else ``log`` in it
+ """
+ path = self.user_cache_dir
+ if self.opinion:
+ path = os.path.join(path, "log")
+ return path
+
+ @property
+ def user_documents_dir(self) -> str:
+ """
+ :return: documents directory tied to the user, e.g. ``~/Documents``
+ """
+ documents_dir = _get_user_dirs_folder("XDG_DOCUMENTS_DIR")
+ if documents_dir is None:
+ documents_dir = os.environ.get("XDG_DOCUMENTS_DIR", "").strip()
+ if not documents_dir:
+ documents_dir = os.path.expanduser("~/Documents")
+
+ return documents_dir
+
+ @property
+ def user_runtime_dir(self) -> str:
+ """
+ :return: runtime directory tied to the user, e.g. ``/run/user/$(id -u)/$appname/$version`` or
+ ``$XDG_RUNTIME_DIR/$appname/$version``
+ """
+ path = os.environ.get("XDG_RUNTIME_DIR", "")
+ if not path.strip():
+ path = f"/run/user/{getuid()}"
+ return self._append_app_name_and_version(path)
+
+ @property
+ def site_data_path(self) -> Path:
+ """:return: data path shared by users. Only return first item, even if ``multipath`` is set to ``True``"""
+ return self._first_item_as_path_if_multipath(self.site_data_dir)
+
+ @property
+ def site_config_path(self) -> Path:
+ """:return: config path shared by the users. Only return first item, even if ``multipath`` is set to ``True``"""
+ return self._first_item_as_path_if_multipath(self.site_config_dir)
+
+ def _first_item_as_path_if_multipath(self, directory: str) -> Path:
+ if self.multipath:
+ # If multipath is True, the first path is returned.
+ directory = directory.split(os.pathsep)[0]
+ return Path(directory)
+
+
+def _get_user_dirs_folder(key: str) -> str | None:
+ """Return directory from user-dirs.dirs config file. See https://freedesktop.org/wiki/Software/xdg-user-dirs/"""
+ user_dirs_config_path = os.path.join(Unix().user_config_dir, "user-dirs.dirs")
+ if os.path.exists(user_dirs_config_path):
+ parser = ConfigParser()
+
+ with open(user_dirs_config_path) as stream:
+ # Add fake section header, so ConfigParser doesn't complain
+ parser.read_string(f"[top]\n{stream.read()}")
+
+ if key not in parser["top"]:
+ return None
+
+ path = parser["top"][key].strip('"')
+ # Handle relative home paths
+ path = path.replace("$HOME", os.path.expanduser("~"))
+ return path
+
+ return None
+
+
+__all__ = [
+ "Unix",
+]
diff --git a/src/pip/_vendor/platformdirs/version.py b/src/pip/_vendor/platformdirs/version.py
new file mode 100644
index 000000000..4552c02af
--- /dev/null
+++ b/src/pip/_vendor/platformdirs/version.py
@@ -0,0 +1,4 @@
+"""Version information"""
+
+__version__ = "2.5.2"
+__version_info__ = (2, 5, 2)
diff --git a/src/pip/_vendor/platformdirs/windows.py b/src/pip/_vendor/platformdirs/windows.py
new file mode 100644
index 000000000..ef972bdf2
--- /dev/null
+++ b/src/pip/_vendor/platformdirs/windows.py
@@ -0,0 +1,182 @@
+from __future__ import annotations
+
+import ctypes
+import os
+from functools import lru_cache
+from typing import Callable
+
+from .api import PlatformDirsABC
+
+
+class Windows(PlatformDirsABC):
+ """`MSDN on where to store app data files
+ <http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120>`_.
+ Makes use of the
+ `appname <platformdirs.api.PlatformDirsABC.appname>`,
+ `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`,
+ `version <platformdirs.api.PlatformDirsABC.version>`,
+ `roaming <platformdirs.api.PlatformDirsABC.roaming>`,
+ `opinion <platformdirs.api.PlatformDirsABC.opinion>`."""
+
+ @property
+ def user_data_dir(self) -> str:
+ """
+ :return: data directory tied to the user, e.g.
+ ``%USERPROFILE%\\AppData\\Local\\$appauthor\\$appname`` (not roaming) or
+ ``%USERPROFILE%\\AppData\\Roaming\\$appauthor\\$appname`` (roaming)
+ """
+ const = "CSIDL_APPDATA" if self.roaming else "CSIDL_LOCAL_APPDATA"
+ path = os.path.normpath(get_win_folder(const))
+ return self._append_parts(path)
+
+ def _append_parts(self, path: str, *, opinion_value: str | None = None) -> str:
+ params = []
+ if self.appname:
+ if self.appauthor is not False:
+ author = self.appauthor or self.appname
+ params.append(author)
+ params.append(self.appname)
+ if opinion_value is not None and self.opinion:
+ params.append(opinion_value)
+ if self.version:
+ params.append(self.version)
+ return os.path.join(path, *params)
+
+ @property
+ def site_data_dir(self) -> str:
+ """:return: data directory shared by users, e.g. ``C:\\ProgramData\\$appauthor\\$appname``"""
+ path = os.path.normpath(get_win_folder("CSIDL_COMMON_APPDATA"))
+ return self._append_parts(path)
+
+ @property
+ def user_config_dir(self) -> str:
+ """:return: config directory tied to the user, same as `user_data_dir`"""
+ return self.user_data_dir
+
+ @property
+ def site_config_dir(self) -> str:
+ """:return: config directory shared by the users, same as `site_data_dir`"""
+ return self.site_data_dir
+
+ @property
+ def user_cache_dir(self) -> str:
+ """
+ :return: cache directory tied to the user (if opinionated with ``Cache`` folder within ``$appname``) e.g.
+ ``%USERPROFILE%\\AppData\\Local\\$appauthor\\$appname\\Cache\\$version``
+ """
+ path = os.path.normpath(get_win_folder("CSIDL_LOCAL_APPDATA"))
+ return self._append_parts(path, opinion_value="Cache")
+
+ @property
+ def user_state_dir(self) -> str:
+ """:return: state directory tied to the user, same as `user_data_dir`"""
+ return self.user_data_dir
+
+ @property
+ def user_log_dir(self) -> str:
+ """
+ :return: log directory tied to the user, same as `user_data_dir` if not opinionated else ``Logs`` in it
+ """
+ path = self.user_data_dir
+ if self.opinion:
+ path = os.path.join(path, "Logs")
+ return path
+
+ @property
+ def user_documents_dir(self) -> str:
+ """
+ :return: documents directory tied to the user e.g. ``%USERPROFILE%\\Documents``
+ """
+ return os.path.normpath(get_win_folder("CSIDL_PERSONAL"))
+
+ @property
+ def user_runtime_dir(self) -> str:
+ """
+ :return: runtime directory tied to the user, e.g.
+ ``%USERPROFILE%\\AppData\\Local\\Temp\\$appauthor\\$appname``
+ """
+ path = os.path.normpath(os.path.join(get_win_folder("CSIDL_LOCAL_APPDATA"), "Temp"))
+ return self._append_parts(path)
+
+
+def get_win_folder_from_env_vars(csidl_name: str) -> str:
+ """Get folder from environment variables."""
+ if csidl_name == "CSIDL_PERSONAL": # does not have an environment name
+ return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Documents")
+
+ env_var_name = {
+ "CSIDL_APPDATA": "APPDATA",
+ "CSIDL_COMMON_APPDATA": "ALLUSERSPROFILE",
+ "CSIDL_LOCAL_APPDATA": "LOCALAPPDATA",
+ }.get(csidl_name)
+ if env_var_name is None:
+ raise ValueError(f"Unknown CSIDL name: {csidl_name}")
+ result = os.environ.get(env_var_name)
+ if result is None:
+ raise ValueError(f"Unset environment variable: {env_var_name}")
+ return result
+
+
+def get_win_folder_from_registry(csidl_name: str) -> str:
+ """Get folder from the registry.
+
+ This is a fallback technique at best. I'm not sure if using the
+ registry for this guarantees us the correct answer for all CSIDL_*
+ names.
+ """
+ shell_folder_name = {
+ "CSIDL_APPDATA": "AppData",
+ "CSIDL_COMMON_APPDATA": "Common AppData",
+ "CSIDL_LOCAL_APPDATA": "Local AppData",
+ "CSIDL_PERSONAL": "Personal",
+ }.get(csidl_name)
+ if shell_folder_name is None:
+ raise ValueError(f"Unknown CSIDL name: {csidl_name}")
+
+ import winreg
+
+ key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")
+ directory, _ = winreg.QueryValueEx(key, shell_folder_name)
+ return str(directory)
+
+
+def get_win_folder_via_ctypes(csidl_name: str) -> str:
+ """Get folder with ctypes."""
+ csidl_const = {
+ "CSIDL_APPDATA": 26,
+ "CSIDL_COMMON_APPDATA": 35,
+ "CSIDL_LOCAL_APPDATA": 28,
+ "CSIDL_PERSONAL": 5,
+ }.get(csidl_name)
+ if csidl_const is None:
+ raise ValueError(f"Unknown CSIDL name: {csidl_name}")
+
+ buf = ctypes.create_unicode_buffer(1024)
+ windll = getattr(ctypes, "windll") # noqa: B009 # using getattr to avoid false positive with mypy type checker
+ windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
+
+ # Downgrade to short path name if it has highbit chars.
+ if any(ord(c) > 255 for c in buf):
+ buf2 = ctypes.create_unicode_buffer(1024)
+ if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
+ buf = buf2
+
+ return buf.value
+
+
+def _pick_get_win_folder() -> Callable[[str], str]:
+ if hasattr(ctypes, "windll"):
+ return get_win_folder_via_ctypes
+ try:
+ import winreg # noqa: F401
+ except ImportError:
+ return get_win_folder_from_env_vars
+ else:
+ return get_win_folder_from_registry
+
+
+get_win_folder = lru_cache(maxsize=None)(_pick_get_win_folder())
+
+__all__ = [
+ "Windows",
+]
diff --git a/src/pip/_vendor/progress.pyi b/src/pip/_vendor/progress.pyi
deleted file mode 100644
index c92de832b..000000000
--- a/src/pip/_vendor/progress.pyi
+++ /dev/null
@@ -1 +0,0 @@
-from progress import * \ No newline at end of file
diff --git a/src/pip/_vendor/progress/LICENSE b/src/pip/_vendor/progress/LICENSE
deleted file mode 100644
index 059cc0566..000000000
--- a/src/pip/_vendor/progress/LICENSE
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright (c) 2012 Giorgos Verigakis <verigak@gmail.com>
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/src/pip/_vendor/progress/__init__.py b/src/pip/_vendor/progress/__init__.py
deleted file mode 100644
index e434c257f..000000000
--- a/src/pip/_vendor/progress/__init__.py
+++ /dev/null
@@ -1,177 +0,0 @@
-# Copyright (c) 2012 Giorgos Verigakis <verigak@gmail.com>
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-from __future__ import division, print_function
-
-from collections import deque
-from datetime import timedelta
-from math import ceil
-from sys import stderr
-try:
- from time import monotonic
-except ImportError:
- from time import time as monotonic
-
-
-__version__ = '1.5'
-
-HIDE_CURSOR = '\x1b[?25l'
-SHOW_CURSOR = '\x1b[?25h'
-
-
-class Infinite(object):
- file = stderr
- sma_window = 10 # Simple Moving Average window
- check_tty = True
- hide_cursor = True
-
- def __init__(self, message='', **kwargs):
- self.index = 0
- self.start_ts = monotonic()
- self.avg = 0
- self._avg_update_ts = self.start_ts
- self._ts = self.start_ts
- self._xput = deque(maxlen=self.sma_window)
- for key, val in kwargs.items():
- setattr(self, key, val)
-
- self._width = 0
- self.message = message
-
- if self.file and self.is_tty():
- if self.hide_cursor:
- print(HIDE_CURSOR, end='', file=self.file)
- print(self.message, end='', file=self.file)
- self.file.flush()
-
- def __getitem__(self, key):
- if key.startswith('_'):
- return None
- return getattr(self, key, None)
-
- @property
- def elapsed(self):
- return int(monotonic() - self.start_ts)
-
- @property
- def elapsed_td(self):
- return timedelta(seconds=self.elapsed)
-
- def update_avg(self, n, dt):
- if n > 0:
- xput_len = len(self._xput)
- self._xput.append(dt / n)
- now = monotonic()
- # update when we're still filling _xput, then after every second
- if (xput_len < self.sma_window or
- now - self._avg_update_ts > 1):
- self.avg = sum(self._xput) / len(self._xput)
- self._avg_update_ts = now
-
- def update(self):
- pass
-
- def start(self):
- pass
-
- def clearln(self):
- if self.file and self.is_tty():
- print('\r\x1b[K', end='', file=self.file)
-
- def write(self, s):
- if self.file and self.is_tty():
- line = self.message + s.ljust(self._width)
- print('\r' + line, end='', file=self.file)
- self._width = max(self._width, len(s))
- self.file.flush()
-
- def writeln(self, line):
- if self.file and self.is_tty():
- self.clearln()
- print(line, end='', file=self.file)
- self.file.flush()
-
- def finish(self):
- if self.file and self.is_tty():
- print(file=self.file)
- if self.hide_cursor:
- print(SHOW_CURSOR, end='', file=self.file)
-
- def is_tty(self):
- return self.file.isatty() if self.check_tty else True
-
- def next(self, n=1):
- now = monotonic()
- dt = now - self._ts
- self.update_avg(n, dt)
- self._ts = now
- self.index = self.index + n
- self.update()
-
- def iter(self, it):
- with self:
- for x in it:
- yield x
- self.next()
-
- def __enter__(self):
- self.start()
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.finish()
-
-
-class Progress(Infinite):
- def __init__(self, *args, **kwargs):
- super(Progress, self).__init__(*args, **kwargs)
- self.max = kwargs.get('max', 100)
-
- @property
- def eta(self):
- return int(ceil(self.avg * self.remaining))
-
- @property
- def eta_td(self):
- return timedelta(seconds=self.eta)
-
- @property
- def percent(self):
- return self.progress * 100
-
- @property
- def progress(self):
- return min(1, self.index / self.max)
-
- @property
- def remaining(self):
- return max(self.max - self.index, 0)
-
- def start(self):
- self.update()
-
- def goto(self, index):
- incr = index - self.index
- self.next(incr)
-
- def iter(self, it):
- try:
- self.max = len(it)
- except TypeError:
- pass
-
- with self:
- for x in it:
- yield x
- self.next()
diff --git a/src/pip/_vendor/progress/bar.py b/src/pip/_vendor/progress/bar.py
deleted file mode 100644
index 8819efda6..000000000
--- a/src/pip/_vendor/progress/bar.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2012 Giorgos Verigakis <verigak@gmail.com>
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-from __future__ import unicode_literals
-
-import sys
-
-from . import Progress
-
-
-class Bar(Progress):
- width = 32
- suffix = '%(index)d/%(max)d'
- bar_prefix = ' |'
- bar_suffix = '| '
- empty_fill = ' '
- fill = '#'
-
- def update(self):
- filled_length = int(self.width * self.progress)
- empty_length = self.width - filled_length
-
- message = self.message % self
- bar = self.fill * filled_length
- empty = self.empty_fill * empty_length
- suffix = self.suffix % self
- line = ''.join([message, self.bar_prefix, bar, empty, self.bar_suffix,
- suffix])
- self.writeln(line)
-
-
-class ChargingBar(Bar):
- suffix = '%(percent)d%%'
- bar_prefix = ' '
- bar_suffix = ' '
- empty_fill = '∙'
- fill = 'â–ˆ'
-
-
-class FillingSquaresBar(ChargingBar):
- empty_fill = 'â–¢'
- fill = 'â–£'
-
-
-class FillingCirclesBar(ChargingBar):
- empty_fill = 'â—¯'
- fill = 'â—‰'
-
-
-class IncrementalBar(Bar):
- if sys.platform.startswith('win'):
- phases = (u' ', u'▌', u'█')
- else:
- phases = (' ', 'â–', 'â–Ž', 'â–', 'â–Œ', 'â–‹', 'â–Š', 'â–‰', 'â–ˆ')
-
- def update(self):
- nphases = len(self.phases)
- filled_len = self.width * self.progress
- nfull = int(filled_len) # Number of full chars
- phase = int((filled_len - nfull) * nphases) # Phase of last char
- nempty = self.width - nfull # Number of empty chars
-
- message = self.message % self
- bar = self.phases[-1] * nfull
- current = self.phases[phase] if phase > 0 else ''
- empty = self.empty_fill * max(0, nempty - len(current))
- suffix = self.suffix % self
- line = ''.join([message, self.bar_prefix, bar, current, empty,
- self.bar_suffix, suffix])
- self.writeln(line)
-
-
-class PixelBar(IncrementalBar):
- phases = ('⡀', '⡄', '⡆', '⡇', '⣇', '⣧', '⣷', '⣿')
-
-
-class ShadyBar(IncrementalBar):
- phases = (' ', 'â–‘', 'â–’', 'â–“', 'â–ˆ')
diff --git a/src/pip/_vendor/progress/counter.py b/src/pip/_vendor/progress/counter.py
deleted file mode 100644
index d955ca477..000000000
--- a/src/pip/_vendor/progress/counter.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2012 Giorgos Verigakis <verigak@gmail.com>
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-from __future__ import unicode_literals
-from . import Infinite, Progress
-
-
-class Counter(Infinite):
- def update(self):
- self.write(str(self.index))
-
-
-class Countdown(Progress):
- def update(self):
- self.write(str(self.remaining))
-
-
-class Stack(Progress):
- phases = (' ', 'â–', 'â–‚', 'â–ƒ', 'â–„', 'â–…', 'â–†', 'â–‡', 'â–ˆ')
-
- def update(self):
- nphases = len(self.phases)
- i = min(nphases - 1, int(self.progress * nphases))
- self.write(self.phases[i])
-
-
-class Pie(Stack):
- phases = ('â—‹', 'â—”', 'â—‘', 'â—•', 'â—')
diff --git a/src/pip/_vendor/progress/spinner.py b/src/pip/_vendor/progress/spinner.py
deleted file mode 100644
index 4e100cabb..000000000
--- a/src/pip/_vendor/progress/spinner.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2012 Giorgos Verigakis <verigak@gmail.com>
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-from __future__ import unicode_literals
-from . import Infinite
-
-
-class Spinner(Infinite):
- phases = ('-', '\\', '|', '/')
- hide_cursor = True
-
- def update(self):
- i = self.index % len(self.phases)
- self.write(self.phases[i])
-
-
-class PieSpinner(Spinner):
- phases = ['â—·', 'â—¶', 'â—µ', 'â—´']
-
-
-class MoonSpinner(Spinner):
- phases = ['â—‘', 'â—’', 'â—', 'â—“']
-
-
-class LineSpinner(Spinner):
- phases = ['⎺', '⎻', '⎼', '⎽', '⎼', '⎻']
-
-
-class PixelSpinner(Spinner):
- phases = ['⣾', '⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽']
diff --git a/src/pip/_vendor/pygments.pyi b/src/pip/_vendor/pygments.pyi
new file mode 100644
index 000000000..566eaff36
--- /dev/null
+++ b/src/pip/_vendor/pygments.pyi
@@ -0,0 +1 @@
+from pygments import * \ No newline at end of file
diff --git a/src/pip/_vendor/pygments/LICENSE b/src/pip/_vendor/pygments/LICENSE
new file mode 100644
index 000000000..446a1a805
--- /dev/null
+++ b/src/pip/_vendor/pygments/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) 2006-2022 by the respective authors (see AUTHORS file).
+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.
+
+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 THE COPYRIGHT
+OWNER OR CONTRIBUTORS 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.
diff --git a/src/pip/_vendor/pygments/__init__.py b/src/pip/_vendor/pygments/__init__.py
new file mode 100644
index 000000000..52ff035dd
--- /dev/null
+++ b/src/pip/_vendor/pygments/__init__.py
@@ -0,0 +1,83 @@
+"""
+ Pygments
+ ~~~~~~~~
+
+ Pygments is a syntax highlighting package written in Python.
+
+ It is a generic syntax highlighter for general use in all kinds of software
+ such as forum systems, wikis or other applications that need to prettify
+ source code. Highlights are:
+
+ * a wide range of common languages and markup formats is supported
+ * special attention is paid to details, increasing quality by a fair amount
+ * support for new languages and formats are added easily
+ * a number of output formats, presently HTML, LaTeX, RTF, SVG, all image
+ formats that PIL supports, and ANSI sequences
+ * it is usable as a command-line tool and as a library
+ * ... and it highlights even Brainfuck!
+
+ The `Pygments master branch`_ is installable with ``easy_install Pygments==dev``.
+
+ .. _Pygments master branch:
+ https://github.com/pygments/pygments/archive/master.zip#egg=Pygments-dev
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+from io import StringIO, BytesIO
+
+__version__ = '2.12.0'
+__docformat__ = 'restructuredtext'
+
+__all__ = ['lex', 'format', 'highlight']
+
+
+def lex(code, lexer):
+ """
+ Lex ``code`` with ``lexer`` and return an iterable of tokens.
+ """
+ try:
+ return lexer.get_tokens(code)
+ except TypeError as err:
+ if (isinstance(err.args[0], str) and
+ ('unbound method get_tokens' in err.args[0] or
+ 'missing 1 required positional argument' in err.args[0])):
+ raise TypeError('lex() argument must be a lexer instance, '
+ 'not a class')
+ raise
+
+
+def format(tokens, formatter, outfile=None): # pylint: disable=redefined-builtin
+ """
+ Format a tokenlist ``tokens`` with the formatter ``formatter``.
+
+ If ``outfile`` is given and a valid file object (an object
+ with a ``write`` method), the result will be written to it, otherwise
+ it is returned as a string.
+ """
+ try:
+ if not outfile:
+ realoutfile = getattr(formatter, 'encoding', None) and BytesIO() or StringIO()
+ formatter.format(tokens, realoutfile)
+ return realoutfile.getvalue()
+ else:
+ formatter.format(tokens, outfile)
+ except TypeError as err:
+ if (isinstance(err.args[0], str) and
+ ('unbound method format' in err.args[0] or
+ 'missing 1 required positional argument' in err.args[0])):
+ raise TypeError('format() argument must be a formatter instance, '
+ 'not a class')
+ raise
+
+
+def highlight(code, lexer, formatter, outfile=None):
+ """
+ Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
+
+ If ``outfile`` is given and a valid file object (an object
+ with a ``write`` method), the result will be written to it, otherwise
+ it is returned as a string.
+ """
+ return format(lex(code, lexer), formatter, outfile)
+
diff --git a/src/pip/_vendor/pygments/__main__.py b/src/pip/_vendor/pygments/__main__.py
new file mode 100644
index 000000000..90cafd934
--- /dev/null
+++ b/src/pip/_vendor/pygments/__main__.py
@@ -0,0 +1,17 @@
+"""
+ pygments.__main__
+ ~~~~~~~~~~~~~~~~~
+
+ Main entry point for ``python -m pygments``.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import sys
+from pip._vendor.pygments.cmdline import main
+
+try:
+ sys.exit(main(sys.argv))
+except KeyboardInterrupt:
+ sys.exit(1)
diff --git a/src/pip/_vendor/pygments/cmdline.py b/src/pip/_vendor/pygments/cmdline.py
new file mode 100644
index 000000000..349c626f1
--- /dev/null
+++ b/src/pip/_vendor/pygments/cmdline.py
@@ -0,0 +1,663 @@
+"""
+ pygments.cmdline
+ ~~~~~~~~~~~~~~~~
+
+ Command line interface.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import os
+import sys
+import shutil
+import argparse
+from textwrap import dedent
+
+from pip._vendor.pygments import __version__, highlight
+from pip._vendor.pygments.util import ClassNotFound, OptionError, docstring_headline, \
+ guess_decode, guess_decode_from_terminal, terminal_encoding, \
+ UnclosingTextIOWrapper
+from pip._vendor.pygments.lexers import get_all_lexers, get_lexer_by_name, guess_lexer, \
+ load_lexer_from_file, get_lexer_for_filename, find_lexer_class_for_filename
+from pip._vendor.pygments.lexers.special import TextLexer
+from pip._vendor.pygments.formatters.latex import LatexEmbeddedLexer, LatexFormatter
+from pip._vendor.pygments.formatters import get_all_formatters, get_formatter_by_name, \
+ load_formatter_from_file, get_formatter_for_filename, find_formatter_class
+from pip._vendor.pygments.formatters.terminal import TerminalFormatter
+from pip._vendor.pygments.formatters.terminal256 import Terminal256Formatter
+from pip._vendor.pygments.filters import get_all_filters, find_filter_class
+from pip._vendor.pygments.styles import get_all_styles, get_style_by_name
+
+
+def _parse_options(o_strs):
+ opts = {}
+ if not o_strs:
+ return opts
+ for o_str in o_strs:
+ if not o_str.strip():
+ continue
+ o_args = o_str.split(',')
+ for o_arg in o_args:
+ o_arg = o_arg.strip()
+ try:
+ o_key, o_val = o_arg.split('=', 1)
+ o_key = o_key.strip()
+ o_val = o_val.strip()
+ except ValueError:
+ opts[o_arg] = True
+ else:
+ opts[o_key] = o_val
+ return opts
+
+
+def _parse_filters(f_strs):
+ filters = []
+ if not f_strs:
+ return filters
+ for f_str in f_strs:
+ if ':' in f_str:
+ fname, fopts = f_str.split(':', 1)
+ filters.append((fname, _parse_options([fopts])))
+ else:
+ filters.append((f_str, {}))
+ return filters
+
+
+def _print_help(what, name):
+ try:
+ if what == 'lexer':
+ cls = get_lexer_by_name(name)
+ print("Help on the %s lexer:" % cls.name)
+ print(dedent(cls.__doc__))
+ elif what == 'formatter':
+ cls = find_formatter_class(name)
+ print("Help on the %s formatter:" % cls.name)
+ print(dedent(cls.__doc__))
+ elif what == 'filter':
+ cls = find_filter_class(name)
+ print("Help on the %s filter:" % name)
+ print(dedent(cls.__doc__))
+ return 0
+ except (AttributeError, ValueError):
+ print("%s not found!" % what, file=sys.stderr)
+ return 1
+
+
+def _print_list(what):
+ if what == 'lexer':
+ print()
+ print("Lexers:")
+ print("~~~~~~~")
+
+ info = []
+ for fullname, names, exts, _ in get_all_lexers():
+ tup = (', '.join(names)+':', fullname,
+ exts and '(filenames ' + ', '.join(exts) + ')' or '')
+ info.append(tup)
+ info.sort()
+ for i in info:
+ print(('* %s\n %s %s') % i)
+
+ elif what == 'formatter':
+ print()
+ print("Formatters:")
+ print("~~~~~~~~~~~")
+
+ info = []
+ for cls in get_all_formatters():
+ doc = docstring_headline(cls)
+ tup = (', '.join(cls.aliases) + ':', doc, cls.filenames and
+ '(filenames ' + ', '.join(cls.filenames) + ')' or '')
+ info.append(tup)
+ info.sort()
+ for i in info:
+ print(('* %s\n %s %s') % i)
+
+ elif what == 'filter':
+ print()
+ print("Filters:")
+ print("~~~~~~~~")
+
+ for name in get_all_filters():
+ cls = find_filter_class(name)
+ print("* " + name + ':')
+ print(" %s" % docstring_headline(cls))
+
+ elif what == 'style':
+ print()
+ print("Styles:")
+ print("~~~~~~~")
+
+ for name in get_all_styles():
+ cls = get_style_by_name(name)
+ print("* " + name + ':')
+ print(" %s" % docstring_headline(cls))
+
+
+def _print_list_as_json(requested_items):
+ import json
+ result = {}
+ if 'lexer' in requested_items:
+ info = {}
+ for fullname, names, filenames, mimetypes in get_all_lexers():
+ info[fullname] = {
+ 'aliases': names,
+ 'filenames': filenames,
+ 'mimetypes': mimetypes
+ }
+ result['lexers'] = info
+
+ if 'formatter' in requested_items:
+ info = {}
+ for cls in get_all_formatters():
+ doc = docstring_headline(cls)
+ info[cls.name] = {
+ 'aliases': cls.aliases,
+ 'filenames': cls.filenames,
+ 'doc': doc
+ }
+ result['formatters'] = info
+
+ if 'filter' in requested_items:
+ info = {}
+ for name in get_all_filters():
+ cls = find_filter_class(name)
+ info[name] = {
+ 'doc': docstring_headline(cls)
+ }
+ result['filters'] = info
+
+ if 'style' in requested_items:
+ info = {}
+ for name in get_all_styles():
+ cls = get_style_by_name(name)
+ info[name] = {
+ 'doc': docstring_headline(cls)
+ }
+ result['styles'] = info
+
+ json.dump(result, sys.stdout)
+
+def main_inner(parser, argns):
+ if argns.help:
+ parser.print_help()
+ return 0
+
+ if argns.V:
+ print('Pygments version %s, (c) 2006-2022 by Georg Brandl, Matthäus '
+ 'Chajdas and contributors.' % __version__)
+ return 0
+
+ def is_only_option(opt):
+ return not any(v for (k, v) in vars(argns).items() if k != opt)
+
+ # handle ``pygmentize -L``
+ if argns.L is not None:
+ arg_set = set()
+ for k, v in vars(argns).items():
+ if v:
+ arg_set.add(k)
+
+ arg_set.discard('L')
+ arg_set.discard('json')
+
+ if arg_set:
+ parser.print_help(sys.stderr)
+ return 2
+
+ # print version
+ if not argns.json:
+ main(['', '-V'])
+ allowed_types = {'lexer', 'formatter', 'filter', 'style'}
+ largs = [arg.rstrip('s') for arg in argns.L]
+ if any(arg not in allowed_types for arg in largs):
+ parser.print_help(sys.stderr)
+ return 0
+ if not largs:
+ largs = allowed_types
+ if not argns.json:
+ for arg in largs:
+ _print_list(arg)
+ else:
+ _print_list_as_json(largs)
+ return 0
+
+ # handle ``pygmentize -H``
+ if argns.H:
+ if not is_only_option('H'):
+ parser.print_help(sys.stderr)
+ return 2
+ what, name = argns.H
+ if what not in ('lexer', 'formatter', 'filter'):
+ parser.print_help(sys.stderr)
+ return 2
+ return _print_help(what, name)
+
+ # parse -O options
+ parsed_opts = _parse_options(argns.O or [])
+
+ # parse -P options
+ for p_opt in argns.P or []:
+ try:
+ name, value = p_opt.split('=', 1)
+ except ValueError:
+ parsed_opts[p_opt] = True
+ else:
+ parsed_opts[name] = value
+
+ # encodings
+ inencoding = parsed_opts.get('inencoding', parsed_opts.get('encoding'))
+ outencoding = parsed_opts.get('outencoding', parsed_opts.get('encoding'))
+
+ # handle ``pygmentize -N``
+ if argns.N:
+ lexer = find_lexer_class_for_filename(argns.N)
+ if lexer is None:
+ lexer = TextLexer
+
+ print(lexer.aliases[0])
+ return 0
+
+ # handle ``pygmentize -C``
+ if argns.C:
+ inp = sys.stdin.buffer.read()
+ try:
+ lexer = guess_lexer(inp, inencoding=inencoding)
+ except ClassNotFound:
+ lexer = TextLexer
+
+ print(lexer.aliases[0])
+ return 0
+
+ # handle ``pygmentize -S``
+ S_opt = argns.S
+ a_opt = argns.a
+ if S_opt is not None:
+ f_opt = argns.f
+ if not f_opt:
+ parser.print_help(sys.stderr)
+ return 2
+ if argns.l or argns.INPUTFILE:
+ parser.print_help(sys.stderr)
+ return 2
+
+ try:
+ parsed_opts['style'] = S_opt
+ fmter = get_formatter_by_name(f_opt, **parsed_opts)
+ except ClassNotFound as err:
+ print(err, file=sys.stderr)
+ return 1
+
+ print(fmter.get_style_defs(a_opt or ''))
+ return 0
+
+ # if no -S is given, -a is not allowed
+ if argns.a is not None:
+ parser.print_help(sys.stderr)
+ return 2
+
+ # parse -F options
+ F_opts = _parse_filters(argns.F or [])
+
+ # -x: allow custom (eXternal) lexers and formatters
+ allow_custom_lexer_formatter = bool(argns.x)
+
+ # select lexer
+ lexer = None
+
+ # given by name?
+ lexername = argns.l
+ if lexername:
+ # custom lexer, located relative to user's cwd
+ if allow_custom_lexer_formatter and '.py' in lexername:
+ try:
+ filename = None
+ name = None
+ if ':' in lexername:
+ filename, name = lexername.rsplit(':', 1)
+
+ if '.py' in name:
+ # This can happen on Windows: If the lexername is
+ # C:\lexer.py -- return to normal load path in that case
+ name = None
+
+ if filename and name:
+ lexer = load_lexer_from_file(filename, name,
+ **parsed_opts)
+ else:
+ lexer = load_lexer_from_file(lexername, **parsed_opts)
+ except ClassNotFound as err:
+ print('Error:', err, file=sys.stderr)
+ return 1
+ else:
+ try:
+ lexer = get_lexer_by_name(lexername, **parsed_opts)
+ except (OptionError, ClassNotFound) as err:
+ print('Error:', err, file=sys.stderr)
+ return 1
+
+ # read input code
+ code = None
+
+ if argns.INPUTFILE:
+ if argns.s:
+ print('Error: -s option not usable when input file specified',
+ file=sys.stderr)
+ return 2
+
+ infn = argns.INPUTFILE
+ try:
+ with open(infn, 'rb') as infp:
+ code = infp.read()
+ except Exception as err:
+ print('Error: cannot read infile:', err, file=sys.stderr)
+ return 1
+ if not inencoding:
+ code, inencoding = guess_decode(code)
+
+ # do we have to guess the lexer?
+ if not lexer:
+ try:
+ lexer = get_lexer_for_filename(infn, code, **parsed_opts)
+ except ClassNotFound as err:
+ if argns.g:
+ try:
+ lexer = guess_lexer(code, **parsed_opts)
+ except ClassNotFound:
+ lexer = TextLexer(**parsed_opts)
+ else:
+ print('Error:', err, file=sys.stderr)
+ return 1
+ except OptionError as err:
+ print('Error:', err, file=sys.stderr)
+ return 1
+
+ elif not argns.s: # treat stdin as full file (-s support is later)
+ # read code from terminal, always in binary mode since we want to
+ # decode ourselves and be tolerant with it
+ code = sys.stdin.buffer.read() # use .buffer to get a binary stream
+ if not inencoding:
+ code, inencoding = guess_decode_from_terminal(code, sys.stdin)
+ # else the lexer will do the decoding
+ if not lexer:
+ try:
+ lexer = guess_lexer(code, **parsed_opts)
+ except ClassNotFound:
+ lexer = TextLexer(**parsed_opts)
+
+ else: # -s option needs a lexer with -l
+ if not lexer:
+ print('Error: when using -s a lexer has to be selected with -l',
+ file=sys.stderr)
+ return 2
+
+ # process filters
+ for fname, fopts in F_opts:
+ try:
+ lexer.add_filter(fname, **fopts)
+ except ClassNotFound as err:
+ print('Error:', err, file=sys.stderr)
+ return 1
+
+ # select formatter
+ outfn = argns.o
+ fmter = argns.f
+ if fmter:
+ # custom formatter, located relative to user's cwd
+ if allow_custom_lexer_formatter and '.py' in fmter:
+ try:
+ filename = None
+ name = None
+ if ':' in fmter:
+ # Same logic as above for custom lexer
+ filename, name = fmter.rsplit(':', 1)
+
+ if '.py' in name:
+ name = None
+
+ if filename and name:
+ fmter = load_formatter_from_file(filename, name,
+ **parsed_opts)
+ else:
+ fmter = load_formatter_from_file(fmter, **parsed_opts)
+ except ClassNotFound as err:
+ print('Error:', err, file=sys.stderr)
+ return 1
+ else:
+ try:
+ fmter = get_formatter_by_name(fmter, **parsed_opts)
+ except (OptionError, ClassNotFound) as err:
+ print('Error:', err, file=sys.stderr)
+ return 1
+
+ if outfn:
+ if not fmter:
+ try:
+ fmter = get_formatter_for_filename(outfn, **parsed_opts)
+ except (OptionError, ClassNotFound) as err:
+ print('Error:', err, file=sys.stderr)
+ return 1
+ try:
+ outfile = open(outfn, 'wb')
+ except Exception as err:
+ print('Error: cannot open outfile:', err, file=sys.stderr)
+ return 1
+ else:
+ if not fmter:
+ if '256' in os.environ.get('TERM', ''):
+ fmter = Terminal256Formatter(**parsed_opts)
+ else:
+ fmter = TerminalFormatter(**parsed_opts)
+ outfile = sys.stdout.buffer
+
+ # determine output encoding if not explicitly selected
+ if not outencoding:
+ if outfn:
+ # output file? use lexer encoding for now (can still be None)
+ fmter.encoding = inencoding
+ else:
+ # else use terminal encoding
+ fmter.encoding = terminal_encoding(sys.stdout)
+
+ # provide coloring under Windows, if possible
+ if not outfn and sys.platform in ('win32', 'cygwin') and \
+ fmter.name in ('Terminal', 'Terminal256'): # pragma: no cover
+ # unfortunately colorama doesn't support binary streams on Py3
+ outfile = UnclosingTextIOWrapper(outfile, encoding=fmter.encoding)
+ fmter.encoding = None
+ try:
+ import pip._vendor.colorama.initialise as colorama_initialise
+ except ImportError:
+ pass
+ else:
+ outfile = colorama_initialise.wrap_stream(
+ outfile, convert=None, strip=None, autoreset=False, wrap=True)
+
+ # When using the LaTeX formatter and the option `escapeinside` is
+ # specified, we need a special lexer which collects escaped text
+ # before running the chosen language lexer.
+ escapeinside = parsed_opts.get('escapeinside', '')
+ if len(escapeinside) == 2 and isinstance(fmter, LatexFormatter):
+ left = escapeinside[0]
+ right = escapeinside[1]
+ lexer = LatexEmbeddedLexer(left, right, lexer)
+
+ # ... and do it!
+ if not argns.s:
+ # process whole input as per normal...
+ try:
+ highlight(code, lexer, fmter, outfile)
+ finally:
+ if outfn:
+ outfile.close()
+ return 0
+ else:
+ # line by line processing of stdin (eg: for 'tail -f')...
+ try:
+ while 1:
+ line = sys.stdin.buffer.readline()
+ if not line:
+ break
+ if not inencoding:
+ line = guess_decode_from_terminal(line, sys.stdin)[0]
+ highlight(line, lexer, fmter, outfile)
+ if hasattr(outfile, 'flush'):
+ outfile.flush()
+ return 0
+ except KeyboardInterrupt: # pragma: no cover
+ return 0
+ finally:
+ if outfn:
+ outfile.close()
+
+
+class HelpFormatter(argparse.HelpFormatter):
+ def __init__(self, prog, indent_increment=2, max_help_position=16, width=None):
+ if width is None:
+ try:
+ width = shutil.get_terminal_size().columns - 2
+ except Exception:
+ pass
+ argparse.HelpFormatter.__init__(self, prog, indent_increment,
+ max_help_position, width)
+
+
+def main(args=sys.argv):
+ """
+ Main command line entry point.
+ """
+ desc = "Highlight an input file and write the result to an output file."
+ parser = argparse.ArgumentParser(description=desc, add_help=False,
+ formatter_class=HelpFormatter)
+
+ operation = parser.add_argument_group('Main operation')
+ lexersel = operation.add_mutually_exclusive_group()
+ lexersel.add_argument(
+ '-l', metavar='LEXER',
+ help='Specify the lexer to use. (Query names with -L.) If not '
+ 'given and -g is not present, the lexer is guessed from the filename.')
+ lexersel.add_argument(
+ '-g', action='store_true',
+ help='Guess the lexer from the file contents, or pass through '
+ 'as plain text if nothing can be guessed.')
+ operation.add_argument(
+ '-F', metavar='FILTER[:options]', action='append',
+ help='Add a filter to the token stream. (Query names with -L.) '
+ 'Filter options are given after a colon if necessary.')
+ operation.add_argument(
+ '-f', metavar='FORMATTER',
+ help='Specify the formatter to use. (Query names with -L.) '
+ 'If not given, the formatter is guessed from the output filename, '
+ 'and defaults to the terminal formatter if the output is to the '
+ 'terminal or an unknown file extension.')
+ operation.add_argument(
+ '-O', metavar='OPTION=value[,OPTION=value,...]', action='append',
+ help='Give options to the lexer and formatter as a comma-separated '
+ 'list of key-value pairs. '
+ 'Example: `-O bg=light,python=cool`.')
+ operation.add_argument(
+ '-P', metavar='OPTION=value', action='append',
+ help='Give a single option to the lexer and formatter - with this '
+ 'you can pass options whose value contains commas and equal signs. '
+ 'Example: `-P "heading=Pygments, the Python highlighter"`.')
+ operation.add_argument(
+ '-o', metavar='OUTPUTFILE',
+ help='Where to write the output. Defaults to standard output.')
+
+ operation.add_argument(
+ 'INPUTFILE', nargs='?',
+ help='Where to read the input. Defaults to standard input.')
+
+ flags = parser.add_argument_group('Operation flags')
+ flags.add_argument(
+ '-v', action='store_true',
+ help='Print a detailed traceback on unhandled exceptions, which '
+ 'is useful for debugging and bug reports.')
+ flags.add_argument(
+ '-s', action='store_true',
+ help='Process lines one at a time until EOF, rather than waiting to '
+ 'process the entire file. This only works for stdin, only for lexers '
+ 'with no line-spanning constructs, and is intended for streaming '
+ 'input such as you get from `tail -f`. '
+ 'Example usage: `tail -f sql.log | pygmentize -s -l sql`.')
+ flags.add_argument(
+ '-x', action='store_true',
+ help='Allow custom lexers and formatters to be loaded from a .py file '
+ 'relative to the current working directory. For example, '
+ '`-l ./customlexer.py -x`. By default, this option expects a file '
+ 'with a class named CustomLexer or CustomFormatter; you can also '
+ 'specify your own class name with a colon (`-l ./lexer.py:MyLexer`). '
+ 'Users should be very careful not to use this option with untrusted '
+ 'files, because it will import and run them.')
+ flags.add_argument('--json', help='Output as JSON. This can '
+ 'be only used in conjunction with -L.',
+ default=False,
+ action='store_true')
+
+ special_modes_group = parser.add_argument_group(
+ 'Special modes - do not do any highlighting')
+ special_modes = special_modes_group.add_mutually_exclusive_group()
+ special_modes.add_argument(
+ '-S', metavar='STYLE -f formatter',
+ help='Print style definitions for STYLE for a formatter '
+ 'given with -f. The argument given by -a is formatter '
+ 'dependent.')
+ special_modes.add_argument(
+ '-L', nargs='*', metavar='WHAT',
+ help='List lexers, formatters, styles or filters -- '
+ 'give additional arguments for the thing(s) you want to list '
+ '(e.g. "styles"), or omit them to list everything.')
+ special_modes.add_argument(
+ '-N', metavar='FILENAME',
+ help='Guess and print out a lexer name based solely on the given '
+ 'filename. Does not take input or highlight anything. If no specific '
+ 'lexer can be determined, "text" is printed.')
+ special_modes.add_argument(
+ '-C', action='store_true',
+ help='Like -N, but print out a lexer name based solely on '
+ 'a given content from standard input.')
+ special_modes.add_argument(
+ '-H', action='store', nargs=2, metavar=('NAME', 'TYPE'),
+ help='Print detailed help for the object <name> of type <type>, '
+ 'where <type> is one of "lexer", "formatter" or "filter".')
+ special_modes.add_argument(
+ '-V', action='store_true',
+ help='Print the package version.')
+ special_modes.add_argument(
+ '-h', '--help', action='store_true',
+ help='Print this help.')
+ special_modes_group.add_argument(
+ '-a', metavar='ARG',
+ help='Formatter-specific additional argument for the -S (print '
+ 'style sheet) mode.')
+
+ argns = parser.parse_args(args[1:])
+
+ try:
+ return main_inner(parser, argns)
+ except Exception:
+ if argns.v:
+ print(file=sys.stderr)
+ print('*' * 65, file=sys.stderr)
+ print('An unhandled exception occurred while highlighting.',
+ file=sys.stderr)
+ print('Please report the whole traceback to the issue tracker at',
+ file=sys.stderr)
+ print('<https://github.com/pygments/pygments/issues>.',
+ file=sys.stderr)
+ print('*' * 65, file=sys.stderr)
+ print(file=sys.stderr)
+ raise
+ import traceback
+ info = traceback.format_exception(*sys.exc_info())
+ msg = info[-1].strip()
+ if len(info) >= 3:
+ # extract relevant file and position info
+ msg += '\n (f%s)' % info[-2].split('\n')[0].strip()[1:]
+ print(file=sys.stderr)
+ print('*** Error while highlighting:', file=sys.stderr)
+ print(msg, file=sys.stderr)
+ print('*** If this is a bug you want to report, please rerun with -v.',
+ file=sys.stderr)
+ return 1
diff --git a/src/pip/_vendor/pygments/console.py b/src/pip/_vendor/pygments/console.py
new file mode 100644
index 000000000..2ada68e03
--- /dev/null
+++ b/src/pip/_vendor/pygments/console.py
@@ -0,0 +1,70 @@
+"""
+ pygments.console
+ ~~~~~~~~~~~~~~~~
+
+ Format colored console output.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+esc = "\x1b["
+
+codes = {}
+codes[""] = ""
+codes["reset"] = esc + "39;49;00m"
+
+codes["bold"] = esc + "01m"
+codes["faint"] = esc + "02m"
+codes["standout"] = esc + "03m"
+codes["underline"] = esc + "04m"
+codes["blink"] = esc + "05m"
+codes["overline"] = esc + "06m"
+
+dark_colors = ["black", "red", "green", "yellow", "blue",
+ "magenta", "cyan", "gray"]
+light_colors = ["brightblack", "brightred", "brightgreen", "brightyellow", "brightblue",
+ "brightmagenta", "brightcyan", "white"]
+
+x = 30
+for d, l in zip(dark_colors, light_colors):
+ codes[d] = esc + "%im" % x
+ codes[l] = esc + "%im" % (60 + x)
+ x += 1
+
+del d, l, x
+
+codes["white"] = codes["bold"]
+
+
+def reset_color():
+ return codes["reset"]
+
+
+def colorize(color_key, text):
+ return codes[color_key] + text + codes["reset"]
+
+
+def ansiformat(attr, text):
+ """
+ Format ``text`` with a color and/or some attributes::
+
+ color normal color
+ *color* bold color
+ _color_ underlined color
+ +color+ blinking color
+ """
+ result = []
+ if attr[:1] == attr[-1:] == '+':
+ result.append(codes['blink'])
+ attr = attr[1:-1]
+ if attr[:1] == attr[-1:] == '*':
+ result.append(codes['bold'])
+ attr = attr[1:-1]
+ if attr[:1] == attr[-1:] == '_':
+ result.append(codes['underline'])
+ attr = attr[1:-1]
+ result.append(codes[attr])
+ result.append(text)
+ result.append(codes['reset'])
+ return ''.join(result)
diff --git a/src/pip/_vendor/pygments/filter.py b/src/pip/_vendor/pygments/filter.py
new file mode 100644
index 000000000..e5c966493
--- /dev/null
+++ b/src/pip/_vendor/pygments/filter.py
@@ -0,0 +1,71 @@
+"""
+ pygments.filter
+ ~~~~~~~~~~~~~~~
+
+ Module that implements the default filter.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+
+def apply_filters(stream, filters, lexer=None):
+ """
+ Use this method to apply an iterable of filters to
+ a stream. If lexer is given it's forwarded to the
+ filter, otherwise the filter receives `None`.
+ """
+ def _apply(filter_, stream):
+ yield from filter_.filter(lexer, stream)
+ for filter_ in filters:
+ stream = _apply(filter_, stream)
+ return stream
+
+
+def simplefilter(f):
+ """
+ Decorator that converts a function into a filter::
+
+ @simplefilter
+ def lowercase(self, lexer, stream, options):
+ for ttype, value in stream:
+ yield ttype, value.lower()
+ """
+ return type(f.__name__, (FunctionFilter,), {
+ '__module__': getattr(f, '__module__'),
+ '__doc__': f.__doc__,
+ 'function': f,
+ })
+
+
+class Filter:
+ """
+ Default filter. Subclass this class or use the `simplefilter`
+ decorator to create own filters.
+ """
+
+ def __init__(self, **options):
+ self.options = options
+
+ def filter(self, lexer, stream):
+ raise NotImplementedError()
+
+
+class FunctionFilter(Filter):
+ """
+ Abstract class used by `simplefilter` to create simple
+ function filters on the fly. The `simplefilter` decorator
+ automatically creates subclasses of this class for
+ functions passed to it.
+ """
+ function = None
+
+ def __init__(self, **options):
+ if not hasattr(self, 'function'):
+ raise TypeError('%r used without bound function' %
+ self.__class__.__name__)
+ Filter.__init__(self, **options)
+
+ def filter(self, lexer, stream):
+ # pylint: disable=not-callable
+ yield from self.function(lexer, stream, self.options)
diff --git a/src/pip/_vendor/pygments/filters/__init__.py b/src/pip/_vendor/pygments/filters/__init__.py
new file mode 100644
index 000000000..5c99ce271
--- /dev/null
+++ b/src/pip/_vendor/pygments/filters/__init__.py
@@ -0,0 +1,937 @@
+"""
+ pygments.filters
+ ~~~~~~~~~~~~~~~~
+
+ Module containing filter lookup functions and default
+ filters.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from pip._vendor.pygments.token import String, Comment, Keyword, Name, Error, Whitespace, \
+ string_to_tokentype
+from pip._vendor.pygments.filter import Filter
+from pip._vendor.pygments.util import get_list_opt, get_int_opt, get_bool_opt, \
+ get_choice_opt, ClassNotFound, OptionError
+from pip._vendor.pygments.plugin import find_plugin_filters
+
+
+def find_filter_class(filtername):
+ """Lookup a filter by name. Return None if not found."""
+ if filtername in FILTERS:
+ return FILTERS[filtername]
+ for name, cls in find_plugin_filters():
+ if name == filtername:
+ return cls
+ return None
+
+
+def get_filter_by_name(filtername, **options):
+ """Return an instantiated filter.
+
+ Options are passed to the filter initializer if wanted.
+ Raise a ClassNotFound if not found.
+ """
+ cls = find_filter_class(filtername)
+ if cls:
+ return cls(**options)
+ else:
+ raise ClassNotFound('filter %r not found' % filtername)
+
+
+def get_all_filters():
+ """Return a generator of all filter names."""
+ yield from FILTERS
+ for name, _ in find_plugin_filters():
+ yield name
+
+
+def _replace_special(ttype, value, regex, specialttype,
+ replacefunc=lambda x: x):
+ last = 0
+ for match in regex.finditer(value):
+ start, end = match.start(), match.end()
+ if start != last:
+ yield ttype, value[last:start]
+ yield specialttype, replacefunc(value[start:end])
+ last = end
+ if last != len(value):
+ yield ttype, value[last:]
+
+
+class CodeTagFilter(Filter):
+ """Highlight special code tags in comments and docstrings.
+
+ Options accepted:
+
+ `codetags` : list of strings
+ A list of strings that are flagged as code tags. The default is to
+ highlight ``XXX``, ``TODO``, ``BUG`` and ``NOTE``.
+ """
+
+ def __init__(self, **options):
+ Filter.__init__(self, **options)
+ tags = get_list_opt(options, 'codetags',
+ ['XXX', 'TODO', 'BUG', 'NOTE'])
+ self.tag_re = re.compile(r'\b(%s)\b' % '|'.join([
+ re.escape(tag) for tag in tags if tag
+ ]))
+
+ def filter(self, lexer, stream):
+ regex = self.tag_re
+ for ttype, value in stream:
+ if ttype in String.Doc or \
+ ttype in Comment and \
+ ttype not in Comment.Preproc:
+ yield from _replace_special(ttype, value, regex, Comment.Special)
+ else:
+ yield ttype, value
+
+
+class SymbolFilter(Filter):
+ """Convert mathematical symbols such as \\<longrightarrow> in Isabelle
+ or \\longrightarrow in LaTeX into Unicode characters.
+
+ This is mostly useful for HTML or console output when you want to
+ approximate the source rendering you'd see in an IDE.
+
+ Options accepted:
+
+ `lang` : string
+ The symbol language. Must be one of ``'isabelle'`` or
+ ``'latex'``. The default is ``'isabelle'``.
+ """
+
+ latex_symbols = {
+ '\\alpha' : '\U000003b1',
+ '\\beta' : '\U000003b2',
+ '\\gamma' : '\U000003b3',
+ '\\delta' : '\U000003b4',
+ '\\varepsilon' : '\U000003b5',
+ '\\zeta' : '\U000003b6',
+ '\\eta' : '\U000003b7',
+ '\\vartheta' : '\U000003b8',
+ '\\iota' : '\U000003b9',
+ '\\kappa' : '\U000003ba',
+ '\\lambda' : '\U000003bb',
+ '\\mu' : '\U000003bc',
+ '\\nu' : '\U000003bd',
+ '\\xi' : '\U000003be',
+ '\\pi' : '\U000003c0',
+ '\\varrho' : '\U000003c1',
+ '\\sigma' : '\U000003c3',
+ '\\tau' : '\U000003c4',
+ '\\upsilon' : '\U000003c5',
+ '\\varphi' : '\U000003c6',
+ '\\chi' : '\U000003c7',
+ '\\psi' : '\U000003c8',
+ '\\omega' : '\U000003c9',
+ '\\Gamma' : '\U00000393',
+ '\\Delta' : '\U00000394',
+ '\\Theta' : '\U00000398',
+ '\\Lambda' : '\U0000039b',
+ '\\Xi' : '\U0000039e',
+ '\\Pi' : '\U000003a0',
+ '\\Sigma' : '\U000003a3',
+ '\\Upsilon' : '\U000003a5',
+ '\\Phi' : '\U000003a6',
+ '\\Psi' : '\U000003a8',
+ '\\Omega' : '\U000003a9',
+ '\\leftarrow' : '\U00002190',
+ '\\longleftarrow' : '\U000027f5',
+ '\\rightarrow' : '\U00002192',
+ '\\longrightarrow' : '\U000027f6',
+ '\\Leftarrow' : '\U000021d0',
+ '\\Longleftarrow' : '\U000027f8',
+ '\\Rightarrow' : '\U000021d2',
+ '\\Longrightarrow' : '\U000027f9',
+ '\\leftrightarrow' : '\U00002194',
+ '\\longleftrightarrow' : '\U000027f7',
+ '\\Leftrightarrow' : '\U000021d4',
+ '\\Longleftrightarrow' : '\U000027fa',
+ '\\mapsto' : '\U000021a6',
+ '\\longmapsto' : '\U000027fc',
+ '\\relbar' : '\U00002500',
+ '\\Relbar' : '\U00002550',
+ '\\hookleftarrow' : '\U000021a9',
+ '\\hookrightarrow' : '\U000021aa',
+ '\\leftharpoondown' : '\U000021bd',
+ '\\rightharpoondown' : '\U000021c1',
+ '\\leftharpoonup' : '\U000021bc',
+ '\\rightharpoonup' : '\U000021c0',
+ '\\rightleftharpoons' : '\U000021cc',
+ '\\leadsto' : '\U0000219d',
+ '\\downharpoonleft' : '\U000021c3',
+ '\\downharpoonright' : '\U000021c2',
+ '\\upharpoonleft' : '\U000021bf',
+ '\\upharpoonright' : '\U000021be',
+ '\\restriction' : '\U000021be',
+ '\\uparrow' : '\U00002191',
+ '\\Uparrow' : '\U000021d1',
+ '\\downarrow' : '\U00002193',
+ '\\Downarrow' : '\U000021d3',
+ '\\updownarrow' : '\U00002195',
+ '\\Updownarrow' : '\U000021d5',
+ '\\langle' : '\U000027e8',
+ '\\rangle' : '\U000027e9',
+ '\\lceil' : '\U00002308',
+ '\\rceil' : '\U00002309',
+ '\\lfloor' : '\U0000230a',
+ '\\rfloor' : '\U0000230b',
+ '\\flqq' : '\U000000ab',
+ '\\frqq' : '\U000000bb',
+ '\\bot' : '\U000022a5',
+ '\\top' : '\U000022a4',
+ '\\wedge' : '\U00002227',
+ '\\bigwedge' : '\U000022c0',
+ '\\vee' : '\U00002228',
+ '\\bigvee' : '\U000022c1',
+ '\\forall' : '\U00002200',
+ '\\exists' : '\U00002203',
+ '\\nexists' : '\U00002204',
+ '\\neg' : '\U000000ac',
+ '\\Box' : '\U000025a1',
+ '\\Diamond' : '\U000025c7',
+ '\\vdash' : '\U000022a2',
+ '\\models' : '\U000022a8',
+ '\\dashv' : '\U000022a3',
+ '\\surd' : '\U0000221a',
+ '\\le' : '\U00002264',
+ '\\ge' : '\U00002265',
+ '\\ll' : '\U0000226a',
+ '\\gg' : '\U0000226b',
+ '\\lesssim' : '\U00002272',
+ '\\gtrsim' : '\U00002273',
+ '\\lessapprox' : '\U00002a85',
+ '\\gtrapprox' : '\U00002a86',
+ '\\in' : '\U00002208',
+ '\\notin' : '\U00002209',
+ '\\subset' : '\U00002282',
+ '\\supset' : '\U00002283',
+ '\\subseteq' : '\U00002286',
+ '\\supseteq' : '\U00002287',
+ '\\sqsubset' : '\U0000228f',
+ '\\sqsupset' : '\U00002290',
+ '\\sqsubseteq' : '\U00002291',
+ '\\sqsupseteq' : '\U00002292',
+ '\\cap' : '\U00002229',
+ '\\bigcap' : '\U000022c2',
+ '\\cup' : '\U0000222a',
+ '\\bigcup' : '\U000022c3',
+ '\\sqcup' : '\U00002294',
+ '\\bigsqcup' : '\U00002a06',
+ '\\sqcap' : '\U00002293',
+ '\\Bigsqcap' : '\U00002a05',
+ '\\setminus' : '\U00002216',
+ '\\propto' : '\U0000221d',
+ '\\uplus' : '\U0000228e',
+ '\\bigplus' : '\U00002a04',
+ '\\sim' : '\U0000223c',
+ '\\doteq' : '\U00002250',
+ '\\simeq' : '\U00002243',
+ '\\approx' : '\U00002248',
+ '\\asymp' : '\U0000224d',
+ '\\cong' : '\U00002245',
+ '\\equiv' : '\U00002261',
+ '\\Join' : '\U000022c8',
+ '\\bowtie' : '\U00002a1d',
+ '\\prec' : '\U0000227a',
+ '\\succ' : '\U0000227b',
+ '\\preceq' : '\U0000227c',
+ '\\succeq' : '\U0000227d',
+ '\\parallel' : '\U00002225',
+ '\\mid' : '\U000000a6',
+ '\\pm' : '\U000000b1',
+ '\\mp' : '\U00002213',
+ '\\times' : '\U000000d7',
+ '\\div' : '\U000000f7',
+ '\\cdot' : '\U000022c5',
+ '\\star' : '\U000022c6',
+ '\\circ' : '\U00002218',
+ '\\dagger' : '\U00002020',
+ '\\ddagger' : '\U00002021',
+ '\\lhd' : '\U000022b2',
+ '\\rhd' : '\U000022b3',
+ '\\unlhd' : '\U000022b4',
+ '\\unrhd' : '\U000022b5',
+ '\\triangleleft' : '\U000025c3',
+ '\\triangleright' : '\U000025b9',
+ '\\triangle' : '\U000025b3',
+ '\\triangleq' : '\U0000225c',
+ '\\oplus' : '\U00002295',
+ '\\bigoplus' : '\U00002a01',
+ '\\otimes' : '\U00002297',
+ '\\bigotimes' : '\U00002a02',
+ '\\odot' : '\U00002299',
+ '\\bigodot' : '\U00002a00',
+ '\\ominus' : '\U00002296',
+ '\\oslash' : '\U00002298',
+ '\\dots' : '\U00002026',
+ '\\cdots' : '\U000022ef',
+ '\\sum' : '\U00002211',
+ '\\prod' : '\U0000220f',
+ '\\coprod' : '\U00002210',
+ '\\infty' : '\U0000221e',
+ '\\int' : '\U0000222b',
+ '\\oint' : '\U0000222e',
+ '\\clubsuit' : '\U00002663',
+ '\\diamondsuit' : '\U00002662',
+ '\\heartsuit' : '\U00002661',
+ '\\spadesuit' : '\U00002660',
+ '\\aleph' : '\U00002135',
+ '\\emptyset' : '\U00002205',
+ '\\nabla' : '\U00002207',
+ '\\partial' : '\U00002202',
+ '\\flat' : '\U0000266d',
+ '\\natural' : '\U0000266e',
+ '\\sharp' : '\U0000266f',
+ '\\angle' : '\U00002220',
+ '\\copyright' : '\U000000a9',
+ '\\textregistered' : '\U000000ae',
+ '\\textonequarter' : '\U000000bc',
+ '\\textonehalf' : '\U000000bd',
+ '\\textthreequarters' : '\U000000be',
+ '\\textordfeminine' : '\U000000aa',
+ '\\textordmasculine' : '\U000000ba',
+ '\\euro' : '\U000020ac',
+ '\\pounds' : '\U000000a3',
+ '\\yen' : '\U000000a5',
+ '\\textcent' : '\U000000a2',
+ '\\textcurrency' : '\U000000a4',
+ '\\textdegree' : '\U000000b0',
+ }
+
+ isabelle_symbols = {
+ '\\<zero>' : '\U0001d7ec',
+ '\\<one>' : '\U0001d7ed',
+ '\\<two>' : '\U0001d7ee',
+ '\\<three>' : '\U0001d7ef',
+ '\\<four>' : '\U0001d7f0',
+ '\\<five>' : '\U0001d7f1',
+ '\\<six>' : '\U0001d7f2',
+ '\\<seven>' : '\U0001d7f3',
+ '\\<eight>' : '\U0001d7f4',
+ '\\<nine>' : '\U0001d7f5',
+ '\\<A>' : '\U0001d49c',
+ '\\<B>' : '\U0000212c',
+ '\\<C>' : '\U0001d49e',
+ '\\<D>' : '\U0001d49f',
+ '\\<E>' : '\U00002130',
+ '\\<F>' : '\U00002131',
+ '\\<G>' : '\U0001d4a2',
+ '\\<H>' : '\U0000210b',
+ '\\<I>' : '\U00002110',
+ '\\<J>' : '\U0001d4a5',
+ '\\<K>' : '\U0001d4a6',
+ '\\<L>' : '\U00002112',
+ '\\<M>' : '\U00002133',
+ '\\<N>' : '\U0001d4a9',
+ '\\<O>' : '\U0001d4aa',
+ '\\<P>' : '\U0001d4ab',
+ '\\<Q>' : '\U0001d4ac',
+ '\\<R>' : '\U0000211b',
+ '\\<S>' : '\U0001d4ae',
+ '\\<T>' : '\U0001d4af',
+ '\\<U>' : '\U0001d4b0',
+ '\\<V>' : '\U0001d4b1',
+ '\\<W>' : '\U0001d4b2',
+ '\\<X>' : '\U0001d4b3',
+ '\\<Y>' : '\U0001d4b4',
+ '\\<Z>' : '\U0001d4b5',
+ '\\<a>' : '\U0001d5ba',
+ '\\<b>' : '\U0001d5bb',
+ '\\<c>' : '\U0001d5bc',
+ '\\<d>' : '\U0001d5bd',
+ '\\<e>' : '\U0001d5be',
+ '\\<f>' : '\U0001d5bf',
+ '\\<g>' : '\U0001d5c0',
+ '\\<h>' : '\U0001d5c1',
+ '\\<i>' : '\U0001d5c2',
+ '\\<j>' : '\U0001d5c3',
+ '\\<k>' : '\U0001d5c4',
+ '\\<l>' : '\U0001d5c5',
+ '\\<m>' : '\U0001d5c6',
+ '\\<n>' : '\U0001d5c7',
+ '\\<o>' : '\U0001d5c8',
+ '\\<p>' : '\U0001d5c9',
+ '\\<q>' : '\U0001d5ca',
+ '\\<r>' : '\U0001d5cb',
+ '\\<s>' : '\U0001d5cc',
+ '\\<t>' : '\U0001d5cd',
+ '\\<u>' : '\U0001d5ce',
+ '\\<v>' : '\U0001d5cf',
+ '\\<w>' : '\U0001d5d0',
+ '\\<x>' : '\U0001d5d1',
+ '\\<y>' : '\U0001d5d2',
+ '\\<z>' : '\U0001d5d3',
+ '\\<AA>' : '\U0001d504',
+ '\\<BB>' : '\U0001d505',
+ '\\<CC>' : '\U0000212d',
+ '\\<DD>' : '\U0001d507',
+ '\\<EE>' : '\U0001d508',
+ '\\<FF>' : '\U0001d509',
+ '\\<GG>' : '\U0001d50a',
+ '\\<HH>' : '\U0000210c',
+ '\\<II>' : '\U00002111',
+ '\\<JJ>' : '\U0001d50d',
+ '\\<KK>' : '\U0001d50e',
+ '\\<LL>' : '\U0001d50f',
+ '\\<MM>' : '\U0001d510',
+ '\\<NN>' : '\U0001d511',
+ '\\<OO>' : '\U0001d512',
+ '\\<PP>' : '\U0001d513',
+ '\\<QQ>' : '\U0001d514',
+ '\\<RR>' : '\U0000211c',
+ '\\<SS>' : '\U0001d516',
+ '\\<TT>' : '\U0001d517',
+ '\\<UU>' : '\U0001d518',
+ '\\<VV>' : '\U0001d519',
+ '\\<WW>' : '\U0001d51a',
+ '\\<XX>' : '\U0001d51b',
+ '\\<YY>' : '\U0001d51c',
+ '\\<ZZ>' : '\U00002128',
+ '\\<aa>' : '\U0001d51e',
+ '\\<bb>' : '\U0001d51f',
+ '\\<cc>' : '\U0001d520',
+ '\\<dd>' : '\U0001d521',
+ '\\<ee>' : '\U0001d522',
+ '\\<ff>' : '\U0001d523',
+ '\\<gg>' : '\U0001d524',
+ '\\<hh>' : '\U0001d525',
+ '\\<ii>' : '\U0001d526',
+ '\\<jj>' : '\U0001d527',
+ '\\<kk>' : '\U0001d528',
+ '\\<ll>' : '\U0001d529',
+ '\\<mm>' : '\U0001d52a',
+ '\\<nn>' : '\U0001d52b',
+ '\\<oo>' : '\U0001d52c',
+ '\\<pp>' : '\U0001d52d',
+ '\\<qq>' : '\U0001d52e',
+ '\\<rr>' : '\U0001d52f',
+ '\\<ss>' : '\U0001d530',
+ '\\<tt>' : '\U0001d531',
+ '\\<uu>' : '\U0001d532',
+ '\\<vv>' : '\U0001d533',
+ '\\<ww>' : '\U0001d534',
+ '\\<xx>' : '\U0001d535',
+ '\\<yy>' : '\U0001d536',
+ '\\<zz>' : '\U0001d537',
+ '\\<alpha>' : '\U000003b1',
+ '\\<beta>' : '\U000003b2',
+ '\\<gamma>' : '\U000003b3',
+ '\\<delta>' : '\U000003b4',
+ '\\<epsilon>' : '\U000003b5',
+ '\\<zeta>' : '\U000003b6',
+ '\\<eta>' : '\U000003b7',
+ '\\<theta>' : '\U000003b8',
+ '\\<iota>' : '\U000003b9',
+ '\\<kappa>' : '\U000003ba',
+ '\\<lambda>' : '\U000003bb',
+ '\\<mu>' : '\U000003bc',
+ '\\<nu>' : '\U000003bd',
+ '\\<xi>' : '\U000003be',
+ '\\<pi>' : '\U000003c0',
+ '\\<rho>' : '\U000003c1',
+ '\\<sigma>' : '\U000003c3',
+ '\\<tau>' : '\U000003c4',
+ '\\<upsilon>' : '\U000003c5',
+ '\\<phi>' : '\U000003c6',
+ '\\<chi>' : '\U000003c7',
+ '\\<psi>' : '\U000003c8',
+ '\\<omega>' : '\U000003c9',
+ '\\<Gamma>' : '\U00000393',
+ '\\<Delta>' : '\U00000394',
+ '\\<Theta>' : '\U00000398',
+ '\\<Lambda>' : '\U0000039b',
+ '\\<Xi>' : '\U0000039e',
+ '\\<Pi>' : '\U000003a0',
+ '\\<Sigma>' : '\U000003a3',
+ '\\<Upsilon>' : '\U000003a5',
+ '\\<Phi>' : '\U000003a6',
+ '\\<Psi>' : '\U000003a8',
+ '\\<Omega>' : '\U000003a9',
+ '\\<bool>' : '\U0001d539',
+ '\\<complex>' : '\U00002102',
+ '\\<nat>' : '\U00002115',
+ '\\<rat>' : '\U0000211a',
+ '\\<real>' : '\U0000211d',
+ '\\<int>' : '\U00002124',
+ '\\<leftarrow>' : '\U00002190',
+ '\\<longleftarrow>' : '\U000027f5',
+ '\\<rightarrow>' : '\U00002192',
+ '\\<longrightarrow>' : '\U000027f6',
+ '\\<Leftarrow>' : '\U000021d0',
+ '\\<Longleftarrow>' : '\U000027f8',
+ '\\<Rightarrow>' : '\U000021d2',
+ '\\<Longrightarrow>' : '\U000027f9',
+ '\\<leftrightarrow>' : '\U00002194',
+ '\\<longleftrightarrow>' : '\U000027f7',
+ '\\<Leftrightarrow>' : '\U000021d4',
+ '\\<Longleftrightarrow>' : '\U000027fa',
+ '\\<mapsto>' : '\U000021a6',
+ '\\<longmapsto>' : '\U000027fc',
+ '\\<midarrow>' : '\U00002500',
+ '\\<Midarrow>' : '\U00002550',
+ '\\<hookleftarrow>' : '\U000021a9',
+ '\\<hookrightarrow>' : '\U000021aa',
+ '\\<leftharpoondown>' : '\U000021bd',
+ '\\<rightharpoondown>' : '\U000021c1',
+ '\\<leftharpoonup>' : '\U000021bc',
+ '\\<rightharpoonup>' : '\U000021c0',
+ '\\<rightleftharpoons>' : '\U000021cc',
+ '\\<leadsto>' : '\U0000219d',
+ '\\<downharpoonleft>' : '\U000021c3',
+ '\\<downharpoonright>' : '\U000021c2',
+ '\\<upharpoonleft>' : '\U000021bf',
+ '\\<upharpoonright>' : '\U000021be',
+ '\\<restriction>' : '\U000021be',
+ '\\<Colon>' : '\U00002237',
+ '\\<up>' : '\U00002191',
+ '\\<Up>' : '\U000021d1',
+ '\\<down>' : '\U00002193',
+ '\\<Down>' : '\U000021d3',
+ '\\<updown>' : '\U00002195',
+ '\\<Updown>' : '\U000021d5',
+ '\\<langle>' : '\U000027e8',
+ '\\<rangle>' : '\U000027e9',
+ '\\<lceil>' : '\U00002308',
+ '\\<rceil>' : '\U00002309',
+ '\\<lfloor>' : '\U0000230a',
+ '\\<rfloor>' : '\U0000230b',
+ '\\<lparr>' : '\U00002987',
+ '\\<rparr>' : '\U00002988',
+ '\\<lbrakk>' : '\U000027e6',
+ '\\<rbrakk>' : '\U000027e7',
+ '\\<lbrace>' : '\U00002983',
+ '\\<rbrace>' : '\U00002984',
+ '\\<guillemotleft>' : '\U000000ab',
+ '\\<guillemotright>' : '\U000000bb',
+ '\\<bottom>' : '\U000022a5',
+ '\\<top>' : '\U000022a4',
+ '\\<and>' : '\U00002227',
+ '\\<And>' : '\U000022c0',
+ '\\<or>' : '\U00002228',
+ '\\<Or>' : '\U000022c1',
+ '\\<forall>' : '\U00002200',
+ '\\<exists>' : '\U00002203',
+ '\\<nexists>' : '\U00002204',
+ '\\<not>' : '\U000000ac',
+ '\\<box>' : '\U000025a1',
+ '\\<diamond>' : '\U000025c7',
+ '\\<turnstile>' : '\U000022a2',
+ '\\<Turnstile>' : '\U000022a8',
+ '\\<tturnstile>' : '\U000022a9',
+ '\\<TTurnstile>' : '\U000022ab',
+ '\\<stileturn>' : '\U000022a3',
+ '\\<surd>' : '\U0000221a',
+ '\\<le>' : '\U00002264',
+ '\\<ge>' : '\U00002265',
+ '\\<lless>' : '\U0000226a',
+ '\\<ggreater>' : '\U0000226b',
+ '\\<lesssim>' : '\U00002272',
+ '\\<greatersim>' : '\U00002273',
+ '\\<lessapprox>' : '\U00002a85',
+ '\\<greaterapprox>' : '\U00002a86',
+ '\\<in>' : '\U00002208',
+ '\\<notin>' : '\U00002209',
+ '\\<subset>' : '\U00002282',
+ '\\<supset>' : '\U00002283',
+ '\\<subseteq>' : '\U00002286',
+ '\\<supseteq>' : '\U00002287',
+ '\\<sqsubset>' : '\U0000228f',
+ '\\<sqsupset>' : '\U00002290',
+ '\\<sqsubseteq>' : '\U00002291',
+ '\\<sqsupseteq>' : '\U00002292',
+ '\\<inter>' : '\U00002229',
+ '\\<Inter>' : '\U000022c2',
+ '\\<union>' : '\U0000222a',
+ '\\<Union>' : '\U000022c3',
+ '\\<squnion>' : '\U00002294',
+ '\\<Squnion>' : '\U00002a06',
+ '\\<sqinter>' : '\U00002293',
+ '\\<Sqinter>' : '\U00002a05',
+ '\\<setminus>' : '\U00002216',
+ '\\<propto>' : '\U0000221d',
+ '\\<uplus>' : '\U0000228e',
+ '\\<Uplus>' : '\U00002a04',
+ '\\<noteq>' : '\U00002260',
+ '\\<sim>' : '\U0000223c',
+ '\\<doteq>' : '\U00002250',
+ '\\<simeq>' : '\U00002243',
+ '\\<approx>' : '\U00002248',
+ '\\<asymp>' : '\U0000224d',
+ '\\<cong>' : '\U00002245',
+ '\\<smile>' : '\U00002323',
+ '\\<equiv>' : '\U00002261',
+ '\\<frown>' : '\U00002322',
+ '\\<Join>' : '\U000022c8',
+ '\\<bowtie>' : '\U00002a1d',
+ '\\<prec>' : '\U0000227a',
+ '\\<succ>' : '\U0000227b',
+ '\\<preceq>' : '\U0000227c',
+ '\\<succeq>' : '\U0000227d',
+ '\\<parallel>' : '\U00002225',
+ '\\<bar>' : '\U000000a6',
+ '\\<plusminus>' : '\U000000b1',
+ '\\<minusplus>' : '\U00002213',
+ '\\<times>' : '\U000000d7',
+ '\\<div>' : '\U000000f7',
+ '\\<cdot>' : '\U000022c5',
+ '\\<star>' : '\U000022c6',
+ '\\<bullet>' : '\U00002219',
+ '\\<circ>' : '\U00002218',
+ '\\<dagger>' : '\U00002020',
+ '\\<ddagger>' : '\U00002021',
+ '\\<lhd>' : '\U000022b2',
+ '\\<rhd>' : '\U000022b3',
+ '\\<unlhd>' : '\U000022b4',
+ '\\<unrhd>' : '\U000022b5',
+ '\\<triangleleft>' : '\U000025c3',
+ '\\<triangleright>' : '\U000025b9',
+ '\\<triangle>' : '\U000025b3',
+ '\\<triangleq>' : '\U0000225c',
+ '\\<oplus>' : '\U00002295',
+ '\\<Oplus>' : '\U00002a01',
+ '\\<otimes>' : '\U00002297',
+ '\\<Otimes>' : '\U00002a02',
+ '\\<odot>' : '\U00002299',
+ '\\<Odot>' : '\U00002a00',
+ '\\<ominus>' : '\U00002296',
+ '\\<oslash>' : '\U00002298',
+ '\\<dots>' : '\U00002026',
+ '\\<cdots>' : '\U000022ef',
+ '\\<Sum>' : '\U00002211',
+ '\\<Prod>' : '\U0000220f',
+ '\\<Coprod>' : '\U00002210',
+ '\\<infinity>' : '\U0000221e',
+ '\\<integral>' : '\U0000222b',
+ '\\<ointegral>' : '\U0000222e',
+ '\\<clubsuit>' : '\U00002663',
+ '\\<diamondsuit>' : '\U00002662',
+ '\\<heartsuit>' : '\U00002661',
+ '\\<spadesuit>' : '\U00002660',
+ '\\<aleph>' : '\U00002135',
+ '\\<emptyset>' : '\U00002205',
+ '\\<nabla>' : '\U00002207',
+ '\\<partial>' : '\U00002202',
+ '\\<flat>' : '\U0000266d',
+ '\\<natural>' : '\U0000266e',
+ '\\<sharp>' : '\U0000266f',
+ '\\<angle>' : '\U00002220',
+ '\\<copyright>' : '\U000000a9',
+ '\\<registered>' : '\U000000ae',
+ '\\<hyphen>' : '\U000000ad',
+ '\\<inverse>' : '\U000000af',
+ '\\<onequarter>' : '\U000000bc',
+ '\\<onehalf>' : '\U000000bd',
+ '\\<threequarters>' : '\U000000be',
+ '\\<ordfeminine>' : '\U000000aa',
+ '\\<ordmasculine>' : '\U000000ba',
+ '\\<section>' : '\U000000a7',
+ '\\<paragraph>' : '\U000000b6',
+ '\\<exclamdown>' : '\U000000a1',
+ '\\<questiondown>' : '\U000000bf',
+ '\\<euro>' : '\U000020ac',
+ '\\<pounds>' : '\U000000a3',
+ '\\<yen>' : '\U000000a5',
+ '\\<cent>' : '\U000000a2',
+ '\\<currency>' : '\U000000a4',
+ '\\<degree>' : '\U000000b0',
+ '\\<amalg>' : '\U00002a3f',
+ '\\<mho>' : '\U00002127',
+ '\\<lozenge>' : '\U000025ca',
+ '\\<wp>' : '\U00002118',
+ '\\<wrong>' : '\U00002240',
+ '\\<struct>' : '\U000022c4',
+ '\\<acute>' : '\U000000b4',
+ '\\<index>' : '\U00000131',
+ '\\<dieresis>' : '\U000000a8',
+ '\\<cedilla>' : '\U000000b8',
+ '\\<hungarumlaut>' : '\U000002dd',
+ '\\<some>' : '\U000003f5',
+ '\\<newline>' : '\U000023ce',
+ '\\<open>' : '\U00002039',
+ '\\<close>' : '\U0000203a',
+ '\\<here>' : '\U00002302',
+ '\\<^sub>' : '\U000021e9',
+ '\\<^sup>' : '\U000021e7',
+ '\\<^bold>' : '\U00002759',
+ '\\<^bsub>' : '\U000021d8',
+ '\\<^esub>' : '\U000021d9',
+ '\\<^bsup>' : '\U000021d7',
+ '\\<^esup>' : '\U000021d6',
+ }
+
+ lang_map = {'isabelle' : isabelle_symbols, 'latex' : latex_symbols}
+
+ def __init__(self, **options):
+ Filter.__init__(self, **options)
+ lang = get_choice_opt(options, 'lang',
+ ['isabelle', 'latex'], 'isabelle')
+ self.symbols = self.lang_map[lang]
+
+ def filter(self, lexer, stream):
+ for ttype, value in stream:
+ if value in self.symbols:
+ yield ttype, self.symbols[value]
+ else:
+ yield ttype, value
+
+
+class KeywordCaseFilter(Filter):
+ """Convert keywords to lowercase or uppercase or capitalize them, which
+ means first letter uppercase, rest lowercase.
+
+ This can be useful e.g. if you highlight Pascal code and want to adapt the
+ code to your styleguide.
+
+ Options accepted:
+
+ `case` : string
+ The casing to convert keywords to. Must be one of ``'lower'``,
+ ``'upper'`` or ``'capitalize'``. The default is ``'lower'``.
+ """
+
+ def __init__(self, **options):
+ Filter.__init__(self, **options)
+ case = get_choice_opt(options, 'case',
+ ['lower', 'upper', 'capitalize'], 'lower')
+ self.convert = getattr(str, case)
+
+ def filter(self, lexer, stream):
+ for ttype, value in stream:
+ if ttype in Keyword:
+ yield ttype, self.convert(value)
+ else:
+ yield ttype, value
+
+
+class NameHighlightFilter(Filter):
+ """Highlight a normal Name (and Name.*) token with a different token type.
+
+ Example::
+
+ filter = NameHighlightFilter(
+ names=['foo', 'bar', 'baz'],
+ tokentype=Name.Function,
+ )
+
+ This would highlight the names "foo", "bar" and "baz"
+ as functions. `Name.Function` is the default token type.
+
+ Options accepted:
+
+ `names` : list of strings
+ A list of names that should be given the different token type.
+ There is no default.
+ `tokentype` : TokenType or string
+ A token type or a string containing a token type name that is
+ used for highlighting the strings in `names`. The default is
+ `Name.Function`.
+ """
+
+ def __init__(self, **options):
+ Filter.__init__(self, **options)
+ self.names = set(get_list_opt(options, 'names', []))
+ tokentype = options.get('tokentype')
+ if tokentype:
+ self.tokentype = string_to_tokentype(tokentype)
+ else:
+ self.tokentype = Name.Function
+
+ def filter(self, lexer, stream):
+ for ttype, value in stream:
+ if ttype in Name and value in self.names:
+ yield self.tokentype, value
+ else:
+ yield ttype, value
+
+
+class ErrorToken(Exception):
+ pass
+
+
+class RaiseOnErrorTokenFilter(Filter):
+ """Raise an exception when the lexer generates an error token.
+
+ Options accepted:
+
+ `excclass` : Exception class
+ The exception class to raise.
+ The default is `pygments.filters.ErrorToken`.
+
+ .. versionadded:: 0.8
+ """
+
+ def __init__(self, **options):
+ Filter.__init__(self, **options)
+ self.exception = options.get('excclass', ErrorToken)
+ try:
+ # issubclass() will raise TypeError if first argument is not a class
+ if not issubclass(self.exception, Exception):
+ raise TypeError
+ except TypeError:
+ raise OptionError('excclass option is not an exception class')
+
+ def filter(self, lexer, stream):
+ for ttype, value in stream:
+ if ttype is Error:
+ raise self.exception(value)
+ yield ttype, value
+
+
+class VisibleWhitespaceFilter(Filter):
+ """Convert tabs, newlines and/or spaces to visible characters.
+
+ Options accepted:
+
+ `spaces` : string or bool
+ If this is a one-character string, spaces will be replaces by this string.
+ If it is another true value, spaces will be replaced by ``·`` (unicode
+ MIDDLE DOT). If it is a false value, spaces will not be replaced. The
+ default is ``False``.
+ `tabs` : string or bool
+ The same as for `spaces`, but the default replacement character is ``»``
+ (unicode RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK). The default value
+ is ``False``. Note: this will not work if the `tabsize` option for the
+ lexer is nonzero, as tabs will already have been expanded then.
+ `tabsize` : int
+ If tabs are to be replaced by this filter (see the `tabs` option), this
+ is the total number of characters that a tab should be expanded to.
+ The default is ``8``.
+ `newlines` : string or bool
+ The same as for `spaces`, but the default replacement character is ``¶``
+ (unicode PILCROW SIGN). The default value is ``False``.
+ `wstokentype` : bool
+ If true, give whitespace the special `Whitespace` token type. This allows
+ styling the visible whitespace differently (e.g. greyed out), but it can
+ disrupt background colors. The default is ``True``.
+
+ .. versionadded:: 0.8
+ """
+
+ def __init__(self, **options):
+ Filter.__init__(self, **options)
+ for name, default in [('spaces', '·'),
+ ('tabs', '»'),
+ ('newlines', '¶')]:
+ opt = options.get(name, False)
+ if isinstance(opt, str) and len(opt) == 1:
+ setattr(self, name, opt)
+ else:
+ setattr(self, name, (opt and default or ''))
+ tabsize = get_int_opt(options, 'tabsize', 8)
+ if self.tabs:
+ self.tabs += ' ' * (tabsize - 1)
+ if self.newlines:
+ self.newlines += '\n'
+ self.wstt = get_bool_opt(options, 'wstokentype', True)
+
+ def filter(self, lexer, stream):
+ if self.wstt:
+ spaces = self.spaces or ' '
+ tabs = self.tabs or '\t'
+ newlines = self.newlines or '\n'
+ regex = re.compile(r'\s')
+
+ def replacefunc(wschar):
+ if wschar == ' ':
+ return spaces
+ elif wschar == '\t':
+ return tabs
+ elif wschar == '\n':
+ return newlines
+ return wschar
+
+ for ttype, value in stream:
+ yield from _replace_special(ttype, value, regex, Whitespace,
+ replacefunc)
+ else:
+ spaces, tabs, newlines = self.spaces, self.tabs, self.newlines
+ # simpler processing
+ for ttype, value in stream:
+ if spaces:
+ value = value.replace(' ', spaces)
+ if tabs:
+ value = value.replace('\t', tabs)
+ if newlines:
+ value = value.replace('\n', newlines)
+ yield ttype, value
+
+
+class GobbleFilter(Filter):
+ """Gobbles source code lines (eats initial characters).
+
+ This filter drops the first ``n`` characters off every line of code. This
+ may be useful when the source code fed to the lexer is indented by a fixed
+ amount of space that isn't desired in the output.
+
+ Options accepted:
+
+ `n` : int
+ The number of characters to gobble.
+
+ .. versionadded:: 1.2
+ """
+ def __init__(self, **options):
+ Filter.__init__(self, **options)
+ self.n = get_int_opt(options, 'n', 0)
+
+ def gobble(self, value, left):
+ if left < len(value):
+ return value[left:], 0
+ else:
+ return '', left - len(value)
+
+ def filter(self, lexer, stream):
+ n = self.n
+ left = n # How many characters left to gobble.
+ for ttype, value in stream:
+ # Remove ``left`` tokens from first line, ``n`` from all others.
+ parts = value.split('\n')
+ (parts[0], left) = self.gobble(parts[0], left)
+ for i in range(1, len(parts)):
+ (parts[i], left) = self.gobble(parts[i], n)
+ value = '\n'.join(parts)
+
+ if value != '':
+ yield ttype, value
+
+
+class TokenMergeFilter(Filter):
+ """Merges consecutive tokens with the same token type in the output
+ stream of a lexer.
+
+ .. versionadded:: 1.2
+ """
+ def __init__(self, **options):
+ Filter.__init__(self, **options)
+
+ def filter(self, lexer, stream):
+ current_type = None
+ current_value = None
+ for ttype, value in stream:
+ if ttype is current_type:
+ current_value += value
+ else:
+ if current_type is not None:
+ yield current_type, current_value
+ current_type = ttype
+ current_value = value
+ if current_type is not None:
+ yield current_type, current_value
+
+
+FILTERS = {
+ 'codetagify': CodeTagFilter,
+ 'keywordcase': KeywordCaseFilter,
+ 'highlight': NameHighlightFilter,
+ 'raiseonerror': RaiseOnErrorTokenFilter,
+ 'whitespace': VisibleWhitespaceFilter,
+ 'gobble': GobbleFilter,
+ 'tokenmerge': TokenMergeFilter,
+ 'symbols': SymbolFilter,
+}
diff --git a/src/pip/_vendor/pygments/formatter.py b/src/pip/_vendor/pygments/formatter.py
new file mode 100644
index 000000000..a2349ef86
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatter.py
@@ -0,0 +1,94 @@
+"""
+ pygments.formatter
+ ~~~~~~~~~~~~~~~~~~
+
+ Base formatter class.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import codecs
+
+from pip._vendor.pygments.util import get_bool_opt
+from pip._vendor.pygments.styles import get_style_by_name
+
+__all__ = ['Formatter']
+
+
+def _lookup_style(style):
+ if isinstance(style, str):
+ return get_style_by_name(style)
+ return style
+
+
+class Formatter:
+ """
+ Converts a token stream to text.
+
+ Options accepted:
+
+ ``style``
+ The style to use, can be a string or a Style subclass
+ (default: "default"). Not used by e.g. the
+ TerminalFormatter.
+ ``full``
+ Tells the formatter to output a "full" document, i.e.
+ a complete self-contained document. This doesn't have
+ any effect for some formatters (default: false).
+ ``title``
+ If ``full`` is true, the title that should be used to
+ caption the document (default: '').
+ ``encoding``
+ If given, must be an encoding name. This will be used to
+ convert the Unicode token strings to byte strings in the
+ output. If it is "" or None, Unicode strings will be written
+ to the output file, which most file-like objects do not
+ support (default: None).
+ ``outencoding``
+ Overrides ``encoding`` if given.
+ """
+
+ #: Name of the formatter
+ name = None
+
+ #: Shortcuts for the formatter
+ aliases = []
+
+ #: fn match rules
+ filenames = []
+
+ #: If True, this formatter outputs Unicode strings when no encoding
+ #: option is given.
+ unicodeoutput = True
+
+ def __init__(self, **options):
+ self.style = _lookup_style(options.get('style', 'default'))
+ self.full = get_bool_opt(options, 'full', False)
+ self.title = options.get('title', '')
+ self.encoding = options.get('encoding', None) or None
+ if self.encoding in ('guess', 'chardet'):
+ # can happen for e.g. pygmentize -O encoding=guess
+ self.encoding = 'utf-8'
+ self.encoding = options.get('outencoding') or self.encoding
+ self.options = options
+
+ def get_style_defs(self, arg=''):
+ """
+ Return the style definitions for the current style as a string.
+
+ ``arg`` is an additional argument whose meaning depends on the
+ formatter used. Note that ``arg`` can also be a list or tuple
+ for some formatters like the html formatter.
+ """
+ return ''
+
+ def format(self, tokensource, outfile):
+ """
+ Format ``tokensource``, an iterable of ``(tokentype, tokenstring)``
+ tuples and write it into ``outfile``.
+ """
+ if self.encoding:
+ # wrap the outfile in a StreamWriter
+ outfile = codecs.lookup(self.encoding)[3](outfile)
+ return self.format_unencoded(tokensource, outfile)
diff --git a/src/pip/_vendor/pygments/formatters/__init__.py b/src/pip/_vendor/pygments/formatters/__init__.py
new file mode 100644
index 000000000..7023aae4a
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/__init__.py
@@ -0,0 +1,153 @@
+"""
+ pygments.formatters
+ ~~~~~~~~~~~~~~~~~~~
+
+ Pygments formatters.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import re
+import sys
+import types
+import fnmatch
+from os.path import basename
+
+from pip._vendor.pygments.formatters._mapping import FORMATTERS
+from pip._vendor.pygments.plugin import find_plugin_formatters
+from pip._vendor.pygments.util import ClassNotFound
+
+__all__ = ['get_formatter_by_name', 'get_formatter_for_filename',
+ 'get_all_formatters', 'load_formatter_from_file'] + list(FORMATTERS)
+
+_formatter_cache = {} # classes by name
+_pattern_cache = {}
+
+
+def _fn_matches(fn, glob):
+ """Return whether the supplied file name fn matches pattern filename."""
+ if glob not in _pattern_cache:
+ pattern = _pattern_cache[glob] = re.compile(fnmatch.translate(glob))
+ return pattern.match(fn)
+ return _pattern_cache[glob].match(fn)
+
+
+def _load_formatters(module_name):
+ """Load a formatter (and all others in the module too)."""
+ mod = __import__(module_name, None, None, ['__all__'])
+ for formatter_name in mod.__all__:
+ cls = getattr(mod, formatter_name)
+ _formatter_cache[cls.name] = cls
+
+
+def get_all_formatters():
+ """Return a generator for all formatter classes."""
+ # NB: this returns formatter classes, not info like get_all_lexers().
+ for info in FORMATTERS.values():
+ if info[1] not in _formatter_cache:
+ _load_formatters(info[0])
+ yield _formatter_cache[info[1]]
+ for _, formatter in find_plugin_formatters():
+ yield formatter
+
+
+def find_formatter_class(alias):
+ """Lookup a formatter by alias.
+
+ Returns None if not found.
+ """
+ for module_name, name, aliases, _, _ in FORMATTERS.values():
+ if alias in aliases:
+ if name not in _formatter_cache:
+ _load_formatters(module_name)
+ return _formatter_cache[name]
+ for _, cls in find_plugin_formatters():
+ if alias in cls.aliases:
+ return cls
+
+
+def get_formatter_by_name(_alias, **options):
+ """Lookup and instantiate a formatter by alias.
+
+ Raises ClassNotFound if not found.
+ """
+ cls = find_formatter_class(_alias)
+ if cls is None:
+ raise ClassNotFound("no formatter found for name %r" % _alias)
+ return cls(**options)
+
+
+def load_formatter_from_file(filename, formattername="CustomFormatter",
+ **options):
+ """Load a formatter from a file.
+
+ This method expects a file located relative to the current working
+ directory, which contains a class named CustomFormatter. By default,
+ it expects the Formatter to be named CustomFormatter; you can specify
+ your own class name as the second argument to this function.
+
+ Users should be very careful with the input, because this method
+ is equivalent to running eval on the input file.
+
+ Raises ClassNotFound if there are any problems importing the Formatter.
+
+ .. versionadded:: 2.2
+ """
+ try:
+ # This empty dict will contain the namespace for the exec'd file
+ custom_namespace = {}
+ with open(filename, 'rb') as f:
+ exec(f.read(), custom_namespace)
+ # Retrieve the class `formattername` from that namespace
+ if formattername not in custom_namespace:
+ raise ClassNotFound('no valid %s class found in %s' %
+ (formattername, filename))
+ formatter_class = custom_namespace[formattername]
+ # And finally instantiate it with the options
+ return formatter_class(**options)
+ except OSError as err:
+ raise ClassNotFound('cannot read %s: %s' % (filename, err))
+ except ClassNotFound:
+ raise
+ except Exception as err:
+ raise ClassNotFound('error when loading custom formatter: %s' % err)
+
+
+def get_formatter_for_filename(fn, **options):
+ """Lookup and instantiate a formatter by filename pattern.
+
+ Raises ClassNotFound if not found.
+ """
+ fn = basename(fn)
+ for modname, name, _, filenames, _ in FORMATTERS.values():
+ for filename in filenames:
+ if _fn_matches(fn, filename):
+ if name not in _formatter_cache:
+ _load_formatters(modname)
+ return _formatter_cache[name](**options)
+ for cls in find_plugin_formatters():
+ for filename in cls.filenames:
+ if _fn_matches(fn, filename):
+ return cls(**options)
+ raise ClassNotFound("no formatter found for file name %r" % fn)
+
+
+class _automodule(types.ModuleType):
+ """Automatically import formatters."""
+
+ def __getattr__(self, name):
+ info = FORMATTERS.get(name)
+ if info:
+ _load_formatters(info[0])
+ cls = _formatter_cache[info[1]]
+ setattr(self, name, cls)
+ return cls
+ raise AttributeError(name)
+
+
+oldmod = sys.modules[__name__]
+newmod = _automodule(__name__)
+newmod.__dict__.update(oldmod.__dict__)
+sys.modules[__name__] = newmod
+del newmod.newmod, newmod.oldmod, newmod.sys, newmod.types
diff --git a/src/pip/_vendor/pygments/formatters/_mapping.py b/src/pip/_vendor/pygments/formatters/_mapping.py
new file mode 100644
index 000000000..db1a8d17a
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/_mapping.py
@@ -0,0 +1,84 @@
+"""
+ pygments.formatters._mapping
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Formatter mapping definitions. This file is generated by itself. Every time
+ you change something on a builtin formatter definition, run this script from
+ the formatters folder to update it.
+
+ Do not alter the FORMATTERS dictionary by hand.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+FORMATTERS = {
+ 'BBCodeFormatter': ('pygments.formatters.bbcode', 'BBCode', ('bbcode', 'bb'), (), 'Format tokens with BBcodes. These formatting codes are used by many bulletin boards, so you can highlight your sourcecode with pygments before posting it there.'),
+ 'BmpImageFormatter': ('pygments.formatters.img', 'img_bmp', ('bmp', 'bitmap'), ('*.bmp',), 'Create a bitmap image from source code. This uses the Python Imaging Library to generate a pixmap from the source code.'),
+ 'GifImageFormatter': ('pygments.formatters.img', 'img_gif', ('gif',), ('*.gif',), 'Create a GIF image from source code. This uses the Python Imaging Library to generate a pixmap from the source code.'),
+ 'GroffFormatter': ('pygments.formatters.groff', 'groff', ('groff', 'troff', 'roff'), (), 'Format tokens with groff escapes to change their color and font style.'),
+ 'HtmlFormatter': ('pygments.formatters.html', 'HTML', ('html',), ('*.html', '*.htm'), "Format tokens as HTML 4 ``<span>`` tags within a ``<pre>`` tag, wrapped in a ``<div>`` tag. The ``<div>``'s CSS class can be set by the `cssclass` option."),
+ 'IRCFormatter': ('pygments.formatters.irc', 'IRC', ('irc', 'IRC'), (), 'Format tokens with IRC color sequences'),
+ 'ImageFormatter': ('pygments.formatters.img', 'img', ('img', 'IMG', 'png'), ('*.png',), 'Create a PNG image from source code. This uses the Python Imaging Library to generate a pixmap from the source code.'),
+ 'JpgImageFormatter': ('pygments.formatters.img', 'img_jpg', ('jpg', 'jpeg'), ('*.jpg',), 'Create a JPEG image from source code. This uses the Python Imaging Library to generate a pixmap from the source code.'),
+ 'LatexFormatter': ('pygments.formatters.latex', 'LaTeX', ('latex', 'tex'), ('*.tex',), 'Format tokens as LaTeX code. This needs the `fancyvrb` and `color` standard packages.'),
+ 'NullFormatter': ('pygments.formatters.other', 'Text only', ('text', 'null'), ('*.txt',), 'Output the text unchanged without any formatting.'),
+ 'PangoMarkupFormatter': ('pygments.formatters.pangomarkup', 'Pango Markup', ('pango', 'pangomarkup'), (), 'Format tokens as Pango Markup code. It can then be rendered to an SVG.'),
+ 'RawTokenFormatter': ('pygments.formatters.other', 'Raw tokens', ('raw', 'tokens'), ('*.raw',), 'Format tokens as a raw representation for storing token streams.'),
+ 'RtfFormatter': ('pygments.formatters.rtf', 'RTF', ('rtf',), ('*.rtf',), 'Format tokens as RTF markup. This formatter automatically outputs full RTF documents with color information and other useful stuff. Perfect for Copy and Paste into Microsoft(R) Word(R) documents.'),
+ 'SvgFormatter': ('pygments.formatters.svg', 'SVG', ('svg',), ('*.svg',), 'Format tokens as an SVG graphics file. This formatter is still experimental. Each line of code is a ``<text>`` element with explicit ``x`` and ``y`` coordinates containing ``<tspan>`` elements with the individual token styles.'),
+ 'Terminal256Formatter': ('pygments.formatters.terminal256', 'Terminal256', ('terminal256', 'console256', '256'), (), 'Format tokens with ANSI color sequences, for output in a 256-color terminal or console. Like in `TerminalFormatter` color sequences are terminated at newlines, so that paging the output works correctly.'),
+ 'TerminalFormatter': ('pygments.formatters.terminal', 'Terminal', ('terminal', 'console'), (), 'Format tokens with ANSI color sequences, for output in a text console. Color sequences are terminated at newlines, so that paging the output works correctly.'),
+ 'TerminalTrueColorFormatter': ('pygments.formatters.terminal256', 'TerminalTrueColor', ('terminal16m', 'console16m', '16m'), (), 'Format tokens with ANSI color sequences, for output in a true-color terminal or console. Like in `TerminalFormatter` color sequences are terminated at newlines, so that paging the output works correctly.'),
+ 'TestcaseFormatter': ('pygments.formatters.other', 'Testcase', ('testcase',), (), 'Format tokens as appropriate for a new testcase.')
+}
+
+if __name__ == '__main__': # pragma: no cover
+ import sys
+ import os
+
+ # lookup formatters
+ found_formatters = []
+ imports = []
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
+ from pip._vendor.pygments.util import docstring_headline
+
+ for root, dirs, files in os.walk('.'):
+ for filename in files:
+ if filename.endswith('.py') and not filename.startswith('_'):
+ module_name = 'pygments.formatters%s.%s' % (
+ root[1:].replace('/', '.'), filename[:-3])
+ print(module_name)
+ module = __import__(module_name, None, None, [''])
+ for formatter_name in module.__all__:
+ formatter = getattr(module, formatter_name)
+ found_formatters.append(
+ '%r: %r' % (formatter_name,
+ (module_name,
+ formatter.name,
+ tuple(formatter.aliases),
+ tuple(formatter.filenames),
+ docstring_headline(formatter))))
+ # sort them to make the diff minimal
+ found_formatters.sort()
+
+ # extract useful sourcecode from this file
+ with open(__file__) as fp:
+ content = fp.read()
+ # replace crnl to nl for Windows.
+ #
+ # Note that, originally, contributors should keep nl of master
+ # repository, for example by using some kind of automatic
+ # management EOL, like `EolExtension
+ # <https://www.mercurial-scm.org/wiki/EolExtension>`.
+ content = content.replace("\r\n", "\n")
+ header = content[:content.find('FORMATTERS = {')]
+ footer = content[content.find("if __name__ == '__main__':"):]
+
+ # write new file
+ with open(__file__, 'w') as fp:
+ fp.write(header)
+ fp.write('FORMATTERS = {\n %s\n}\n\n' % ',\n '.join(found_formatters))
+ fp.write(footer)
+
+ print ('=== %d formatters processed.' % len(found_formatters))
diff --git a/src/pip/_vendor/pygments/formatters/bbcode.py b/src/pip/_vendor/pygments/formatters/bbcode.py
new file mode 100644
index 000000000..2be2b4e31
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/bbcode.py
@@ -0,0 +1,108 @@
+"""
+ pygments.formatters.bbcode
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ BBcode formatter.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+
+from pip._vendor.pygments.formatter import Formatter
+from pip._vendor.pygments.util import get_bool_opt
+
+__all__ = ['BBCodeFormatter']
+
+
+class BBCodeFormatter(Formatter):
+ """
+ Format tokens with BBcodes. These formatting codes are used by many
+ bulletin boards, so you can highlight your sourcecode with pygments before
+ posting it there.
+
+ This formatter has no support for background colors and borders, as there
+ are no common BBcode tags for that.
+
+ Some board systems (e.g. phpBB) don't support colors in their [code] tag,
+ so you can't use the highlighting together with that tag.
+ Text in a [code] tag usually is shown with a monospace font (which this
+ formatter can do with the ``monofont`` option) and no spaces (which you
+ need for indentation) are removed.
+
+ Additional options accepted:
+
+ `style`
+ The style to use, can be a string or a Style subclass (default:
+ ``'default'``).
+
+ `codetag`
+ If set to true, put the output into ``[code]`` tags (default:
+ ``false``)
+
+ `monofont`
+ If set to true, add a tag to show the code with a monospace font
+ (default: ``false``).
+ """
+ name = 'BBCode'
+ aliases = ['bbcode', 'bb']
+ filenames = []
+
+ def __init__(self, **options):
+ Formatter.__init__(self, **options)
+ self._code = get_bool_opt(options, 'codetag', False)
+ self._mono = get_bool_opt(options, 'monofont', False)
+
+ self.styles = {}
+ self._make_styles()
+
+ def _make_styles(self):
+ for ttype, ndef in self.style:
+ start = end = ''
+ if ndef['color']:
+ start += '[color=#%s]' % ndef['color']
+ end = '[/color]' + end
+ if ndef['bold']:
+ start += '[b]'
+ end = '[/b]' + end
+ if ndef['italic']:
+ start += '[i]'
+ end = '[/i]' + end
+ if ndef['underline']:
+ start += '[u]'
+ end = '[/u]' + end
+ # there are no common BBcodes for background-color and border
+
+ self.styles[ttype] = start, end
+
+ def format_unencoded(self, tokensource, outfile):
+ if self._code:
+ outfile.write('[code]')
+ if self._mono:
+ outfile.write('[font=monospace]')
+
+ lastval = ''
+ lasttype = None
+
+ for ttype, value in tokensource:
+ while ttype not in self.styles:
+ ttype = ttype.parent
+ if ttype == lasttype:
+ lastval += value
+ else:
+ if lastval:
+ start, end = self.styles[lasttype]
+ outfile.write(''.join((start, lastval, end)))
+ lastval = value
+ lasttype = ttype
+
+ if lastval:
+ start, end = self.styles[lasttype]
+ outfile.write(''.join((start, lastval, end)))
+
+ if self._mono:
+ outfile.write('[/font]')
+ if self._code:
+ outfile.write('[/code]')
+ if self._code or self._mono:
+ outfile.write('\n')
diff --git a/src/pip/_vendor/pygments/formatters/groff.py b/src/pip/_vendor/pygments/formatters/groff.py
new file mode 100644
index 000000000..f3dcbce9b
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/groff.py
@@ -0,0 +1,170 @@
+"""
+ pygments.formatters.groff
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Formatter for groff output.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import math
+from pip._vendor.pygments.formatter import Formatter
+from pip._vendor.pygments.util import get_bool_opt, get_int_opt
+
+__all__ = ['GroffFormatter']
+
+
+class GroffFormatter(Formatter):
+ """
+ Format tokens with groff escapes to change their color and font style.
+
+ .. versionadded:: 2.11
+
+ Additional options accepted:
+
+ `style`
+ The style to use, can be a string or a Style subclass (default:
+ ``'default'``).
+
+ `monospaced`
+ If set to true, monospace font will be used (default: ``true``).
+
+ `linenos`
+ If set to true, print the line numbers (default: ``false``).
+
+ `wrap`
+ Wrap lines to the specified number of characters. Disabled if set to 0
+ (default: ``0``).
+ """
+
+ name = 'groff'
+ aliases = ['groff','troff','roff']
+ filenames = []
+
+ def __init__(self, **options):
+ Formatter.__init__(self, **options)
+
+ self.monospaced = get_bool_opt(options, 'monospaced', True)
+ self.linenos = get_bool_opt(options, 'linenos', False)
+ self._lineno = 0
+ self.wrap = get_int_opt(options, 'wrap', 0)
+ self._linelen = 0
+
+ self.styles = {}
+ self._make_styles()
+
+
+ def _make_styles(self):
+ regular = '\\f[CR]' if self.monospaced else '\\f[R]'
+ bold = '\\f[CB]' if self.monospaced else '\\f[B]'
+ italic = '\\f[CI]' if self.monospaced else '\\f[I]'
+
+ for ttype, ndef in self.style:
+ start = end = ''
+ if ndef['color']:
+ start += '\\m[%s]' % ndef['color']
+ end = '\\m[]' + end
+ if ndef['bold']:
+ start += bold
+ end = regular + end
+ if ndef['italic']:
+ start += italic
+ end = regular + end
+ if ndef['bgcolor']:
+ start += '\\M[%s]' % ndef['bgcolor']
+ end = '\\M[]' + end
+
+ self.styles[ttype] = start, end
+
+
+ def _define_colors(self, outfile):
+ colors = set()
+ for _, ndef in self.style:
+ if ndef['color'] is not None:
+ colors.add(ndef['color'])
+
+ for color in colors:
+ outfile.write('.defcolor ' + color + ' rgb #' + color + '\n')
+
+
+ def _write_lineno(self, outfile):
+ self._lineno += 1
+ outfile.write("%s% 4d " % (self._lineno != 1 and '\n' or '', self._lineno))
+
+
+ def _wrap_line(self, line):
+ length = len(line.rstrip('\n'))
+ space = ' ' if self.linenos else ''
+ newline = ''
+
+ if length > self.wrap:
+ for i in range(0, math.floor(length / self.wrap)):
+ chunk = line[i*self.wrap:i*self.wrap+self.wrap]
+ newline += (chunk + '\n' + space)
+ remainder = length % self.wrap
+ if remainder > 0:
+ newline += line[-remainder-1:]
+ self._linelen = remainder
+ elif self._linelen + length > self.wrap:
+ newline = ('\n' + space) + line
+ self._linelen = length
+ else:
+ newline = line
+ self._linelen += length
+
+ return newline
+
+
+ def _escape_chars(self, text):
+ text = text.replace('\\', '\\[u005C]'). \
+ replace('.', '\\[char46]'). \
+ replace('\'', '\\[u0027]'). \
+ replace('`', '\\[u0060]'). \
+ replace('~', '\\[u007E]')
+ copy = text
+
+ for char in copy:
+ if len(char) != len(char.encode()):
+ uni = char.encode('unicode_escape') \
+ .decode()[1:] \
+ .replace('x', 'u00') \
+ .upper()
+ text = text.replace(char, '\\[u' + uni[1:] + ']')
+
+ return text
+
+
+ def format_unencoded(self, tokensource, outfile):
+ self._define_colors(outfile)
+
+ outfile.write('.nf\n\\f[CR]\n')
+
+ if self.linenos:
+ self._write_lineno(outfile)
+
+ for ttype, value in tokensource:
+ while ttype not in self.styles:
+ ttype = ttype.parent
+ start, end = self.styles[ttype]
+
+ for line in value.splitlines(True):
+ if self.wrap > 0:
+ line = self._wrap_line(line)
+
+ if start and end:
+ text = self._escape_chars(line.rstrip('\n'))
+ if text != '':
+ outfile.write(''.join((start, text, end)))
+ else:
+ outfile.write(self._escape_chars(line.rstrip('\n')))
+
+ if line.endswith('\n'):
+ if self.linenos:
+ self._write_lineno(outfile)
+ self._linelen = 0
+ else:
+ outfile.write('\n')
+ self._linelen = 0
+
+ outfile.write('\n.fi')
diff --git a/src/pip/_vendor/pygments/formatters/html.py b/src/pip/_vendor/pygments/formatters/html.py
new file mode 100644
index 000000000..d5cda4c4b
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/html.py
@@ -0,0 +1,989 @@
+"""
+ pygments.formatters.html
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Formatter for HTML output.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import functools
+import os
+import sys
+import os.path
+from io import StringIO
+
+from pip._vendor.pygments.formatter import Formatter
+from pip._vendor.pygments.token import Token, Text, STANDARD_TYPES
+from pip._vendor.pygments.util import get_bool_opt, get_int_opt, get_list_opt
+
+try:
+ import ctags
+except ImportError:
+ ctags = None
+
+__all__ = ['HtmlFormatter']
+
+
+_escape_html_table = {
+ ord('&'): '&amp;',
+ ord('<'): '&lt;',
+ ord('>'): '&gt;',
+ ord('"'): '&quot;',
+ ord("'"): '&#39;',
+}
+
+
+def escape_html(text, table=_escape_html_table):
+ """Escape &, <, > as well as single and double quotes for HTML."""
+ return text.translate(table)
+
+
+def webify(color):
+ if color.startswith('calc') or color.startswith('var'):
+ return color
+ else:
+ return '#' + color
+
+
+def _get_ttype_class(ttype):
+ fname = STANDARD_TYPES.get(ttype)
+ if fname:
+ return fname
+ aname = ''
+ while fname is None:
+ aname = '-' + ttype[-1] + aname
+ ttype = ttype.parent
+ fname = STANDARD_TYPES.get(ttype)
+ return fname + aname
+
+
+CSSFILE_TEMPLATE = '''\
+/*
+generated by Pygments <https://pygments.org/>
+Copyright 2006-2022 by the Pygments team.
+Licensed under the BSD license, see LICENSE for details.
+*/
+%(styledefs)s
+'''
+
+DOC_HEADER = '''\
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+<!--
+generated by Pygments <https://pygments.org/>
+Copyright 2006-2022 by the Pygments team.
+Licensed under the BSD license, see LICENSE for details.
+-->
+<html>
+<head>
+ <title>%(title)s</title>
+ <meta http-equiv="content-type" content="text/html; charset=%(encoding)s">
+ <style type="text/css">
+''' + CSSFILE_TEMPLATE + '''
+ </style>
+</head>
+<body>
+<h2>%(title)s</h2>
+
+'''
+
+DOC_HEADER_EXTERNALCSS = '''\
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+
+<html>
+<head>
+ <title>%(title)s</title>
+ <meta http-equiv="content-type" content="text/html; charset=%(encoding)s">
+ <link rel="stylesheet" href="%(cssfile)s" type="text/css">
+</head>
+<body>
+<h2>%(title)s</h2>
+
+'''
+
+DOC_FOOTER = '''\
+</body>
+</html>
+'''
+
+
+class HtmlFormatter(Formatter):
+ r"""
+ Format tokens as HTML 4 ``<span>`` tags within a ``<pre>`` tag, wrapped
+ in a ``<div>`` tag. The ``<div>``'s CSS class can be set by the `cssclass`
+ option.
+
+ If the `linenos` option is set to ``"table"``, the ``<pre>`` is
+ additionally wrapped inside a ``<table>`` which has one row and two
+ cells: one containing the line numbers and one containing the code.
+ Example:
+
+ .. sourcecode:: html
+
+ <div class="highlight" >
+ <table><tr>
+ <td class="linenos" title="click to toggle"
+ onclick="with (this.firstChild.style)
+ { display = (display == '') ? 'none' : '' }">
+ <pre>1
+ 2</pre>
+ </td>
+ <td class="code">
+ <pre><span class="Ke">def </span><span class="NaFu">foo</span>(bar):
+ <span class="Ke">pass</span>
+ </pre>
+ </td>
+ </tr></table></div>
+
+ (whitespace added to improve clarity).
+
+ Wrapping can be disabled using the `nowrap` option.
+
+ A list of lines can be specified using the `hl_lines` option to make these
+ lines highlighted (as of Pygments 0.11).
+
+ With the `full` option, a complete HTML 4 document is output, including
+ the style definitions inside a ``<style>`` tag, or in a separate file if
+ the `cssfile` option is given.
+
+ When `tagsfile` is set to the path of a ctags index file, it is used to
+ generate hyperlinks from names to their definition. You must enable
+ `lineanchors` and run ctags with the `-n` option for this to work. The
+ `python-ctags` module from PyPI must be installed to use this feature;
+ otherwise a `RuntimeError` will be raised.
+
+ The `get_style_defs(arg='')` method of a `HtmlFormatter` returns a string
+ containing CSS rules for the CSS classes used by the formatter. The
+ argument `arg` can be used to specify additional CSS selectors that
+ are prepended to the classes. A call `fmter.get_style_defs('td .code')`
+ would result in the following CSS classes:
+
+ .. sourcecode:: css
+
+ td .code .kw { font-weight: bold; color: #00FF00 }
+ td .code .cm { color: #999999 }
+ ...
+
+ If you have Pygments 0.6 or higher, you can also pass a list or tuple to the
+ `get_style_defs()` method to request multiple prefixes for the tokens:
+
+ .. sourcecode:: python
+
+ formatter.get_style_defs(['div.syntax pre', 'pre.syntax'])
+
+ The output would then look like this:
+
+ .. sourcecode:: css
+
+ div.syntax pre .kw,
+ pre.syntax .kw { font-weight: bold; color: #00FF00 }
+ div.syntax pre .cm,
+ pre.syntax .cm { color: #999999 }
+ ...
+
+ Additional options accepted:
+
+ `nowrap`
+ If set to ``True``, don't wrap the tokens at all, not even inside a ``<pre>``
+ tag. This disables most other options (default: ``False``).
+
+ `full`
+ Tells the formatter to output a "full" document, i.e. a complete
+ self-contained document (default: ``False``).
+
+ `title`
+ If `full` is true, the title that should be used to caption the
+ document (default: ``''``).
+
+ `style`
+ The style to use, can be a string or a Style subclass (default:
+ ``'default'``). This option has no effect if the `cssfile`
+ and `noclobber_cssfile` option are given and the file specified in
+ `cssfile` exists.
+
+ `noclasses`
+ If set to true, token ``<span>`` tags (as well as line number elements)
+ will not use CSS classes, but inline styles. This is not recommended
+ for larger pieces of code since it increases output size by quite a bit
+ (default: ``False``).
+
+ `classprefix`
+ Since the token types use relatively short class names, they may clash
+ with some of your own class names. In this case you can use the
+ `classprefix` option to give a string to prepend to all Pygments-generated
+ CSS class names for token types.
+ Note that this option also affects the output of `get_style_defs()`.
+
+ `cssclass`
+ CSS class for the wrapping ``<div>`` tag (default: ``'highlight'``).
+ If you set this option, the default selector for `get_style_defs()`
+ will be this class.
+
+ .. versionadded:: 0.9
+ If you select the ``'table'`` line numbers, the wrapping table will
+ have a CSS class of this string plus ``'table'``, the default is
+ accordingly ``'highlighttable'``.
+
+ `cssstyles`
+ Inline CSS styles for the wrapping ``<div>`` tag (default: ``''``).
+
+ `prestyles`
+ Inline CSS styles for the ``<pre>`` tag (default: ``''``).
+
+ .. versionadded:: 0.11
+
+ `cssfile`
+ If the `full` option is true and this option is given, it must be the
+ name of an external file. If the filename does not include an absolute
+ path, the file's path will be assumed to be relative to the main output
+ file's path, if the latter can be found. The stylesheet is then written
+ to this file instead of the HTML file.
+
+ .. versionadded:: 0.6
+
+ `noclobber_cssfile`
+ If `cssfile` is given and the specified file exists, the css file will
+ not be overwritten. This allows the use of the `full` option in
+ combination with a user specified css file. Default is ``False``.
+
+ .. versionadded:: 1.1
+
+ `linenos`
+ If set to ``'table'``, output line numbers as a table with two cells,
+ one containing the line numbers, the other the whole code. This is
+ copy-and-paste-friendly, but may cause alignment problems with some
+ browsers or fonts. If set to ``'inline'``, the line numbers will be
+ integrated in the ``<pre>`` tag that contains the code (that setting
+ is *new in Pygments 0.8*).
+
+ For compatibility with Pygments 0.7 and earlier, every true value
+ except ``'inline'`` means the same as ``'table'`` (in particular, that
+ means also ``True``).
+
+ The default value is ``False``, which means no line numbers at all.
+
+ **Note:** with the default ("table") line number mechanism, the line
+ numbers and code can have different line heights in Internet Explorer
+ unless you give the enclosing ``<pre>`` tags an explicit ``line-height``
+ CSS property (you get the default line spacing with ``line-height:
+ 125%``).
+
+ `hl_lines`
+ Specify a list of lines to be highlighted. The line numbers are always
+ relative to the input (i.e. the first line is line 1) and are
+ independent of `linenostart`.
+
+ .. versionadded:: 0.11
+
+ `linenostart`
+ The line number for the first line (default: ``1``).
+
+ `linenostep`
+ If set to a number n > 1, only every nth line number is printed.
+
+ `linenospecial`
+ If set to a number n > 0, every nth line number is given the CSS
+ class ``"special"`` (default: ``0``).
+
+ `nobackground`
+ If set to ``True``, the formatter won't output the background color
+ for the wrapping element (this automatically defaults to ``False``
+ when there is no wrapping element [eg: no argument for the
+ `get_syntax_defs` method given]) (default: ``False``).
+
+ .. versionadded:: 0.6
+
+ `lineseparator`
+ This string is output between lines of code. It defaults to ``"\n"``,
+ which is enough to break a line inside ``<pre>`` tags, but you can
+ e.g. set it to ``"<br>"`` to get HTML line breaks.
+
+ .. versionadded:: 0.7
+
+ `lineanchors`
+ If set to a nonempty string, e.g. ``foo``, the formatter will wrap each
+ output line in an anchor tag with an ``id`` (and `name`) of ``foo-linenumber``.
+ This allows easy linking to certain lines.
+
+ .. versionadded:: 0.9
+
+ `linespans`
+ If set to a nonempty string, e.g. ``foo``, the formatter will wrap each
+ output line in a span tag with an ``id`` of ``foo-linenumber``.
+ This allows easy access to lines via javascript.
+
+ .. versionadded:: 1.6
+
+ `anchorlinenos`
+ If set to `True`, will wrap line numbers in <a> tags. Used in
+ combination with `linenos` and `lineanchors`.
+
+ `tagsfile`
+ If set to the path of a ctags file, wrap names in anchor tags that
+ link to their definitions. `lineanchors` should be used, and the
+ tags file should specify line numbers (see the `-n` option to ctags).
+
+ .. versionadded:: 1.6
+
+ `tagurlformat`
+ A string formatting pattern used to generate links to ctags definitions.
+ Available variables are `%(path)s`, `%(fname)s` and `%(fext)s`.
+ Defaults to an empty string, resulting in just `#prefix-number` links.
+
+ .. versionadded:: 1.6
+
+ `filename`
+ A string used to generate a filename when rendering ``<pre>`` blocks,
+ for example if displaying source code. If `linenos` is set to
+ ``'table'`` then the filename will be rendered in an initial row
+ containing a single `<th>` which spans both columns.
+
+ .. versionadded:: 2.1
+
+ `wrapcode`
+ Wrap the code inside ``<pre>`` blocks using ``<code>``, as recommended
+ by the HTML5 specification.
+
+ .. versionadded:: 2.4
+
+ `debug_token_types`
+ Add ``title`` attributes to all token ``<span>`` tags that show the
+ name of the token.
+
+ .. versionadded:: 2.10
+
+
+ **Subclassing the HTML formatter**
+
+ .. versionadded:: 0.7
+
+ The HTML formatter is now built in a way that allows easy subclassing, thus
+ customizing the output HTML code. The `format()` method calls
+ `self._format_lines()` which returns a generator that yields tuples of ``(1,
+ line)``, where the ``1`` indicates that the ``line`` is a line of the
+ formatted source code.
+
+ If the `nowrap` option is set, the generator is the iterated over and the
+ resulting HTML is output.
+
+ Otherwise, `format()` calls `self.wrap()`, which wraps the generator with
+ other generators. These may add some HTML code to the one generated by
+ `_format_lines()`, either by modifying the lines generated by the latter,
+ then yielding them again with ``(1, line)``, and/or by yielding other HTML
+ code before or after the lines, with ``(0, html)``. The distinction between
+ source lines and other code makes it possible to wrap the generator multiple
+ times.
+
+ The default `wrap()` implementation adds a ``<div>`` and a ``<pre>`` tag.
+
+ A custom `HtmlFormatter` subclass could look like this:
+
+ .. sourcecode:: python
+
+ class CodeHtmlFormatter(HtmlFormatter):
+
+ def wrap(self, source, *, include_div):
+ return self._wrap_code(source)
+
+ def _wrap_code(self, source):
+ yield 0, '<code>'
+ for i, t in source:
+ if i == 1:
+ # it's a line of formatted code
+ t += '<br>'
+ yield i, t
+ yield 0, '</code>'
+
+ This results in wrapping the formatted lines with a ``<code>`` tag, where the
+ source lines are broken using ``<br>`` tags.
+
+ After calling `wrap()`, the `format()` method also adds the "line numbers"
+ and/or "full document" wrappers if the respective options are set. Then, all
+ HTML yielded by the wrapped generator is output.
+ """
+
+ name = 'HTML'
+ aliases = ['html']
+ filenames = ['*.html', '*.htm']
+
+ def __init__(self, **options):
+ Formatter.__init__(self, **options)
+ self.title = self._decodeifneeded(self.title)
+ self.nowrap = get_bool_opt(options, 'nowrap', False)
+ self.noclasses = get_bool_opt(options, 'noclasses', False)
+ self.classprefix = options.get('classprefix', '')
+ self.cssclass = self._decodeifneeded(options.get('cssclass', 'highlight'))
+ self.cssstyles = self._decodeifneeded(options.get('cssstyles', ''))
+ self.prestyles = self._decodeifneeded(options.get('prestyles', ''))
+ self.cssfile = self._decodeifneeded(options.get('cssfile', ''))
+ self.noclobber_cssfile = get_bool_opt(options, 'noclobber_cssfile', False)
+ self.tagsfile = self._decodeifneeded(options.get('tagsfile', ''))
+ self.tagurlformat = self._decodeifneeded(options.get('tagurlformat', ''))
+ self.filename = self._decodeifneeded(options.get('filename', ''))
+ self.wrapcode = get_bool_opt(options, 'wrapcode', False)
+ self.span_element_openers = {}
+ self.debug_token_types = get_bool_opt(options, 'debug_token_types', False)
+
+ if self.tagsfile:
+ if not ctags:
+ raise RuntimeError('The "ctags" package must to be installed '
+ 'to be able to use the "tagsfile" feature.')
+ self._ctags = ctags.CTags(self.tagsfile)
+
+ linenos = options.get('linenos', False)
+ if linenos == 'inline':
+ self.linenos = 2
+ elif linenos:
+ # compatibility with <= 0.7
+ self.linenos = 1
+ else:
+ self.linenos = 0
+ self.linenostart = abs(get_int_opt(options, 'linenostart', 1))
+ self.linenostep = abs(get_int_opt(options, 'linenostep', 1))
+ self.linenospecial = abs(get_int_opt(options, 'linenospecial', 0))
+ self.nobackground = get_bool_opt(options, 'nobackground', False)
+ self.lineseparator = options.get('lineseparator', '\n')
+ self.lineanchors = options.get('lineanchors', '')
+ self.linespans = options.get('linespans', '')
+ self.anchorlinenos = get_bool_opt(options, 'anchorlinenos', False)
+ self.hl_lines = set()
+ for lineno in get_list_opt(options, 'hl_lines', []):
+ try:
+ self.hl_lines.add(int(lineno))
+ except ValueError:
+ pass
+
+ self._create_stylesheet()
+
+ def _get_css_class(self, ttype):
+ """Return the css class of this token type prefixed with
+ the classprefix option."""
+ ttypeclass = _get_ttype_class(ttype)
+ if ttypeclass:
+ return self.classprefix + ttypeclass
+ return ''
+
+ def _get_css_classes(self, ttype):
+ """Return the CSS classes of this token type prefixed with the classprefix option."""
+ cls = self._get_css_class(ttype)
+ while ttype not in STANDARD_TYPES:
+ ttype = ttype.parent
+ cls = self._get_css_class(ttype) + ' ' + cls
+ return cls or ''
+
+ def _get_css_inline_styles(self, ttype):
+ """Return the inline CSS styles for this token type."""
+ cclass = self.ttype2class.get(ttype)
+ while cclass is None:
+ ttype = ttype.parent
+ cclass = self.ttype2class.get(ttype)
+ return cclass or ''
+
+ def _create_stylesheet(self):
+ t2c = self.ttype2class = {Token: ''}
+ c2s = self.class2style = {}
+ for ttype, ndef in self.style:
+ name = self._get_css_class(ttype)
+ style = ''
+ if ndef['color']:
+ style += 'color: %s; ' % webify(ndef['color'])
+ if ndef['bold']:
+ style += 'font-weight: bold; '
+ if ndef['italic']:
+ style += 'font-style: italic; '
+ if ndef['underline']:
+ style += 'text-decoration: underline; '
+ if ndef['bgcolor']:
+ style += 'background-color: %s; ' % webify(ndef['bgcolor'])
+ if ndef['border']:
+ style += 'border: 1px solid %s; ' % webify(ndef['border'])
+ if style:
+ t2c[ttype] = name
+ # save len(ttype) to enable ordering the styles by
+ # hierarchy (necessary for CSS cascading rules!)
+ c2s[name] = (style[:-2], ttype, len(ttype))
+
+ def get_style_defs(self, arg=None):
+ """
+ Return CSS style definitions for the classes produced by the current
+ highlighting style. ``arg`` can be a string or list of selectors to
+ insert before the token type classes.
+ """
+ style_lines = []
+
+ style_lines.extend(self.get_linenos_style_defs())
+ style_lines.extend(self.get_background_style_defs(arg))
+ style_lines.extend(self.get_token_style_defs(arg))
+
+ return '\n'.join(style_lines)
+
+ def get_token_style_defs(self, arg=None):
+ prefix = self.get_css_prefix(arg)
+
+ styles = [
+ (level, ttype, cls, style)
+ for cls, (style, ttype, level) in self.class2style.items()
+ if cls and style
+ ]
+ styles.sort()
+
+ lines = [
+ '%s { %s } /* %s */' % (prefix(cls), style, repr(ttype)[6:])
+ for (level, ttype, cls, style) in styles
+ ]
+
+ return lines
+
+ def get_background_style_defs(self, arg=None):
+ prefix = self.get_css_prefix(arg)
+ bg_color = self.style.background_color
+ hl_color = self.style.highlight_color
+
+ lines = []
+
+ if arg and not self.nobackground and bg_color is not None:
+ text_style = ''
+ if Text in self.ttype2class:
+ text_style = ' ' + self.class2style[self.ttype2class[Text]][0]
+ lines.insert(
+ 0, '%s{ background: %s;%s }' % (
+ prefix(''), bg_color, text_style
+ )
+ )
+ if hl_color is not None:
+ lines.insert(
+ 0, '%s { background-color: %s }' % (prefix('hll'), hl_color)
+ )
+
+ return lines
+
+ def get_linenos_style_defs(self):
+ lines = [
+ 'pre { %s }' % self._pre_style,
+ 'td.linenos .normal { %s }' % self._linenos_style,
+ 'span.linenos { %s }' % self._linenos_style,
+ 'td.linenos .special { %s }' % self._linenos_special_style,
+ 'span.linenos.special { %s }' % self._linenos_special_style,
+ ]
+
+ return lines
+
+ def get_css_prefix(self, arg):
+ if arg is None:
+ arg = ('cssclass' in self.options and '.'+self.cssclass or '')
+ if isinstance(arg, str):
+ args = [arg]
+ else:
+ args = list(arg)
+
+ def prefix(cls):
+ if cls:
+ cls = '.' + cls
+ tmp = []
+ for arg in args:
+ tmp.append((arg and arg + ' ' or '') + cls)
+ return ', '.join(tmp)
+
+ return prefix
+
+ @property
+ def _pre_style(self):
+ return 'line-height: 125%;'
+
+ @property
+ def _linenos_style(self):
+ return 'color: %s; background-color: %s; padding-left: 5px; padding-right: 5px;' % (
+ self.style.line_number_color,
+ self.style.line_number_background_color
+ )
+
+ @property
+ def _linenos_special_style(self):
+ return 'color: %s; background-color: %s; padding-left: 5px; padding-right: 5px;' % (
+ self.style.line_number_special_color,
+ self.style.line_number_special_background_color
+ )
+
+ def _decodeifneeded(self, value):
+ if isinstance(value, bytes):
+ if self.encoding:
+ return value.decode(self.encoding)
+ return value.decode()
+ return value
+
+ def _wrap_full(self, inner, outfile):
+ if self.cssfile:
+ if os.path.isabs(self.cssfile):
+ # it's an absolute filename
+ cssfilename = self.cssfile
+ else:
+ try:
+ filename = outfile.name
+ if not filename or filename[0] == '<':
+ # pseudo files, e.g. name == '<fdopen>'
+ raise AttributeError
+ cssfilename = os.path.join(os.path.dirname(filename),
+ self.cssfile)
+ except AttributeError:
+ print('Note: Cannot determine output file name, '
+ 'using current directory as base for the CSS file name',
+ file=sys.stderr)
+ cssfilename = self.cssfile
+ # write CSS file only if noclobber_cssfile isn't given as an option.
+ try:
+ if not os.path.exists(cssfilename) or not self.noclobber_cssfile:
+ with open(cssfilename, "w") as cf:
+ cf.write(CSSFILE_TEMPLATE %
+ {'styledefs': self.get_style_defs('body')})
+ except OSError as err:
+ err.strerror = 'Error writing CSS file: ' + err.strerror
+ raise
+
+ yield 0, (DOC_HEADER_EXTERNALCSS %
+ dict(title=self.title,
+ cssfile=self.cssfile,
+ encoding=self.encoding))
+ else:
+ yield 0, (DOC_HEADER %
+ dict(title=self.title,
+ styledefs=self.get_style_defs('body'),
+ encoding=self.encoding))
+
+ yield from inner
+ yield 0, DOC_FOOTER
+
+ def _wrap_tablelinenos(self, inner):
+ dummyoutfile = StringIO()
+ lncount = 0
+ for t, line in inner:
+ if t:
+ lncount += 1
+ dummyoutfile.write(line)
+
+ fl = self.linenostart
+ mw = len(str(lncount + fl - 1))
+ sp = self.linenospecial
+ st = self.linenostep
+ anchor_name = self.lineanchors or self.linespans
+ aln = self.anchorlinenos
+ nocls = self.noclasses
+
+ lines = []
+
+ for i in range(fl, fl+lncount):
+ print_line = i % st == 0
+ special_line = sp and i % sp == 0
+
+ if print_line:
+ line = '%*d' % (mw, i)
+ if aln:
+ line = '<a href="#%s-%d">%s</a>' % (anchor_name, i, line)
+ else:
+ line = ' ' * mw
+
+ if nocls:
+ if special_line:
+ style = ' style="%s"' % self._linenos_special_style
+ else:
+ style = ' style="%s"' % self._linenos_style
+ else:
+ if special_line:
+ style = ' class="special"'
+ else:
+ style = ' class="normal"'
+
+ if style:
+ line = '<span%s>%s</span>' % (style, line)
+
+ lines.append(line)
+
+ ls = '\n'.join(lines)
+
+ # If a filename was specified, we can't put it into the code table as it
+ # would misalign the line numbers. Hence we emit a separate row for it.
+ filename_tr = ""
+ if self.filename:
+ filename_tr = (
+ '<tr><th colspan="2" class="filename">'
+ '<span class="filename">' + self.filename + '</span>'
+ '</th></tr>')
+
+ # in case you wonder about the seemingly redundant <div> here: since the
+ # content in the other cell also is wrapped in a div, some browsers in
+ # some configurations seem to mess up the formatting...
+ yield 0, (f'<table class="{self.cssclass}table">' + filename_tr +
+ '<tr><td class="linenos"><div class="linenodiv"><pre>' +
+ ls + '</pre></div></td><td class="code">')
+ yield 0, '<div>'
+ yield 0, dummyoutfile.getvalue()
+ yield 0, '</div>'
+ yield 0, '</td></tr></table>'
+
+
+ def _wrap_inlinelinenos(self, inner):
+ # need a list of lines since we need the width of a single number :(
+ inner_lines = list(inner)
+ sp = self.linenospecial
+ st = self.linenostep
+ num = self.linenostart
+ mw = len(str(len(inner_lines) + num - 1))
+ anchor_name = self.lineanchors or self.linespans
+ aln = self.anchorlinenos
+ nocls = self.noclasses
+
+ for _, inner_line in inner_lines:
+ print_line = num % st == 0
+ special_line = sp and num % sp == 0
+
+ if print_line:
+ line = '%*d' % (mw, num)
+ else:
+ line = ' ' * mw
+
+ if nocls:
+ if special_line:
+ style = ' style="%s"' % self._linenos_special_style
+ else:
+ style = ' style="%s"' % self._linenos_style
+ else:
+ if special_line:
+ style = ' class="linenos special"'
+ else:
+ style = ' class="linenos"'
+
+ if style:
+ linenos = '<span%s>%s</span>' % (style, line)
+ else:
+ linenos = line
+
+ if aln:
+ yield 1, ('<a href="#%s-%d">%s</a>' % (anchor_name, num, linenos) +
+ inner_line)
+ else:
+ yield 1, linenos + inner_line
+ num += 1
+
+ def _wrap_lineanchors(self, inner):
+ s = self.lineanchors
+ # subtract 1 since we have to increment i *before* yielding
+ i = self.linenostart - 1
+ for t, line in inner:
+ if t:
+ i += 1
+ href = "" if self.linenos else ' href="#%s-%d"' % (s, i)
+ yield 1, '<a id="%s-%d" name="%s-%d"%s></a>' % (s, i, s, i, href) + line
+ else:
+ yield 0, line
+
+ def _wrap_linespans(self, inner):
+ s = self.linespans
+ i = self.linenostart - 1
+ for t, line in inner:
+ if t:
+ i += 1
+ yield 1, '<span id="%s-%d">%s</span>' % (s, i, line)
+ else:
+ yield 0, line
+
+ def _wrap_div(self, inner):
+ style = []
+ if (self.noclasses and not self.nobackground and
+ self.style.background_color is not None):
+ style.append('background: %s' % (self.style.background_color,))
+ if self.cssstyles:
+ style.append(self.cssstyles)
+ style = '; '.join(style)
+
+ yield 0, ('<div' + (self.cssclass and ' class="%s"' % self.cssclass) +
+ (style and (' style="%s"' % style)) + '>')
+ yield from inner
+ yield 0, '</div>\n'
+
+ def _wrap_pre(self, inner):
+ style = []
+ if self.prestyles:
+ style.append(self.prestyles)
+ if self.noclasses:
+ style.append(self._pre_style)
+ style = '; '.join(style)
+
+ if self.filename and self.linenos != 1:
+ yield 0, ('<span class="filename">' + self.filename + '</span>')
+
+ # the empty span here is to keep leading empty lines from being
+ # ignored by HTML parsers
+ yield 0, ('<pre' + (style and ' style="%s"' % style) + '><span></span>')
+ yield from inner
+ yield 0, '</pre>'
+
+ def _wrap_code(self, inner):
+ yield 0, '<code>'
+ yield from inner
+ yield 0, '</code>'
+
+ @functools.lru_cache(maxsize=100)
+ def _translate_parts(self, value):
+ """HTML-escape a value and split it by newlines."""
+ return value.translate(_escape_html_table).split('\n')
+
+ def _format_lines(self, tokensource):
+ """
+ Just format the tokens, without any wrapping tags.
+ Yield individual lines.
+ """
+ nocls = self.noclasses
+ lsep = self.lineseparator
+ tagsfile = self.tagsfile
+
+ lspan = ''
+ line = []
+ for ttype, value in tokensource:
+ try:
+ cspan = self.span_element_openers[ttype]
+ except KeyError:
+ title = ' title="%s"' % '.'.join(ttype) if self.debug_token_types else ''
+ if nocls:
+ css_style = self._get_css_inline_styles(ttype)
+ if css_style:
+ css_style = self.class2style[css_style][0]
+ cspan = '<span style="%s"%s>' % (css_style, title)
+ else:
+ cspan = ''
+ else:
+ css_class = self._get_css_classes(ttype)
+ if css_class:
+ cspan = '<span class="%s"%s>' % (css_class, title)
+ else:
+ cspan = ''
+ self.span_element_openers[ttype] = cspan
+
+ parts = self._translate_parts(value)
+
+ if tagsfile and ttype in Token.Name:
+ filename, linenumber = self._lookup_ctag(value)
+ if linenumber:
+ base, filename = os.path.split(filename)
+ if base:
+ base += '/'
+ filename, extension = os.path.splitext(filename)
+ url = self.tagurlformat % {'path': base, 'fname': filename,
+ 'fext': extension}
+ parts[0] = "<a href=\"%s#%s-%d\">%s" % \
+ (url, self.lineanchors, linenumber, parts[0])
+ parts[-1] = parts[-1] + "</a>"
+
+ # for all but the last line
+ for part in parts[:-1]:
+ if line:
+ if lspan != cspan:
+ line.extend(((lspan and '</span>'), cspan, part,
+ (cspan and '</span>'), lsep))
+ else: # both are the same
+ line.extend((part, (lspan and '</span>'), lsep))
+ yield 1, ''.join(line)
+ line = []
+ elif part:
+ yield 1, ''.join((cspan, part, (cspan and '</span>'), lsep))
+ else:
+ yield 1, lsep
+ # for the last line
+ if line and parts[-1]:
+ if lspan != cspan:
+ line.extend(((lspan and '</span>'), cspan, parts[-1]))
+ lspan = cspan
+ else:
+ line.append(parts[-1])
+ elif parts[-1]:
+ line = [cspan, parts[-1]]
+ lspan = cspan
+ # else we neither have to open a new span nor set lspan
+
+ if line:
+ line.extend(((lspan and '</span>'), lsep))
+ yield 1, ''.join(line)
+
+ def _lookup_ctag(self, token):
+ entry = ctags.TagEntry()
+ if self._ctags.find(entry, token.encode(), 0):
+ return entry['file'], entry['lineNumber']
+ else:
+ return None, None
+
+ def _highlight_lines(self, tokensource):
+ """
+ Highlighted the lines specified in the `hl_lines` option by
+ post-processing the token stream coming from `_format_lines`.
+ """
+ hls = self.hl_lines
+
+ for i, (t, value) in enumerate(tokensource):
+ if t != 1:
+ yield t, value
+ if i + 1 in hls: # i + 1 because Python indexes start at 0
+ if self.noclasses:
+ style = ''
+ if self.style.highlight_color is not None:
+ style = (' style="background-color: %s"' %
+ (self.style.highlight_color,))
+ yield 1, '<span%s>%s</span>' % (style, value)
+ else:
+ yield 1, '<span class="hll">%s</span>' % value
+ else:
+ yield 1, value
+
+ def wrap(self, source):
+ """
+ Wrap the ``source``, which is a generator yielding
+ individual lines, in custom generators. See docstring
+ for `format`. Can be overridden.
+ """
+
+ output = source
+ if self.wrapcode:
+ output = self._wrap_code(output)
+
+ output = self._wrap_pre(output)
+
+ return output
+
+ def format_unencoded(self, tokensource, outfile):
+ """
+ The formatting process uses several nested generators; which of
+ them are used is determined by the user's options.
+
+ Each generator should take at least one argument, ``inner``,
+ and wrap the pieces of text generated by this.
+
+ Always yield 2-tuples: (code, text). If "code" is 1, the text
+ is part of the original tokensource being highlighted, if it's
+ 0, the text is some piece of wrapping. This makes it possible to
+ use several different wrappers that process the original source
+ linewise, e.g. line number generators.
+ """
+ source = self._format_lines(tokensource)
+
+ # As a special case, we wrap line numbers before line highlighting
+ # so the line numbers get wrapped in the highlighting tag.
+ if not self.nowrap and self.linenos == 2:
+ source = self._wrap_inlinelinenos(source)
+
+ if self.hl_lines:
+ source = self._highlight_lines(source)
+
+ if not self.nowrap:
+ if self.lineanchors:
+ source = self._wrap_lineanchors(source)
+ if self.linespans:
+ source = self._wrap_linespans(source)
+ source = self.wrap(source)
+ if self.linenos == 1:
+ source = self._wrap_tablelinenos(source)
+ source = self._wrap_div(source)
+ if self.full:
+ source = self._wrap_full(source, outfile)
+
+ for t, piece in source:
+ outfile.write(piece)
diff --git a/src/pip/_vendor/pygments/formatters/img.py b/src/pip/_vendor/pygments/formatters/img.py
new file mode 100644
index 000000000..2cc0b2b5b
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/img.py
@@ -0,0 +1,641 @@
+"""
+ pygments.formatters.img
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ Formatter for Pixmap output.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import os
+import sys
+
+from pip._vendor.pygments.formatter import Formatter
+from pip._vendor.pygments.util import get_bool_opt, get_int_opt, get_list_opt, \
+ get_choice_opt
+
+import subprocess
+
+# Import this carefully
+try:
+ from PIL import Image, ImageDraw, ImageFont
+ pil_available = True
+except ImportError:
+ pil_available = False
+
+try:
+ import _winreg
+except ImportError:
+ try:
+ import winreg as _winreg
+ except ImportError:
+ _winreg = None
+
+__all__ = ['ImageFormatter', 'GifImageFormatter', 'JpgImageFormatter',
+ 'BmpImageFormatter']
+
+
+# For some unknown reason every font calls it something different
+STYLES = {
+ 'NORMAL': ['', 'Roman', 'Book', 'Normal', 'Regular', 'Medium'],
+ 'ITALIC': ['Oblique', 'Italic'],
+ 'BOLD': ['Bold'],
+ 'BOLDITALIC': ['Bold Oblique', 'Bold Italic'],
+}
+
+# A sane default for modern systems
+DEFAULT_FONT_NAME_NIX = 'DejaVu Sans Mono'
+DEFAULT_FONT_NAME_WIN = 'Courier New'
+DEFAULT_FONT_NAME_MAC = 'Menlo'
+
+
+class PilNotAvailable(ImportError):
+ """When Python imaging library is not available"""
+
+
+class FontNotFound(Exception):
+ """When there are no usable fonts specified"""
+
+
+class FontManager:
+ """
+ Manages a set of fonts: normal, italic, bold, etc...
+ """
+
+ def __init__(self, font_name, font_size=14):
+ self.font_name = font_name
+ self.font_size = font_size
+ self.fonts = {}
+ self.encoding = None
+ if sys.platform.startswith('win'):
+ if not font_name:
+ self.font_name = DEFAULT_FONT_NAME_WIN
+ self._create_win()
+ elif sys.platform.startswith('darwin'):
+ if not font_name:
+ self.font_name = DEFAULT_FONT_NAME_MAC
+ self._create_mac()
+ else:
+ if not font_name:
+ self.font_name = DEFAULT_FONT_NAME_NIX
+ self._create_nix()
+
+ def _get_nix_font_path(self, name, style):
+ proc = subprocess.Popen(['fc-list', "%s:style=%s" % (name, style), 'file'],
+ stdout=subprocess.PIPE, stderr=None)
+ stdout, _ = proc.communicate()
+ if proc.returncode == 0:
+ lines = stdout.splitlines()
+ for line in lines:
+ if line.startswith(b'Fontconfig warning:'):
+ continue
+ path = line.decode().strip().strip(':')
+ if path:
+ return path
+ return None
+
+ def _create_nix(self):
+ for name in STYLES['NORMAL']:
+ path = self._get_nix_font_path(self.font_name, name)
+ if path is not None:
+ self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size)
+ break
+ else:
+ raise FontNotFound('No usable fonts named: "%s"' %
+ self.font_name)
+ for style in ('ITALIC', 'BOLD', 'BOLDITALIC'):
+ for stylename in STYLES[style]:
+ path = self._get_nix_font_path(self.font_name, stylename)
+ if path is not None:
+ self.fonts[style] = ImageFont.truetype(path, self.font_size)
+ break
+ else:
+ if style == 'BOLDITALIC':
+ self.fonts[style] = self.fonts['BOLD']
+ else:
+ self.fonts[style] = self.fonts['NORMAL']
+
+ def _get_mac_font_path(self, font_map, name, style):
+ return font_map.get((name + ' ' + style).strip().lower())
+
+ def _create_mac(self):
+ font_map = {}
+ for font_dir in (os.path.join(os.getenv("HOME"), 'Library/Fonts/'),
+ '/Library/Fonts/', '/System/Library/Fonts/'):
+ font_map.update(
+ (os.path.splitext(f)[0].lower(), os.path.join(font_dir, f))
+ for f in os.listdir(font_dir)
+ if f.lower().endswith(('ttf', 'ttc')))
+
+ for name in STYLES['NORMAL']:
+ path = self._get_mac_font_path(font_map, self.font_name, name)
+ if path is not None:
+ self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size)
+ break
+ else:
+ raise FontNotFound('No usable fonts named: "%s"' %
+ self.font_name)
+ for style in ('ITALIC', 'BOLD', 'BOLDITALIC'):
+ for stylename in STYLES[style]:
+ path = self._get_mac_font_path(font_map, self.font_name, stylename)
+ if path is not None:
+ self.fonts[style] = ImageFont.truetype(path, self.font_size)
+ break
+ else:
+ if style == 'BOLDITALIC':
+ self.fonts[style] = self.fonts['BOLD']
+ else:
+ self.fonts[style] = self.fonts['NORMAL']
+
+ def _lookup_win(self, key, basename, styles, fail=False):
+ for suffix in ('', ' (TrueType)'):
+ for style in styles:
+ try:
+ valname = '%s%s%s' % (basename, style and ' '+style, suffix)
+ val, _ = _winreg.QueryValueEx(key, valname)
+ return val
+ except OSError:
+ continue
+ else:
+ if fail:
+ raise FontNotFound('Font %s (%s) not found in registry' %
+ (basename, styles[0]))
+ return None
+
+ def _create_win(self):
+ lookuperror = None
+ keynames = [ (_winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows NT\CurrentVersion\Fonts'),
+ (_winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows\CurrentVersion\Fonts'),
+ (_winreg.HKEY_LOCAL_MACHINE, r'Software\Microsoft\Windows NT\CurrentVersion\Fonts'),
+ (_winreg.HKEY_LOCAL_MACHINE, r'Software\Microsoft\Windows\CurrentVersion\Fonts') ]
+ for keyname in keynames:
+ try:
+ key = _winreg.OpenKey(*keyname)
+ try:
+ path = self._lookup_win(key, self.font_name, STYLES['NORMAL'], True)
+ self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size)
+ for style in ('ITALIC', 'BOLD', 'BOLDITALIC'):
+ path = self._lookup_win(key, self.font_name, STYLES[style])
+ if path:
+ self.fonts[style] = ImageFont.truetype(path, self.font_size)
+ else:
+ if style == 'BOLDITALIC':
+ self.fonts[style] = self.fonts['BOLD']
+ else:
+ self.fonts[style] = self.fonts['NORMAL']
+ return
+ except FontNotFound as err:
+ lookuperror = err
+ finally:
+ _winreg.CloseKey(key)
+ except OSError:
+ pass
+ else:
+ # If we get here, we checked all registry keys and had no luck
+ # We can be in one of two situations now:
+ # * All key lookups failed. In this case lookuperror is None and we
+ # will raise a generic error
+ # * At least one lookup failed with a FontNotFound error. In this
+ # case, we will raise that as a more specific error
+ if lookuperror:
+ raise lookuperror
+ raise FontNotFound('Can\'t open Windows font registry key')
+
+ def get_char_size(self):
+ """
+ Get the character size.
+ """
+ return self.fonts['NORMAL'].getsize('M')
+
+ def get_text_size(self, text):
+ """
+ Get the text size(width, height).
+ """
+ return self.fonts['NORMAL'].getsize(text)
+
+ def get_font(self, bold, oblique):
+ """
+ Get the font based on bold and italic flags.
+ """
+ if bold and oblique:
+ return self.fonts['BOLDITALIC']
+ elif bold:
+ return self.fonts['BOLD']
+ elif oblique:
+ return self.fonts['ITALIC']
+ else:
+ return self.fonts['NORMAL']
+
+
+class ImageFormatter(Formatter):
+ """
+ Create a PNG image from source code. This uses the Python Imaging Library to
+ generate a pixmap from the source code.
+
+ .. versionadded:: 0.10
+
+ Additional options accepted:
+
+ `image_format`
+ An image format to output to that is recognised by PIL, these include:
+
+ * "PNG" (default)
+ * "JPEG"
+ * "BMP"
+ * "GIF"
+
+ `line_pad`
+ The extra spacing (in pixels) between each line of text.
+
+ Default: 2
+
+ `font_name`
+ The font name to be used as the base font from which others, such as
+ bold and italic fonts will be generated. This really should be a
+ monospace font to look sane.
+
+ Default: "Courier New" on Windows, "Menlo" on Mac OS, and
+ "DejaVu Sans Mono" on \\*nix
+
+ `font_size`
+ The font size in points to be used.
+
+ Default: 14
+
+ `image_pad`
+ The padding, in pixels to be used at each edge of the resulting image.
+
+ Default: 10
+
+ `line_numbers`
+ Whether line numbers should be shown: True/False
+
+ Default: True
+
+ `line_number_start`
+ The line number of the first line.
+
+ Default: 1
+
+ `line_number_step`
+ The step used when printing line numbers.
+
+ Default: 1
+
+ `line_number_bg`
+ The background colour (in "#123456" format) of the line number bar, or
+ None to use the style background color.
+
+ Default: "#eed"
+
+ `line_number_fg`
+ The text color of the line numbers (in "#123456"-like format).
+
+ Default: "#886"
+
+ `line_number_chars`
+ The number of columns of line numbers allowable in the line number
+ margin.
+
+ Default: 2
+
+ `line_number_bold`
+ Whether line numbers will be bold: True/False
+
+ Default: False
+
+ `line_number_italic`
+ Whether line numbers will be italicized: True/False
+
+ Default: False
+
+ `line_number_separator`
+ Whether a line will be drawn between the line number area and the
+ source code area: True/False
+
+ Default: True
+
+ `line_number_pad`
+ The horizontal padding (in pixels) between the line number margin, and
+ the source code area.
+
+ Default: 6
+
+ `hl_lines`
+ Specify a list of lines to be highlighted.
+
+ .. versionadded:: 1.2
+
+ Default: empty list
+
+ `hl_color`
+ Specify the color for highlighting lines.
+
+ .. versionadded:: 1.2
+
+ Default: highlight color of the selected style
+ """
+
+ # Required by the pygments mapper
+ name = 'img'
+ aliases = ['img', 'IMG', 'png']
+ filenames = ['*.png']
+
+ unicodeoutput = False
+
+ default_image_format = 'png'
+
+ def __init__(self, **options):
+ """
+ See the class docstring for explanation of options.
+ """
+ if not pil_available:
+ raise PilNotAvailable(
+ 'Python Imaging Library is required for this formatter')
+ Formatter.__init__(self, **options)
+ self.encoding = 'latin1' # let pygments.format() do the right thing
+ # Read the style
+ self.styles = dict(self.style)
+ if self.style.background_color is None:
+ self.background_color = '#fff'
+ else:
+ self.background_color = self.style.background_color
+ # Image options
+ self.image_format = get_choice_opt(
+ options, 'image_format', ['png', 'jpeg', 'gif', 'bmp'],
+ self.default_image_format, normcase=True)
+ self.image_pad = get_int_opt(options, 'image_pad', 10)
+ self.line_pad = get_int_opt(options, 'line_pad', 2)
+ # The fonts
+ fontsize = get_int_opt(options, 'font_size', 14)
+ self.fonts = FontManager(options.get('font_name', ''), fontsize)
+ self.fontw, self.fonth = self.fonts.get_char_size()
+ # Line number options
+ self.line_number_fg = options.get('line_number_fg', '#886')
+ self.line_number_bg = options.get('line_number_bg', '#eed')
+ self.line_number_chars = get_int_opt(options,
+ 'line_number_chars', 2)
+ self.line_number_bold = get_bool_opt(options,
+ 'line_number_bold', False)
+ self.line_number_italic = get_bool_opt(options,
+ 'line_number_italic', False)
+ self.line_number_pad = get_int_opt(options, 'line_number_pad', 6)
+ self.line_numbers = get_bool_opt(options, 'line_numbers', True)
+ self.line_number_separator = get_bool_opt(options,
+ 'line_number_separator', True)
+ self.line_number_step = get_int_opt(options, 'line_number_step', 1)
+ self.line_number_start = get_int_opt(options, 'line_number_start', 1)
+ if self.line_numbers:
+ self.line_number_width = (self.fontw * self.line_number_chars +
+ self.line_number_pad * 2)
+ else:
+ self.line_number_width = 0
+ self.hl_lines = []
+ hl_lines_str = get_list_opt(options, 'hl_lines', [])
+ for line in hl_lines_str:
+ try:
+ self.hl_lines.append(int(line))
+ except ValueError:
+ pass
+ self.hl_color = options.get('hl_color',
+ self.style.highlight_color) or '#f90'
+ self.drawables = []
+
+ def get_style_defs(self, arg=''):
+ raise NotImplementedError('The -S option is meaningless for the image '
+ 'formatter. Use -O style=<stylename> instead.')
+
+ def _get_line_height(self):
+ """
+ Get the height of a line.
+ """
+ return self.fonth + self.line_pad
+
+ def _get_line_y(self, lineno):
+ """
+ Get the Y coordinate of a line number.
+ """
+ return lineno * self._get_line_height() + self.image_pad
+
+ def _get_char_width(self):
+ """
+ Get the width of a character.
+ """
+ return self.fontw
+
+ def _get_char_x(self, linelength):
+ """
+ Get the X coordinate of a character position.
+ """
+ return linelength + self.image_pad + self.line_number_width
+
+ def _get_text_pos(self, linelength, lineno):
+ """
+ Get the actual position for a character and line position.
+ """
+ return self._get_char_x(linelength), self._get_line_y(lineno)
+
+ def _get_linenumber_pos(self, lineno):
+ """
+ Get the actual position for the start of a line number.
+ """
+ return (self.image_pad, self._get_line_y(lineno))
+
+ def _get_text_color(self, style):
+ """
+ Get the correct color for the token from the style.
+ """
+ if style['color'] is not None:
+ fill = '#' + style['color']
+ else:
+ fill = '#000'
+ return fill
+
+ def _get_text_bg_color(self, style):
+ """
+ Get the correct background color for the token from the style.
+ """
+ if style['bgcolor'] is not None:
+ bg_color = '#' + style['bgcolor']
+ else:
+ bg_color = None
+ return bg_color
+
+ def _get_style_font(self, style):
+ """
+ Get the correct font for the style.
+ """
+ return self.fonts.get_font(style['bold'], style['italic'])
+
+ def _get_image_size(self, maxlinelength, maxlineno):
+ """
+ Get the required image size.
+ """
+ return (self._get_char_x(maxlinelength) + self.image_pad,
+ self._get_line_y(maxlineno + 0) + self.image_pad)
+
+ def _draw_linenumber(self, posno, lineno):
+ """
+ Remember a line number drawable to paint later.
+ """
+ self._draw_text(
+ self._get_linenumber_pos(posno),
+ str(lineno).rjust(self.line_number_chars),
+ font=self.fonts.get_font(self.line_number_bold,
+ self.line_number_italic),
+ text_fg=self.line_number_fg,
+ text_bg=None,
+ )
+
+ def _draw_text(self, pos, text, font, text_fg, text_bg):
+ """
+ Remember a single drawable tuple to paint later.
+ """
+ self.drawables.append((pos, text, font, text_fg, text_bg))
+
+ def _create_drawables(self, tokensource):
+ """
+ Create drawables for the token content.
+ """
+ lineno = charno = maxcharno = 0
+ maxlinelength = linelength = 0
+ for ttype, value in tokensource:
+ while ttype not in self.styles:
+ ttype = ttype.parent
+ style = self.styles[ttype]
+ # TODO: make sure tab expansion happens earlier in the chain. It
+ # really ought to be done on the input, as to do it right here is
+ # quite complex.
+ value = value.expandtabs(4)
+ lines = value.splitlines(True)
+ # print lines
+ for i, line in enumerate(lines):
+ temp = line.rstrip('\n')
+ if temp:
+ self._draw_text(
+ self._get_text_pos(linelength, lineno),
+ temp,
+ font = self._get_style_font(style),
+ text_fg = self._get_text_color(style),
+ text_bg = self._get_text_bg_color(style),
+ )
+ temp_width, temp_hight = self.fonts.get_text_size(temp)
+ linelength += temp_width
+ maxlinelength = max(maxlinelength, linelength)
+ charno += len(temp)
+ maxcharno = max(maxcharno, charno)
+ if line.endswith('\n'):
+ # add a line for each extra line in the value
+ linelength = 0
+ charno = 0
+ lineno += 1
+ self.maxlinelength = maxlinelength
+ self.maxcharno = maxcharno
+ self.maxlineno = lineno
+
+ def _draw_line_numbers(self):
+ """
+ Create drawables for the line numbers.
+ """
+ if not self.line_numbers:
+ return
+ for p in range(self.maxlineno):
+ n = p + self.line_number_start
+ if (n % self.line_number_step) == 0:
+ self._draw_linenumber(p, n)
+
+ def _paint_line_number_bg(self, im):
+ """
+ Paint the line number background on the image.
+ """
+ if not self.line_numbers:
+ return
+ if self.line_number_fg is None:
+ return
+ draw = ImageDraw.Draw(im)
+ recth = im.size[-1]
+ rectw = self.image_pad + self.line_number_width - self.line_number_pad
+ draw.rectangle([(0, 0), (rectw, recth)],
+ fill=self.line_number_bg)
+ if self.line_number_separator:
+ draw.line([(rectw, 0), (rectw, recth)], fill=self.line_number_fg)
+ del draw
+
+ def format(self, tokensource, outfile):
+ """
+ Format ``tokensource``, an iterable of ``(tokentype, tokenstring)``
+ tuples and write it into ``outfile``.
+
+ This implementation calculates where it should draw each token on the
+ pixmap, then calculates the required pixmap size and draws the items.
+ """
+ self._create_drawables(tokensource)
+ self._draw_line_numbers()
+ im = Image.new(
+ 'RGB',
+ self._get_image_size(self.maxlinelength, self.maxlineno),
+ self.background_color
+ )
+ self._paint_line_number_bg(im)
+ draw = ImageDraw.Draw(im)
+ # Highlight
+ if self.hl_lines:
+ x = self.image_pad + self.line_number_width - self.line_number_pad + 1
+ recth = self._get_line_height()
+ rectw = im.size[0] - x
+ for linenumber in self.hl_lines:
+ y = self._get_line_y(linenumber - 1)
+ draw.rectangle([(x, y), (x + rectw, y + recth)],
+ fill=self.hl_color)
+ for pos, value, font, text_fg, text_bg in self.drawables:
+ if text_bg:
+ text_size = draw.textsize(text=value, font=font)
+ draw.rectangle([pos[0], pos[1], pos[0] + text_size[0], pos[1] + text_size[1]], fill=text_bg)
+ draw.text(pos, value, font=font, fill=text_fg)
+ im.save(outfile, self.image_format.upper())
+
+
+# Add one formatter per format, so that the "-f gif" option gives the correct result
+# when used in pygmentize.
+
+class GifImageFormatter(ImageFormatter):
+ """
+ Create a GIF image from source code. This uses the Python Imaging Library to
+ generate a pixmap from the source code.
+
+ .. versionadded:: 1.0
+ """
+
+ name = 'img_gif'
+ aliases = ['gif']
+ filenames = ['*.gif']
+ default_image_format = 'gif'
+
+
+class JpgImageFormatter(ImageFormatter):
+ """
+ Create a JPEG image from source code. This uses the Python Imaging Library to
+ generate a pixmap from the source code.
+
+ .. versionadded:: 1.0
+ """
+
+ name = 'img_jpg'
+ aliases = ['jpg', 'jpeg']
+ filenames = ['*.jpg']
+ default_image_format = 'jpeg'
+
+
+class BmpImageFormatter(ImageFormatter):
+ """
+ Create a bitmap image from source code. This uses the Python Imaging Library to
+ generate a pixmap from the source code.
+
+ .. versionadded:: 1.0
+ """
+
+ name = 'img_bmp'
+ aliases = ['bmp', 'bitmap']
+ filenames = ['*.bmp']
+ default_image_format = 'bmp'
diff --git a/src/pip/_vendor/pygments/formatters/irc.py b/src/pip/_vendor/pygments/formatters/irc.py
new file mode 100644
index 000000000..3f6d52deb
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/irc.py
@@ -0,0 +1,179 @@
+"""
+ pygments.formatters.irc
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ Formatter for IRC output
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+from pip._vendor.pygments.formatter import Formatter
+from pip._vendor.pygments.token import Keyword, Name, Comment, String, Error, \
+ Number, Operator, Generic, Token, Whitespace
+from pip._vendor.pygments.util import get_choice_opt
+
+
+__all__ = ['IRCFormatter']
+
+
+#: Map token types to a tuple of color values for light and dark
+#: backgrounds.
+IRC_COLORS = {
+ Token: ('', ''),
+
+ Whitespace: ('gray', 'brightblack'),
+ Comment: ('gray', 'brightblack'),
+ Comment.Preproc: ('cyan', 'brightcyan'),
+ Keyword: ('blue', 'brightblue'),
+ Keyword.Type: ('cyan', 'brightcyan'),
+ Operator.Word: ('magenta', 'brightcyan'),
+ Name.Builtin: ('cyan', 'brightcyan'),
+ Name.Function: ('green', 'brightgreen'),
+ Name.Namespace: ('_cyan_', '_brightcyan_'),
+ Name.Class: ('_green_', '_brightgreen_'),
+ Name.Exception: ('cyan', 'brightcyan'),
+ Name.Decorator: ('brightblack', 'gray'),
+ Name.Variable: ('red', 'brightred'),
+ Name.Constant: ('red', 'brightred'),
+ Name.Attribute: ('cyan', 'brightcyan'),
+ Name.Tag: ('brightblue', 'brightblue'),
+ String: ('yellow', 'yellow'),
+ Number: ('blue', 'brightblue'),
+
+ Generic.Deleted: ('brightred', 'brightred'),
+ Generic.Inserted: ('green', 'brightgreen'),
+ Generic.Heading: ('**', '**'),
+ Generic.Subheading: ('*magenta*', '*brightmagenta*'),
+ Generic.Error: ('brightred', 'brightred'),
+
+ Error: ('_brightred_', '_brightred_'),
+}
+
+
+IRC_COLOR_MAP = {
+ 'white': 0,
+ 'black': 1,
+ 'blue': 2,
+ 'brightgreen': 3,
+ 'brightred': 4,
+ 'yellow': 5,
+ 'magenta': 6,
+ 'orange': 7,
+ 'green': 7, #compat w/ ansi
+ 'brightyellow': 8,
+ 'lightgreen': 9,
+ 'brightcyan': 9, # compat w/ ansi
+ 'cyan': 10,
+ 'lightblue': 11,
+ 'red': 11, # compat w/ ansi
+ 'brightblue': 12,
+ 'brightmagenta': 13,
+ 'brightblack': 14,
+ 'gray': 15,
+}
+
+def ircformat(color, text):
+ if len(color) < 1:
+ return text
+ add = sub = ''
+ if '_' in color: # italic
+ add += '\x1D'
+ sub = '\x1D' + sub
+ color = color.strip('_')
+ if '*' in color: # bold
+ add += '\x02'
+ sub = '\x02' + sub
+ color = color.strip('*')
+ # underline (\x1F) not supported
+ # backgrounds (\x03FF,BB) not supported
+ if len(color) > 0: # actual color - may have issues with ircformat("red", "blah")+"10" type stuff
+ add += '\x03' + str(IRC_COLOR_MAP[color]).zfill(2)
+ sub = '\x03' + sub
+ return add + text + sub
+ return '<'+add+'>'+text+'</'+sub+'>'
+
+
+class IRCFormatter(Formatter):
+ r"""
+ Format tokens with IRC color sequences
+
+ The `get_style_defs()` method doesn't do anything special since there is
+ no support for common styles.
+
+ Options accepted:
+
+ `bg`
+ Set to ``"light"`` or ``"dark"`` depending on the terminal's background
+ (default: ``"light"``).
+
+ `colorscheme`
+ A dictionary mapping token types to (lightbg, darkbg) color names or
+ ``None`` (default: ``None`` = use builtin colorscheme).
+
+ `linenos`
+ Set to ``True`` to have line numbers in the output as well
+ (default: ``False`` = no line numbers).
+ """
+ name = 'IRC'
+ aliases = ['irc', 'IRC']
+ filenames = []
+
+ def __init__(self, **options):
+ Formatter.__init__(self, **options)
+ self.darkbg = get_choice_opt(options, 'bg',
+ ['light', 'dark'], 'light') == 'dark'
+ self.colorscheme = options.get('colorscheme', None) or IRC_COLORS
+ self.linenos = options.get('linenos', False)
+ self._lineno = 0
+
+ def _write_lineno(self, outfile):
+ self._lineno += 1
+ outfile.write("\n%04d: " % self._lineno)
+
+ def _format_unencoded_with_lineno(self, tokensource, outfile):
+ self._write_lineno(outfile)
+
+ for ttype, value in tokensource:
+ if value.endswith("\n"):
+ self._write_lineno(outfile)
+ value = value[:-1]
+ color = self.colorscheme.get(ttype)
+ while color is None:
+ ttype = ttype.parent
+ color = self.colorscheme.get(ttype)
+ if color:
+ color = color[self.darkbg]
+ spl = value.split('\n')
+ for line in spl[:-1]:
+ self._write_lineno(outfile)
+ if line:
+ outfile.write(ircformat(color, line[:-1]))
+ if spl[-1]:
+ outfile.write(ircformat(color, spl[-1]))
+ else:
+ outfile.write(value)
+
+ outfile.write("\n")
+
+ def format_unencoded(self, tokensource, outfile):
+ if self.linenos:
+ self._format_unencoded_with_lineno(tokensource, outfile)
+ return
+
+ for ttype, value in tokensource:
+ color = self.colorscheme.get(ttype)
+ while color is None:
+ ttype = ttype[:-1]
+ color = self.colorscheme.get(ttype)
+ if color:
+ color = color[self.darkbg]
+ spl = value.split('\n')
+ for line in spl[:-1]:
+ if line:
+ outfile.write(ircformat(color, line))
+ outfile.write('\n')
+ if spl[-1]:
+ outfile.write(ircformat(color, spl[-1]))
+ else:
+ outfile.write(value)
diff --git a/src/pip/_vendor/pygments/formatters/latex.py b/src/pip/_vendor/pygments/formatters/latex.py
new file mode 100644
index 000000000..4a7375a5c
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/latex.py
@@ -0,0 +1,521 @@
+"""
+ pygments.formatters.latex
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Formatter for LaTeX fancyvrb output.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+from io import StringIO
+
+from pip._vendor.pygments.formatter import Formatter
+from pip._vendor.pygments.lexer import Lexer, do_insertions
+from pip._vendor.pygments.token import Token, STANDARD_TYPES
+from pip._vendor.pygments.util import get_bool_opt, get_int_opt
+
+
+__all__ = ['LatexFormatter']
+
+
+def escape_tex(text, commandprefix):
+ return text.replace('\\', '\x00'). \
+ replace('{', '\x01'). \
+ replace('}', '\x02'). \
+ replace('\x00', r'\%sZbs{}' % commandprefix). \
+ replace('\x01', r'\%sZob{}' % commandprefix). \
+ replace('\x02', r'\%sZcb{}' % commandprefix). \
+ replace('^', r'\%sZca{}' % commandprefix). \
+ replace('_', r'\%sZus{}' % commandprefix). \
+ replace('&', r'\%sZam{}' % commandprefix). \
+ replace('<', r'\%sZlt{}' % commandprefix). \
+ replace('>', r'\%sZgt{}' % commandprefix). \
+ replace('#', r'\%sZsh{}' % commandprefix). \
+ replace('%', r'\%sZpc{}' % commandprefix). \
+ replace('$', r'\%sZdl{}' % commandprefix). \
+ replace('-', r'\%sZhy{}' % commandprefix). \
+ replace("'", r'\%sZsq{}' % commandprefix). \
+ replace('"', r'\%sZdq{}' % commandprefix). \
+ replace('~', r'\%sZti{}' % commandprefix)
+
+
+DOC_TEMPLATE = r'''
+\documentclass{%(docclass)s}
+\usepackage{fancyvrb}
+\usepackage{color}
+\usepackage[%(encoding)s]{inputenc}
+%(preamble)s
+
+%(styledefs)s
+
+\begin{document}
+
+\section*{%(title)s}
+
+%(code)s
+\end{document}
+'''
+
+## Small explanation of the mess below :)
+#
+# The previous version of the LaTeX formatter just assigned a command to
+# each token type defined in the current style. That obviously is
+# problematic if the highlighted code is produced for a different style
+# than the style commands themselves.
+#
+# This version works much like the HTML formatter which assigns multiple
+# CSS classes to each <span> tag, from the most specific to the least
+# specific token type, thus falling back to the parent token type if one
+# is not defined. Here, the classes are there too and use the same short
+# forms given in token.STANDARD_TYPES.
+#
+# Highlighted code now only uses one custom command, which by default is
+# \PY and selectable by the commandprefix option (and in addition the
+# escapes \PYZat, \PYZlb and \PYZrb which haven't been renamed for
+# backwards compatibility purposes).
+#
+# \PY has two arguments: the classes, separated by +, and the text to
+# render in that style. The classes are resolved into the respective
+# style commands by magic, which serves to ignore unknown classes.
+#
+# The magic macros are:
+# * \PY@it, \PY@bf, etc. are unconditionally wrapped around the text
+# to render in \PY@do. Their definition determines the style.
+# * \PY@reset resets \PY@it etc. to do nothing.
+# * \PY@toks parses the list of classes, using magic inspired by the
+# keyval package (but modified to use plusses instead of commas
+# because fancyvrb redefines commas inside its environments).
+# * \PY@tok processes one class, calling the \PY@tok@classname command
+# if it exists.
+# * \PY@tok@classname sets the \PY@it etc. to reflect the chosen style
+# for its class.
+# * \PY resets the style, parses the classnames and then calls \PY@do.
+#
+# Tip: to read this code, print it out in substituted form using e.g.
+# >>> print STYLE_TEMPLATE % {'cp': 'PY'}
+
+STYLE_TEMPLATE = r'''
+\makeatletter
+\def\%(cp)s@reset{\let\%(cp)s@it=\relax \let\%(cp)s@bf=\relax%%
+ \let\%(cp)s@ul=\relax \let\%(cp)s@tc=\relax%%
+ \let\%(cp)s@bc=\relax \let\%(cp)s@ff=\relax}
+\def\%(cp)s@tok#1{\csname %(cp)s@tok@#1\endcsname}
+\def\%(cp)s@toks#1+{\ifx\relax#1\empty\else%%
+ \%(cp)s@tok{#1}\expandafter\%(cp)s@toks\fi}
+\def\%(cp)s@do#1{\%(cp)s@bc{\%(cp)s@tc{\%(cp)s@ul{%%
+ \%(cp)s@it{\%(cp)s@bf{\%(cp)s@ff{#1}}}}}}}
+\def\%(cp)s#1#2{\%(cp)s@reset\%(cp)s@toks#1+\relax+\%(cp)s@do{#2}}
+
+%(styles)s
+
+\def\%(cp)sZbs{\char`\\}
+\def\%(cp)sZus{\char`\_}
+\def\%(cp)sZob{\char`\{}
+\def\%(cp)sZcb{\char`\}}
+\def\%(cp)sZca{\char`\^}
+\def\%(cp)sZam{\char`\&}
+\def\%(cp)sZlt{\char`\<}
+\def\%(cp)sZgt{\char`\>}
+\def\%(cp)sZsh{\char`\#}
+\def\%(cp)sZpc{\char`\%%}
+\def\%(cp)sZdl{\char`\$}
+\def\%(cp)sZhy{\char`\-}
+\def\%(cp)sZsq{\char`\'}
+\def\%(cp)sZdq{\char`\"}
+\def\%(cp)sZti{\char`\~}
+%% for compatibility with earlier versions
+\def\%(cp)sZat{@}
+\def\%(cp)sZlb{[}
+\def\%(cp)sZrb{]}
+\makeatother
+'''
+
+
+def _get_ttype_name(ttype):
+ fname = STANDARD_TYPES.get(ttype)
+ if fname:
+ return fname
+ aname = ''
+ while fname is None:
+ aname = ttype[-1] + aname
+ ttype = ttype.parent
+ fname = STANDARD_TYPES.get(ttype)
+ return fname + aname
+
+
+class LatexFormatter(Formatter):
+ r"""
+ Format tokens as LaTeX code. This needs the `fancyvrb` and `color`
+ standard packages.
+
+ Without the `full` option, code is formatted as one ``Verbatim``
+ environment, like this:
+
+ .. sourcecode:: latex
+
+ \begin{Verbatim}[commandchars=\\\{\}]
+ \PY{k}{def }\PY{n+nf}{foo}(\PY{n}{bar}):
+ \PY{k}{pass}
+ \end{Verbatim}
+
+ Wrapping can be disabled using the `nowrap` option.
+
+ The special command used here (``\PY``) and all the other macros it needs
+ are output by the `get_style_defs` method.
+
+ With the `full` option, a complete LaTeX document is output, including
+ the command definitions in the preamble.
+
+ The `get_style_defs()` method of a `LatexFormatter` returns a string
+ containing ``\def`` commands defining the macros needed inside the
+ ``Verbatim`` environments.
+
+ Additional options accepted:
+
+ `nowrap`
+ If set to ``True``, don't wrap the tokens at all, not even inside a
+ ``\begin{Verbatim}`` environment. This disables most other options
+ (default: ``False``).
+
+ `style`
+ The style to use, can be a string or a Style subclass (default:
+ ``'default'``).
+
+ `full`
+ Tells the formatter to output a "full" document, i.e. a complete
+ self-contained document (default: ``False``).
+
+ `title`
+ If `full` is true, the title that should be used to caption the
+ document (default: ``''``).
+
+ `docclass`
+ If the `full` option is enabled, this is the document class to use
+ (default: ``'article'``).
+
+ `preamble`
+ If the `full` option is enabled, this can be further preamble commands,
+ e.g. ``\usepackage`` (default: ``''``).
+
+ `linenos`
+ If set to ``True``, output line numbers (default: ``False``).
+
+ `linenostart`
+ The line number for the first line (default: ``1``).
+
+ `linenostep`
+ If set to a number n > 1, only every nth line number is printed.
+
+ `verboptions`
+ Additional options given to the Verbatim environment (see the *fancyvrb*
+ docs for possible values) (default: ``''``).
+
+ `commandprefix`
+ The LaTeX commands used to produce colored output are constructed
+ using this prefix and some letters (default: ``'PY'``).
+
+ .. versionadded:: 0.7
+ .. versionchanged:: 0.10
+ The default is now ``'PY'`` instead of ``'C'``.
+
+ `texcomments`
+ If set to ``True``, enables LaTeX comment lines. That is, LaTex markup
+ in comment tokens is not escaped so that LaTeX can render it (default:
+ ``False``).
+
+ .. versionadded:: 1.2
+
+ `mathescape`
+ If set to ``True``, enables LaTeX math mode escape in comments. That
+ is, ``'$...$'`` inside a comment will trigger math mode (default:
+ ``False``).
+
+ .. versionadded:: 1.2
+
+ `escapeinside`
+ If set to a string of length 2, enables escaping to LaTeX. Text
+ delimited by these 2 characters is read as LaTeX code and
+ typeset accordingly. It has no effect in string literals. It has
+ no effect in comments if `texcomments` or `mathescape` is
+ set. (default: ``''``).
+
+ .. versionadded:: 2.0
+
+ `envname`
+ Allows you to pick an alternative environment name replacing Verbatim.
+ The alternate environment still has to support Verbatim's option syntax.
+ (default: ``'Verbatim'``).
+
+ .. versionadded:: 2.0
+ """
+ name = 'LaTeX'
+ aliases = ['latex', 'tex']
+ filenames = ['*.tex']
+
+ def __init__(self, **options):
+ Formatter.__init__(self, **options)
+ self.nowrap = get_bool_opt(options, 'nowrap', False)
+ self.docclass = options.get('docclass', 'article')
+ self.preamble = options.get('preamble', '')
+ self.linenos = get_bool_opt(options, 'linenos', False)
+ self.linenostart = abs(get_int_opt(options, 'linenostart', 1))
+ self.linenostep = abs(get_int_opt(options, 'linenostep', 1))
+ self.verboptions = options.get('verboptions', '')
+ self.nobackground = get_bool_opt(options, 'nobackground', False)
+ self.commandprefix = options.get('commandprefix', 'PY')
+ self.texcomments = get_bool_opt(options, 'texcomments', False)
+ self.mathescape = get_bool_opt(options, 'mathescape', False)
+ self.escapeinside = options.get('escapeinside', '')
+ if len(self.escapeinside) == 2:
+ self.left = self.escapeinside[0]
+ self.right = self.escapeinside[1]
+ else:
+ self.escapeinside = ''
+ self.envname = options.get('envname', 'Verbatim')
+
+ self._create_stylesheet()
+
+ def _create_stylesheet(self):
+ t2n = self.ttype2name = {Token: ''}
+ c2d = self.cmd2def = {}
+ cp = self.commandprefix
+
+ def rgbcolor(col):
+ if col:
+ return ','.join(['%.2f' % (int(col[i] + col[i + 1], 16) / 255.0)
+ for i in (0, 2, 4)])
+ else:
+ return '1,1,1'
+
+ for ttype, ndef in self.style:
+ name = _get_ttype_name(ttype)
+ cmndef = ''
+ if ndef['bold']:
+ cmndef += r'\let\$$@bf=\textbf'
+ if ndef['italic']:
+ cmndef += r'\let\$$@it=\textit'
+ if ndef['underline']:
+ cmndef += r'\let\$$@ul=\underline'
+ if ndef['roman']:
+ cmndef += r'\let\$$@ff=\textrm'
+ if ndef['sans']:
+ cmndef += r'\let\$$@ff=\textsf'
+ if ndef['mono']:
+ cmndef += r'\let\$$@ff=\textsf'
+ if ndef['color']:
+ cmndef += (r'\def\$$@tc##1{\textcolor[rgb]{%s}{##1}}' %
+ rgbcolor(ndef['color']))
+ if ndef['border']:
+ cmndef += (r'\def\$$@bc##1{{\setlength{\fboxsep}{\string -\fboxrule}'
+ r'\fcolorbox[rgb]{%s}{%s}{\strut ##1}}}' %
+ (rgbcolor(ndef['border']),
+ rgbcolor(ndef['bgcolor'])))
+ elif ndef['bgcolor']:
+ cmndef += (r'\def\$$@bc##1{{\setlength{\fboxsep}{0pt}'
+ r'\colorbox[rgb]{%s}{\strut ##1}}}' %
+ rgbcolor(ndef['bgcolor']))
+ if cmndef == '':
+ continue
+ cmndef = cmndef.replace('$$', cp)
+ t2n[ttype] = name
+ c2d[name] = cmndef
+
+ def get_style_defs(self, arg=''):
+ """
+ Return the command sequences needed to define the commands
+ used to format text in the verbatim environment. ``arg`` is ignored.
+ """
+ cp = self.commandprefix
+ styles = []
+ for name, definition in self.cmd2def.items():
+ styles.append(r'\@namedef{%s@tok@%s}{%s}' % (cp, name, definition))
+ return STYLE_TEMPLATE % {'cp': self.commandprefix,
+ 'styles': '\n'.join(styles)}
+
+ def format_unencoded(self, tokensource, outfile):
+ # TODO: add support for background colors
+ t2n = self.ttype2name
+ cp = self.commandprefix
+
+ if self.full:
+ realoutfile = outfile
+ outfile = StringIO()
+
+ if not self.nowrap:
+ outfile.write('\\begin{' + self.envname + '}[commandchars=\\\\\\{\\}')
+ if self.linenos:
+ start, step = self.linenostart, self.linenostep
+ outfile.write(',numbers=left' +
+ (start and ',firstnumber=%d' % start or '') +
+ (step and ',stepnumber=%d' % step or ''))
+ if self.mathescape or self.texcomments or self.escapeinside:
+ outfile.write(',codes={\\catcode`\\$=3\\catcode`\\^=7'
+ '\\catcode`\\_=8\\relax}')
+ if self.verboptions:
+ outfile.write(',' + self.verboptions)
+ outfile.write(']\n')
+
+ for ttype, value in tokensource:
+ if ttype in Token.Comment:
+ if self.texcomments:
+ # Try to guess comment starting lexeme and escape it ...
+ start = value[0:1]
+ for i in range(1, len(value)):
+ if start[0] != value[i]:
+ break
+ start += value[i]
+
+ value = value[len(start):]
+ start = escape_tex(start, cp)
+
+ # ... but do not escape inside comment.
+ value = start + value
+ elif self.mathescape:
+ # Only escape parts not inside a math environment.
+ parts = value.split('$')
+ in_math = False
+ for i, part in enumerate(parts):
+ if not in_math:
+ parts[i] = escape_tex(part, cp)
+ in_math = not in_math
+ value = '$'.join(parts)
+ elif self.escapeinside:
+ text = value
+ value = ''
+ while text:
+ a, sep1, text = text.partition(self.left)
+ if sep1:
+ b, sep2, text = text.partition(self.right)
+ if sep2:
+ value += escape_tex(a, cp) + b
+ else:
+ value += escape_tex(a + sep1 + b, cp)
+ else:
+ value += escape_tex(a, cp)
+ else:
+ value = escape_tex(value, cp)
+ elif ttype not in Token.Escape:
+ value = escape_tex(value, cp)
+ styles = []
+ while ttype is not Token:
+ try:
+ styles.append(t2n[ttype])
+ except KeyError:
+ # not in current style
+ styles.append(_get_ttype_name(ttype))
+ ttype = ttype.parent
+ styleval = '+'.join(reversed(styles))
+ if styleval:
+ spl = value.split('\n')
+ for line in spl[:-1]:
+ if line:
+ outfile.write("\\%s{%s}{%s}" % (cp, styleval, line))
+ outfile.write('\n')
+ if spl[-1]:
+ outfile.write("\\%s{%s}{%s}" % (cp, styleval, spl[-1]))
+ else:
+ outfile.write(value)
+
+ if not self.nowrap:
+ outfile.write('\\end{' + self.envname + '}\n')
+
+ if self.full:
+ encoding = self.encoding or 'utf8'
+ # map known existings encodings from LaTeX distribution
+ encoding = {
+ 'utf_8': 'utf8',
+ 'latin_1': 'latin1',
+ 'iso_8859_1': 'latin1',
+ }.get(encoding.replace('-', '_'), encoding)
+ realoutfile.write(DOC_TEMPLATE %
+ dict(docclass = self.docclass,
+ preamble = self.preamble,
+ title = self.title,
+ encoding = encoding,
+ styledefs = self.get_style_defs(),
+ code = outfile.getvalue()))
+
+
+class LatexEmbeddedLexer(Lexer):
+ """
+ This lexer takes one lexer as argument, the lexer for the language
+ being formatted, and the left and right delimiters for escaped text.
+
+ First everything is scanned using the language lexer to obtain
+ strings and comments. All other consecutive tokens are merged and
+ the resulting text is scanned for escaped segments, which are given
+ the Token.Escape type. Finally text that is not escaped is scanned
+ again with the language lexer.
+ """
+ def __init__(self, left, right, lang, **options):
+ self.left = left
+ self.right = right
+ self.lang = lang
+ Lexer.__init__(self, **options)
+
+ def get_tokens_unprocessed(self, text):
+ # find and remove all the escape tokens (replace with an empty string)
+ # this is very similar to DelegatingLexer.get_tokens_unprocessed.
+ buffered = ''
+ insertions = []
+ insertion_buf = []
+ for i, t, v in self._find_safe_escape_tokens(text):
+ if t is None:
+ if insertion_buf:
+ insertions.append((len(buffered), insertion_buf))
+ insertion_buf = []
+ buffered += v
+ else:
+ insertion_buf.append((i, t, v))
+ if insertion_buf:
+ insertions.append((len(buffered), insertion_buf))
+ return do_insertions(insertions,
+ self.lang.get_tokens_unprocessed(buffered))
+
+ def _find_safe_escape_tokens(self, text):
+ """ find escape tokens that are not in strings or comments """
+ for i, t, v in self._filter_to(
+ self.lang.get_tokens_unprocessed(text),
+ lambda t: t in Token.Comment or t in Token.String
+ ):
+ if t is None:
+ for i2, t2, v2 in self._find_escape_tokens(v):
+ yield i + i2, t2, v2
+ else:
+ yield i, None, v
+
+ def _filter_to(self, it, pred):
+ """ Keep only the tokens that match `pred`, merge the others together """
+ buf = ''
+ idx = 0
+ for i, t, v in it:
+ if pred(t):
+ if buf:
+ yield idx, None, buf
+ buf = ''
+ yield i, t, v
+ else:
+ if not buf:
+ idx = i
+ buf += v
+ if buf:
+ yield idx, None, buf
+
+ def _find_escape_tokens(self, text):
+ """ Find escape tokens within text, give token=None otherwise """
+ index = 0
+ while text:
+ a, sep1, text = text.partition(self.left)
+ if a:
+ yield index, None, a
+ index += len(a)
+ if sep1:
+ b, sep2, text = text.partition(self.right)
+ if sep2:
+ yield index + len(sep1), Token.Escape, b
+ index += len(sep1) + len(b) + len(sep2)
+ else:
+ yield index, Token.Error, sep1
+ index += len(sep1)
+ text = b
diff --git a/src/pip/_vendor/pygments/formatters/other.py b/src/pip/_vendor/pygments/formatters/other.py
new file mode 100644
index 000000000..1e39cd42a
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/other.py
@@ -0,0 +1,161 @@
+"""
+ pygments.formatters.other
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Other formatters: NullFormatter, RawTokenFormatter.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+from pip._vendor.pygments.formatter import Formatter
+from pip._vendor.pygments.util import get_choice_opt
+from pip._vendor.pygments.token import Token
+from pip._vendor.pygments.console import colorize
+
+__all__ = ['NullFormatter', 'RawTokenFormatter', 'TestcaseFormatter']
+
+
+class NullFormatter(Formatter):
+ """
+ Output the text unchanged without any formatting.
+ """
+ name = 'Text only'
+ aliases = ['text', 'null']
+ filenames = ['*.txt']
+
+ def format(self, tokensource, outfile):
+ enc = self.encoding
+ for ttype, value in tokensource:
+ if enc:
+ outfile.write(value.encode(enc))
+ else:
+ outfile.write(value)
+
+
+class RawTokenFormatter(Formatter):
+ r"""
+ Format tokens as a raw representation for storing token streams.
+
+ The format is ``tokentype<TAB>repr(tokenstring)\n``. The output can later
+ be converted to a token stream with the `RawTokenLexer`, described in the
+ :doc:`lexer list <lexers>`.
+
+ Only two options are accepted:
+
+ `compress`
+ If set to ``'gz'`` or ``'bz2'``, compress the output with the given
+ compression algorithm after encoding (default: ``''``).
+ `error_color`
+ If set to a color name, highlight error tokens using that color. If
+ set but with no value, defaults to ``'red'``.
+
+ .. versionadded:: 0.11
+
+ """
+ name = 'Raw tokens'
+ aliases = ['raw', 'tokens']
+ filenames = ['*.raw']
+
+ unicodeoutput = False
+
+ def __init__(self, **options):
+ Formatter.__init__(self, **options)
+ # We ignore self.encoding if it is set, since it gets set for lexer
+ # and formatter if given with -Oencoding on the command line.
+ # The RawTokenFormatter outputs only ASCII. Override here.
+ self.encoding = 'ascii' # let pygments.format() do the right thing
+ self.compress = get_choice_opt(options, 'compress',
+ ['', 'none', 'gz', 'bz2'], '')
+ self.error_color = options.get('error_color', None)
+ if self.error_color is True:
+ self.error_color = 'red'
+ if self.error_color is not None:
+ try:
+ colorize(self.error_color, '')
+ except KeyError:
+ raise ValueError("Invalid color %r specified" %
+ self.error_color)
+
+ def format(self, tokensource, outfile):
+ try:
+ outfile.write(b'')
+ except TypeError:
+ raise TypeError('The raw tokens formatter needs a binary '
+ 'output file')
+ if self.compress == 'gz':
+ import gzip
+ outfile = gzip.GzipFile('', 'wb', 9, outfile)
+
+ write = outfile.write
+ flush = outfile.close
+ elif self.compress == 'bz2':
+ import bz2
+ compressor = bz2.BZ2Compressor(9)
+
+ def write(text):
+ outfile.write(compressor.compress(text))
+
+ def flush():
+ outfile.write(compressor.flush())
+ outfile.flush()
+ else:
+ write = outfile.write
+ flush = outfile.flush
+
+ if self.error_color:
+ for ttype, value in tokensource:
+ line = b"%r\t%r\n" % (ttype, value)
+ if ttype is Token.Error:
+ write(colorize(self.error_color, line))
+ else:
+ write(line)
+ else:
+ for ttype, value in tokensource:
+ write(b"%r\t%r\n" % (ttype, value))
+ flush()
+
+
+TESTCASE_BEFORE = '''\
+ def testNeedsName(lexer):
+ fragment = %r
+ tokens = [
+'''
+TESTCASE_AFTER = '''\
+ ]
+ assert list(lexer.get_tokens(fragment)) == tokens
+'''
+
+
+class TestcaseFormatter(Formatter):
+ """
+ Format tokens as appropriate for a new testcase.
+
+ .. versionadded:: 2.0
+ """
+ name = 'Testcase'
+ aliases = ['testcase']
+
+ def __init__(self, **options):
+ Formatter.__init__(self, **options)
+ if self.encoding is not None and self.encoding != 'utf-8':
+ raise ValueError("Only None and utf-8 are allowed encodings.")
+
+ def format(self, tokensource, outfile):
+ indentation = ' ' * 12
+ rawbuf = []
+ outbuf = []
+ for ttype, value in tokensource:
+ rawbuf.append(value)
+ outbuf.append('%s(%s, %r),\n' % (indentation, ttype, value))
+
+ before = TESTCASE_BEFORE % (''.join(rawbuf),)
+ during = ''.join(outbuf)
+ after = TESTCASE_AFTER
+ if self.encoding is None:
+ outfile.write(before + during + after)
+ else:
+ outfile.write(before.encode('utf-8'))
+ outfile.write(during.encode('utf-8'))
+ outfile.write(after.encode('utf-8'))
+ outfile.flush()
diff --git a/src/pip/_vendor/pygments/formatters/pangomarkup.py b/src/pip/_vendor/pygments/formatters/pangomarkup.py
new file mode 100644
index 000000000..bd00866b8
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/pangomarkup.py
@@ -0,0 +1,83 @@
+"""
+ pygments.formatters.pangomarkup
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Formatter for Pango markup output.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+from pip._vendor.pygments.formatter import Formatter
+
+
+__all__ = ['PangoMarkupFormatter']
+
+
+_escape_table = {
+ ord('&'): '&amp;',
+ ord('<'): '&lt;',
+}
+
+
+def escape_special_chars(text, table=_escape_table):
+ """Escape & and < for Pango Markup."""
+ return text.translate(table)
+
+
+class PangoMarkupFormatter(Formatter):
+ """
+ Format tokens as Pango Markup code. It can then be rendered to an SVG.
+
+ .. versionadded:: 2.9
+ """
+
+ name = 'Pango Markup'
+ aliases = ['pango', 'pangomarkup']
+ filenames = []
+
+ def __init__(self, **options):
+ Formatter.__init__(self, **options)
+
+ self.styles = {}
+
+ for token, style in self.style:
+ start = ''
+ end = ''
+ if style['color']:
+ start += '<span fgcolor="#%s">' % style['color']
+ end = '</span>' + end
+ if style['bold']:
+ start += '<b>'
+ end = '</b>' + end
+ if style['italic']:
+ start += '<i>'
+ end = '</i>' + end
+ if style['underline']:
+ start += '<u>'
+ end = '</u>' + end
+ self.styles[token] = (start, end)
+
+ def format_unencoded(self, tokensource, outfile):
+ lastval = ''
+ lasttype = None
+
+ outfile.write('<tt>')
+
+ for ttype, value in tokensource:
+ while ttype not in self.styles:
+ ttype = ttype.parent
+ if ttype == lasttype:
+ lastval += escape_special_chars(value)
+ else:
+ if lastval:
+ stylebegin, styleend = self.styles[lasttype]
+ outfile.write(stylebegin + lastval + styleend)
+ lastval = escape_special_chars(value)
+ lasttype = ttype
+
+ if lastval:
+ stylebegin, styleend = self.styles[lasttype]
+ outfile.write(stylebegin + lastval + styleend)
+
+ outfile.write('</tt>')
diff --git a/src/pip/_vendor/pygments/formatters/rtf.py b/src/pip/_vendor/pygments/formatters/rtf.py
new file mode 100644
index 000000000..4114d1688
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/rtf.py
@@ -0,0 +1,146 @@
+"""
+ pygments.formatters.rtf
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ A formatter that generates RTF files.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+from pip._vendor.pygments.formatter import Formatter
+from pip._vendor.pygments.util import get_int_opt, surrogatepair
+
+
+__all__ = ['RtfFormatter']
+
+
+class RtfFormatter(Formatter):
+ """
+ Format tokens as RTF markup. This formatter automatically outputs full RTF
+ documents with color information and other useful stuff. Perfect for Copy and
+ Paste into Microsoft(R) Word(R) documents.
+
+ Please note that ``encoding`` and ``outencoding`` options are ignored.
+ The RTF format is ASCII natively, but handles unicode characters correctly
+ thanks to escape sequences.
+
+ .. versionadded:: 0.6
+
+ Additional options accepted:
+
+ `style`
+ The style to use, can be a string or a Style subclass (default:
+ ``'default'``).
+
+ `fontface`
+ The used font family, for example ``Bitstream Vera Sans``. Defaults to
+ some generic font which is supposed to have fixed width.
+
+ `fontsize`
+ Size of the font used. Size is specified in half points. The
+ default is 24 half-points, giving a size 12 font.
+
+ .. versionadded:: 2.0
+ """
+ name = 'RTF'
+ aliases = ['rtf']
+ filenames = ['*.rtf']
+
+ def __init__(self, **options):
+ r"""
+ Additional options accepted:
+
+ ``fontface``
+ Name of the font used. Could for example be ``'Courier New'``
+ to further specify the default which is ``'\fmodern'``. The RTF
+ specification claims that ``\fmodern`` are "Fixed-pitch serif
+ and sans serif fonts". Hope every RTF implementation thinks
+ the same about modern...
+
+ """
+ Formatter.__init__(self, **options)
+ self.fontface = options.get('fontface') or ''
+ self.fontsize = get_int_opt(options, 'fontsize', 0)
+
+ def _escape(self, text):
+ return text.replace('\\', '\\\\') \
+ .replace('{', '\\{') \
+ .replace('}', '\\}')
+
+ def _escape_text(self, text):
+ # empty strings, should give a small performance improvement
+ if not text:
+ return ''
+
+ # escape text
+ text = self._escape(text)
+
+ buf = []
+ for c in text:
+ cn = ord(c)
+ if cn < (2**7):
+ # ASCII character
+ buf.append(str(c))
+ elif (2**7) <= cn < (2**16):
+ # single unicode escape sequence
+ buf.append('{\\u%d}' % cn)
+ elif (2**16) <= cn:
+ # RTF limits unicode to 16 bits.
+ # Force surrogate pairs
+ buf.append('{\\u%d}{\\u%d}' % surrogatepair(cn))
+
+ return ''.join(buf).replace('\n', '\\par\n')
+
+ def format_unencoded(self, tokensource, outfile):
+ # rtf 1.8 header
+ outfile.write('{\\rtf1\\ansi\\uc0\\deff0'
+ '{\\fonttbl{\\f0\\fmodern\\fprq1\\fcharset0%s;}}'
+ '{\\colortbl;' % (self.fontface and
+ ' ' + self._escape(self.fontface) or
+ ''))
+
+ # convert colors and save them in a mapping to access them later.
+ color_mapping = {}
+ offset = 1
+ for _, style in self.style:
+ for color in style['color'], style['bgcolor'], style['border']:
+ if color and color not in color_mapping:
+ color_mapping[color] = offset
+ outfile.write('\\red%d\\green%d\\blue%d;' % (
+ int(color[0:2], 16),
+ int(color[2:4], 16),
+ int(color[4:6], 16)
+ ))
+ offset += 1
+ outfile.write('}\\f0 ')
+ if self.fontsize:
+ outfile.write('\\fs%d' % self.fontsize)
+
+ # highlight stream
+ for ttype, value in tokensource:
+ while not self.style.styles_token(ttype) and ttype.parent:
+ ttype = ttype.parent
+ style = self.style.style_for_token(ttype)
+ buf = []
+ if style['bgcolor']:
+ buf.append('\\cb%d' % color_mapping[style['bgcolor']])
+ if style['color']:
+ buf.append('\\cf%d' % color_mapping[style['color']])
+ if style['bold']:
+ buf.append('\\b')
+ if style['italic']:
+ buf.append('\\i')
+ if style['underline']:
+ buf.append('\\ul')
+ if style['border']:
+ buf.append('\\chbrdr\\chcfpat%d' %
+ color_mapping[style['border']])
+ start = ''.join(buf)
+ if start:
+ outfile.write('{%s ' % start)
+ outfile.write(self._escape_text(value))
+ if start:
+ outfile.write('}')
+
+ outfile.write('}')
diff --git a/src/pip/_vendor/pygments/formatters/svg.py b/src/pip/_vendor/pygments/formatters/svg.py
new file mode 100644
index 000000000..075150a4b
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/svg.py
@@ -0,0 +1,188 @@
+"""
+ pygments.formatters.svg
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ Formatter for SVG output.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+from pip._vendor.pygments.formatter import Formatter
+from pip._vendor.pygments.token import Comment
+from pip._vendor.pygments.util import get_bool_opt, get_int_opt
+
+__all__ = ['SvgFormatter']
+
+
+def escape_html(text):
+ """Escape &, <, > as well as single and double quotes for HTML."""
+ return text.replace('&', '&amp;'). \
+ replace('<', '&lt;'). \
+ replace('>', '&gt;'). \
+ replace('"', '&quot;'). \
+ replace("'", '&#39;')
+
+
+class2style = {}
+
+class SvgFormatter(Formatter):
+ """
+ Format tokens as an SVG graphics file. This formatter is still experimental.
+ Each line of code is a ``<text>`` element with explicit ``x`` and ``y``
+ coordinates containing ``<tspan>`` elements with the individual token styles.
+
+ By default, this formatter outputs a full SVG document including doctype
+ declaration and the ``<svg>`` root element.
+
+ .. versionadded:: 0.9
+
+ Additional options accepted:
+
+ `nowrap`
+ Don't wrap the SVG ``<text>`` elements in ``<svg><g>`` elements and
+ don't add a XML declaration and a doctype. If true, the `fontfamily`
+ and `fontsize` options are ignored. Defaults to ``False``.
+
+ `fontfamily`
+ The value to give the wrapping ``<g>`` element's ``font-family``
+ attribute, defaults to ``"monospace"``.
+
+ `fontsize`
+ The value to give the wrapping ``<g>`` element's ``font-size``
+ attribute, defaults to ``"14px"``.
+
+ `linenos`
+ If ``True``, add line numbers (default: ``False``).
+
+ `linenostart`
+ The line number for the first line (default: ``1``).
+
+ `linenostep`
+ If set to a number n > 1, only every nth line number is printed.
+
+ `linenowidth`
+ Maximum width devoted to line numbers (default: ``3*ystep``, sufficient
+ for up to 4-digit line numbers. Increase width for longer code blocks).
+
+ `xoffset`
+ Starting offset in X direction, defaults to ``0``.
+
+ `yoffset`
+ Starting offset in Y direction, defaults to the font size if it is given
+ in pixels, or ``20`` else. (This is necessary since text coordinates
+ refer to the text baseline, not the top edge.)
+
+ `ystep`
+ Offset to add to the Y coordinate for each subsequent line. This should
+ roughly be the text size plus 5. It defaults to that value if the text
+ size is given in pixels, or ``25`` else.
+
+ `spacehack`
+ Convert spaces in the source to ``&#160;``, which are non-breaking
+ spaces. SVG provides the ``xml:space`` attribute to control how
+ whitespace inside tags is handled, in theory, the ``preserve`` value
+ could be used to keep all whitespace as-is. However, many current SVG
+ viewers don't obey that rule, so this option is provided as a workaround
+ and defaults to ``True``.
+ """
+ name = 'SVG'
+ aliases = ['svg']
+ filenames = ['*.svg']
+
+ def __init__(self, **options):
+ Formatter.__init__(self, **options)
+ self.nowrap = get_bool_opt(options, 'nowrap', False)
+ self.fontfamily = options.get('fontfamily', 'monospace')
+ self.fontsize = options.get('fontsize', '14px')
+ self.xoffset = get_int_opt(options, 'xoffset', 0)
+ fs = self.fontsize.strip()
+ if fs.endswith('px'): fs = fs[:-2].strip()
+ try:
+ int_fs = int(fs)
+ except:
+ int_fs = 20
+ self.yoffset = get_int_opt(options, 'yoffset', int_fs)
+ self.ystep = get_int_opt(options, 'ystep', int_fs + 5)
+ self.spacehack = get_bool_opt(options, 'spacehack', True)
+ self.linenos = get_bool_opt(options,'linenos',False)
+ self.linenostart = get_int_opt(options,'linenostart',1)
+ self.linenostep = get_int_opt(options,'linenostep',1)
+ self.linenowidth = get_int_opt(options,'linenowidth', 3*self.ystep)
+ self._stylecache = {}
+
+ def format_unencoded(self, tokensource, outfile):
+ """
+ Format ``tokensource``, an iterable of ``(tokentype, tokenstring)``
+ tuples and write it into ``outfile``.
+
+ For our implementation we put all lines in their own 'line group'.
+ """
+ x = self.xoffset
+ y = self.yoffset
+ if not self.nowrap:
+ if self.encoding:
+ outfile.write('<?xml version="1.0" encoding="%s"?>\n' %
+ self.encoding)
+ else:
+ outfile.write('<?xml version="1.0"?>\n')
+ outfile.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" '
+ '"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/'
+ 'svg10.dtd">\n')
+ outfile.write('<svg xmlns="http://www.w3.org/2000/svg">\n')
+ outfile.write('<g font-family="%s" font-size="%s">\n' %
+ (self.fontfamily, self.fontsize))
+
+ counter = self.linenostart
+ counter_step = self.linenostep
+ counter_style = self._get_style(Comment)
+ line_x = x
+
+ if self.linenos:
+ if counter % counter_step == 0:
+ outfile.write('<text x="%s" y="%s" %s text-anchor="end">%s</text>' %
+ (x+self.linenowidth,y,counter_style,counter))
+ line_x += self.linenowidth + self.ystep
+ counter += 1
+
+ outfile.write('<text x="%s" y="%s" xml:space="preserve">' % (line_x, y))
+ for ttype, value in tokensource:
+ style = self._get_style(ttype)
+ tspan = style and '<tspan' + style + '>' or ''
+ tspanend = tspan and '</tspan>' or ''
+ value = escape_html(value)
+ if self.spacehack:
+ value = value.expandtabs().replace(' ', '&#160;')
+ parts = value.split('\n')
+ for part in parts[:-1]:
+ outfile.write(tspan + part + tspanend)
+ y += self.ystep
+ outfile.write('</text>\n')
+ if self.linenos and counter % counter_step == 0:
+ outfile.write('<text x="%s" y="%s" text-anchor="end" %s>%s</text>' %
+ (x+self.linenowidth,y,counter_style,counter))
+
+ counter += 1
+ outfile.write('<text x="%s" y="%s" ' 'xml:space="preserve">' % (line_x,y))
+ outfile.write(tspan + parts[-1] + tspanend)
+ outfile.write('</text>')
+
+ if not self.nowrap:
+ outfile.write('</g></svg>\n')
+
+ def _get_style(self, tokentype):
+ if tokentype in self._stylecache:
+ return self._stylecache[tokentype]
+ otokentype = tokentype
+ while not self.style.styles_token(tokentype):
+ tokentype = tokentype.parent
+ value = self.style.style_for_token(tokentype)
+ result = ''
+ if value['color']:
+ result = ' fill="#' + value['color'] + '"'
+ if value['bold']:
+ result += ' font-weight="bold"'
+ if value['italic']:
+ result += ' font-style="italic"'
+ self._stylecache[otokentype] = result
+ return result
diff --git a/src/pip/_vendor/pygments/formatters/terminal.py b/src/pip/_vendor/pygments/formatters/terminal.py
new file mode 100644
index 000000000..e0bda16a2
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/terminal.py
@@ -0,0 +1,127 @@
+"""
+ pygments.formatters.terminal
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Formatter for terminal output with ANSI sequences.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+from pip._vendor.pygments.formatter import Formatter
+from pip._vendor.pygments.token import Keyword, Name, Comment, String, Error, \
+ Number, Operator, Generic, Token, Whitespace
+from pip._vendor.pygments.console import ansiformat
+from pip._vendor.pygments.util import get_choice_opt
+
+
+__all__ = ['TerminalFormatter']
+
+
+#: Map token types to a tuple of color values for light and dark
+#: backgrounds.
+TERMINAL_COLORS = {
+ Token: ('', ''),
+
+ Whitespace: ('gray', 'brightblack'),
+ Comment: ('gray', 'brightblack'),
+ Comment.Preproc: ('cyan', 'brightcyan'),
+ Keyword: ('blue', 'brightblue'),
+ Keyword.Type: ('cyan', 'brightcyan'),
+ Operator.Word: ('magenta', 'brightmagenta'),
+ Name.Builtin: ('cyan', 'brightcyan'),
+ Name.Function: ('green', 'brightgreen'),
+ Name.Namespace: ('_cyan_', '_brightcyan_'),
+ Name.Class: ('_green_', '_brightgreen_'),
+ Name.Exception: ('cyan', 'brightcyan'),
+ Name.Decorator: ('brightblack', 'gray'),
+ Name.Variable: ('red', 'brightred'),
+ Name.Constant: ('red', 'brightred'),
+ Name.Attribute: ('cyan', 'brightcyan'),
+ Name.Tag: ('brightblue', 'brightblue'),
+ String: ('yellow', 'yellow'),
+ Number: ('blue', 'brightblue'),
+
+ Generic.Deleted: ('brightred', 'brightred'),
+ Generic.Inserted: ('green', 'brightgreen'),
+ Generic.Heading: ('**', '**'),
+ Generic.Subheading: ('*magenta*', '*brightmagenta*'),
+ Generic.Prompt: ('**', '**'),
+ Generic.Error: ('brightred', 'brightred'),
+
+ Error: ('_brightred_', '_brightred_'),
+}
+
+
+class TerminalFormatter(Formatter):
+ r"""
+ Format tokens with ANSI color sequences, for output in a text console.
+ Color sequences are terminated at newlines, so that paging the output
+ works correctly.
+
+ The `get_style_defs()` method doesn't do anything special since there is
+ no support for common styles.
+
+ Options accepted:
+
+ `bg`
+ Set to ``"light"`` or ``"dark"`` depending on the terminal's background
+ (default: ``"light"``).
+
+ `colorscheme`
+ A dictionary mapping token types to (lightbg, darkbg) color names or
+ ``None`` (default: ``None`` = use builtin colorscheme).
+
+ `linenos`
+ Set to ``True`` to have line numbers on the terminal output as well
+ (default: ``False`` = no line numbers).
+ """
+ name = 'Terminal'
+ aliases = ['terminal', 'console']
+ filenames = []
+
+ def __init__(self, **options):
+ Formatter.__init__(self, **options)
+ self.darkbg = get_choice_opt(options, 'bg',
+ ['light', 'dark'], 'light') == 'dark'
+ self.colorscheme = options.get('colorscheme', None) or TERMINAL_COLORS
+ self.linenos = options.get('linenos', False)
+ self._lineno = 0
+
+ def format(self, tokensource, outfile):
+ return Formatter.format(self, tokensource, outfile)
+
+ def _write_lineno(self, outfile):
+ self._lineno += 1
+ outfile.write("%s%04d: " % (self._lineno != 1 and '\n' or '', self._lineno))
+
+ def _get_color(self, ttype):
+ # self.colorscheme is a dict containing usually generic types, so we
+ # have to walk the tree of dots. The base Token type must be a key,
+ # even if it's empty string, as in the default above.
+ colors = self.colorscheme.get(ttype)
+ while colors is None:
+ ttype = ttype.parent
+ colors = self.colorscheme.get(ttype)
+ return colors[self.darkbg]
+
+ def format_unencoded(self, tokensource, outfile):
+ if self.linenos:
+ self._write_lineno(outfile)
+
+ for ttype, value in tokensource:
+ color = self._get_color(ttype)
+
+ for line in value.splitlines(True):
+ if color:
+ outfile.write(ansiformat(color, line.rstrip('\n')))
+ else:
+ outfile.write(line.rstrip('\n'))
+ if line.endswith('\n'):
+ if self.linenos:
+ self._write_lineno(outfile)
+ else:
+ outfile.write('\n')
+
+ if self.linenos:
+ outfile.write("\n")
diff --git a/src/pip/_vendor/pygments/formatters/terminal256.py b/src/pip/_vendor/pygments/formatters/terminal256.py
new file mode 100644
index 000000000..201b3c328
--- /dev/null
+++ b/src/pip/_vendor/pygments/formatters/terminal256.py
@@ -0,0 +1,338 @@
+"""
+ pygments.formatters.terminal256
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Formatter for 256-color terminal output with ANSI sequences.
+
+ RGB-to-XTERM color conversion routines adapted from xterm256-conv
+ tool (http://frexx.de/xterm-256-notes/data/xterm256-conv2.tar.bz2)
+ by Wolfgang Frisch.
+
+ Formatter version 1.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+# TODO:
+# - Options to map style's bold/underline/italic/border attributes
+# to some ANSI attrbutes (something like 'italic=underline')
+# - An option to output "style RGB to xterm RGB/index" conversion table
+# - An option to indicate that we are running in "reverse background"
+# xterm. This means that default colors are white-on-black, not
+# black-on-while, so colors like "white background" need to be converted
+# to "white background, black foreground", etc...
+
+from pip._vendor.pygments.formatter import Formatter
+from pip._vendor.pygments.console import codes
+from pip._vendor.pygments.style import ansicolors
+
+
+__all__ = ['Terminal256Formatter', 'TerminalTrueColorFormatter']
+
+
+class EscapeSequence:
+ def __init__(self, fg=None, bg=None, bold=False, underline=False, italic=False):
+ self.fg = fg
+ self.bg = bg
+ self.bold = bold
+ self.underline = underline
+ self.italic = italic
+
+ def escape(self, attrs):
+ if len(attrs):
+ return "\x1b[" + ";".join(attrs) + "m"
+ return ""
+
+ def color_string(self):
+ attrs = []
+ if self.fg is not None:
+ if self.fg in ansicolors:
+ esc = codes[self.fg.replace('ansi','')]
+ if ';01m' in esc:
+ self.bold = True
+ # extract fg color code.
+ attrs.append(esc[2:4])
+ else:
+ attrs.extend(("38", "5", "%i" % self.fg))
+ if self.bg is not None:
+ if self.bg in ansicolors:
+ esc = codes[self.bg.replace('ansi','')]
+ # extract fg color code, add 10 for bg.
+ attrs.append(str(int(esc[2:4])+10))
+ else:
+ attrs.extend(("48", "5", "%i" % self.bg))
+ if self.bold:
+ attrs.append("01")
+ if self.underline:
+ attrs.append("04")
+ if self.italic:
+ attrs.append("03")
+ return self.escape(attrs)
+
+ def true_color_string(self):
+ attrs = []
+ if self.fg:
+ attrs.extend(("38", "2", str(self.fg[0]), str(self.fg[1]), str(self.fg[2])))
+ if self.bg:
+ attrs.extend(("48", "2", str(self.bg[0]), str(self.bg[1]), str(self.bg[2])))
+ if self.bold:
+ attrs.append("01")
+ if self.underline:
+ attrs.append("04")
+ if self.italic:
+ attrs.append("03")
+ return self.escape(attrs)
+
+ def reset_string(self):
+ attrs = []
+ if self.fg is not None:
+ attrs.append("39")
+ if self.bg is not None:
+ attrs.append("49")
+ if self.bold or self.underline or self.italic:
+ attrs.append("00")
+ return self.escape(attrs)
+
+
+class Terminal256Formatter(Formatter):
+ """
+ Format tokens with ANSI color sequences, for output in a 256-color
+ terminal or console. Like in `TerminalFormatter` color sequences
+ are terminated at newlines, so that paging the output works correctly.
+
+ The formatter takes colors from a style defined by the `style` option
+ and converts them to nearest ANSI 256-color escape sequences. Bold and
+ underline attributes from the style are preserved (and displayed).
+
+ .. versionadded:: 0.9
+
+ .. versionchanged:: 2.2
+ If the used style defines foreground colors in the form ``#ansi*``, then
+ `Terminal256Formatter` will map these to non extended foreground color.
+ See :ref:`AnsiTerminalStyle` for more information.
+
+ .. versionchanged:: 2.4
+ The ANSI color names have been updated with names that are easier to
+ understand and align with colornames of other projects and terminals.
+ See :ref:`this table <new-ansi-color-names>` for more information.
+
+
+ Options accepted:
+
+ `style`
+ The style to use, can be a string or a Style subclass (default:
+ ``'default'``).
+
+ `linenos`
+ Set to ``True`` to have line numbers on the terminal output as well
+ (default: ``False`` = no line numbers).
+ """
+ name = 'Terminal256'
+ aliases = ['terminal256', 'console256', '256']
+ filenames = []
+
+ def __init__(self, **options):
+ Formatter.__init__(self, **options)
+
+ self.xterm_colors = []
+ self.best_match = {}
+ self.style_string = {}
+
+ self.usebold = 'nobold' not in options
+ self.useunderline = 'nounderline' not in options
+ self.useitalic = 'noitalic' not in options
+
+ self._build_color_table() # build an RGB-to-256 color conversion table
+ self._setup_styles() # convert selected style's colors to term. colors
+
+ self.linenos = options.get('linenos', False)
+ self._lineno = 0
+
+ def _build_color_table(self):
+ # colors 0..15: 16 basic colors
+
+ self.xterm_colors.append((0x00, 0x00, 0x00)) # 0
+ self.xterm_colors.append((0xcd, 0x00, 0x00)) # 1
+ self.xterm_colors.append((0x00, 0xcd, 0x00)) # 2
+ self.xterm_colors.append((0xcd, 0xcd, 0x00)) # 3
+ self.xterm_colors.append((0x00, 0x00, 0xee)) # 4
+ self.xterm_colors.append((0xcd, 0x00, 0xcd)) # 5
+ self.xterm_colors.append((0x00, 0xcd, 0xcd)) # 6
+ self.xterm_colors.append((0xe5, 0xe5, 0xe5)) # 7
+ self.xterm_colors.append((0x7f, 0x7f, 0x7f)) # 8
+ self.xterm_colors.append((0xff, 0x00, 0x00)) # 9
+ self.xterm_colors.append((0x00, 0xff, 0x00)) # 10
+ self.xterm_colors.append((0xff, 0xff, 0x00)) # 11
+ self.xterm_colors.append((0x5c, 0x5c, 0xff)) # 12
+ self.xterm_colors.append((0xff, 0x00, 0xff)) # 13
+ self.xterm_colors.append((0x00, 0xff, 0xff)) # 14
+ self.xterm_colors.append((0xff, 0xff, 0xff)) # 15
+
+ # colors 16..232: the 6x6x6 color cube
+
+ valuerange = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff)
+
+ for i in range(217):
+ r = valuerange[(i // 36) % 6]
+ g = valuerange[(i // 6) % 6]
+ b = valuerange[i % 6]
+ self.xterm_colors.append((r, g, b))
+
+ # colors 233..253: grayscale
+
+ for i in range(1, 22):
+ v = 8 + i * 10
+ self.xterm_colors.append((v, v, v))
+
+ def _closest_color(self, r, g, b):
+ distance = 257*257*3 # "infinity" (>distance from #000000 to #ffffff)
+ match = 0
+
+ for i in range(0, 254):
+ values = self.xterm_colors[i]
+
+ rd = r - values[0]
+ gd = g - values[1]
+ bd = b - values[2]
+ d = rd*rd + gd*gd + bd*bd
+
+ if d < distance:
+ match = i
+ distance = d
+ return match
+
+ def _color_index(self, color):
+ index = self.best_match.get(color, None)
+ if color in ansicolors:
+ # strip the `ansi/#ansi` part and look up code
+ index = color
+ self.best_match[color] = index
+ if index is None:
+ try:
+ rgb = int(str(color), 16)
+ except ValueError:
+ rgb = 0
+
+ r = (rgb >> 16) & 0xff
+ g = (rgb >> 8) & 0xff
+ b = rgb & 0xff
+ index = self._closest_color(r, g, b)
+ self.best_match[color] = index
+ return index
+
+ def _setup_styles(self):
+ for ttype, ndef in self.style:
+ escape = EscapeSequence()
+ # get foreground from ansicolor if set
+ if ndef['ansicolor']:
+ escape.fg = self._color_index(ndef['ansicolor'])
+ elif ndef['color']:
+ escape.fg = self._color_index(ndef['color'])
+ if ndef['bgansicolor']:
+ escape.bg = self._color_index(ndef['bgansicolor'])
+ elif ndef['bgcolor']:
+ escape.bg = self._color_index(ndef['bgcolor'])
+ if self.usebold and ndef['bold']:
+ escape.bold = True
+ if self.useunderline and ndef['underline']:
+ escape.underline = True
+ if self.useitalic and ndef['italic']:
+ escape.italic = True
+ self.style_string[str(ttype)] = (escape.color_string(),
+ escape.reset_string())
+
+ def _write_lineno(self, outfile):
+ self._lineno += 1
+ outfile.write("%s%04d: " % (self._lineno != 1 and '\n' or '', self._lineno))
+
+ def format(self, tokensource, outfile):
+ return Formatter.format(self, tokensource, outfile)
+
+ def format_unencoded(self, tokensource, outfile):
+ if self.linenos:
+ self._write_lineno(outfile)
+
+ for ttype, value in tokensource:
+ not_found = True
+ while ttype and not_found:
+ try:
+ # outfile.write( "<" + str(ttype) + ">" )
+ on, off = self.style_string[str(ttype)]
+
+ # Like TerminalFormatter, add "reset colors" escape sequence
+ # on newline.
+ spl = value.split('\n')
+ for line in spl[:-1]:
+ if line:
+ outfile.write(on + line + off)
+ if self.linenos:
+ self._write_lineno(outfile)
+ else:
+ outfile.write('\n')
+
+ if spl[-1]:
+ outfile.write(on + spl[-1] + off)
+
+ not_found = False
+ # outfile.write( '#' + str(ttype) + '#' )
+
+ except KeyError:
+ # ottype = ttype
+ ttype = ttype.parent
+ # outfile.write( '!' + str(ottype) + '->' + str(ttype) + '!' )
+
+ if not_found:
+ outfile.write(value)
+
+ if self.linenos:
+ outfile.write("\n")
+
+
+
+class TerminalTrueColorFormatter(Terminal256Formatter):
+ r"""
+ Format tokens with ANSI color sequences, for output in a true-color
+ terminal or console. Like in `TerminalFormatter` color sequences
+ are terminated at newlines, so that paging the output works correctly.
+
+ .. versionadded:: 2.1
+
+ Options accepted:
+
+ `style`
+ The style to use, can be a string or a Style subclass (default:
+ ``'default'``).
+ """
+ name = 'TerminalTrueColor'
+ aliases = ['terminal16m', 'console16m', '16m']
+ filenames = []
+
+ def _build_color_table(self):
+ pass
+
+ def _color_tuple(self, color):
+ try:
+ rgb = int(str(color), 16)
+ except ValueError:
+ return None
+ r = (rgb >> 16) & 0xff
+ g = (rgb >> 8) & 0xff
+ b = rgb & 0xff
+ return (r, g, b)
+
+ def _setup_styles(self):
+ for ttype, ndef in self.style:
+ escape = EscapeSequence()
+ if ndef['color']:
+ escape.fg = self._color_tuple(ndef['color'])
+ if ndef['bgcolor']:
+ escape.bg = self._color_tuple(ndef['bgcolor'])
+ if self.usebold and ndef['bold']:
+ escape.bold = True
+ if self.useunderline and ndef['underline']:
+ escape.underline = True
+ if self.useitalic and ndef['italic']:
+ escape.italic = True
+ self.style_string[str(ttype)] = (escape.true_color_string(),
+ escape.reset_string())
diff --git a/src/pip/_vendor/pygments/lexer.py b/src/pip/_vendor/pygments/lexer.py
new file mode 100644
index 000000000..ec7f4de32
--- /dev/null
+++ b/src/pip/_vendor/pygments/lexer.py
@@ -0,0 +1,882 @@
+"""
+ pygments.lexer
+ ~~~~~~~~~~~~~~
+
+ Base lexer classes.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import re
+import sys
+import time
+
+from pip._vendor.pygments.filter import apply_filters, Filter
+from pip._vendor.pygments.filters import get_filter_by_name
+from pip._vendor.pygments.token import Error, Text, Other, _TokenType
+from pip._vendor.pygments.util import get_bool_opt, get_int_opt, get_list_opt, \
+ make_analysator, Future, guess_decode
+from pip._vendor.pygments.regexopt import regex_opt
+
+__all__ = ['Lexer', 'RegexLexer', 'ExtendedRegexLexer', 'DelegatingLexer',
+ 'LexerContext', 'include', 'inherit', 'bygroups', 'using', 'this',
+ 'default', 'words']
+
+
+_encoding_map = [(b'\xef\xbb\xbf', 'utf-8'),
+ (b'\xff\xfe\0\0', 'utf-32'),
+ (b'\0\0\xfe\xff', 'utf-32be'),
+ (b'\xff\xfe', 'utf-16'),
+ (b'\xfe\xff', 'utf-16be')]
+
+_default_analyse = staticmethod(lambda x: 0.0)
+
+
+class LexerMeta(type):
+ """
+ This metaclass automagically converts ``analyse_text`` methods into
+ static methods which always return float values.
+ """
+
+ def __new__(mcs, name, bases, d):
+ if 'analyse_text' in d:
+ d['analyse_text'] = make_analysator(d['analyse_text'])
+ return type.__new__(mcs, name, bases, d)
+
+
+class Lexer(metaclass=LexerMeta):
+ """
+ Lexer for a specific language.
+
+ Basic options recognized:
+ ``stripnl``
+ Strip leading and trailing newlines from the input (default: True).
+ ``stripall``
+ Strip all leading and trailing whitespace from the input
+ (default: False).
+ ``ensurenl``
+ Make sure that the input ends with a newline (default: True). This
+ is required for some lexers that consume input linewise.
+
+ .. versionadded:: 1.3
+
+ ``tabsize``
+ If given and greater than 0, expand tabs in the input (default: 0).
+ ``encoding``
+ If given, must be an encoding name. This encoding will be used to
+ convert the input string to Unicode, if it is not already a Unicode
+ string (default: ``'guess'``, which uses a simple UTF-8 / Locale /
+ Latin1 detection. Can also be ``'chardet'`` to use the chardet
+ library, if it is installed.
+ ``inencoding``
+ Overrides the ``encoding`` if given.
+ """
+
+ #: Name of the lexer
+ name = None
+
+ #: URL of the language specification/definition
+ url = None
+
+ #: Shortcuts for the lexer
+ aliases = []
+
+ #: File name globs
+ filenames = []
+
+ #: Secondary file name globs
+ alias_filenames = []
+
+ #: MIME types
+ mimetypes = []
+
+ #: Priority, should multiple lexers match and no content is provided
+ priority = 0
+
+ def __init__(self, **options):
+ self.options = options
+ self.stripnl = get_bool_opt(options, 'stripnl', True)
+ self.stripall = get_bool_opt(options, 'stripall', False)
+ self.ensurenl = get_bool_opt(options, 'ensurenl', True)
+ self.tabsize = get_int_opt(options, 'tabsize', 0)
+ self.encoding = options.get('encoding', 'guess')
+ self.encoding = options.get('inencoding') or self.encoding
+ self.filters = []
+ for filter_ in get_list_opt(options, 'filters', ()):
+ self.add_filter(filter_)
+
+ def __repr__(self):
+ if self.options:
+ return '<pygments.lexers.%s with %r>' % (self.__class__.__name__,
+ self.options)
+ else:
+ return '<pygments.lexers.%s>' % self.__class__.__name__
+
+ def add_filter(self, filter_, **options):
+ """
+ Add a new stream filter to this lexer.
+ """
+ if not isinstance(filter_, Filter):
+ filter_ = get_filter_by_name(filter_, **options)
+ self.filters.append(filter_)
+
+ def analyse_text(text):
+ """
+ Has to return a float between ``0`` and ``1`` that indicates
+ if a lexer wants to highlight this text. Used by ``guess_lexer``.
+ If this method returns ``0`` it won't highlight it in any case, if
+ it returns ``1`` highlighting with this lexer is guaranteed.
+
+ The `LexerMeta` metaclass automatically wraps this function so
+ that it works like a static method (no ``self`` or ``cls``
+ parameter) and the return value is automatically converted to
+ `float`. If the return value is an object that is boolean `False`
+ it's the same as if the return values was ``0.0``.
+ """
+
+ def get_tokens(self, text, unfiltered=False):
+ """
+ Return an iterable of (tokentype, value) pairs generated from
+ `text`. If `unfiltered` is set to `True`, the filtering mechanism
+ is bypassed even if filters are defined.
+
+ Also preprocess the text, i.e. expand tabs and strip it if
+ wanted and applies registered filters.
+ """
+ if not isinstance(text, str):
+ if self.encoding == 'guess':
+ text, _ = guess_decode(text)
+ elif self.encoding == 'chardet':
+ try:
+ from pip._vendor import chardet
+ except ImportError as e:
+ raise ImportError('To enable chardet encoding guessing, '
+ 'please install the chardet library '
+ 'from http://chardet.feedparser.org/') from e
+ # check for BOM first
+ decoded = None
+ for bom, encoding in _encoding_map:
+ if text.startswith(bom):
+ decoded = text[len(bom):].decode(encoding, 'replace')
+ break
+ # no BOM found, so use chardet
+ if decoded is None:
+ enc = chardet.detect(text[:1024]) # Guess using first 1KB
+ decoded = text.decode(enc.get('encoding') or 'utf-8',
+ 'replace')
+ text = decoded
+ else:
+ text = text.decode(self.encoding)
+ if text.startswith('\ufeff'):
+ text = text[len('\ufeff'):]
+ else:
+ if text.startswith('\ufeff'):
+ text = text[len('\ufeff'):]
+
+ # text now *is* a unicode string
+ text = text.replace('\r\n', '\n')
+ text = text.replace('\r', '\n')
+ if self.stripall:
+ text = text.strip()
+ elif self.stripnl:
+ text = text.strip('\n')
+ if self.tabsize > 0:
+ text = text.expandtabs(self.tabsize)
+ if self.ensurenl and not text.endswith('\n'):
+ text += '\n'
+
+ def streamer():
+ for _, t, v in self.get_tokens_unprocessed(text):
+ yield t, v
+ stream = streamer()
+ if not unfiltered:
+ stream = apply_filters(stream, self.filters, self)
+ return stream
+
+ def get_tokens_unprocessed(self, text):
+ """
+ Return an iterable of (index, tokentype, value) pairs where "index"
+ is the starting position of the token within the input text.
+
+ In subclasses, implement this method as a generator to
+ maximize effectiveness.
+ """
+ raise NotImplementedError
+
+
+class DelegatingLexer(Lexer):
+ """
+ This lexer takes two lexer as arguments. A root lexer and
+ a language lexer. First everything is scanned using the language
+ lexer, afterwards all ``Other`` tokens are lexed using the root
+ lexer.
+
+ The lexers from the ``template`` lexer package use this base lexer.
+ """
+
+ def __init__(self, _root_lexer, _language_lexer, _needle=Other, **options):
+ self.root_lexer = _root_lexer(**options)
+ self.language_lexer = _language_lexer(**options)
+ self.needle = _needle
+ Lexer.__init__(self, **options)
+
+ def get_tokens_unprocessed(self, text):
+ buffered = ''
+ insertions = []
+ lng_buffer = []
+ for i, t, v in self.language_lexer.get_tokens_unprocessed(text):
+ if t is self.needle:
+ if lng_buffer:
+ insertions.append((len(buffered), lng_buffer))
+ lng_buffer = []
+ buffered += v
+ else:
+ lng_buffer.append((i, t, v))
+ if lng_buffer:
+ insertions.append((len(buffered), lng_buffer))
+ return do_insertions(insertions,
+ self.root_lexer.get_tokens_unprocessed(buffered))
+
+
+# ------------------------------------------------------------------------------
+# RegexLexer and ExtendedRegexLexer
+#
+
+
+class include(str): # pylint: disable=invalid-name
+ """
+ Indicates that a state should include rules from another state.
+ """
+ pass
+
+
+class _inherit:
+ """
+ Indicates the a state should inherit from its superclass.
+ """
+ def __repr__(self):
+ return 'inherit'
+
+inherit = _inherit() # pylint: disable=invalid-name
+
+
+class combined(tuple): # pylint: disable=invalid-name
+ """
+ Indicates a state combined from multiple states.
+ """
+
+ def __new__(cls, *args):
+ return tuple.__new__(cls, args)
+
+ def __init__(self, *args):
+ # tuple.__init__ doesn't do anything
+ pass
+
+
+class _PseudoMatch:
+ """
+ A pseudo match object constructed from a string.
+ """
+
+ def __init__(self, start, text):
+ self._text = text
+ self._start = start
+
+ def start(self, arg=None):
+ return self._start
+
+ def end(self, arg=None):
+ return self._start + len(self._text)
+
+ def group(self, arg=None):
+ if arg:
+ raise IndexError('No such group')
+ return self._text
+
+ def groups(self):
+ return (self._text,)
+
+ def groupdict(self):
+ return {}
+
+
+def bygroups(*args):
+ """
+ Callback that yields multiple actions for each group in the match.
+ """
+ def callback(lexer, match, ctx=None):
+ for i, action in enumerate(args):
+ if action is None:
+ continue
+ elif type(action) is _TokenType:
+ data = match.group(i + 1)
+ if data:
+ yield match.start(i + 1), action, data
+ else:
+ data = match.group(i + 1)
+ if data is not None:
+ if ctx:
+ ctx.pos = match.start(i + 1)
+ for item in action(lexer,
+ _PseudoMatch(match.start(i + 1), data), ctx):
+ if item:
+ yield item
+ if ctx:
+ ctx.pos = match.end()
+ return callback
+
+
+class _This:
+ """
+ Special singleton used for indicating the caller class.
+ Used by ``using``.
+ """
+
+this = _This()
+
+
+def using(_other, **kwargs):
+ """
+ Callback that processes the match with a different lexer.
+
+ The keyword arguments are forwarded to the lexer, except `state` which
+ is handled separately.
+
+ `state` specifies the state that the new lexer will start in, and can
+ be an enumerable such as ('root', 'inline', 'string') or a simple
+ string which is assumed to be on top of the root state.
+
+ Note: For that to work, `_other` must not be an `ExtendedRegexLexer`.
+ """
+ gt_kwargs = {}
+ if 'state' in kwargs:
+ s = kwargs.pop('state')
+ if isinstance(s, (list, tuple)):
+ gt_kwargs['stack'] = s
+ else:
+ gt_kwargs['stack'] = ('root', s)
+
+ if _other is this:
+ def callback(lexer, match, ctx=None):
+ # if keyword arguments are given the callback
+ # function has to create a new lexer instance
+ if kwargs:
+ # XXX: cache that somehow
+ kwargs.update(lexer.options)
+ lx = lexer.__class__(**kwargs)
+ else:
+ lx = lexer
+ s = match.start()
+ for i, t, v in lx.get_tokens_unprocessed(match.group(), **gt_kwargs):
+ yield i + s, t, v
+ if ctx:
+ ctx.pos = match.end()
+ else:
+ def callback(lexer, match, ctx=None):
+ # XXX: cache that somehow
+ kwargs.update(lexer.options)
+ lx = _other(**kwargs)
+
+ s = match.start()
+ for i, t, v in lx.get_tokens_unprocessed(match.group(), **gt_kwargs):
+ yield i + s, t, v
+ if ctx:
+ ctx.pos = match.end()
+ return callback
+
+
+class default:
+ """
+ Indicates a state or state action (e.g. #pop) to apply.
+ For example default('#pop') is equivalent to ('', Token, '#pop')
+ Note that state tuples may be used as well.
+
+ .. versionadded:: 2.0
+ """
+ def __init__(self, state):
+ self.state = state
+
+
+class words(Future):
+ """
+ Indicates a list of literal words that is transformed into an optimized
+ regex that matches any of the words.
+
+ .. versionadded:: 2.0
+ """
+ def __init__(self, words, prefix='', suffix=''):
+ self.words = words
+ self.prefix = prefix
+ self.suffix = suffix
+
+ def get(self):
+ return regex_opt(self.words, prefix=self.prefix, suffix=self.suffix)
+
+
+class RegexLexerMeta(LexerMeta):
+ """
+ Metaclass for RegexLexer, creates the self._tokens attribute from
+ self.tokens on the first instantiation.
+ """
+
+ def _process_regex(cls, regex, rflags, state):
+ """Preprocess the regular expression component of a token definition."""
+ if isinstance(regex, Future):
+ regex = regex.get()
+ return re.compile(regex, rflags).match
+
+ def _process_token(cls, token):
+ """Preprocess the token component of a token definition."""
+ assert type(token) is _TokenType or callable(token), \
+ 'token type must be simple type or callable, not %r' % (token,)
+ return token
+
+ def _process_new_state(cls, new_state, unprocessed, processed):
+ """Preprocess the state transition action of a token definition."""
+ if isinstance(new_state, str):
+ # an existing state
+ if new_state == '#pop':
+ return -1
+ elif new_state in unprocessed:
+ return (new_state,)
+ elif new_state == '#push':
+ return new_state
+ elif new_state[:5] == '#pop:':
+ return -int(new_state[5:])
+ else:
+ assert False, 'unknown new state %r' % new_state
+ elif isinstance(new_state, combined):
+ # combine a new state from existing ones
+ tmp_state = '_tmp_%d' % cls._tmpname
+ cls._tmpname += 1
+ itokens = []
+ for istate in new_state:
+ assert istate != new_state, 'circular state ref %r' % istate
+ itokens.extend(cls._process_state(unprocessed,
+ processed, istate))
+ processed[tmp_state] = itokens
+ return (tmp_state,)
+ elif isinstance(new_state, tuple):
+ # push more than one state
+ for istate in new_state:
+ assert (istate in unprocessed or
+ istate in ('#pop', '#push')), \
+ 'unknown new state ' + istate
+ return new_state
+ else:
+ assert False, 'unknown new state def %r' % new_state
+
+ def _process_state(cls, unprocessed, processed, state):
+ """Preprocess a single state definition."""
+ assert type(state) is str, "wrong state name %r" % state
+ assert state[0] != '#', "invalid state name %r" % state
+ if state in processed:
+ return processed[state]
+ tokens = processed[state] = []
+ rflags = cls.flags
+ for tdef in unprocessed[state]:
+ if isinstance(tdef, include):
+ # it's a state reference
+ assert tdef != state, "circular state reference %r" % state
+ tokens.extend(cls._process_state(unprocessed, processed,
+ str(tdef)))
+ continue
+ if isinstance(tdef, _inherit):
+ # should be processed already, but may not in the case of:
+ # 1. the state has no counterpart in any parent
+ # 2. the state includes more than one 'inherit'
+ continue
+ if isinstance(tdef, default):
+ new_state = cls._process_new_state(tdef.state, unprocessed, processed)
+ tokens.append((re.compile('').match, None, new_state))
+ continue
+
+ assert type(tdef) is tuple, "wrong rule def %r" % tdef
+
+ try:
+ rex = cls._process_regex(tdef[0], rflags, state)
+ except Exception as err:
+ raise ValueError("uncompilable regex %r in state %r of %r: %s" %
+ (tdef[0], state, cls, err)) from err
+
+ token = cls._process_token(tdef[1])
+
+ if len(tdef) == 2:
+ new_state = None
+ else:
+ new_state = cls._process_new_state(tdef[2],
+ unprocessed, processed)
+
+ tokens.append((rex, token, new_state))
+ return tokens
+
+ def process_tokendef(cls, name, tokendefs=None):
+ """Preprocess a dictionary of token definitions."""
+ processed = cls._all_tokens[name] = {}
+ tokendefs = tokendefs or cls.tokens[name]
+ for state in list(tokendefs):
+ cls._process_state(tokendefs, processed, state)
+ return processed
+
+ def get_tokendefs(cls):
+ """
+ Merge tokens from superclasses in MRO order, returning a single tokendef
+ dictionary.
+
+ Any state that is not defined by a subclass will be inherited
+ automatically. States that *are* defined by subclasses will, by
+ default, override that state in the superclass. If a subclass wishes to
+ inherit definitions from a superclass, it can use the special value
+ "inherit", which will cause the superclass' state definition to be
+ included at that point in the state.
+ """
+ tokens = {}
+ inheritable = {}
+ for c in cls.__mro__:
+ toks = c.__dict__.get('tokens', {})
+
+ for state, items in toks.items():
+ curitems = tokens.get(state)
+ if curitems is None:
+ # N.b. because this is assigned by reference, sufficiently
+ # deep hierarchies are processed incrementally (e.g. for
+ # A(B), B(C), C(RegexLexer), B will be premodified so X(B)
+ # will not see any inherits in B).
+ tokens[state] = items
+ try:
+ inherit_ndx = items.index(inherit)
+ except ValueError:
+ continue
+ inheritable[state] = inherit_ndx
+ continue
+
+ inherit_ndx = inheritable.pop(state, None)
+ if inherit_ndx is None:
+ continue
+
+ # Replace the "inherit" value with the items
+ curitems[inherit_ndx:inherit_ndx+1] = items
+ try:
+ # N.b. this is the index in items (that is, the superclass
+ # copy), so offset required when storing below.
+ new_inh_ndx = items.index(inherit)
+ except ValueError:
+ pass
+ else:
+ inheritable[state] = inherit_ndx + new_inh_ndx
+
+ return tokens
+
+ def __call__(cls, *args, **kwds):
+ """Instantiate cls after preprocessing its token definitions."""
+ if '_tokens' not in cls.__dict__:
+ cls._all_tokens = {}
+ cls._tmpname = 0
+ if hasattr(cls, 'token_variants') and cls.token_variants:
+ # don't process yet
+ pass
+ else:
+ cls._tokens = cls.process_tokendef('', cls.get_tokendefs())
+
+ return type.__call__(cls, *args, **kwds)
+
+
+class RegexLexer(Lexer, metaclass=RegexLexerMeta):
+ """
+ Base for simple stateful regular expression-based lexers.
+ Simplifies the lexing process so that you need only
+ provide a list of states and regular expressions.
+ """
+
+ #: Flags for compiling the regular expressions.
+ #: Defaults to MULTILINE.
+ flags = re.MULTILINE
+
+ #: At all time there is a stack of states. Initially, the stack contains
+ #: a single state 'root'. The top of the stack is called "the current state".
+ #:
+ #: Dict of ``{'state': [(regex, tokentype, new_state), ...], ...}``
+ #:
+ #: ``new_state`` can be omitted to signify no state transition.
+ #: If ``new_state`` is a string, it is pushed on the stack. This ensure
+ #: the new current state is ``new_state``.
+ #: If ``new_state`` is a tuple of strings, all of those strings are pushed
+ #: on the stack and the current state will be the last element of the list.
+ #: ``new_state`` can also be ``combined('state1', 'state2', ...)``
+ #: to signify a new, anonymous state combined from the rules of two
+ #: or more existing ones.
+ #: Furthermore, it can be '#pop' to signify going back one step in
+ #: the state stack, or '#push' to push the current state on the stack
+ #: again. Note that if you push while in a combined state, the combined
+ #: state itself is pushed, and not only the state in which the rule is
+ #: defined.
+ #:
+ #: The tuple can also be replaced with ``include('state')``, in which
+ #: case the rules from the state named by the string are included in the
+ #: current one.
+ tokens = {}
+
+ def get_tokens_unprocessed(self, text, stack=('root',)):
+ """
+ Split ``text`` into (tokentype, text) pairs.
+
+ ``stack`` is the initial stack (default: ``['root']``)
+ """
+ pos = 0
+ tokendefs = self._tokens
+ statestack = list(stack)
+ statetokens = tokendefs[statestack[-1]]
+ while 1:
+ for rexmatch, action, new_state in statetokens:
+ m = rexmatch(text, pos)
+ if m:
+ if action is not None:
+ if type(action) is _TokenType:
+ yield pos, action, m.group()
+ else:
+ yield from action(self, m)
+ pos = m.end()
+ if new_state is not None:
+ # state transition
+ if isinstance(new_state, tuple):
+ for state in new_state:
+ if state == '#pop':
+ if len(statestack) > 1:
+ statestack.pop()
+ elif state == '#push':
+ statestack.append(statestack[-1])
+ else:
+ statestack.append(state)
+ elif isinstance(new_state, int):
+ # pop, but keep at least one state on the stack
+ # (random code leading to unexpected pops should
+ # not allow exceptions)
+ if abs(new_state) >= len(statestack):
+ del statestack[1:]
+ else:
+ del statestack[new_state:]
+ elif new_state == '#push':
+ statestack.append(statestack[-1])
+ else:
+ assert False, "wrong state def: %r" % new_state
+ statetokens = tokendefs[statestack[-1]]
+ break
+ else:
+ # We are here only if all state tokens have been considered
+ # and there was not a match on any of them.
+ try:
+ if text[pos] == '\n':
+ # at EOL, reset state to "root"
+ statestack = ['root']
+ statetokens = tokendefs['root']
+ yield pos, Text, '\n'
+ pos += 1
+ continue
+ yield pos, Error, text[pos]
+ pos += 1
+ except IndexError:
+ break
+
+
+class LexerContext:
+ """
+ A helper object that holds lexer position data.
+ """
+
+ def __init__(self, text, pos, stack=None, end=None):
+ self.text = text
+ self.pos = pos
+ self.end = end or len(text) # end=0 not supported ;-)
+ self.stack = stack or ['root']
+
+ def __repr__(self):
+ return 'LexerContext(%r, %r, %r)' % (
+ self.text, self.pos, self.stack)
+
+
+class ExtendedRegexLexer(RegexLexer):
+ """
+ A RegexLexer that uses a context object to store its state.
+ """
+
+ def get_tokens_unprocessed(self, text=None, context=None):
+ """
+ Split ``text`` into (tokentype, text) pairs.
+ If ``context`` is given, use this lexer context instead.
+ """
+ tokendefs = self._tokens
+ if not context:
+ ctx = LexerContext(text, 0)
+ statetokens = tokendefs['root']
+ else:
+ ctx = context
+ statetokens = tokendefs[ctx.stack[-1]]
+ text = ctx.text
+ while 1:
+ for rexmatch, action, new_state in statetokens:
+ m = rexmatch(text, ctx.pos, ctx.end)
+ if m:
+ if action is not None:
+ if type(action) is _TokenType:
+ yield ctx.pos, action, m.group()
+ ctx.pos = m.end()
+ else:
+ yield from action(self, m, ctx)
+ if not new_state:
+ # altered the state stack?
+ statetokens = tokendefs[ctx.stack[-1]]
+ # CAUTION: callback must set ctx.pos!
+ if new_state is not None:
+ # state transition
+ if isinstance(new_state, tuple):
+ for state in new_state:
+ if state == '#pop':
+ if len(ctx.stack) > 1:
+ ctx.stack.pop()
+ elif state == '#push':
+ ctx.stack.append(ctx.stack[-1])
+ else:
+ ctx.stack.append(state)
+ elif isinstance(new_state, int):
+ # see RegexLexer for why this check is made
+ if abs(new_state) >= len(ctx.stack):
+ del ctx.stack[1:]
+ else:
+ del ctx.stack[new_state:]
+ elif new_state == '#push':
+ ctx.stack.append(ctx.stack[-1])
+ else:
+ assert False, "wrong state def: %r" % new_state
+ statetokens = tokendefs[ctx.stack[-1]]
+ break
+ else:
+ try:
+ if ctx.pos >= ctx.end:
+ break
+ if text[ctx.pos] == '\n':
+ # at EOL, reset state to "root"
+ ctx.stack = ['root']
+ statetokens = tokendefs['root']
+ yield ctx.pos, Text, '\n'
+ ctx.pos += 1
+ continue
+ yield ctx.pos, Error, text[ctx.pos]
+ ctx.pos += 1
+ except IndexError:
+ break
+
+
+def do_insertions(insertions, tokens):
+ """
+ Helper for lexers which must combine the results of several
+ sublexers.
+
+ ``insertions`` is a list of ``(index, itokens)`` pairs.
+ Each ``itokens`` iterable should be inserted at position
+ ``index`` into the token stream given by the ``tokens``
+ argument.
+
+ The result is a combined token stream.
+
+ TODO: clean up the code here.
+ """
+ insertions = iter(insertions)
+ try:
+ index, itokens = next(insertions)
+ except StopIteration:
+ # no insertions
+ yield from tokens
+ return
+
+ realpos = None
+ insleft = True
+
+ # iterate over the token stream where we want to insert
+ # the tokens from the insertion list.
+ for i, t, v in tokens:
+ # first iteration. store the position of first item
+ if realpos is None:
+ realpos = i
+ oldi = 0
+ while insleft and i + len(v) >= index:
+ tmpval = v[oldi:index - i]
+ if tmpval:
+ yield realpos, t, tmpval
+ realpos += len(tmpval)
+ for it_index, it_token, it_value in itokens:
+ yield realpos, it_token, it_value
+ realpos += len(it_value)
+ oldi = index - i
+ try:
+ index, itokens = next(insertions)
+ except StopIteration:
+ insleft = False
+ break # not strictly necessary
+ if oldi < len(v):
+ yield realpos, t, v[oldi:]
+ realpos += len(v) - oldi
+
+ # leftover tokens
+ while insleft:
+ # no normal tokens, set realpos to zero
+ realpos = realpos or 0
+ for p, t, v in itokens:
+ yield realpos, t, v
+ realpos += len(v)
+ try:
+ index, itokens = next(insertions)
+ except StopIteration:
+ insleft = False
+ break # not strictly necessary
+
+
+class ProfilingRegexLexerMeta(RegexLexerMeta):
+ """Metaclass for ProfilingRegexLexer, collects regex timing info."""
+
+ def _process_regex(cls, regex, rflags, state):
+ if isinstance(regex, words):
+ rex = regex_opt(regex.words, prefix=regex.prefix,
+ suffix=regex.suffix)
+ else:
+ rex = regex
+ compiled = re.compile(rex, rflags)
+
+ def match_func(text, pos, endpos=sys.maxsize):
+ info = cls._prof_data[-1].setdefault((state, rex), [0, 0.0])
+ t0 = time.time()
+ res = compiled.match(text, pos, endpos)
+ t1 = time.time()
+ info[0] += 1
+ info[1] += t1 - t0
+ return res
+ return match_func
+
+
+class ProfilingRegexLexer(RegexLexer, metaclass=ProfilingRegexLexerMeta):
+ """Drop-in replacement for RegexLexer that does profiling of its regexes."""
+
+ _prof_data = []
+ _prof_sort_index = 4 # defaults to time per call
+
+ def get_tokens_unprocessed(self, text, stack=('root',)):
+ # this needs to be a stack, since using(this) will produce nested calls
+ self.__class__._prof_data.append({})
+ yield from RegexLexer.get_tokens_unprocessed(self, text, stack)
+ rawdata = self.__class__._prof_data.pop()
+ data = sorted(((s, repr(r).strip('u\'').replace('\\\\', '\\')[:65],
+ n, 1000 * t, 1000 * t / n)
+ for ((s, r), (n, t)) in rawdata.items()),
+ key=lambda x: x[self._prof_sort_index],
+ reverse=True)
+ sum_total = sum(x[3] for x in data)
+
+ print()
+ print('Profiling result for %s lexing %d chars in %.3f ms' %
+ (self.__class__.__name__, len(text), sum_total))
+ print('=' * 110)
+ print('%-20s %-64s ncalls tottime percall' % ('state', 'regex'))
+ print('-' * 110)
+ for d in data:
+ print('%-20s %-65s %5d %8.4f %8.4f' % d)
+ print('=' * 110)
diff --git a/src/pip/_vendor/pygments/lexers/__init__.py b/src/pip/_vendor/pygments/lexers/__init__.py
new file mode 100644
index 000000000..3f404e4f7
--- /dev/null
+++ b/src/pip/_vendor/pygments/lexers/__init__.py
@@ -0,0 +1,345 @@
+"""
+ pygments.lexers
+ ~~~~~~~~~~~~~~~
+
+ Pygments lexers.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import re
+import sys
+import types
+import fnmatch
+from os.path import basename
+
+from pip._vendor.pygments.lexers._mapping import LEXERS
+from pip._vendor.pygments.modeline import get_filetype_from_buffer
+from pip._vendor.pygments.plugin import find_plugin_lexers
+from pip._vendor.pygments.util import ClassNotFound, guess_decode
+
+COMPAT = {
+ 'Python3Lexer': 'PythonLexer',
+ 'Python3TracebackLexer': 'PythonTracebackLexer',
+}
+
+__all__ = ['get_lexer_by_name', 'get_lexer_for_filename', 'find_lexer_class',
+ 'guess_lexer', 'load_lexer_from_file'] + list(LEXERS) + list(COMPAT)
+
+_lexer_cache = {}
+_pattern_cache = {}
+
+
+def _fn_matches(fn, glob):
+ """Return whether the supplied file name fn matches pattern filename."""
+ if glob not in _pattern_cache:
+ pattern = _pattern_cache[glob] = re.compile(fnmatch.translate(glob))
+ return pattern.match(fn)
+ return _pattern_cache[glob].match(fn)
+
+
+def _load_lexers(module_name):
+ """Load a lexer (and all others in the module too)."""
+ mod = __import__(module_name, None, None, ['__all__'])
+ for lexer_name in mod.__all__:
+ cls = getattr(mod, lexer_name)
+ _lexer_cache[cls.name] = cls
+
+
+def get_all_lexers(plugins=True):
+ """Return a generator of tuples in the form ``(name, aliases,
+ filenames, mimetypes)`` of all know lexers.
+
+ If *plugins* is true (the default), plugin lexers supplied by entrypoints
+ are also returned. Otherwise, only builtin ones are considered.
+ """
+ for item in LEXERS.values():
+ yield item[1:]
+ if plugins:
+ for lexer in find_plugin_lexers():
+ yield lexer.name, lexer.aliases, lexer.filenames, lexer.mimetypes
+
+
+def find_lexer_class(name):
+ """Lookup a lexer class by name.
+
+ Return None if not found.
+ """
+ if name in _lexer_cache:
+ return _lexer_cache[name]
+ # lookup builtin lexers
+ for module_name, lname, aliases, _, _ in LEXERS.values():
+ if name == lname:
+ _load_lexers(module_name)
+ return _lexer_cache[name]
+ # continue with lexers from setuptools entrypoints
+ for cls in find_plugin_lexers():
+ if cls.name == name:
+ return cls
+
+
+def find_lexer_class_by_name(_alias):
+ """Lookup a lexer class by alias.
+
+ Like `get_lexer_by_name`, but does not instantiate the class.
+
+ .. versionadded:: 2.2
+ """
+ if not _alias:
+ raise ClassNotFound('no lexer for alias %r found' % _alias)
+ # lookup builtin lexers
+ for module_name, name, aliases, _, _ in LEXERS.values():
+ if _alias.lower() in aliases:
+ if name not in _lexer_cache:
+ _load_lexers(module_name)
+ return _lexer_cache[name]
+ # continue with lexers from setuptools entrypoints
+ for cls in find_plugin_lexers():
+ if _alias.lower() in cls.aliases:
+ return cls
+ raise ClassNotFound('no lexer for alias %r found' % _alias)
+
+
+def get_lexer_by_name(_alias, **options):
+ """Get a lexer by an alias.
+
+ Raises ClassNotFound if not found.
+ """
+ if not _alias:
+ raise ClassNotFound('no lexer for alias %r found' % _alias)
+
+ # lookup builtin lexers
+ for module_name, name, aliases, _, _ in LEXERS.values():
+ if _alias.lower() in aliases:
+ if name not in _lexer_cache:
+ _load_lexers(module_name)
+ return _lexer_cache[name](**options)
+ # continue with lexers from setuptools entrypoints
+ for cls in find_plugin_lexers():
+ if _alias.lower() in cls.aliases:
+ return cls(**options)
+ raise ClassNotFound('no lexer for alias %r found' % _alias)
+
+
+def load_lexer_from_file(filename, lexername="CustomLexer", **options):
+ """Load a lexer from a file.
+
+ This method expects a file located relative to the current working
+ directory, which contains a Lexer class. By default, it expects the
+ Lexer to be name CustomLexer; you can specify your own class name
+ as the second argument to this function.
+
+ Users should be very careful with the input, because this method
+ is equivalent to running eval on the input file.
+
+ Raises ClassNotFound if there are any problems importing the Lexer.
+
+ .. versionadded:: 2.2
+ """
+ try:
+ # This empty dict will contain the namespace for the exec'd file
+ custom_namespace = {}
+ with open(filename, 'rb') as f:
+ exec(f.read(), custom_namespace)
+ # Retrieve the class `lexername` from that namespace
+ if lexername not in custom_namespace:
+ raise ClassNotFound('no valid %s class found in %s' %
+ (lexername, filename))
+ lexer_class = custom_namespace[lexername]
+ # And finally instantiate it with the options
+ return lexer_class(**options)
+ except OSError as err:
+ raise ClassNotFound('cannot read %s: %s' % (filename, err))
+ except ClassNotFound:
+ raise
+ except Exception as err:
+ raise ClassNotFound('error when loading custom lexer: %s' % err)
+
+
+def find_lexer_class_for_filename(_fn, code=None):
+ """Get a lexer for a filename.
+
+ If multiple lexers match the filename pattern, use ``analyse_text()`` to
+ figure out which one is more appropriate.
+
+ Returns None if not found.
+ """
+ matches = []
+ fn = basename(_fn)
+ for modname, name, _, filenames, _ in LEXERS.values():
+ for filename in filenames:
+ if _fn_matches(fn, filename):
+ if name not in _lexer_cache:
+ _load_lexers(modname)
+ matches.append((_lexer_cache[name], filename))
+ for cls in find_plugin_lexers():
+ for filename in cls.filenames:
+ if _fn_matches(fn, filename):
+ matches.append((cls, filename))
+
+ if isinstance(code, bytes):
+ # decode it, since all analyse_text functions expect unicode
+ code = guess_decode(code)
+
+ def get_rating(info):
+ cls, filename = info
+ # explicit patterns get a bonus
+ bonus = '*' not in filename and 0.5 or 0
+ # The class _always_ defines analyse_text because it's included in
+ # the Lexer class. The default implementation returns None which
+ # gets turned into 0.0. Run scripts/detect_missing_analyse_text.py
+ # to find lexers which need it overridden.
+ if code:
+ return cls.analyse_text(code) + bonus, cls.__name__
+ return cls.priority + bonus, cls.__name__
+
+ if matches:
+ matches.sort(key=get_rating)
+ # print "Possible lexers, after sort:", matches
+ return matches[-1][0]
+
+
+def get_lexer_for_filename(_fn, code=None, **options):
+ """Get a lexer for a filename.
+
+ If multiple lexers match the filename pattern, use ``analyse_text()`` to
+ figure out which one is more appropriate.
+
+ Raises ClassNotFound if not found.
+ """
+ res = find_lexer_class_for_filename(_fn, code)
+ if not res:
+ raise ClassNotFound('no lexer for filename %r found' % _fn)
+ return res(**options)
+
+
+def get_lexer_for_mimetype(_mime, **options):
+ """Get a lexer for a mimetype.
+
+ Raises ClassNotFound if not found.
+ """
+ for modname, name, _, _, mimetypes in LEXERS.values():
+ if _mime in mimetypes:
+ if name not in _lexer_cache:
+ _load_lexers(modname)
+ return _lexer_cache[name](**options)
+ for cls in find_plugin_lexers():
+ if _mime in cls.mimetypes:
+ return cls(**options)
+ raise ClassNotFound('no lexer for mimetype %r found' % _mime)
+
+
+def _iter_lexerclasses(plugins=True):
+ """Return an iterator over all lexer classes."""
+ for key in sorted(LEXERS):
+ module_name, name = LEXERS[key][:2]
+ if name not in _lexer_cache:
+ _load_lexers(module_name)
+ yield _lexer_cache[name]
+ if plugins:
+ yield from find_plugin_lexers()
+
+
+def guess_lexer_for_filename(_fn, _text, **options):
+ """
+ Lookup all lexers that handle those filenames primary (``filenames``)
+ or secondary (``alias_filenames``). Then run a text analysis for those
+ lexers and choose the best result.
+
+ usage::
+
+ >>> from pygments.lexers import guess_lexer_for_filename
+ >>> guess_lexer_for_filename('hello.html', '<%= @foo %>')
+ <pygments.lexers.templates.RhtmlLexer object at 0xb7d2f32c>
+ >>> guess_lexer_for_filename('hello.html', '<h1>{{ title|e }}</h1>')
+ <pygments.lexers.templates.HtmlDjangoLexer object at 0xb7d2f2ac>
+ >>> guess_lexer_for_filename('style.css', 'a { color: <?= $link ?> }')
+ <pygments.lexers.templates.CssPhpLexer object at 0xb7ba518c>
+ """
+ fn = basename(_fn)
+ primary = {}
+ matching_lexers = set()
+ for lexer in _iter_lexerclasses():
+ for filename in lexer.filenames:
+ if _fn_matches(fn, filename):
+ matching_lexers.add(lexer)
+ primary[lexer] = True
+ for filename in lexer.alias_filenames:
+ if _fn_matches(fn, filename):
+ matching_lexers.add(lexer)
+ primary[lexer] = False
+ if not matching_lexers:
+ raise ClassNotFound('no lexer for filename %r found' % fn)
+ if len(matching_lexers) == 1:
+ return matching_lexers.pop()(**options)
+ result = []
+ for lexer in matching_lexers:
+ rv = lexer.analyse_text(_text)
+ if rv == 1.0:
+ return lexer(**options)
+ result.append((rv, lexer))
+
+ def type_sort(t):
+ # sort by:
+ # - analyse score
+ # - is primary filename pattern?
+ # - priority
+ # - last resort: class name
+ return (t[0], primary[t[1]], t[1].priority, t[1].__name__)
+ result.sort(key=type_sort)
+
+ return result[-1][1](**options)
+
+
+def guess_lexer(_text, **options):
+ """Guess a lexer by strong distinctions in the text (eg, shebang)."""
+
+ if not isinstance(_text, str):
+ inencoding = options.get('inencoding', options.get('encoding'))
+ if inencoding:
+ _text = _text.decode(inencoding or 'utf8')
+ else:
+ _text, _ = guess_decode(_text)
+
+ # try to get a vim modeline first
+ ft = get_filetype_from_buffer(_text)
+
+ if ft is not None:
+ try:
+ return get_lexer_by_name(ft, **options)
+ except ClassNotFound:
+ pass
+
+ best_lexer = [0.0, None]
+ for lexer in _iter_lexerclasses():
+ rv = lexer.analyse_text(_text)
+ if rv == 1.0:
+ return lexer(**options)
+ if rv > best_lexer[0]:
+ best_lexer[:] = (rv, lexer)
+ if not best_lexer[0] or best_lexer[1] is None:
+ raise ClassNotFound('no lexer matching the text found')
+ return best_lexer[1](**options)
+
+
+class _automodule(types.ModuleType):
+ """Automatically import lexers."""
+
+ def __getattr__(self, name):
+ info = LEXERS.get(name)
+ if info:
+ _load_lexers(info[0])
+ cls = _lexer_cache[info[1]]
+ setattr(self, name, cls)
+ return cls
+ if name in COMPAT:
+ return getattr(self, COMPAT[name])
+ raise AttributeError(name)
+
+
+oldmod = sys.modules[__name__]
+newmod = _automodule(__name__)
+newmod.__dict__.update(oldmod.__dict__)
+sys.modules[__name__] = newmod
+del newmod.newmod, newmod.oldmod, newmod.sys, newmod.types
diff --git a/src/pip/_vendor/pygments/lexers/_mapping.py b/src/pip/_vendor/pygments/lexers/_mapping.py
new file mode 100644
index 000000000..44dbfe677
--- /dev/null
+++ b/src/pip/_vendor/pygments/lexers/_mapping.py
@@ -0,0 +1,596 @@
+"""
+ pygments.lexers._mapping
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Lexer mapping definitions. This file is generated by itself. Every time
+ you change something on a builtin lexer definition, run this script from
+ the lexers folder to update it.
+
+ Do not alter the LEXERS dictionary by hand.
+
+ :copyright: Copyright 2006-2014, 2016 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+LEXERS = {
+ 'ABAPLexer': ('pip._vendor.pygments.lexers.business', 'ABAP', ('abap',), ('*.abap', '*.ABAP'), ('text/x-abap',)),
+ 'AMDGPULexer': ('pip._vendor.pygments.lexers.amdgpu', 'AMDGPU', ('amdgpu',), ('*.isa',), ()),
+ 'APLLexer': ('pip._vendor.pygments.lexers.apl', 'APL', ('apl',), ('*.apl', '*.aplf', '*.aplo', '*.apln', '*.aplc', '*.apli', '*.dyalog'), ()),
+ 'AbnfLexer': ('pip._vendor.pygments.lexers.grammar_notation', 'ABNF', ('abnf',), ('*.abnf',), ('text/x-abnf',)),
+ 'ActionScript3Lexer': ('pip._vendor.pygments.lexers.actionscript', 'ActionScript 3', ('actionscript3', 'as3'), ('*.as',), ('application/x-actionscript3', 'text/x-actionscript3', 'text/actionscript3')),
+ 'ActionScriptLexer': ('pip._vendor.pygments.lexers.actionscript', 'ActionScript', ('actionscript', 'as'), ('*.as',), ('application/x-actionscript', 'text/x-actionscript', 'text/actionscript')),
+ 'AdaLexer': ('pip._vendor.pygments.lexers.ada', 'Ada', ('ada', 'ada95', 'ada2005'), ('*.adb', '*.ads', '*.ada'), ('text/x-ada',)),
+ 'AdlLexer': ('pip._vendor.pygments.lexers.archetype', 'ADL', ('adl',), ('*.adl', '*.adls', '*.adlf', '*.adlx'), ()),
+ 'AgdaLexer': ('pip._vendor.pygments.lexers.haskell', 'Agda', ('agda',), ('*.agda',), ('text/x-agda',)),
+ 'AheuiLexer': ('pip._vendor.pygments.lexers.esoteric', 'Aheui', ('aheui',), ('*.aheui',), ()),
+ 'AlloyLexer': ('pip._vendor.pygments.lexers.dsls', 'Alloy', ('alloy',), ('*.als',), ('text/x-alloy',)),
+ 'AmbientTalkLexer': ('pip._vendor.pygments.lexers.ambient', 'AmbientTalk', ('ambienttalk', 'ambienttalk/2', 'at'), ('*.at',), ('text/x-ambienttalk',)),
+ 'AmplLexer': ('pip._vendor.pygments.lexers.ampl', 'Ampl', ('ampl',), ('*.run',), ()),
+ 'Angular2HtmlLexer': ('pip._vendor.pygments.lexers.templates', 'HTML + Angular2', ('html+ng2',), ('*.ng2',), ()),
+ 'Angular2Lexer': ('pip._vendor.pygments.lexers.templates', 'Angular2', ('ng2',), (), ()),
+ 'AntlrActionScriptLexer': ('pip._vendor.pygments.lexers.parsers', 'ANTLR With ActionScript Target', ('antlr-actionscript', 'antlr-as'), ('*.G', '*.g'), ()),
+ 'AntlrCSharpLexer': ('pip._vendor.pygments.lexers.parsers', 'ANTLR With C# Target', ('antlr-csharp', 'antlr-c#'), ('*.G', '*.g'), ()),
+ 'AntlrCppLexer': ('pip._vendor.pygments.lexers.parsers', 'ANTLR With CPP Target', ('antlr-cpp',), ('*.G', '*.g'), ()),
+ 'AntlrJavaLexer': ('pip._vendor.pygments.lexers.parsers', 'ANTLR With Java Target', ('antlr-java',), ('*.G', '*.g'), ()),
+ 'AntlrLexer': ('pip._vendor.pygments.lexers.parsers', 'ANTLR', ('antlr',), (), ()),
+ 'AntlrObjectiveCLexer': ('pip._vendor.pygments.lexers.parsers', 'ANTLR With ObjectiveC Target', ('antlr-objc',), ('*.G', '*.g'), ()),
+ 'AntlrPerlLexer': ('pip._vendor.pygments.lexers.parsers', 'ANTLR With Perl Target', ('antlr-perl',), ('*.G', '*.g'), ()),
+ 'AntlrPythonLexer': ('pip._vendor.pygments.lexers.parsers', 'ANTLR With Python Target', ('antlr-python',), ('*.G', '*.g'), ()),
+ 'AntlrRubyLexer': ('pip._vendor.pygments.lexers.parsers', 'ANTLR With Ruby Target', ('antlr-ruby', 'antlr-rb'), ('*.G', '*.g'), ()),
+ 'ApacheConfLexer': ('pip._vendor.pygments.lexers.configs', 'ApacheConf', ('apacheconf', 'aconf', 'apache'), ('.htaccess', 'apache.conf', 'apache2.conf'), ('text/x-apacheconf',)),
+ 'AppleScriptLexer': ('pip._vendor.pygments.lexers.scripting', 'AppleScript', ('applescript',), ('*.applescript',), ()),
+ 'ArduinoLexer': ('pip._vendor.pygments.lexers.c_like', 'Arduino', ('arduino',), ('*.ino',), ('text/x-arduino',)),
+ 'ArrowLexer': ('pip._vendor.pygments.lexers.arrow', 'Arrow', ('arrow',), ('*.arw',), ()),
+ 'AscLexer': ('pip._vendor.pygments.lexers.asc', 'ASCII armored', ('asc', 'pem'), ('*.asc', '*.pem', 'id_dsa', 'id_ecdsa', 'id_ecdsa_sk', 'id_ed25519', 'id_ed25519_sk', 'id_rsa'), ('application/pgp-keys', 'application/pgp-encrypted', 'application/pgp-signature')),
+ 'AspectJLexer': ('pip._vendor.pygments.lexers.jvm', 'AspectJ', ('aspectj',), ('*.aj',), ('text/x-aspectj',)),
+ 'AsymptoteLexer': ('pip._vendor.pygments.lexers.graphics', 'Asymptote', ('asymptote', 'asy'), ('*.asy',), ('text/x-asymptote',)),
+ 'AugeasLexer': ('pip._vendor.pygments.lexers.configs', 'Augeas', ('augeas',), ('*.aug',), ()),
+ 'AutoItLexer': ('pip._vendor.pygments.lexers.automation', 'AutoIt', ('autoit',), ('*.au3',), ('text/x-autoit',)),
+ 'AutohotkeyLexer': ('pip._vendor.pygments.lexers.automation', 'autohotkey', ('autohotkey', 'ahk'), ('*.ahk', '*.ahkl'), ('text/x-autohotkey',)),
+ 'AwkLexer': ('pip._vendor.pygments.lexers.textedit', 'Awk', ('awk', 'gawk', 'mawk', 'nawk'), ('*.awk',), ('application/x-awk',)),
+ 'BBCBasicLexer': ('pip._vendor.pygments.lexers.basic', 'BBC Basic', ('bbcbasic',), ('*.bbc',), ()),
+ 'BBCodeLexer': ('pip._vendor.pygments.lexers.markup', 'BBCode', ('bbcode',), (), ('text/x-bbcode',)),
+ 'BCLexer': ('pip._vendor.pygments.lexers.algebra', 'BC', ('bc',), ('*.bc',), ()),
+ 'BSTLexer': ('pip._vendor.pygments.lexers.bibtex', 'BST', ('bst', 'bst-pybtex'), ('*.bst',), ()),
+ 'BareLexer': ('pip._vendor.pygments.lexers.bare', 'BARE', ('bare',), ('*.bare',), ()),
+ 'BaseMakefileLexer': ('pip._vendor.pygments.lexers.make', 'Base Makefile', ('basemake',), (), ()),
+ 'BashLexer': ('pip._vendor.pygments.lexers.shell', 'Bash', ('bash', 'sh', 'ksh', 'zsh', 'shell'), ('*.sh', '*.ksh', '*.bash', '*.ebuild', '*.eclass', '*.exheres-0', '*.exlib', '*.zsh', '.bashrc', 'bashrc', '.bash_*', 'bash_*', 'zshrc', '.zshrc', '.kshrc', 'kshrc', 'PKGBUILD'), ('application/x-sh', 'application/x-shellscript', 'text/x-shellscript')),
+ 'BashSessionLexer': ('pip._vendor.pygments.lexers.shell', 'Bash Session', ('console', 'shell-session'), ('*.sh-session', '*.shell-session'), ('application/x-shell-session', 'application/x-sh-session')),
+ 'BatchLexer': ('pip._vendor.pygments.lexers.shell', 'Batchfile', ('batch', 'bat', 'dosbatch', 'winbatch'), ('*.bat', '*.cmd'), ('application/x-dos-batch',)),
+ 'BddLexer': ('pip._vendor.pygments.lexers.bdd', 'Bdd', ('bdd',), ('*.feature',), ('text/x-bdd',)),
+ 'BefungeLexer': ('pip._vendor.pygments.lexers.esoteric', 'Befunge', ('befunge',), ('*.befunge',), ('application/x-befunge',)),
+ 'BerryLexer': ('pip._vendor.pygments.lexers.berry', 'Berry', ('berry', 'be'), ('*.be',), ('text/x-berry', 'application/x-berry')),
+ 'BibTeXLexer': ('pip._vendor.pygments.lexers.bibtex', 'BibTeX', ('bibtex', 'bib'), ('*.bib',), ('text/x-bibtex',)),
+ 'BlitzBasicLexer': ('pip._vendor.pygments.lexers.basic', 'BlitzBasic', ('blitzbasic', 'b3d', 'bplus'), ('*.bb', '*.decls'), ('text/x-bb',)),
+ 'BlitzMaxLexer': ('pip._vendor.pygments.lexers.basic', 'BlitzMax', ('blitzmax', 'bmax'), ('*.bmx',), ('text/x-bmx',)),
+ 'BnfLexer': ('pip._vendor.pygments.lexers.grammar_notation', 'BNF', ('bnf',), ('*.bnf',), ('text/x-bnf',)),
+ 'BoaLexer': ('pip._vendor.pygments.lexers.boa', 'Boa', ('boa',), ('*.boa',), ()),
+ 'BooLexer': ('pip._vendor.pygments.lexers.dotnet', 'Boo', ('boo',), ('*.boo',), ('text/x-boo',)),
+ 'BoogieLexer': ('pip._vendor.pygments.lexers.verification', 'Boogie', ('boogie',), ('*.bpl',), ()),
+ 'BrainfuckLexer': ('pip._vendor.pygments.lexers.esoteric', 'Brainfuck', ('brainfuck', 'bf'), ('*.bf', '*.b'), ('application/x-brainfuck',)),
+ 'BugsLexer': ('pip._vendor.pygments.lexers.modeling', 'BUGS', ('bugs', 'winbugs', 'openbugs'), ('*.bug',), ()),
+ 'CAmkESLexer': ('pip._vendor.pygments.lexers.esoteric', 'CAmkES', ('camkes', 'idl4'), ('*.camkes', '*.idl4'), ()),
+ 'CLexer': ('pip._vendor.pygments.lexers.c_cpp', 'C', ('c',), ('*.c', '*.h', '*.idc', '*.x[bp]m'), ('text/x-chdr', 'text/x-csrc', 'image/x-xbitmap', 'image/x-xpixmap')),
+ 'CMakeLexer': ('pip._vendor.pygments.lexers.make', 'CMake', ('cmake',), ('*.cmake', 'CMakeLists.txt'), ('text/x-cmake',)),
+ 'CObjdumpLexer': ('pip._vendor.pygments.lexers.asm', 'c-objdump', ('c-objdump',), ('*.c-objdump',), ('text/x-c-objdump',)),
+ 'CPSALexer': ('pip._vendor.pygments.lexers.lisp', 'CPSA', ('cpsa',), ('*.cpsa',), ()),
+ 'CSSUL4Lexer': ('pip._vendor.pygments.lexers.ul4', 'CSS+UL4', ('css+ul4',), ('*.cssul4',), ()),
+ 'CSharpAspxLexer': ('pip._vendor.pygments.lexers.dotnet', 'aspx-cs', ('aspx-cs',), ('*.aspx', '*.asax', '*.ascx', '*.ashx', '*.asmx', '*.axd'), ()),
+ 'CSharpLexer': ('pip._vendor.pygments.lexers.dotnet', 'C#', ('csharp', 'c#', 'cs'), ('*.cs',), ('text/x-csharp',)),
+ 'Ca65Lexer': ('pip._vendor.pygments.lexers.asm', 'ca65 assembler', ('ca65',), ('*.s',), ()),
+ 'CadlLexer': ('pip._vendor.pygments.lexers.archetype', 'cADL', ('cadl',), ('*.cadl',), ()),
+ 'CapDLLexer': ('pip._vendor.pygments.lexers.esoteric', 'CapDL', ('capdl',), ('*.cdl',), ()),
+ 'CapnProtoLexer': ('pip._vendor.pygments.lexers.capnproto', "Cap'n Proto", ('capnp',), ('*.capnp',), ()),
+ 'CbmBasicV2Lexer': ('pip._vendor.pygments.lexers.basic', 'CBM BASIC V2', ('cbmbas',), ('*.bas',), ()),
+ 'CddlLexer': ('pip._vendor.pygments.lexers.cddl', 'CDDL', ('cddl',), ('*.cddl',), ('text/x-cddl',)),
+ 'CeylonLexer': ('pip._vendor.pygments.lexers.jvm', 'Ceylon', ('ceylon',), ('*.ceylon',), ('text/x-ceylon',)),
+ 'Cfengine3Lexer': ('pip._vendor.pygments.lexers.configs', 'CFEngine3', ('cfengine3', 'cf3'), ('*.cf',), ()),
+ 'ChaiscriptLexer': ('pip._vendor.pygments.lexers.scripting', 'ChaiScript', ('chaiscript', 'chai'), ('*.chai',), ('text/x-chaiscript', 'application/x-chaiscript')),
+ 'ChapelLexer': ('pip._vendor.pygments.lexers.chapel', 'Chapel', ('chapel', 'chpl'), ('*.chpl',), ()),
+ 'CharmciLexer': ('pip._vendor.pygments.lexers.c_like', 'Charmci', ('charmci',), ('*.ci',), ()),
+ 'CheetahHtmlLexer': ('pip._vendor.pygments.lexers.templates', 'HTML+Cheetah', ('html+cheetah', 'html+spitfire', 'htmlcheetah'), (), ('text/html+cheetah', 'text/html+spitfire')),
+ 'CheetahJavascriptLexer': ('pip._vendor.pygments.lexers.templates', 'JavaScript+Cheetah', ('javascript+cheetah', 'js+cheetah', 'javascript+spitfire', 'js+spitfire'), (), ('application/x-javascript+cheetah', 'text/x-javascript+cheetah', 'text/javascript+cheetah', 'application/x-javascript+spitfire', 'text/x-javascript+spitfire', 'text/javascript+spitfire')),
+ 'CheetahLexer': ('pip._vendor.pygments.lexers.templates', 'Cheetah', ('cheetah', 'spitfire'), ('*.tmpl', '*.spt'), ('application/x-cheetah', 'application/x-spitfire')),
+ 'CheetahXmlLexer': ('pip._vendor.pygments.lexers.templates', 'XML+Cheetah', ('xml+cheetah', 'xml+spitfire'), (), ('application/xml+cheetah', 'application/xml+spitfire')),
+ 'CirruLexer': ('pip._vendor.pygments.lexers.webmisc', 'Cirru', ('cirru',), ('*.cirru',), ('text/x-cirru',)),
+ 'ClayLexer': ('pip._vendor.pygments.lexers.c_like', 'Clay', ('clay',), ('*.clay',), ('text/x-clay',)),
+ 'CleanLexer': ('pip._vendor.pygments.lexers.clean', 'Clean', ('clean',), ('*.icl', '*.dcl'), ()),
+ 'ClojureLexer': ('pip._vendor.pygments.lexers.jvm', 'Clojure', ('clojure', 'clj'), ('*.clj', '*.cljc'), ('text/x-clojure', 'application/x-clojure')),
+ 'ClojureScriptLexer': ('pip._vendor.pygments.lexers.jvm', 'ClojureScript', ('clojurescript', 'cljs'), ('*.cljs',), ('text/x-clojurescript', 'application/x-clojurescript')),
+ 'CobolFreeformatLexer': ('pip._vendor.pygments.lexers.business', 'COBOLFree', ('cobolfree',), ('*.cbl', '*.CBL'), ()),
+ 'CobolLexer': ('pip._vendor.pygments.lexers.business', 'COBOL', ('cobol',), ('*.cob', '*.COB', '*.cpy', '*.CPY'), ('text/x-cobol',)),
+ 'CoffeeScriptLexer': ('pip._vendor.pygments.lexers.javascript', 'CoffeeScript', ('coffeescript', 'coffee-script', 'coffee'), ('*.coffee',), ('text/coffeescript',)),
+ 'ColdfusionCFCLexer': ('pip._vendor.pygments.lexers.templates', 'Coldfusion CFC', ('cfc',), ('*.cfc',), ()),
+ 'ColdfusionHtmlLexer': ('pip._vendor.pygments.lexers.templates', 'Coldfusion HTML', ('cfm',), ('*.cfm', '*.cfml'), ('application/x-coldfusion',)),
+ 'ColdfusionLexer': ('pip._vendor.pygments.lexers.templates', 'cfstatement', ('cfs',), (), ()),
+ 'CommonLispLexer': ('pip._vendor.pygments.lexers.lisp', 'Common Lisp', ('common-lisp', 'cl', 'lisp'), ('*.cl', '*.lisp'), ('text/x-common-lisp',)),
+ 'ComponentPascalLexer': ('pip._vendor.pygments.lexers.oberon', 'Component Pascal', ('componentpascal', 'cp'), ('*.cp', '*.cps'), ('text/x-component-pascal',)),
+ 'CoqLexer': ('pip._vendor.pygments.lexers.theorem', 'Coq', ('coq',), ('*.v',), ('text/x-coq',)),
+ 'CplintLexer': ('pip._vendor.pygments.lexers.cplint', 'cplint', ('cplint',), ('*.ecl', '*.prolog', '*.pro', '*.pl', '*.P', '*.lpad', '*.cpl'), ('text/x-cplint',)),
+ 'CppLexer': ('pip._vendor.pygments.lexers.c_cpp', 'C++', ('cpp', 'c++'), ('*.cpp', '*.hpp', '*.c++', '*.h++', '*.cc', '*.hh', '*.cxx', '*.hxx', '*.C', '*.H', '*.cp', '*.CPP', '*.tpp'), ('text/x-c++hdr', 'text/x-c++src')),
+ 'CppObjdumpLexer': ('pip._vendor.pygments.lexers.asm', 'cpp-objdump', ('cpp-objdump', 'c++-objdumb', 'cxx-objdump'), ('*.cpp-objdump', '*.c++-objdump', '*.cxx-objdump'), ('text/x-cpp-objdump',)),
+ 'CrmshLexer': ('pip._vendor.pygments.lexers.dsls', 'Crmsh', ('crmsh', 'pcmk'), ('*.crmsh', '*.pcmk'), ()),
+ 'CrocLexer': ('pip._vendor.pygments.lexers.d', 'Croc', ('croc',), ('*.croc',), ('text/x-crocsrc',)),
+ 'CryptolLexer': ('pip._vendor.pygments.lexers.haskell', 'Cryptol', ('cryptol', 'cry'), ('*.cry',), ('text/x-cryptol',)),
+ 'CrystalLexer': ('pip._vendor.pygments.lexers.crystal', 'Crystal', ('cr', 'crystal'), ('*.cr',), ('text/x-crystal',)),
+ 'CsoundDocumentLexer': ('pip._vendor.pygments.lexers.csound', 'Csound Document', ('csound-document', 'csound-csd'), ('*.csd',), ()),
+ 'CsoundOrchestraLexer': ('pip._vendor.pygments.lexers.csound', 'Csound Orchestra', ('csound', 'csound-orc'), ('*.orc', '*.udo'), ()),
+ 'CsoundScoreLexer': ('pip._vendor.pygments.lexers.csound', 'Csound Score', ('csound-score', 'csound-sco'), ('*.sco',), ()),
+ 'CssDjangoLexer': ('pip._vendor.pygments.lexers.templates', 'CSS+Django/Jinja', ('css+django', 'css+jinja'), ('*.css.j2', '*.css.jinja2'), ('text/css+django', 'text/css+jinja')),
+ 'CssErbLexer': ('pip._vendor.pygments.lexers.templates', 'CSS+Ruby', ('css+ruby', 'css+erb'), (), ('text/css+ruby',)),
+ 'CssGenshiLexer': ('pip._vendor.pygments.lexers.templates', 'CSS+Genshi Text', ('css+genshitext', 'css+genshi'), (), ('text/css+genshi',)),
+ 'CssLexer': ('pip._vendor.pygments.lexers.css', 'CSS', ('css',), ('*.css',), ('text/css',)),
+ 'CssPhpLexer': ('pip._vendor.pygments.lexers.templates', 'CSS+PHP', ('css+php',), (), ('text/css+php',)),
+ 'CssSmartyLexer': ('pip._vendor.pygments.lexers.templates', 'CSS+Smarty', ('css+smarty',), (), ('text/css+smarty',)),
+ 'CudaLexer': ('pip._vendor.pygments.lexers.c_like', 'CUDA', ('cuda', 'cu'), ('*.cu', '*.cuh'), ('text/x-cuda',)),
+ 'CypherLexer': ('pip._vendor.pygments.lexers.graph', 'Cypher', ('cypher',), ('*.cyp', '*.cypher'), ()),
+ 'CythonLexer': ('pip._vendor.pygments.lexers.python', 'Cython', ('cython', 'pyx', 'pyrex'), ('*.pyx', '*.pxd', '*.pxi'), ('text/x-cython', 'application/x-cython')),
+ 'DLexer': ('pip._vendor.pygments.lexers.d', 'D', ('d',), ('*.d', '*.di'), ('text/x-dsrc',)),
+ 'DObjdumpLexer': ('pip._vendor.pygments.lexers.asm', 'd-objdump', ('d-objdump',), ('*.d-objdump',), ('text/x-d-objdump',)),
+ 'DarcsPatchLexer': ('pip._vendor.pygments.lexers.diff', 'Darcs Patch', ('dpatch',), ('*.dpatch', '*.darcspatch'), ()),
+ 'DartLexer': ('pip._vendor.pygments.lexers.javascript', 'Dart', ('dart',), ('*.dart',), ('text/x-dart',)),
+ 'Dasm16Lexer': ('pip._vendor.pygments.lexers.asm', 'DASM16', ('dasm16',), ('*.dasm16', '*.dasm'), ('text/x-dasm16',)),
+ 'DebianControlLexer': ('pip._vendor.pygments.lexers.installers', 'Debian Control file', ('debcontrol', 'control'), ('control',), ()),
+ 'DelphiLexer': ('pip._vendor.pygments.lexers.pascal', 'Delphi', ('delphi', 'pas', 'pascal', 'objectpascal'), ('*.pas', '*.dpr'), ('text/x-pascal',)),
+ 'DevicetreeLexer': ('pip._vendor.pygments.lexers.devicetree', 'Devicetree', ('devicetree', 'dts'), ('*.dts', '*.dtsi'), ('text/x-c',)),
+ 'DgLexer': ('pip._vendor.pygments.lexers.python', 'dg', ('dg',), ('*.dg',), ('text/x-dg',)),
+ 'DiffLexer': ('pip._vendor.pygments.lexers.diff', 'Diff', ('diff', 'udiff'), ('*.diff', '*.patch'), ('text/x-diff', 'text/x-patch')),
+ 'DjangoLexer': ('pip._vendor.pygments.lexers.templates', 'Django/Jinja', ('django', 'jinja'), (), ('application/x-django-templating', 'application/x-jinja')),
+ 'DockerLexer': ('pip._vendor.pygments.lexers.configs', 'Docker', ('docker', 'dockerfile'), ('Dockerfile', '*.docker'), ('text/x-dockerfile-config',)),
+ 'DtdLexer': ('pip._vendor.pygments.lexers.html', 'DTD', ('dtd',), ('*.dtd',), ('application/xml-dtd',)),
+ 'DuelLexer': ('pip._vendor.pygments.lexers.webmisc', 'Duel', ('duel', 'jbst', 'jsonml+bst'), ('*.duel', '*.jbst'), ('text/x-duel', 'text/x-jbst')),
+ 'DylanConsoleLexer': ('pip._vendor.pygments.lexers.dylan', 'Dylan session', ('dylan-console', 'dylan-repl'), ('*.dylan-console',), ('text/x-dylan-console',)),
+ 'DylanLexer': ('pip._vendor.pygments.lexers.dylan', 'Dylan', ('dylan',), ('*.dylan', '*.dyl', '*.intr'), ('text/x-dylan',)),
+ 'DylanLidLexer': ('pip._vendor.pygments.lexers.dylan', 'DylanLID', ('dylan-lid', 'lid'), ('*.lid', '*.hdp'), ('text/x-dylan-lid',)),
+ 'ECLLexer': ('pip._vendor.pygments.lexers.ecl', 'ECL', ('ecl',), ('*.ecl',), ('application/x-ecl',)),
+ 'ECLexer': ('pip._vendor.pygments.lexers.c_like', 'eC', ('ec',), ('*.ec', '*.eh'), ('text/x-echdr', 'text/x-ecsrc')),
+ 'EarlGreyLexer': ('pip._vendor.pygments.lexers.javascript', 'Earl Grey', ('earl-grey', 'earlgrey', 'eg'), ('*.eg',), ('text/x-earl-grey',)),
+ 'EasytrieveLexer': ('pip._vendor.pygments.lexers.scripting', 'Easytrieve', ('easytrieve',), ('*.ezt', '*.mac'), ('text/x-easytrieve',)),
+ 'EbnfLexer': ('pip._vendor.pygments.lexers.parsers', 'EBNF', ('ebnf',), ('*.ebnf',), ('text/x-ebnf',)),
+ 'EiffelLexer': ('pip._vendor.pygments.lexers.eiffel', 'Eiffel', ('eiffel',), ('*.e',), ('text/x-eiffel',)),
+ 'ElixirConsoleLexer': ('pip._vendor.pygments.lexers.erlang', 'Elixir iex session', ('iex',), (), ('text/x-elixir-shellsession',)),
+ 'ElixirLexer': ('pip._vendor.pygments.lexers.erlang', 'Elixir', ('elixir', 'ex', 'exs'), ('*.ex', '*.eex', '*.exs', '*.leex'), ('text/x-elixir',)),
+ 'ElmLexer': ('pip._vendor.pygments.lexers.elm', 'Elm', ('elm',), ('*.elm',), ('text/x-elm',)),
+ 'ElpiLexer': ('pip._vendor.pygments.lexers.elpi', 'Elpi', ('elpi',), ('*.elpi',), ('text/x-elpi',)),
+ 'EmacsLispLexer': ('pip._vendor.pygments.lexers.lisp', 'EmacsLisp', ('emacs-lisp', 'elisp', 'emacs'), ('*.el',), ('text/x-elisp', 'application/x-elisp')),
+ 'EmailLexer': ('pip._vendor.pygments.lexers.email', 'E-mail', ('email', 'eml'), ('*.eml',), ('message/rfc822',)),
+ 'ErbLexer': ('pip._vendor.pygments.lexers.templates', 'ERB', ('erb',), (), ('application/x-ruby-templating',)),
+ 'ErlangLexer': ('pip._vendor.pygments.lexers.erlang', 'Erlang', ('erlang',), ('*.erl', '*.hrl', '*.es', '*.escript'), ('text/x-erlang',)),
+ 'ErlangShellLexer': ('pip._vendor.pygments.lexers.erlang', 'Erlang erl session', ('erl',), ('*.erl-sh',), ('text/x-erl-shellsession',)),
+ 'EvoqueHtmlLexer': ('pip._vendor.pygments.lexers.templates', 'HTML+Evoque', ('html+evoque',), ('*.html',), ('text/html+evoque',)),
+ 'EvoqueLexer': ('pip._vendor.pygments.lexers.templates', 'Evoque', ('evoque',), ('*.evoque',), ('application/x-evoque',)),
+ 'EvoqueXmlLexer': ('pip._vendor.pygments.lexers.templates', 'XML+Evoque', ('xml+evoque',), ('*.xml',), ('application/xml+evoque',)),
+ 'ExeclineLexer': ('pip._vendor.pygments.lexers.shell', 'execline', ('execline',), ('*.exec',), ()),
+ 'EzhilLexer': ('pip._vendor.pygments.lexers.ezhil', 'Ezhil', ('ezhil',), ('*.n',), ('text/x-ezhil',)),
+ 'FSharpLexer': ('pip._vendor.pygments.lexers.dotnet', 'F#', ('fsharp', 'f#'), ('*.fs', '*.fsi'), ('text/x-fsharp',)),
+ 'FStarLexer': ('pip._vendor.pygments.lexers.ml', 'FStar', ('fstar',), ('*.fst', '*.fsti'), ('text/x-fstar',)),
+ 'FactorLexer': ('pip._vendor.pygments.lexers.factor', 'Factor', ('factor',), ('*.factor',), ('text/x-factor',)),
+ 'FancyLexer': ('pip._vendor.pygments.lexers.ruby', 'Fancy', ('fancy', 'fy'), ('*.fy', '*.fancypack'), ('text/x-fancysrc',)),
+ 'FantomLexer': ('pip._vendor.pygments.lexers.fantom', 'Fantom', ('fan',), ('*.fan',), ('application/x-fantom',)),
+ 'FelixLexer': ('pip._vendor.pygments.lexers.felix', 'Felix', ('felix', 'flx'), ('*.flx', '*.flxh'), ('text/x-felix',)),
+ 'FennelLexer': ('pip._vendor.pygments.lexers.lisp', 'Fennel', ('fennel', 'fnl'), ('*.fnl',), ()),
+ 'FishShellLexer': ('pip._vendor.pygments.lexers.shell', 'Fish', ('fish', 'fishshell'), ('*.fish', '*.load'), ('application/x-fish',)),
+ 'FlatlineLexer': ('pip._vendor.pygments.lexers.dsls', 'Flatline', ('flatline',), (), ('text/x-flatline',)),
+ 'FloScriptLexer': ('pip._vendor.pygments.lexers.floscript', 'FloScript', ('floscript', 'flo'), ('*.flo',), ()),
+ 'ForthLexer': ('pip._vendor.pygments.lexers.forth', 'Forth', ('forth',), ('*.frt', '*.fs'), ('application/x-forth',)),
+ 'FortranFixedLexer': ('pip._vendor.pygments.lexers.fortran', 'FortranFixed', ('fortranfixed',), ('*.f', '*.F'), ()),
+ 'FortranLexer': ('pip._vendor.pygments.lexers.fortran', 'Fortran', ('fortran', 'f90'), ('*.f03', '*.f90', '*.F03', '*.F90'), ('text/x-fortran',)),
+ 'FoxProLexer': ('pip._vendor.pygments.lexers.foxpro', 'FoxPro', ('foxpro', 'vfp', 'clipper', 'xbase'), ('*.PRG', '*.prg'), ()),
+ 'FreeFemLexer': ('pip._vendor.pygments.lexers.freefem', 'Freefem', ('freefem',), ('*.edp',), ('text/x-freefem',)),
+ 'FutharkLexer': ('pip._vendor.pygments.lexers.futhark', 'Futhark', ('futhark',), ('*.fut',), ('text/x-futhark',)),
+ 'GAPLexer': ('pip._vendor.pygments.lexers.algebra', 'GAP', ('gap',), ('*.g', '*.gd', '*.gi', '*.gap'), ()),
+ 'GDScriptLexer': ('pip._vendor.pygments.lexers.gdscript', 'GDScript', ('gdscript', 'gd'), ('*.gd',), ('text/x-gdscript', 'application/x-gdscript')),
+ 'GLShaderLexer': ('pip._vendor.pygments.lexers.graphics', 'GLSL', ('glsl',), ('*.vert', '*.frag', '*.geo'), ('text/x-glslsrc',)),
+ 'GSQLLexer': ('pip._vendor.pygments.lexers.gsql', 'GSQL', ('gsql',), ('*.gsql',), ()),
+ 'GasLexer': ('pip._vendor.pygments.lexers.asm', 'GAS', ('gas', 'asm'), ('*.s', '*.S'), ('text/x-gas',)),
+ 'GcodeLexer': ('pip._vendor.pygments.lexers.gcodelexer', 'g-code', ('gcode',), ('*.gcode',), ()),
+ 'GenshiLexer': ('pip._vendor.pygments.lexers.templates', 'Genshi', ('genshi', 'kid', 'xml+genshi', 'xml+kid'), ('*.kid',), ('application/x-genshi', 'application/x-kid')),
+ 'GenshiTextLexer': ('pip._vendor.pygments.lexers.templates', 'Genshi Text', ('genshitext',), (), ('application/x-genshi-text', 'text/x-genshi')),
+ 'GettextLexer': ('pip._vendor.pygments.lexers.textfmts', 'Gettext Catalog', ('pot', 'po'), ('*.pot', '*.po'), ('application/x-gettext', 'text/x-gettext', 'text/gettext')),
+ 'GherkinLexer': ('pip._vendor.pygments.lexers.testing', 'Gherkin', ('gherkin', 'cucumber'), ('*.feature',), ('text/x-gherkin',)),
+ 'GnuplotLexer': ('pip._vendor.pygments.lexers.graphics', 'Gnuplot', ('gnuplot',), ('*.plot', '*.plt'), ('text/x-gnuplot',)),
+ 'GoLexer': ('pip._vendor.pygments.lexers.go', 'Go', ('go', 'golang'), ('*.go',), ('text/x-gosrc',)),
+ 'GoloLexer': ('pip._vendor.pygments.lexers.jvm', 'Golo', ('golo',), ('*.golo',), ()),
+ 'GoodDataCLLexer': ('pip._vendor.pygments.lexers.business', 'GoodData-CL', ('gooddata-cl',), ('*.gdc',), ('text/x-gooddata-cl',)),
+ 'GosuLexer': ('pip._vendor.pygments.lexers.jvm', 'Gosu', ('gosu',), ('*.gs', '*.gsx', '*.gsp', '*.vark'), ('text/x-gosu',)),
+ 'GosuTemplateLexer': ('pip._vendor.pygments.lexers.jvm', 'Gosu Template', ('gst',), ('*.gst',), ('text/x-gosu-template',)),
+ 'GraphvizLexer': ('pip._vendor.pygments.lexers.graphviz', 'Graphviz', ('graphviz', 'dot'), ('*.gv', '*.dot'), ('text/x-graphviz', 'text/vnd.graphviz')),
+ 'GroffLexer': ('pip._vendor.pygments.lexers.markup', 'Groff', ('groff', 'nroff', 'man'), ('*.[1-9]', '*.man', '*.1p', '*.3pm'), ('application/x-troff', 'text/troff')),
+ 'GroovyLexer': ('pip._vendor.pygments.lexers.jvm', 'Groovy', ('groovy',), ('*.groovy', '*.gradle'), ('text/x-groovy',)),
+ 'HLSLShaderLexer': ('pip._vendor.pygments.lexers.graphics', 'HLSL', ('hlsl',), ('*.hlsl', '*.hlsli'), ('text/x-hlsl',)),
+ 'HTMLUL4Lexer': ('pip._vendor.pygments.lexers.ul4', 'HTML+UL4', ('html+ul4',), ('*.htmlul4',), ()),
+ 'HamlLexer': ('pip._vendor.pygments.lexers.html', 'Haml', ('haml',), ('*.haml',), ('text/x-haml',)),
+ 'HandlebarsHtmlLexer': ('pip._vendor.pygments.lexers.templates', 'HTML+Handlebars', ('html+handlebars',), ('*.handlebars', '*.hbs'), ('text/html+handlebars', 'text/x-handlebars-template')),
+ 'HandlebarsLexer': ('pip._vendor.pygments.lexers.templates', 'Handlebars', ('handlebars',), (), ()),
+ 'HaskellLexer': ('pip._vendor.pygments.lexers.haskell', 'Haskell', ('haskell', 'hs'), ('*.hs',), ('text/x-haskell',)),
+ 'HaxeLexer': ('pip._vendor.pygments.lexers.haxe', 'Haxe', ('haxe', 'hxsl', 'hx'), ('*.hx', '*.hxsl'), ('text/haxe', 'text/x-haxe', 'text/x-hx')),
+ 'HexdumpLexer': ('pip._vendor.pygments.lexers.hexdump', 'Hexdump', ('hexdump',), (), ()),
+ 'HsailLexer': ('pip._vendor.pygments.lexers.asm', 'HSAIL', ('hsail', 'hsa'), ('*.hsail',), ('text/x-hsail',)),
+ 'HspecLexer': ('pip._vendor.pygments.lexers.haskell', 'Hspec', ('hspec',), (), ()),
+ 'HtmlDjangoLexer': ('pip._vendor.pygments.lexers.templates', 'HTML+Django/Jinja', ('html+django', 'html+jinja', 'htmldjango'), ('*.html.j2', '*.htm.j2', '*.xhtml.j2', '*.html.jinja2', '*.htm.jinja2', '*.xhtml.jinja2'), ('text/html+django', 'text/html+jinja')),
+ 'HtmlGenshiLexer': ('pip._vendor.pygments.lexers.templates', 'HTML+Genshi', ('html+genshi', 'html+kid'), (), ('text/html+genshi',)),
+ 'HtmlLexer': ('pip._vendor.pygments.lexers.html', 'HTML', ('html',), ('*.html', '*.htm', '*.xhtml', '*.xslt'), ('text/html', 'application/xhtml+xml')),
+ 'HtmlPhpLexer': ('pip._vendor.pygments.lexers.templates', 'HTML+PHP', ('html+php',), ('*.phtml',), ('application/x-php', 'application/x-httpd-php', 'application/x-httpd-php3', 'application/x-httpd-php4', 'application/x-httpd-php5')),
+ 'HtmlSmartyLexer': ('pip._vendor.pygments.lexers.templates', 'HTML+Smarty', ('html+smarty',), (), ('text/html+smarty',)),
+ 'HttpLexer': ('pip._vendor.pygments.lexers.textfmts', 'HTTP', ('http',), (), ()),
+ 'HxmlLexer': ('pip._vendor.pygments.lexers.haxe', 'Hxml', ('haxeml', 'hxml'), ('*.hxml',), ()),
+ 'HyLexer': ('pip._vendor.pygments.lexers.lisp', 'Hy', ('hylang',), ('*.hy',), ('text/x-hy', 'application/x-hy')),
+ 'HybrisLexer': ('pip._vendor.pygments.lexers.scripting', 'Hybris', ('hybris', 'hy'), ('*.hy', '*.hyb'), ('text/x-hybris', 'application/x-hybris')),
+ 'IDLLexer': ('pip._vendor.pygments.lexers.idl', 'IDL', ('idl',), ('*.pro',), ('text/idl',)),
+ 'IconLexer': ('pip._vendor.pygments.lexers.unicon', 'Icon', ('icon',), ('*.icon', '*.ICON'), ()),
+ 'IdrisLexer': ('pip._vendor.pygments.lexers.haskell', 'Idris', ('idris', 'idr'), ('*.idr',), ('text/x-idris',)),
+ 'IgorLexer': ('pip._vendor.pygments.lexers.igor', 'Igor', ('igor', 'igorpro'), ('*.ipf',), ('text/ipf',)),
+ 'Inform6Lexer': ('pip._vendor.pygments.lexers.int_fiction', 'Inform 6', ('inform6', 'i6'), ('*.inf',), ()),
+ 'Inform6TemplateLexer': ('pip._vendor.pygments.lexers.int_fiction', 'Inform 6 template', ('i6t',), ('*.i6t',), ()),
+ 'Inform7Lexer': ('pip._vendor.pygments.lexers.int_fiction', 'Inform 7', ('inform7', 'i7'), ('*.ni', '*.i7x'), ()),
+ 'IniLexer': ('pip._vendor.pygments.lexers.configs', 'INI', ('ini', 'cfg', 'dosini'), ('*.ini', '*.cfg', '*.inf', '.editorconfig', '*.service', '*.socket', '*.device', '*.mount', '*.automount', '*.swap', '*.target', '*.path', '*.timer', '*.slice', '*.scope'), ('text/x-ini', 'text/inf')),
+ 'IoLexer': ('pip._vendor.pygments.lexers.iolang', 'Io', ('io',), ('*.io',), ('text/x-iosrc',)),
+ 'IokeLexer': ('pip._vendor.pygments.lexers.jvm', 'Ioke', ('ioke', 'ik'), ('*.ik',), ('text/x-iokesrc',)),
+ 'IrcLogsLexer': ('pip._vendor.pygments.lexers.textfmts', 'IRC logs', ('irc',), ('*.weechatlog',), ('text/x-irclog',)),
+ 'IsabelleLexer': ('pip._vendor.pygments.lexers.theorem', 'Isabelle', ('isabelle',), ('*.thy',), ('text/x-isabelle',)),
+ 'JLexer': ('pip._vendor.pygments.lexers.j', 'J', ('j',), ('*.ijs',), ('text/x-j',)),
+ 'JSLTLexer': ('pip._vendor.pygments.lexers.jslt', 'JSLT', ('jslt',), ('*.jslt',), ('text/x-jslt',)),
+ 'JagsLexer': ('pip._vendor.pygments.lexers.modeling', 'JAGS', ('jags',), ('*.jag', '*.bug'), ()),
+ 'JasminLexer': ('pip._vendor.pygments.lexers.jvm', 'Jasmin', ('jasmin', 'jasminxt'), ('*.j',), ()),
+ 'JavaLexer': ('pip._vendor.pygments.lexers.jvm', 'Java', ('java',), ('*.java',), ('text/x-java',)),
+ 'JavascriptDjangoLexer': ('pip._vendor.pygments.lexers.templates', 'JavaScript+Django/Jinja', ('javascript+django', 'js+django', 'javascript+jinja', 'js+jinja'), ('*.js.j2', '*.js.jinja2'), ('application/x-javascript+django', 'application/x-javascript+jinja', 'text/x-javascript+django', 'text/x-javascript+jinja', 'text/javascript+django', 'text/javascript+jinja')),
+ 'JavascriptErbLexer': ('pip._vendor.pygments.lexers.templates', 'JavaScript+Ruby', ('javascript+ruby', 'js+ruby', 'javascript+erb', 'js+erb'), (), ('application/x-javascript+ruby', 'text/x-javascript+ruby', 'text/javascript+ruby')),
+ 'JavascriptGenshiLexer': ('pip._vendor.pygments.lexers.templates', 'JavaScript+Genshi Text', ('js+genshitext', 'js+genshi', 'javascript+genshitext', 'javascript+genshi'), (), ('application/x-javascript+genshi', 'text/x-javascript+genshi', 'text/javascript+genshi')),
+ 'JavascriptLexer': ('pip._vendor.pygments.lexers.javascript', 'JavaScript', ('javascript', 'js'), ('*.js', '*.jsm', '*.mjs', '*.cjs'), ('application/javascript', 'application/x-javascript', 'text/x-javascript', 'text/javascript')),
+ 'JavascriptPhpLexer': ('pip._vendor.pygments.lexers.templates', 'JavaScript+PHP', ('javascript+php', 'js+php'), (), ('application/x-javascript+php', 'text/x-javascript+php', 'text/javascript+php')),
+ 'JavascriptSmartyLexer': ('pip._vendor.pygments.lexers.templates', 'JavaScript+Smarty', ('javascript+smarty', 'js+smarty'), (), ('application/x-javascript+smarty', 'text/x-javascript+smarty', 'text/javascript+smarty')),
+ 'JavascriptUL4Lexer': ('pip._vendor.pygments.lexers.ul4', 'Javascript+UL4', ('js+ul4',), ('*.jsul4',), ()),
+ 'JclLexer': ('pip._vendor.pygments.lexers.scripting', 'JCL', ('jcl',), ('*.jcl',), ('text/x-jcl',)),
+ 'JsgfLexer': ('pip._vendor.pygments.lexers.grammar_notation', 'JSGF', ('jsgf',), ('*.jsgf',), ('application/jsgf', 'application/x-jsgf', 'text/jsgf')),
+ 'JsonBareObjectLexer': ('pip._vendor.pygments.lexers.data', 'JSONBareObject', (), (), ()),
+ 'JsonLdLexer': ('pip._vendor.pygments.lexers.data', 'JSON-LD', ('jsonld', 'json-ld'), ('*.jsonld',), ('application/ld+json',)),
+ 'JsonLexer': ('pip._vendor.pygments.lexers.data', 'JSON', ('json', 'json-object'), ('*.json', 'Pipfile.lock'), ('application/json', 'application/json-object')),
+ 'JspLexer': ('pip._vendor.pygments.lexers.templates', 'Java Server Page', ('jsp',), ('*.jsp',), ('application/x-jsp',)),
+ 'JuliaConsoleLexer': ('pip._vendor.pygments.lexers.julia', 'Julia console', ('jlcon', 'julia-repl'), (), ()),
+ 'JuliaLexer': ('pip._vendor.pygments.lexers.julia', 'Julia', ('julia', 'jl'), ('*.jl',), ('text/x-julia', 'application/x-julia')),
+ 'JuttleLexer': ('pip._vendor.pygments.lexers.javascript', 'Juttle', ('juttle',), ('*.juttle',), ('application/juttle', 'application/x-juttle', 'text/x-juttle', 'text/juttle')),
+ 'KLexer': ('pip._vendor.pygments.lexers.q', 'K', ('k',), ('*.k',), ()),
+ 'KalLexer': ('pip._vendor.pygments.lexers.javascript', 'Kal', ('kal',), ('*.kal',), ('text/kal', 'application/kal')),
+ 'KconfigLexer': ('pip._vendor.pygments.lexers.configs', 'Kconfig', ('kconfig', 'menuconfig', 'linux-config', 'kernel-config'), ('Kconfig*', '*Config.in*', 'external.in*', 'standard-modules.in'), ('text/x-kconfig',)),
+ 'KernelLogLexer': ('pip._vendor.pygments.lexers.textfmts', 'Kernel log', ('kmsg', 'dmesg'), ('*.kmsg', '*.dmesg'), ()),
+ 'KokaLexer': ('pip._vendor.pygments.lexers.haskell', 'Koka', ('koka',), ('*.kk', '*.kki'), ('text/x-koka',)),
+ 'KotlinLexer': ('pip._vendor.pygments.lexers.jvm', 'Kotlin', ('kotlin',), ('*.kt', '*.kts'), ('text/x-kotlin',)),
+ 'KuinLexer': ('pip._vendor.pygments.lexers.kuin', 'Kuin', ('kuin',), ('*.kn',), ()),
+ 'LSLLexer': ('pip._vendor.pygments.lexers.scripting', 'LSL', ('lsl',), ('*.lsl',), ('text/x-lsl',)),
+ 'LassoCssLexer': ('pip._vendor.pygments.lexers.templates', 'CSS+Lasso', ('css+lasso',), (), ('text/css+lasso',)),
+ 'LassoHtmlLexer': ('pip._vendor.pygments.lexers.templates', 'HTML+Lasso', ('html+lasso',), (), ('text/html+lasso', 'application/x-httpd-lasso', 'application/x-httpd-lasso[89]')),
+ 'LassoJavascriptLexer': ('pip._vendor.pygments.lexers.templates', 'JavaScript+Lasso', ('javascript+lasso', 'js+lasso'), (), ('application/x-javascript+lasso', 'text/x-javascript+lasso', 'text/javascript+lasso')),
+ 'LassoLexer': ('pip._vendor.pygments.lexers.javascript', 'Lasso', ('lasso', 'lassoscript'), ('*.lasso', '*.lasso[89]'), ('text/x-lasso',)),
+ 'LassoXmlLexer': ('pip._vendor.pygments.lexers.templates', 'XML+Lasso', ('xml+lasso',), (), ('application/xml+lasso',)),
+ 'LeanLexer': ('pip._vendor.pygments.lexers.theorem', 'Lean', ('lean',), ('*.lean',), ('text/x-lean',)),
+ 'LessCssLexer': ('pip._vendor.pygments.lexers.css', 'LessCss', ('less',), ('*.less',), ('text/x-less-css',)),
+ 'LighttpdConfLexer': ('pip._vendor.pygments.lexers.configs', 'Lighttpd configuration file', ('lighttpd', 'lighty'), ('lighttpd.conf',), ('text/x-lighttpd-conf',)),
+ 'LilyPondLexer': ('pip._vendor.pygments.lexers.lilypond', 'LilyPond', ('lilypond',), ('*.ly',), ()),
+ 'LimboLexer': ('pip._vendor.pygments.lexers.inferno', 'Limbo', ('limbo',), ('*.b',), ('text/limbo',)),
+ 'LiquidLexer': ('pip._vendor.pygments.lexers.templates', 'liquid', ('liquid',), ('*.liquid',), ()),
+ 'LiterateAgdaLexer': ('pip._vendor.pygments.lexers.haskell', 'Literate Agda', ('literate-agda', 'lagda'), ('*.lagda',), ('text/x-literate-agda',)),
+ 'LiterateCryptolLexer': ('pip._vendor.pygments.lexers.haskell', 'Literate Cryptol', ('literate-cryptol', 'lcryptol', 'lcry'), ('*.lcry',), ('text/x-literate-cryptol',)),
+ 'LiterateHaskellLexer': ('pip._vendor.pygments.lexers.haskell', 'Literate Haskell', ('literate-haskell', 'lhaskell', 'lhs'), ('*.lhs',), ('text/x-literate-haskell',)),
+ 'LiterateIdrisLexer': ('pip._vendor.pygments.lexers.haskell', 'Literate Idris', ('literate-idris', 'lidris', 'lidr'), ('*.lidr',), ('text/x-literate-idris',)),
+ 'LiveScriptLexer': ('pip._vendor.pygments.lexers.javascript', 'LiveScript', ('livescript', 'live-script'), ('*.ls',), ('text/livescript',)),
+ 'LlvmLexer': ('pip._vendor.pygments.lexers.asm', 'LLVM', ('llvm',), ('*.ll',), ('text/x-llvm',)),
+ 'LlvmMirBodyLexer': ('pip._vendor.pygments.lexers.asm', 'LLVM-MIR Body', ('llvm-mir-body',), (), ()),
+ 'LlvmMirLexer': ('pip._vendor.pygments.lexers.asm', 'LLVM-MIR', ('llvm-mir',), ('*.mir',), ()),
+ 'LogosLexer': ('pip._vendor.pygments.lexers.objective', 'Logos', ('logos',), ('*.x', '*.xi', '*.xm', '*.xmi'), ('text/x-logos',)),
+ 'LogtalkLexer': ('pip._vendor.pygments.lexers.prolog', 'Logtalk', ('logtalk',), ('*.lgt', '*.logtalk'), ('text/x-logtalk',)),
+ 'LuaLexer': ('pip._vendor.pygments.lexers.scripting', 'Lua', ('lua',), ('*.lua', '*.wlua'), ('text/x-lua', 'application/x-lua')),
+ 'MCFunctionLexer': ('pip._vendor.pygments.lexers.mcfunction', 'MCFunction', ('mcfunction', 'mcf'), ('*.mcfunction',), ('text/mcfunction',)),
+ 'MIMELexer': ('pip._vendor.pygments.lexers.mime', 'MIME', ('mime',), (), ('multipart/mixed', 'multipart/related', 'multipart/alternative')),
+ 'MOOCodeLexer': ('pip._vendor.pygments.lexers.scripting', 'MOOCode', ('moocode', 'moo'), ('*.moo',), ('text/x-moocode',)),
+ 'MSDOSSessionLexer': ('pip._vendor.pygments.lexers.shell', 'MSDOS Session', ('doscon',), (), ()),
+ 'Macaulay2Lexer': ('pip._vendor.pygments.lexers.macaulay2', 'Macaulay2', ('macaulay2',), ('*.m2',), ()),
+ 'MakefileLexer': ('pip._vendor.pygments.lexers.make', 'Makefile', ('make', 'makefile', 'mf', 'bsdmake'), ('*.mak', '*.mk', 'Makefile', 'makefile', 'Makefile.*', 'GNUmakefile'), ('text/x-makefile',)),
+ 'MakoCssLexer': ('pip._vendor.pygments.lexers.templates', 'CSS+Mako', ('css+mako',), (), ('text/css+mako',)),
+ 'MakoHtmlLexer': ('pip._vendor.pygments.lexers.templates', 'HTML+Mako', ('html+mako',), (), ('text/html+mako',)),
+ 'MakoJavascriptLexer': ('pip._vendor.pygments.lexers.templates', 'JavaScript+Mako', ('javascript+mako', 'js+mako'), (), ('application/x-javascript+mako', 'text/x-javascript+mako', 'text/javascript+mako')),
+ 'MakoLexer': ('pip._vendor.pygments.lexers.templates', 'Mako', ('mako',), ('*.mao',), ('application/x-mako',)),
+ 'MakoXmlLexer': ('pip._vendor.pygments.lexers.templates', 'XML+Mako', ('xml+mako',), (), ('application/xml+mako',)),
+ 'MaqlLexer': ('pip._vendor.pygments.lexers.business', 'MAQL', ('maql',), ('*.maql',), ('text/x-gooddata-maql', 'application/x-gooddata-maql')),
+ 'MarkdownLexer': ('pip._vendor.pygments.lexers.markup', 'Markdown', ('markdown', 'md'), ('*.md', '*.markdown'), ('text/x-markdown',)),
+ 'MaskLexer': ('pip._vendor.pygments.lexers.javascript', 'Mask', ('mask',), ('*.mask',), ('text/x-mask',)),
+ 'MasonLexer': ('pip._vendor.pygments.lexers.templates', 'Mason', ('mason',), ('*.m', '*.mhtml', '*.mc', '*.mi', 'autohandler', 'dhandler'), ('application/x-mason',)),
+ 'MathematicaLexer': ('pip._vendor.pygments.lexers.algebra', 'Mathematica', ('mathematica', 'mma', 'nb'), ('*.nb', '*.cdf', '*.nbp', '*.ma'), ('application/mathematica', 'application/vnd.wolfram.mathematica', 'application/vnd.wolfram.mathematica.package', 'application/vnd.wolfram.cdf')),
+ 'MatlabLexer': ('pip._vendor.pygments.lexers.matlab', 'Matlab', ('matlab',), ('*.m',), ('text/matlab',)),
+ 'MatlabSessionLexer': ('pip._vendor.pygments.lexers.matlab', 'Matlab session', ('matlabsession',), (), ()),
+ 'MaximaLexer': ('pip._vendor.pygments.lexers.maxima', 'Maxima', ('maxima', 'macsyma'), ('*.mac', '*.max'), ()),
+ 'MesonLexer': ('pip._vendor.pygments.lexers.meson', 'Meson', ('meson', 'meson.build'), ('meson.build', 'meson_options.txt'), ('text/x-meson',)),
+ 'MiniDLexer': ('pip._vendor.pygments.lexers.d', 'MiniD', ('minid',), (), ('text/x-minidsrc',)),
+ 'MiniScriptLexer': ('pip._vendor.pygments.lexers.scripting', 'MiniScript', ('miniscript', 'ms'), ('*.ms',), ('text/x-minicript', 'application/x-miniscript')),
+ 'ModelicaLexer': ('pip._vendor.pygments.lexers.modeling', 'Modelica', ('modelica',), ('*.mo',), ('text/x-modelica',)),
+ 'Modula2Lexer': ('pip._vendor.pygments.lexers.modula2', 'Modula-2', ('modula2', 'm2'), ('*.def', '*.mod'), ('text/x-modula2',)),
+ 'MoinWikiLexer': ('pip._vendor.pygments.lexers.markup', 'MoinMoin/Trac Wiki markup', ('trac-wiki', 'moin'), (), ('text/x-trac-wiki',)),
+ 'MonkeyLexer': ('pip._vendor.pygments.lexers.basic', 'Monkey', ('monkey',), ('*.monkey',), ('text/x-monkey',)),
+ 'MonteLexer': ('pip._vendor.pygments.lexers.monte', 'Monte', ('monte',), ('*.mt',), ()),
+ 'MoonScriptLexer': ('pip._vendor.pygments.lexers.scripting', 'MoonScript', ('moonscript', 'moon'), ('*.moon',), ('text/x-moonscript', 'application/x-moonscript')),
+ 'MoselLexer': ('pip._vendor.pygments.lexers.mosel', 'Mosel', ('mosel',), ('*.mos',), ()),
+ 'MozPreprocCssLexer': ('pip._vendor.pygments.lexers.markup', 'CSS+mozpreproc', ('css+mozpreproc',), ('*.css.in',), ()),
+ 'MozPreprocHashLexer': ('pip._vendor.pygments.lexers.markup', 'mozhashpreproc', ('mozhashpreproc',), (), ()),
+ 'MozPreprocJavascriptLexer': ('pip._vendor.pygments.lexers.markup', 'Javascript+mozpreproc', ('javascript+mozpreproc',), ('*.js.in',), ()),
+ 'MozPreprocPercentLexer': ('pip._vendor.pygments.lexers.markup', 'mozpercentpreproc', ('mozpercentpreproc',), (), ()),
+ 'MozPreprocXulLexer': ('pip._vendor.pygments.lexers.markup', 'XUL+mozpreproc', ('xul+mozpreproc',), ('*.xul.in',), ()),
+ 'MqlLexer': ('pip._vendor.pygments.lexers.c_like', 'MQL', ('mql', 'mq4', 'mq5', 'mql4', 'mql5'), ('*.mq4', '*.mq5', '*.mqh'), ('text/x-mql',)),
+ 'MscgenLexer': ('pip._vendor.pygments.lexers.dsls', 'Mscgen', ('mscgen', 'msc'), ('*.msc',), ()),
+ 'MuPADLexer': ('pip._vendor.pygments.lexers.algebra', 'MuPAD', ('mupad',), ('*.mu',), ()),
+ 'MxmlLexer': ('pip._vendor.pygments.lexers.actionscript', 'MXML', ('mxml',), ('*.mxml',), ()),
+ 'MySqlLexer': ('pip._vendor.pygments.lexers.sql', 'MySQL', ('mysql',), (), ('text/x-mysql',)),
+ 'MyghtyCssLexer': ('pip._vendor.pygments.lexers.templates', 'CSS+Myghty', ('css+myghty',), (), ('text/css+myghty',)),
+ 'MyghtyHtmlLexer': ('pip._vendor.pygments.lexers.templates', 'HTML+Myghty', ('html+myghty',), (), ('text/html+myghty',)),
+ 'MyghtyJavascriptLexer': ('pip._vendor.pygments.lexers.templates', 'JavaScript+Myghty', ('javascript+myghty', 'js+myghty'), (), ('application/x-javascript+myghty', 'text/x-javascript+myghty', 'text/javascript+mygthy')),
+ 'MyghtyLexer': ('pip._vendor.pygments.lexers.templates', 'Myghty', ('myghty',), ('*.myt', 'autodelegate'), ('application/x-myghty',)),
+ 'MyghtyXmlLexer': ('pip._vendor.pygments.lexers.templates', 'XML+Myghty', ('xml+myghty',), (), ('application/xml+myghty',)),
+ 'NCLLexer': ('pip._vendor.pygments.lexers.ncl', 'NCL', ('ncl',), ('*.ncl',), ('text/ncl',)),
+ 'NSISLexer': ('pip._vendor.pygments.lexers.installers', 'NSIS', ('nsis', 'nsi', 'nsh'), ('*.nsi', '*.nsh'), ('text/x-nsis',)),
+ 'NasmLexer': ('pip._vendor.pygments.lexers.asm', 'NASM', ('nasm',), ('*.asm', '*.ASM'), ('text/x-nasm',)),
+ 'NasmObjdumpLexer': ('pip._vendor.pygments.lexers.asm', 'objdump-nasm', ('objdump-nasm',), ('*.objdump-intel',), ('text/x-nasm-objdump',)),
+ 'NemerleLexer': ('pip._vendor.pygments.lexers.dotnet', 'Nemerle', ('nemerle',), ('*.n',), ('text/x-nemerle',)),
+ 'NesCLexer': ('pip._vendor.pygments.lexers.c_like', 'nesC', ('nesc',), ('*.nc',), ('text/x-nescsrc',)),
+ 'NestedTextLexer': ('pip._vendor.pygments.lexers.configs', 'NestedText', ('nestedtext', 'nt'), ('*.nt',), ()),
+ 'NewLispLexer': ('pip._vendor.pygments.lexers.lisp', 'NewLisp', ('newlisp',), ('*.lsp', '*.nl', '*.kif'), ('text/x-newlisp', 'application/x-newlisp')),
+ 'NewspeakLexer': ('pip._vendor.pygments.lexers.smalltalk', 'Newspeak', ('newspeak',), ('*.ns2',), ('text/x-newspeak',)),
+ 'NginxConfLexer': ('pip._vendor.pygments.lexers.configs', 'Nginx configuration file', ('nginx',), ('nginx.conf',), ('text/x-nginx-conf',)),
+ 'NimrodLexer': ('pip._vendor.pygments.lexers.nimrod', 'Nimrod', ('nimrod', 'nim'), ('*.nim', '*.nimrod'), ('text/x-nim',)),
+ 'NitLexer': ('pip._vendor.pygments.lexers.nit', 'Nit', ('nit',), ('*.nit',), ()),
+ 'NixLexer': ('pip._vendor.pygments.lexers.nix', 'Nix', ('nixos', 'nix'), ('*.nix',), ('text/x-nix',)),
+ 'NodeConsoleLexer': ('pip._vendor.pygments.lexers.javascript', 'Node.js REPL console session', ('nodejsrepl',), (), ('text/x-nodejsrepl',)),
+ 'NotmuchLexer': ('pip._vendor.pygments.lexers.textfmts', 'Notmuch', ('notmuch',), (), ()),
+ 'NuSMVLexer': ('pip._vendor.pygments.lexers.smv', 'NuSMV', ('nusmv',), ('*.smv',), ()),
+ 'NumPyLexer': ('pip._vendor.pygments.lexers.python', 'NumPy', ('numpy',), (), ()),
+ 'ObjdumpLexer': ('pip._vendor.pygments.lexers.asm', 'objdump', ('objdump',), ('*.objdump',), ('text/x-objdump',)),
+ 'ObjectiveCLexer': ('pip._vendor.pygments.lexers.objective', 'Objective-C', ('objective-c', 'objectivec', 'obj-c', 'objc'), ('*.m', '*.h'), ('text/x-objective-c',)),
+ 'ObjectiveCppLexer': ('pip._vendor.pygments.lexers.objective', 'Objective-C++', ('objective-c++', 'objectivec++', 'obj-c++', 'objc++'), ('*.mm', '*.hh'), ('text/x-objective-c++',)),
+ 'ObjectiveJLexer': ('pip._vendor.pygments.lexers.javascript', 'Objective-J', ('objective-j', 'objectivej', 'obj-j', 'objj'), ('*.j',), ('text/x-objective-j',)),
+ 'OcamlLexer': ('pip._vendor.pygments.lexers.ml', 'OCaml', ('ocaml',), ('*.ml', '*.mli', '*.mll', '*.mly'), ('text/x-ocaml',)),
+ 'OctaveLexer': ('pip._vendor.pygments.lexers.matlab', 'Octave', ('octave',), ('*.m',), ('text/octave',)),
+ 'OdinLexer': ('pip._vendor.pygments.lexers.archetype', 'ODIN', ('odin',), ('*.odin',), ('text/odin',)),
+ 'OmgIdlLexer': ('pip._vendor.pygments.lexers.c_like', 'OMG Interface Definition Language', ('omg-idl',), ('*.idl', '*.pidl'), ()),
+ 'OocLexer': ('pip._vendor.pygments.lexers.ooc', 'Ooc', ('ooc',), ('*.ooc',), ('text/x-ooc',)),
+ 'OpaLexer': ('pip._vendor.pygments.lexers.ml', 'Opa', ('opa',), ('*.opa',), ('text/x-opa',)),
+ 'OpenEdgeLexer': ('pip._vendor.pygments.lexers.business', 'OpenEdge ABL', ('openedge', 'abl', 'progress'), ('*.p', '*.cls'), ('text/x-openedge', 'application/x-openedge')),
+ 'OutputLexer': ('pip._vendor.pygments.lexers.special', 'Text output', ('output',), (), ()),
+ 'PacmanConfLexer': ('pip._vendor.pygments.lexers.configs', 'PacmanConf', ('pacmanconf',), ('pacman.conf',), ()),
+ 'PanLexer': ('pip._vendor.pygments.lexers.dsls', 'Pan', ('pan',), ('*.pan',), ()),
+ 'ParaSailLexer': ('pip._vendor.pygments.lexers.parasail', 'ParaSail', ('parasail',), ('*.psi', '*.psl'), ('text/x-parasail',)),
+ 'PawnLexer': ('pip._vendor.pygments.lexers.pawn', 'Pawn', ('pawn',), ('*.p', '*.pwn', '*.inc'), ('text/x-pawn',)),
+ 'PegLexer': ('pip._vendor.pygments.lexers.grammar_notation', 'PEG', ('peg',), ('*.peg',), ('text/x-peg',)),
+ 'Perl6Lexer': ('pip._vendor.pygments.lexers.perl', 'Perl6', ('perl6', 'pl6', 'raku'), ('*.pl', '*.pm', '*.nqp', '*.p6', '*.6pl', '*.p6l', '*.pl6', '*.6pm', '*.p6m', '*.pm6', '*.t', '*.raku', '*.rakumod', '*.rakutest', '*.rakudoc'), ('text/x-perl6', 'application/x-perl6')),
+ 'PerlLexer': ('pip._vendor.pygments.lexers.perl', 'Perl', ('perl', 'pl'), ('*.pl', '*.pm', '*.t', '*.perl'), ('text/x-perl', 'application/x-perl')),
+ 'PhpLexer': ('pip._vendor.pygments.lexers.php', 'PHP', ('php', 'php3', 'php4', 'php5'), ('*.php', '*.php[345]', '*.inc'), ('text/x-php',)),
+ 'PigLexer': ('pip._vendor.pygments.lexers.jvm', 'Pig', ('pig',), ('*.pig',), ('text/x-pig',)),
+ 'PikeLexer': ('pip._vendor.pygments.lexers.c_like', 'Pike', ('pike',), ('*.pike', '*.pmod'), ('text/x-pike',)),
+ 'PkgConfigLexer': ('pip._vendor.pygments.lexers.configs', 'PkgConfig', ('pkgconfig',), ('*.pc',), ()),
+ 'PlPgsqlLexer': ('pip._vendor.pygments.lexers.sql', 'PL/pgSQL', ('plpgsql',), (), ('text/x-plpgsql',)),
+ 'PointlessLexer': ('pip._vendor.pygments.lexers.pointless', 'Pointless', ('pointless',), ('*.ptls',), ()),
+ 'PonyLexer': ('pip._vendor.pygments.lexers.pony', 'Pony', ('pony',), ('*.pony',), ()),
+ 'PostScriptLexer': ('pip._vendor.pygments.lexers.graphics', 'PostScript', ('postscript', 'postscr'), ('*.ps', '*.eps'), ('application/postscript',)),
+ 'PostgresConsoleLexer': ('pip._vendor.pygments.lexers.sql', 'PostgreSQL console (psql)', ('psql', 'postgresql-console', 'postgres-console'), (), ('text/x-postgresql-psql',)),
+ 'PostgresLexer': ('pip._vendor.pygments.lexers.sql', 'PostgreSQL SQL dialect', ('postgresql', 'postgres'), (), ('text/x-postgresql',)),
+ 'PovrayLexer': ('pip._vendor.pygments.lexers.graphics', 'POVRay', ('pov',), ('*.pov', '*.inc'), ('text/x-povray',)),
+ 'PowerShellLexer': ('pip._vendor.pygments.lexers.shell', 'PowerShell', ('powershell', 'pwsh', 'posh', 'ps1', 'psm1'), ('*.ps1', '*.psm1'), ('text/x-powershell',)),
+ 'PowerShellSessionLexer': ('pip._vendor.pygments.lexers.shell', 'PowerShell Session', ('pwsh-session', 'ps1con'), (), ()),
+ 'PraatLexer': ('pip._vendor.pygments.lexers.praat', 'Praat', ('praat',), ('*.praat', '*.proc', '*.psc'), ()),
+ 'ProcfileLexer': ('pip._vendor.pygments.lexers.procfile', 'Procfile', ('procfile',), ('Procfile',), ()),
+ 'PrologLexer': ('pip._vendor.pygments.lexers.prolog', 'Prolog', ('prolog',), ('*.ecl', '*.prolog', '*.pro', '*.pl'), ('text/x-prolog',)),
+ 'PromQLLexer': ('pip._vendor.pygments.lexers.promql', 'PromQL', ('promql',), ('*.promql',), ()),
+ 'PropertiesLexer': ('pip._vendor.pygments.lexers.configs', 'Properties', ('properties', 'jproperties'), ('*.properties',), ('text/x-java-properties',)),
+ 'ProtoBufLexer': ('pip._vendor.pygments.lexers.dsls', 'Protocol Buffer', ('protobuf', 'proto'), ('*.proto',), ()),
+ 'PsyshConsoleLexer': ('pip._vendor.pygments.lexers.php', 'PsySH console session for PHP', ('psysh',), (), ()),
+ 'PugLexer': ('pip._vendor.pygments.lexers.html', 'Pug', ('pug', 'jade'), ('*.pug', '*.jade'), ('text/x-pug', 'text/x-jade')),
+ 'PuppetLexer': ('pip._vendor.pygments.lexers.dsls', 'Puppet', ('puppet',), ('*.pp',), ()),
+ 'PyPyLogLexer': ('pip._vendor.pygments.lexers.console', 'PyPy Log', ('pypylog', 'pypy'), ('*.pypylog',), ('application/x-pypylog',)),
+ 'Python2Lexer': ('pip._vendor.pygments.lexers.python', 'Python 2.x', ('python2', 'py2'), (), ('text/x-python2', 'application/x-python2')),
+ 'Python2TracebackLexer': ('pip._vendor.pygments.lexers.python', 'Python 2.x Traceback', ('py2tb',), ('*.py2tb',), ('text/x-python2-traceback',)),
+ 'PythonConsoleLexer': ('pip._vendor.pygments.lexers.python', 'Python console session', ('pycon',), (), ('text/x-python-doctest',)),
+ 'PythonLexer': ('pip._vendor.pygments.lexers.python', 'Python', ('python', 'py', 'sage', 'python3', 'py3'), ('*.py', '*.pyw', '*.jy', '*.sage', '*.sc', 'SConstruct', 'SConscript', '*.bzl', 'BUCK', 'BUILD', 'BUILD.bazel', 'WORKSPACE', '*.tac'), ('text/x-python', 'application/x-python', 'text/x-python3', 'application/x-python3')),
+ 'PythonTracebackLexer': ('pip._vendor.pygments.lexers.python', 'Python Traceback', ('pytb', 'py3tb'), ('*.pytb', '*.py3tb'), ('text/x-python-traceback', 'text/x-python3-traceback')),
+ 'PythonUL4Lexer': ('pip._vendor.pygments.lexers.ul4', 'Python+UL4', ('py+ul4',), ('*.pyul4',), ()),
+ 'QBasicLexer': ('pip._vendor.pygments.lexers.basic', 'QBasic', ('qbasic', 'basic'), ('*.BAS', '*.bas'), ('text/basic',)),
+ 'QLexer': ('pip._vendor.pygments.lexers.q', 'Q', ('q',), ('*.q',), ()),
+ 'QVToLexer': ('pip._vendor.pygments.lexers.qvt', 'QVTO', ('qvto', 'qvt'), ('*.qvto',), ()),
+ 'QlikLexer': ('pip._vendor.pygments.lexers.qlik', 'Qlik', ('qlik', 'qlikview', 'qliksense', 'qlikscript'), ('*.qvs', '*.qvw'), ()),
+ 'QmlLexer': ('pip._vendor.pygments.lexers.webmisc', 'QML', ('qml', 'qbs'), ('*.qml', '*.qbs'), ('application/x-qml', 'application/x-qt.qbs+qml')),
+ 'RConsoleLexer': ('pip._vendor.pygments.lexers.r', 'RConsole', ('rconsole', 'rout'), ('*.Rout',), ()),
+ 'RNCCompactLexer': ('pip._vendor.pygments.lexers.rnc', 'Relax-NG Compact', ('rng-compact', 'rnc'), ('*.rnc',), ()),
+ 'RPMSpecLexer': ('pip._vendor.pygments.lexers.installers', 'RPMSpec', ('spec',), ('*.spec',), ('text/x-rpm-spec',)),
+ 'RacketLexer': ('pip._vendor.pygments.lexers.lisp', 'Racket', ('racket', 'rkt'), ('*.rkt', '*.rktd', '*.rktl'), ('text/x-racket', 'application/x-racket')),
+ 'RagelCLexer': ('pip._vendor.pygments.lexers.parsers', 'Ragel in C Host', ('ragel-c',), ('*.rl',), ()),
+ 'RagelCppLexer': ('pip._vendor.pygments.lexers.parsers', 'Ragel in CPP Host', ('ragel-cpp',), ('*.rl',), ()),
+ 'RagelDLexer': ('pip._vendor.pygments.lexers.parsers', 'Ragel in D Host', ('ragel-d',), ('*.rl',), ()),
+ 'RagelEmbeddedLexer': ('pip._vendor.pygments.lexers.parsers', 'Embedded Ragel', ('ragel-em',), ('*.rl',), ()),
+ 'RagelJavaLexer': ('pip._vendor.pygments.lexers.parsers', 'Ragel in Java Host', ('ragel-java',), ('*.rl',), ()),
+ 'RagelLexer': ('pip._vendor.pygments.lexers.parsers', 'Ragel', ('ragel',), (), ()),
+ 'RagelObjectiveCLexer': ('pip._vendor.pygments.lexers.parsers', 'Ragel in Objective C Host', ('ragel-objc',), ('*.rl',), ()),
+ 'RagelRubyLexer': ('pip._vendor.pygments.lexers.parsers', 'Ragel in Ruby Host', ('ragel-ruby', 'ragel-rb'), ('*.rl',), ()),
+ 'RawTokenLexer': ('pip._vendor.pygments.lexers.special', 'Raw token data', (), (), ('application/x-pygments-tokens',)),
+ 'RdLexer': ('pip._vendor.pygments.lexers.r', 'Rd', ('rd',), ('*.Rd',), ('text/x-r-doc',)),
+ 'ReasonLexer': ('pip._vendor.pygments.lexers.ml', 'ReasonML', ('reasonml', 'reason'), ('*.re', '*.rei'), ('text/x-reasonml',)),
+ 'RebolLexer': ('pip._vendor.pygments.lexers.rebol', 'REBOL', ('rebol',), ('*.r', '*.r3', '*.reb'), ('text/x-rebol',)),
+ 'RedLexer': ('pip._vendor.pygments.lexers.rebol', 'Red', ('red', 'red/system'), ('*.red', '*.reds'), ('text/x-red', 'text/x-red-system')),
+ 'RedcodeLexer': ('pip._vendor.pygments.lexers.esoteric', 'Redcode', ('redcode',), ('*.cw',), ()),
+ 'RegeditLexer': ('pip._vendor.pygments.lexers.configs', 'reg', ('registry',), ('*.reg',), ('text/x-windows-registry',)),
+ 'ResourceLexer': ('pip._vendor.pygments.lexers.resource', 'ResourceBundle', ('resourcebundle', 'resource'), (), ()),
+ 'RexxLexer': ('pip._vendor.pygments.lexers.scripting', 'Rexx', ('rexx', 'arexx'), ('*.rexx', '*.rex', '*.rx', '*.arexx'), ('text/x-rexx',)),
+ 'RhtmlLexer': ('pip._vendor.pygments.lexers.templates', 'RHTML', ('rhtml', 'html+erb', 'html+ruby'), ('*.rhtml',), ('text/html+ruby',)),
+ 'RideLexer': ('pip._vendor.pygments.lexers.ride', 'Ride', ('ride',), ('*.ride',), ('text/x-ride',)),
+ 'RitaLexer': ('pip._vendor.pygments.lexers.rita', 'Rita', ('rita',), ('*.rita',), ('text/rita',)),
+ 'RoboconfGraphLexer': ('pip._vendor.pygments.lexers.roboconf', 'Roboconf Graph', ('roboconf-graph',), ('*.graph',), ()),
+ 'RoboconfInstancesLexer': ('pip._vendor.pygments.lexers.roboconf', 'Roboconf Instances', ('roboconf-instances',), ('*.instances',), ()),
+ 'RobotFrameworkLexer': ('pip._vendor.pygments.lexers.robotframework', 'RobotFramework', ('robotframework',), ('*.robot', '*.resource'), ('text/x-robotframework',)),
+ 'RqlLexer': ('pip._vendor.pygments.lexers.sql', 'RQL', ('rql',), ('*.rql',), ('text/x-rql',)),
+ 'RslLexer': ('pip._vendor.pygments.lexers.dsls', 'RSL', ('rsl',), ('*.rsl',), ('text/rsl',)),
+ 'RstLexer': ('pip._vendor.pygments.lexers.markup', 'reStructuredText', ('restructuredtext', 'rst', 'rest'), ('*.rst', '*.rest'), ('text/x-rst', 'text/prs.fallenstein.rst')),
+ 'RtsLexer': ('pip._vendor.pygments.lexers.trafficscript', 'TrafficScript', ('trafficscript', 'rts'), ('*.rts',), ()),
+ 'RubyConsoleLexer': ('pip._vendor.pygments.lexers.ruby', 'Ruby irb session', ('rbcon', 'irb'), (), ('text/x-ruby-shellsession',)),
+ 'RubyLexer': ('pip._vendor.pygments.lexers.ruby', 'Ruby', ('ruby', 'rb', 'duby'), ('*.rb', '*.rbw', 'Rakefile', '*.rake', '*.gemspec', '*.rbx', '*.duby', 'Gemfile', 'Vagrantfile'), ('text/x-ruby', 'application/x-ruby')),
+ 'RustLexer': ('pip._vendor.pygments.lexers.rust', 'Rust', ('rust', 'rs'), ('*.rs', '*.rs.in'), ('text/rust', 'text/x-rust')),
+ 'SASLexer': ('pip._vendor.pygments.lexers.sas', 'SAS', ('sas',), ('*.SAS', '*.sas'), ('text/x-sas', 'text/sas', 'application/x-sas')),
+ 'SLexer': ('pip._vendor.pygments.lexers.r', 'S', ('splus', 's', 'r'), ('*.S', '*.R', '.Rhistory', '.Rprofile', '.Renviron'), ('text/S-plus', 'text/S', 'text/x-r-source', 'text/x-r', 'text/x-R', 'text/x-r-history', 'text/x-r-profile')),
+ 'SMLLexer': ('pip._vendor.pygments.lexers.ml', 'Standard ML', ('sml',), ('*.sml', '*.sig', '*.fun'), ('text/x-standardml', 'application/x-standardml')),
+ 'SNBTLexer': ('pip._vendor.pygments.lexers.mcfunction', 'SNBT', ('snbt',), ('*.snbt',), ('text/snbt',)),
+ 'SarlLexer': ('pip._vendor.pygments.lexers.jvm', 'SARL', ('sarl',), ('*.sarl',), ('text/x-sarl',)),
+ 'SassLexer': ('pip._vendor.pygments.lexers.css', 'Sass', ('sass',), ('*.sass',), ('text/x-sass',)),
+ 'SaviLexer': ('pip._vendor.pygments.lexers.savi', 'Savi', ('savi',), ('*.savi',), ()),
+ 'ScalaLexer': ('pip._vendor.pygments.lexers.jvm', 'Scala', ('scala',), ('*.scala',), ('text/x-scala',)),
+ 'ScamlLexer': ('pip._vendor.pygments.lexers.html', 'Scaml', ('scaml',), ('*.scaml',), ('text/x-scaml',)),
+ 'ScdocLexer': ('pip._vendor.pygments.lexers.scdoc', 'scdoc', ('scdoc', 'scd'), ('*.scd', '*.scdoc'), ()),
+ 'SchemeLexer': ('pip._vendor.pygments.lexers.lisp', 'Scheme', ('scheme', 'scm'), ('*.scm', '*.ss'), ('text/x-scheme', 'application/x-scheme')),
+ 'ScilabLexer': ('pip._vendor.pygments.lexers.matlab', 'Scilab', ('scilab',), ('*.sci', '*.sce', '*.tst'), ('text/scilab',)),
+ 'ScssLexer': ('pip._vendor.pygments.lexers.css', 'SCSS', ('scss',), ('*.scss',), ('text/x-scss',)),
+ 'SedLexer': ('pip._vendor.pygments.lexers.textedit', 'Sed', ('sed', 'gsed', 'ssed'), ('*.sed', '*.[gs]sed'), ('text/x-sed',)),
+ 'ShExCLexer': ('pip._vendor.pygments.lexers.rdf', 'ShExC', ('shexc', 'shex'), ('*.shex',), ('text/shex',)),
+ 'ShenLexer': ('pip._vendor.pygments.lexers.lisp', 'Shen', ('shen',), ('*.shen',), ('text/x-shen', 'application/x-shen')),
+ 'SieveLexer': ('pip._vendor.pygments.lexers.sieve', 'Sieve', ('sieve',), ('*.siv', '*.sieve'), ()),
+ 'SilverLexer': ('pip._vendor.pygments.lexers.verification', 'Silver', ('silver',), ('*.sil', '*.vpr'), ()),
+ 'SingularityLexer': ('pip._vendor.pygments.lexers.configs', 'Singularity', ('singularity',), ('*.def', 'Singularity'), ()),
+ 'SlashLexer': ('pip._vendor.pygments.lexers.slash', 'Slash', ('slash',), ('*.sla',), ()),
+ 'SlimLexer': ('pip._vendor.pygments.lexers.webmisc', 'Slim', ('slim',), ('*.slim',), ('text/x-slim',)),
+ 'SlurmBashLexer': ('pip._vendor.pygments.lexers.shell', 'Slurm', ('slurm', 'sbatch'), ('*.sl',), ()),
+ 'SmaliLexer': ('pip._vendor.pygments.lexers.dalvik', 'Smali', ('smali',), ('*.smali',), ('text/smali',)),
+ 'SmalltalkLexer': ('pip._vendor.pygments.lexers.smalltalk', 'Smalltalk', ('smalltalk', 'squeak', 'st'), ('*.st',), ('text/x-smalltalk',)),
+ 'SmartGameFormatLexer': ('pip._vendor.pygments.lexers.sgf', 'SmartGameFormat', ('sgf',), ('*.sgf',), ()),
+ 'SmartyLexer': ('pip._vendor.pygments.lexers.templates', 'Smarty', ('smarty',), ('*.tpl',), ('application/x-smarty',)),
+ 'SmithyLexer': ('pip._vendor.pygments.lexers.smithy', 'Smithy', ('smithy',), ('*.smithy',), ()),
+ 'SnobolLexer': ('pip._vendor.pygments.lexers.snobol', 'Snobol', ('snobol',), ('*.snobol',), ('text/x-snobol',)),
+ 'SnowballLexer': ('pip._vendor.pygments.lexers.dsls', 'Snowball', ('snowball',), ('*.sbl',), ()),
+ 'SolidityLexer': ('pip._vendor.pygments.lexers.solidity', 'Solidity', ('solidity',), ('*.sol',), ()),
+ 'SophiaLexer': ('pip._vendor.pygments.lexers.sophia', 'Sophia', ('sophia',), ('*.aes',), ()),
+ 'SourcePawnLexer': ('pip._vendor.pygments.lexers.pawn', 'SourcePawn', ('sp',), ('*.sp',), ('text/x-sourcepawn',)),
+ 'SourcesListLexer': ('pip._vendor.pygments.lexers.installers', 'Debian Sourcelist', ('debsources', 'sourceslist', 'sources.list'), ('sources.list',), ()),
+ 'SparqlLexer': ('pip._vendor.pygments.lexers.rdf', 'SPARQL', ('sparql',), ('*.rq', '*.sparql'), ('application/sparql-query',)),
+ 'SpiceLexer': ('pip._vendor.pygments.lexers.spice', 'Spice', ('spice', 'spicelang'), ('*.spice',), ('text/x-spice',)),
+ 'SqlLexer': ('pip._vendor.pygments.lexers.sql', 'SQL', ('sql',), ('*.sql',), ('text/x-sql',)),
+ 'SqliteConsoleLexer': ('pip._vendor.pygments.lexers.sql', 'sqlite3con', ('sqlite3',), ('*.sqlite3-console',), ('text/x-sqlite3-console',)),
+ 'SquidConfLexer': ('pip._vendor.pygments.lexers.configs', 'SquidConf', ('squidconf', 'squid.conf', 'squid'), ('squid.conf',), ('text/x-squidconf',)),
+ 'SrcinfoLexer': ('pip._vendor.pygments.lexers.srcinfo', 'Srcinfo', ('srcinfo',), ('.SRCINFO',), ()),
+ 'SspLexer': ('pip._vendor.pygments.lexers.templates', 'Scalate Server Page', ('ssp',), ('*.ssp',), ('application/x-ssp',)),
+ 'StanLexer': ('pip._vendor.pygments.lexers.modeling', 'Stan', ('stan',), ('*.stan',), ()),
+ 'StataLexer': ('pip._vendor.pygments.lexers.stata', 'Stata', ('stata', 'do'), ('*.do', '*.ado'), ('text/x-stata', 'text/stata', 'application/x-stata')),
+ 'SuperColliderLexer': ('pip._vendor.pygments.lexers.supercollider', 'SuperCollider', ('supercollider', 'sc'), ('*.sc', '*.scd'), ('application/supercollider', 'text/supercollider')),
+ 'SwiftLexer': ('pip._vendor.pygments.lexers.objective', 'Swift', ('swift',), ('*.swift',), ('text/x-swift',)),
+ 'SwigLexer': ('pip._vendor.pygments.lexers.c_like', 'SWIG', ('swig',), ('*.swg', '*.i'), ('text/swig',)),
+ 'SystemVerilogLexer': ('pip._vendor.pygments.lexers.hdl', 'systemverilog', ('systemverilog', 'sv'), ('*.sv', '*.svh'), ('text/x-systemverilog',)),
+ 'TAPLexer': ('pip._vendor.pygments.lexers.testing', 'TAP', ('tap',), ('*.tap',), ()),
+ 'TNTLexer': ('pip._vendor.pygments.lexers.tnt', 'Typographic Number Theory', ('tnt',), ('*.tnt',), ()),
+ 'TOMLLexer': ('pip._vendor.pygments.lexers.configs', 'TOML', ('toml',), ('*.toml', 'Pipfile', 'poetry.lock'), ()),
+ 'Tads3Lexer': ('pip._vendor.pygments.lexers.int_fiction', 'TADS 3', ('tads3',), ('*.t',), ()),
+ 'TalLexer': ('pip._vendor.pygments.lexers.tal', 'Tal', ('tal', 'uxntal'), ('*.tal',), ('text/x-uxntal',)),
+ 'TasmLexer': ('pip._vendor.pygments.lexers.asm', 'TASM', ('tasm',), ('*.asm', '*.ASM', '*.tasm'), ('text/x-tasm',)),
+ 'TclLexer': ('pip._vendor.pygments.lexers.tcl', 'Tcl', ('tcl',), ('*.tcl', '*.rvt'), ('text/x-tcl', 'text/x-script.tcl', 'application/x-tcl')),
+ 'TcshLexer': ('pip._vendor.pygments.lexers.shell', 'Tcsh', ('tcsh', 'csh'), ('*.tcsh', '*.csh'), ('application/x-csh',)),
+ 'TcshSessionLexer': ('pip._vendor.pygments.lexers.shell', 'Tcsh Session', ('tcshcon',), (), ()),
+ 'TeaTemplateLexer': ('pip._vendor.pygments.lexers.templates', 'Tea', ('tea',), ('*.tea',), ('text/x-tea',)),
+ 'TealLexer': ('pip._vendor.pygments.lexers.teal', 'teal', ('teal',), ('*.teal',), ()),
+ 'TeraTermLexer': ('pip._vendor.pygments.lexers.teraterm', 'Tera Term macro', ('teratermmacro', 'teraterm', 'ttl'), ('*.ttl',), ('text/x-teratermmacro',)),
+ 'TermcapLexer': ('pip._vendor.pygments.lexers.configs', 'Termcap', ('termcap',), ('termcap', 'termcap.src'), ()),
+ 'TerminfoLexer': ('pip._vendor.pygments.lexers.configs', 'Terminfo', ('terminfo',), ('terminfo', 'terminfo.src'), ()),
+ 'TerraformLexer': ('pip._vendor.pygments.lexers.configs', 'Terraform', ('terraform', 'tf'), ('*.tf',), ('application/x-tf', 'application/x-terraform')),
+ 'TexLexer': ('pip._vendor.pygments.lexers.markup', 'TeX', ('tex', 'latex'), ('*.tex', '*.aux', '*.toc'), ('text/x-tex', 'text/x-latex')),
+ 'TextLexer': ('pip._vendor.pygments.lexers.special', 'Text only', ('text',), ('*.txt',), ('text/plain',)),
+ 'ThingsDBLexer': ('pip._vendor.pygments.lexers.thingsdb', 'ThingsDB', ('ti', 'thingsdb'), ('*.ti',), ()),
+ 'ThriftLexer': ('pip._vendor.pygments.lexers.dsls', 'Thrift', ('thrift',), ('*.thrift',), ('application/x-thrift',)),
+ 'TiddlyWiki5Lexer': ('pip._vendor.pygments.lexers.markup', 'tiddler', ('tid',), ('*.tid',), ('text/vnd.tiddlywiki',)),
+ 'TodotxtLexer': ('pip._vendor.pygments.lexers.textfmts', 'Todotxt', ('todotxt',), ('todo.txt', '*.todotxt'), ('text/x-todo',)),
+ 'TransactSqlLexer': ('pip._vendor.pygments.lexers.sql', 'Transact-SQL', ('tsql', 't-sql'), ('*.sql',), ('text/x-tsql',)),
+ 'TreetopLexer': ('pip._vendor.pygments.lexers.parsers', 'Treetop', ('treetop',), ('*.treetop', '*.tt'), ()),
+ 'TurtleLexer': ('pip._vendor.pygments.lexers.rdf', 'Turtle', ('turtle',), ('*.ttl',), ('text/turtle', 'application/x-turtle')),
+ 'TwigHtmlLexer': ('pip._vendor.pygments.lexers.templates', 'HTML+Twig', ('html+twig',), ('*.twig',), ('text/html+twig',)),
+ 'TwigLexer': ('pip._vendor.pygments.lexers.templates', 'Twig', ('twig',), (), ('application/x-twig',)),
+ 'TypeScriptLexer': ('pip._vendor.pygments.lexers.javascript', 'TypeScript', ('typescript', 'ts'), ('*.ts',), ('application/x-typescript', 'text/x-typescript')),
+ 'TypoScriptCssDataLexer': ('pip._vendor.pygments.lexers.typoscript', 'TypoScriptCssData', ('typoscriptcssdata',), (), ()),
+ 'TypoScriptHtmlDataLexer': ('pip._vendor.pygments.lexers.typoscript', 'TypoScriptHtmlData', ('typoscripthtmldata',), (), ()),
+ 'TypoScriptLexer': ('pip._vendor.pygments.lexers.typoscript', 'TypoScript', ('typoscript',), ('*.typoscript',), ('text/x-typoscript',)),
+ 'UL4Lexer': ('pip._vendor.pygments.lexers.ul4', 'UL4', ('ul4',), ('*.ul4',), ()),
+ 'UcodeLexer': ('pip._vendor.pygments.lexers.unicon', 'ucode', ('ucode',), ('*.u', '*.u1', '*.u2'), ()),
+ 'UniconLexer': ('pip._vendor.pygments.lexers.unicon', 'Unicon', ('unicon',), ('*.icn',), ('text/unicon',)),
+ 'UnixConfigLexer': ('pip._vendor.pygments.lexers.configs', 'Unix/Linux config files', ('unixconfig', 'linuxconfig'), (), ()),
+ 'UrbiscriptLexer': ('pip._vendor.pygments.lexers.urbi', 'UrbiScript', ('urbiscript',), ('*.u',), ('application/x-urbiscript',)),
+ 'UsdLexer': ('pip._vendor.pygments.lexers.usd', 'USD', ('usd', 'usda'), ('*.usd', '*.usda'), ()),
+ 'VBScriptLexer': ('pip._vendor.pygments.lexers.basic', 'VBScript', ('vbscript',), ('*.vbs', '*.VBS'), ()),
+ 'VCLLexer': ('pip._vendor.pygments.lexers.varnish', 'VCL', ('vcl',), ('*.vcl',), ('text/x-vclsrc',)),
+ 'VCLSnippetLexer': ('pip._vendor.pygments.lexers.varnish', 'VCLSnippets', ('vclsnippets', 'vclsnippet'), (), ('text/x-vclsnippet',)),
+ 'VCTreeStatusLexer': ('pip._vendor.pygments.lexers.console', 'VCTreeStatus', ('vctreestatus',), (), ()),
+ 'VGLLexer': ('pip._vendor.pygments.lexers.dsls', 'VGL', ('vgl',), ('*.rpf',), ()),
+ 'ValaLexer': ('pip._vendor.pygments.lexers.c_like', 'Vala', ('vala', 'vapi'), ('*.vala', '*.vapi'), ('text/x-vala',)),
+ 'VbNetAspxLexer': ('pip._vendor.pygments.lexers.dotnet', 'aspx-vb', ('aspx-vb',), ('*.aspx', '*.asax', '*.ascx', '*.ashx', '*.asmx', '*.axd'), ()),
+ 'VbNetLexer': ('pip._vendor.pygments.lexers.dotnet', 'VB.net', ('vb.net', 'vbnet'), ('*.vb', '*.bas'), ('text/x-vbnet', 'text/x-vba')),
+ 'VelocityHtmlLexer': ('pip._vendor.pygments.lexers.templates', 'HTML+Velocity', ('html+velocity',), (), ('text/html+velocity',)),
+ 'VelocityLexer': ('pip._vendor.pygments.lexers.templates', 'Velocity', ('velocity',), ('*.vm', '*.fhtml'), ()),
+ 'VelocityXmlLexer': ('pip._vendor.pygments.lexers.templates', 'XML+Velocity', ('xml+velocity',), (), ('application/xml+velocity',)),
+ 'VerilogLexer': ('pip._vendor.pygments.lexers.hdl', 'verilog', ('verilog', 'v'), ('*.v',), ('text/x-verilog',)),
+ 'VhdlLexer': ('pip._vendor.pygments.lexers.hdl', 'vhdl', ('vhdl',), ('*.vhdl', '*.vhd'), ('text/x-vhdl',)),
+ 'VimLexer': ('pip._vendor.pygments.lexers.textedit', 'VimL', ('vim',), ('*.vim', '.vimrc', '.exrc', '.gvimrc', '_vimrc', '_exrc', '_gvimrc', 'vimrc', 'gvimrc'), ('text/x-vim',)),
+ 'WDiffLexer': ('pip._vendor.pygments.lexers.diff', 'WDiff', ('wdiff',), ('*.wdiff',), ()),
+ 'WatLexer': ('pip._vendor.pygments.lexers.webassembly', 'WebAssembly', ('wast', 'wat'), ('*.wat', '*.wast'), ()),
+ 'WebIDLLexer': ('pip._vendor.pygments.lexers.webidl', 'Web IDL', ('webidl',), ('*.webidl',), ()),
+ 'WhileyLexer': ('pip._vendor.pygments.lexers.whiley', 'Whiley', ('whiley',), ('*.whiley',), ('text/x-whiley',)),
+ 'X10Lexer': ('pip._vendor.pygments.lexers.x10', 'X10', ('x10', 'xten'), ('*.x10',), ('text/x-x10',)),
+ 'XMLUL4Lexer': ('pip._vendor.pygments.lexers.ul4', 'XML+UL4', ('xml+ul4',), ('*.xmlul4',), ()),
+ 'XQueryLexer': ('pip._vendor.pygments.lexers.webmisc', 'XQuery', ('xquery', 'xqy', 'xq', 'xql', 'xqm'), ('*.xqy', '*.xquery', '*.xq', '*.xql', '*.xqm'), ('text/xquery', 'application/xquery')),
+ 'XmlDjangoLexer': ('pip._vendor.pygments.lexers.templates', 'XML+Django/Jinja', ('xml+django', 'xml+jinja'), ('*.xml.j2', '*.xml.jinja2'), ('application/xml+django', 'application/xml+jinja')),
+ 'XmlErbLexer': ('pip._vendor.pygments.lexers.templates', 'XML+Ruby', ('xml+ruby', 'xml+erb'), (), ('application/xml+ruby',)),
+ 'XmlLexer': ('pip._vendor.pygments.lexers.html', 'XML', ('xml',), ('*.xml', '*.xsl', '*.rss', '*.xslt', '*.xsd', '*.wsdl', '*.wsf'), ('text/xml', 'application/xml', 'image/svg+xml', 'application/rss+xml', 'application/atom+xml')),
+ 'XmlPhpLexer': ('pip._vendor.pygments.lexers.templates', 'XML+PHP', ('xml+php',), (), ('application/xml+php',)),
+ 'XmlSmartyLexer': ('pip._vendor.pygments.lexers.templates', 'XML+Smarty', ('xml+smarty',), (), ('application/xml+smarty',)),
+ 'XorgLexer': ('pip._vendor.pygments.lexers.xorg', 'Xorg', ('xorg.conf',), ('xorg.conf',), ()),
+ 'XsltLexer': ('pip._vendor.pygments.lexers.html', 'XSLT', ('xslt',), ('*.xsl', '*.xslt', '*.xpl'), ('application/xsl+xml', 'application/xslt+xml')),
+ 'XtendLexer': ('pip._vendor.pygments.lexers.jvm', 'Xtend', ('xtend',), ('*.xtend',), ('text/x-xtend',)),
+ 'XtlangLexer': ('pip._vendor.pygments.lexers.lisp', 'xtlang', ('extempore',), ('*.xtm',), ()),
+ 'YamlJinjaLexer': ('pip._vendor.pygments.lexers.templates', 'YAML+Jinja', ('yaml+jinja', 'salt', 'sls'), ('*.sls', '*.yaml.j2', '*.yml.j2', '*.yaml.jinja2', '*.yml.jinja2'), ('text/x-yaml+jinja', 'text/x-sls')),
+ 'YamlLexer': ('pip._vendor.pygments.lexers.data', 'YAML', ('yaml',), ('*.yaml', '*.yml'), ('text/x-yaml',)),
+ 'YangLexer': ('pip._vendor.pygments.lexers.yang', 'YANG', ('yang',), ('*.yang',), ('application/yang',)),
+ 'ZeekLexer': ('pip._vendor.pygments.lexers.dsls', 'Zeek', ('zeek', 'bro'), ('*.zeek', '*.bro'), ()),
+ 'ZephirLexer': ('pip._vendor.pygments.lexers.php', 'Zephir', ('zephir',), ('*.zep',), ()),
+ 'ZigLexer': ('pip._vendor.pygments.lexers.zig', 'Zig', ('zig',), ('*.zig',), ('text/zig',)),
+ 'apdlexer': ('pip._vendor.pygments.lexers.apdlexer', 'ANSYS parametric design language', ('ansys', 'apdl'), ('*.ans',), ()),
+}
+
+if __name__ == '__main__': # pragma: no cover
+ import sys
+ import os
+
+ # lookup lexers
+ found_lexers = []
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
+ for root, dirs, files in os.walk('.'):
+ for filename in files:
+ if filename.endswith('.py') and not filename.startswith('_'):
+ module_name = 'pygments.lexers%s.%s' % (
+ root[1:].replace('/', '.'), filename[:-3])
+ print(module_name)
+ module = __import__(module_name, None, None, [''])
+ for lexer_name in module.__all__:
+ lexer = getattr(module, lexer_name)
+ found_lexers.append(
+ '%r: %r' % (lexer_name,
+ (module_name,
+ lexer.name,
+ tuple(lexer.aliases),
+ tuple(lexer.filenames),
+ tuple(lexer.mimetypes))))
+ # sort them to make the diff minimal
+ found_lexers.sort()
+
+ # extract useful sourcecode from this file
+ with open(__file__) as fp:
+ content = fp.read()
+ # replace crnl to nl for Windows.
+ #
+ # Note that, originally, contributors should keep nl of master
+ # repository, for example by using some kind of automatic
+ # management EOL, like `EolExtension
+ # <https://www.mercurial-scm.org/wiki/EolExtension>`.
+ content = content.replace("\r\n", "\n")
+ header = content[:content.find('LEXERS = {')]
+ footer = content[content.find("if __name__ == '__main__':"):]
+
+ # write new file
+ with open(__file__, 'w') as fp:
+ fp.write(header)
+ fp.write('LEXERS = {\n %s,\n}\n\n' % ',\n '.join(found_lexers))
+ fp.write(footer)
+
+ print ('=== %d lexers processed.' % len(found_lexers))
diff --git a/src/pip/_vendor/pygments/lexers/python.py b/src/pip/_vendor/pygments/lexers/python.py
new file mode 100644
index 000000000..6bc7a78b6
--- /dev/null
+++ b/src/pip/_vendor/pygments/lexers/python.py
@@ -0,0 +1,1191 @@
+"""
+ pygments.lexers.python
+ ~~~~~~~~~~~~~~~~~~~~~~
+
+ Lexers for Python and related languages.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import re
+import keyword
+
+from pip._vendor.pygments.lexer import Lexer, RegexLexer, include, bygroups, using, \
+ default, words, combined, do_insertions, this
+from pip._vendor.pygments.util import get_bool_opt, shebang_matches
+from pip._vendor.pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+ Number, Punctuation, Generic, Other, Error
+from pip._vendor.pygments import unistring as uni
+
+__all__ = ['PythonLexer', 'PythonConsoleLexer', 'PythonTracebackLexer',
+ 'Python2Lexer', 'Python2TracebackLexer',
+ 'CythonLexer', 'DgLexer', 'NumPyLexer']
+
+line_re = re.compile('.*?\n')
+
+
+class PythonLexer(RegexLexer):
+ """
+ For Python source code (version 3.x).
+
+ .. versionadded:: 0.10
+
+ .. versionchanged:: 2.5
+ This is now the default ``PythonLexer``. It is still available as the
+ alias ``Python3Lexer``.
+ """
+
+ name = 'Python'
+ url = 'http://www.python.org'
+ aliases = ['python', 'py', 'sage', 'python3', 'py3']
+ filenames = [
+ '*.py',
+ '*.pyw',
+ # Jython
+ '*.jy',
+ # Sage
+ '*.sage',
+ # SCons
+ '*.sc',
+ 'SConstruct',
+ 'SConscript',
+ # Skylark/Starlark (used by Bazel, Buck, and Pants)
+ '*.bzl',
+ 'BUCK',
+ 'BUILD',
+ 'BUILD.bazel',
+ 'WORKSPACE',
+ # Twisted Application infrastructure
+ '*.tac',
+ ]
+ mimetypes = ['text/x-python', 'application/x-python',
+ 'text/x-python3', 'application/x-python3']
+
+ uni_name = "[%s][%s]*" % (uni.xid_start, uni.xid_continue)
+
+ def innerstring_rules(ttype):
+ return [
+ # the old style '%s' % (...) string formatting (still valid in Py3)
+ (r'%(\(\w+\))?[-#0 +]*([0-9]+|[*])?(\.([0-9]+|[*]))?'
+ '[hlL]?[E-GXc-giorsaux%]', String.Interpol),
+ # the new style '{}'.format(...) string formatting
+ (r'\{'
+ r'((\w+)((\.\w+)|(\[[^\]]+\]))*)?' # field name
+ r'(\![sra])?' # conversion
+ r'(\:(.?[<>=\^])?[-+ ]?#?0?(\d+)?,?(\.\d+)?[E-GXb-gnosx%]?)?'
+ r'\}', String.Interpol),
+
+ # backslashes, quotes and formatting signs must be parsed one at a time
+ (r'[^\\\'"%{\n]+', ttype),
+ (r'[\'"\\]', ttype),
+ # unhandled string formatting sign
+ (r'%|(\{{1,2})', ttype)
+ # newlines are an error (use "nl" state)
+ ]
+
+ def fstring_rules(ttype):
+ return [
+ # Assuming that a '}' is the closing brace after format specifier.
+ # Sadly, this means that we won't detect syntax error. But it's
+ # more important to parse correct syntax correctly, than to
+ # highlight invalid syntax.
+ (r'\}', String.Interpol),
+ (r'\{', String.Interpol, 'expr-inside-fstring'),
+ # backslashes, quotes and formatting signs must be parsed one at a time
+ (r'[^\\\'"{}\n]+', ttype),
+ (r'[\'"\\]', ttype),
+ # newlines are an error (use "nl" state)
+ ]
+
+ tokens = {
+ 'root': [
+ (r'\n', Text),
+ (r'^(\s*)([rRuUbB]{,2})("""(?:.|\n)*?""")',
+ bygroups(Text, String.Affix, String.Doc)),
+ (r"^(\s*)([rRuUbB]{,2})('''(?:.|\n)*?''')",
+ bygroups(Text, String.Affix, String.Doc)),
+ (r'\A#!.+$', Comment.Hashbang),
+ (r'#.*$', Comment.Single),
+ (r'\\\n', Text),
+ (r'\\', Text),
+ include('keywords'),
+ include('soft-keywords'),
+ (r'(def)((?:\s|\\\s)+)', bygroups(Keyword, Text), 'funcname'),
+ (r'(class)((?:\s|\\\s)+)', bygroups(Keyword, Text), 'classname'),
+ (r'(from)((?:\s|\\\s)+)', bygroups(Keyword.Namespace, Text),
+ 'fromimport'),
+ (r'(import)((?:\s|\\\s)+)', bygroups(Keyword.Namespace, Text),
+ 'import'),
+ include('expr'),
+ ],
+ 'expr': [
+ # raw f-strings
+ ('(?i)(rf|fr)(""")',
+ bygroups(String.Affix, String.Double),
+ combined('rfstringescape', 'tdqf')),
+ ("(?i)(rf|fr)(''')",
+ bygroups(String.Affix, String.Single),
+ combined('rfstringescape', 'tsqf')),
+ ('(?i)(rf|fr)(")',
+ bygroups(String.Affix, String.Double),
+ combined('rfstringescape', 'dqf')),
+ ("(?i)(rf|fr)(')",
+ bygroups(String.Affix, String.Single),
+ combined('rfstringescape', 'sqf')),
+ # non-raw f-strings
+ ('([fF])(""")', bygroups(String.Affix, String.Double),
+ combined('fstringescape', 'tdqf')),
+ ("([fF])(''')", bygroups(String.Affix, String.Single),
+ combined('fstringescape', 'tsqf')),
+ ('([fF])(")', bygroups(String.Affix, String.Double),
+ combined('fstringescape', 'dqf')),
+ ("([fF])(')", bygroups(String.Affix, String.Single),
+ combined('fstringescape', 'sqf')),
+ # raw strings
+ ('(?i)(rb|br|r)(""")',
+ bygroups(String.Affix, String.Double), 'tdqs'),
+ ("(?i)(rb|br|r)(''')",
+ bygroups(String.Affix, String.Single), 'tsqs'),
+ ('(?i)(rb|br|r)(")',
+ bygroups(String.Affix, String.Double), 'dqs'),
+ ("(?i)(rb|br|r)(')",
+ bygroups(String.Affix, String.Single), 'sqs'),
+ # non-raw strings
+ ('([uUbB]?)(""")', bygroups(String.Affix, String.Double),
+ combined('stringescape', 'tdqs')),
+ ("([uUbB]?)(''')", bygroups(String.Affix, String.Single),
+ combined('stringescape', 'tsqs')),
+ ('([uUbB]?)(")', bygroups(String.Affix, String.Double),
+ combined('stringescape', 'dqs')),
+ ("([uUbB]?)(')", bygroups(String.Affix, String.Single),
+ combined('stringescape', 'sqs')),
+ (r'[^\S\n]+', Text),
+ include('numbers'),
+ (r'!=|==|<<|>>|:=|[-~+/*%=<>&^|.]', Operator),
+ (r'[]{}:(),;[]', Punctuation),
+ (r'(in|is|and|or|not)\b', Operator.Word),
+ include('expr-keywords'),
+ include('builtins'),
+ include('magicfuncs'),
+ include('magicvars'),
+ include('name'),
+ ],
+ 'expr-inside-fstring': [
+ (r'[{([]', Punctuation, 'expr-inside-fstring-inner'),
+ # without format specifier
+ (r'(=\s*)?' # debug (https://bugs.python.org/issue36817)
+ r'(\![sraf])?' # conversion
+ r'\}', String.Interpol, '#pop'),
+ # with format specifier
+ # we'll catch the remaining '}' in the outer scope
+ (r'(=\s*)?' # debug (https://bugs.python.org/issue36817)
+ r'(\![sraf])?' # conversion
+ r':', String.Interpol, '#pop'),
+ (r'\s+', Text), # allow new lines
+ include('expr'),
+ ],
+ 'expr-inside-fstring-inner': [
+ (r'[{([]', Punctuation, 'expr-inside-fstring-inner'),
+ (r'[])}]', Punctuation, '#pop'),
+ (r'\s+', Text), # allow new lines
+ include('expr'),
+ ],
+ 'expr-keywords': [
+ # Based on https://docs.python.org/3/reference/expressions.html
+ (words((
+ 'async for', 'await', 'else', 'for', 'if', 'lambda',
+ 'yield', 'yield from'), suffix=r'\b'),
+ Keyword),
+ (words(('True', 'False', 'None'), suffix=r'\b'), Keyword.Constant),
+ ],
+ 'keywords': [
+ (words((
+ 'assert', 'async', 'await', 'break', 'continue', 'del', 'elif',
+ 'else', 'except', 'finally', 'for', 'global', 'if', 'lambda',
+ 'pass', 'raise', 'nonlocal', 'return', 'try', 'while', 'yield',
+ 'yield from', 'as', 'with'), suffix=r'\b'),
+ Keyword),
+ (words(('True', 'False', 'None'), suffix=r'\b'), Keyword.Constant),
+ ],
+ 'soft-keywords': [
+ # `match`, `case` and `_` soft keywords
+ (r'(^[ \t]*)' # at beginning of line + possible indentation
+ r'(match|case)\b' # a possible keyword
+ r'(?![ \t]*(?:' # not followed by...
+ r'[:,;=^&|@~)\]}]|(?:' + # characters and keywords that mean this isn't
+ r'|'.join(keyword.kwlist) + r')\b))', # pattern matching
+ bygroups(Text, Keyword), 'soft-keywords-inner'),
+ ],
+ 'soft-keywords-inner': [
+ # optional `_` keyword
+ (r'(\s+)([^\n_]*)(_\b)', bygroups(Text, using(this), Keyword)),
+ default('#pop')
+ ],
+ 'builtins': [
+ (words((
+ '__import__', 'abs', 'all', 'any', 'bin', 'bool', 'bytearray',
+ 'breakpoint', 'bytes', 'chr', 'classmethod', 'compile', 'complex',
+ 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'filter',
+ 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr',
+ 'hash', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass',
+ 'iter', 'len', 'list', 'locals', 'map', 'max', 'memoryview',
+ 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print',
+ 'property', 'range', 'repr', 'reversed', 'round', 'set', 'setattr',
+ 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple',
+ 'type', 'vars', 'zip'), prefix=r'(?<!\.)', suffix=r'\b'),
+ Name.Builtin),
+ (r'(?<!\.)(self|Ellipsis|NotImplemented|cls)\b', Name.Builtin.Pseudo),
+ (words((
+ 'ArithmeticError', 'AssertionError', 'AttributeError',
+ 'BaseException', 'BufferError', 'BytesWarning', 'DeprecationWarning',
+ 'EOFError', 'EnvironmentError', 'Exception', 'FloatingPointError',
+ 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError',
+ 'ImportWarning', 'IndentationError', 'IndexError', 'KeyError',
+ 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError',
+ 'NotImplementedError', 'OSError', 'OverflowError',
+ 'PendingDeprecationWarning', 'ReferenceError', 'ResourceWarning',
+ 'RuntimeError', 'RuntimeWarning', 'StopIteration',
+ 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit',
+ 'TabError', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError',
+ 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError',
+ 'UnicodeWarning', 'UserWarning', 'ValueError', 'VMSError',
+ 'Warning', 'WindowsError', 'ZeroDivisionError',
+ # new builtin exceptions from PEP 3151
+ 'BlockingIOError', 'ChildProcessError', 'ConnectionError',
+ 'BrokenPipeError', 'ConnectionAbortedError', 'ConnectionRefusedError',
+ 'ConnectionResetError', 'FileExistsError', 'FileNotFoundError',
+ 'InterruptedError', 'IsADirectoryError', 'NotADirectoryError',
+ 'PermissionError', 'ProcessLookupError', 'TimeoutError',
+ # others new in Python 3
+ 'StopAsyncIteration', 'ModuleNotFoundError', 'RecursionError',
+ 'EncodingWarning'),
+ prefix=r'(?<!\.)', suffix=r'\b'),
+ Name.Exception),
+ ],
+ 'magicfuncs': [
+ (words((
+ '__abs__', '__add__', '__aenter__', '__aexit__', '__aiter__',
+ '__and__', '__anext__', '__await__', '__bool__', '__bytes__',
+ '__call__', '__complex__', '__contains__', '__del__', '__delattr__',
+ '__delete__', '__delitem__', '__dir__', '__divmod__', '__enter__',
+ '__eq__', '__exit__', '__float__', '__floordiv__', '__format__',
+ '__ge__', '__get__', '__getattr__', '__getattribute__',
+ '__getitem__', '__gt__', '__hash__', '__iadd__', '__iand__',
+ '__ifloordiv__', '__ilshift__', '__imatmul__', '__imod__',
+ '__imul__', '__index__', '__init__', '__instancecheck__',
+ '__int__', '__invert__', '__ior__', '__ipow__', '__irshift__',
+ '__isub__', '__iter__', '__itruediv__', '__ixor__', '__le__',
+ '__len__', '__length_hint__', '__lshift__', '__lt__', '__matmul__',
+ '__missing__', '__mod__', '__mul__', '__ne__', '__neg__',
+ '__new__', '__next__', '__or__', '__pos__', '__pow__',
+ '__prepare__', '__radd__', '__rand__', '__rdivmod__', '__repr__',
+ '__reversed__', '__rfloordiv__', '__rlshift__', '__rmatmul__',
+ '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__',
+ '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__',
+ '__rxor__', '__set__', '__setattr__', '__setitem__', '__str__',
+ '__sub__', '__subclasscheck__', '__truediv__',
+ '__xor__'), suffix=r'\b'),
+ Name.Function.Magic),
+ ],
+ 'magicvars': [
+ (words((
+ '__annotations__', '__bases__', '__class__', '__closure__',
+ '__code__', '__defaults__', '__dict__', '__doc__', '__file__',
+ '__func__', '__globals__', '__kwdefaults__', '__module__',
+ '__mro__', '__name__', '__objclass__', '__qualname__',
+ '__self__', '__slots__', '__weakref__'), suffix=r'\b'),
+ Name.Variable.Magic),
+ ],
+ 'numbers': [
+ (r'(\d(?:_?\d)*\.(?:\d(?:_?\d)*)?|(?:\d(?:_?\d)*)?\.\d(?:_?\d)*)'
+ r'([eE][+-]?\d(?:_?\d)*)?', Number.Float),
+ (r'\d(?:_?\d)*[eE][+-]?\d(?:_?\d)*j?', Number.Float),
+ (r'0[oO](?:_?[0-7])+', Number.Oct),
+ (r'0[bB](?:_?[01])+', Number.Bin),
+ (r'0[xX](?:_?[a-fA-F0-9])+', Number.Hex),
+ (r'\d(?:_?\d)*', Number.Integer),
+ ],
+ 'name': [
+ (r'@' + uni_name, Name.Decorator),
+ (r'@', Operator), # new matrix multiplication operator
+ (uni_name, Name),
+ ],
+ 'funcname': [
+ include('magicfuncs'),
+ (uni_name, Name.Function, '#pop'),
+ default('#pop'),
+ ],
+ 'classname': [
+ (uni_name, Name.Class, '#pop'),
+ ],
+ 'import': [
+ (r'(\s+)(as)(\s+)', bygroups(Text, Keyword, Text)),
+ (r'\.', Name.Namespace),
+ (uni_name, Name.Namespace),
+ (r'(\s*)(,)(\s*)', bygroups(Text, Operator, Text)),
+ default('#pop') # all else: go back
+ ],
+ 'fromimport': [
+ (r'(\s+)(import)\b', bygroups(Text, Keyword.Namespace), '#pop'),
+ (r'\.', Name.Namespace),
+ # if None occurs here, it's "raise x from None", since None can
+ # never be a module name
+ (r'None\b', Name.Builtin.Pseudo, '#pop'),
+ (uni_name, Name.Namespace),
+ default('#pop'),
+ ],
+ 'rfstringescape': [
+ (r'\{\{', String.Escape),
+ (r'\}\}', String.Escape),
+ ],
+ 'fstringescape': [
+ include('rfstringescape'),
+ include('stringescape'),
+ ],
+ 'stringescape': [
+ (r'\\([\\abfnrtv"\']|\n|N\{.*?\}|u[a-fA-F0-9]{4}|'
+ r'U[a-fA-F0-9]{8}|x[a-fA-F0-9]{2}|[0-7]{1,3})', String.Escape)
+ ],
+ 'fstrings-single': fstring_rules(String.Single),
+ 'fstrings-double': fstring_rules(String.Double),
+ 'strings-single': innerstring_rules(String.Single),
+ 'strings-double': innerstring_rules(String.Double),
+ 'dqf': [
+ (r'"', String.Double, '#pop'),
+ (r'\\\\|\\"|\\\n', String.Escape), # included here for raw strings
+ include('fstrings-double')
+ ],
+ 'sqf': [
+ (r"'", String.Single, '#pop'),
+ (r"\\\\|\\'|\\\n", String.Escape), # included here for raw strings
+ include('fstrings-single')
+ ],
+ 'dqs': [
+ (r'"', String.Double, '#pop'),
+ (r'\\\\|\\"|\\\n', String.Escape), # included here for raw strings
+ include('strings-double')
+ ],
+ 'sqs': [
+ (r"'", String.Single, '#pop'),
+ (r"\\\\|\\'|\\\n", String.Escape), # included here for raw strings
+ include('strings-single')
+ ],
+ 'tdqf': [
+ (r'"""', String.Double, '#pop'),
+ include('fstrings-double'),
+ (r'\n', String.Double)
+ ],
+ 'tsqf': [
+ (r"'''", String.Single, '#pop'),
+ include('fstrings-single'),
+ (r'\n', String.Single)
+ ],
+ 'tdqs': [
+ (r'"""', String.Double, '#pop'),
+ include('strings-double'),
+ (r'\n', String.Double)
+ ],
+ 'tsqs': [
+ (r"'''", String.Single, '#pop'),
+ include('strings-single'),
+ (r'\n', String.Single)
+ ],
+ }
+
+ def analyse_text(text):
+ return shebang_matches(text, r'pythonw?(3(\.\d)?)?') or \
+ 'import ' in text[:1000]
+
+
+Python3Lexer = PythonLexer
+
+
+class Python2Lexer(RegexLexer):
+ """
+ For Python 2.x source code.
+
+ .. versionchanged:: 2.5
+ This class has been renamed from ``PythonLexer``. ``PythonLexer`` now
+ refers to the Python 3 variant. File name patterns like ``*.py`` have
+ been moved to Python 3 as well.
+ """
+
+ name = 'Python 2.x'
+ url = 'http://www.python.org'
+ aliases = ['python2', 'py2']
+ filenames = [] # now taken over by PythonLexer (3.x)
+ mimetypes = ['text/x-python2', 'application/x-python2']
+
+ def innerstring_rules(ttype):
+ return [
+ # the old style '%s' % (...) string formatting
+ (r'%(\(\w+\))?[-#0 +]*([0-9]+|[*])?(\.([0-9]+|[*]))?'
+ '[hlL]?[E-GXc-giorsux%]', String.Interpol),
+ # backslashes, quotes and formatting signs must be parsed one at a time
+ (r'[^\\\'"%\n]+', ttype),
+ (r'[\'"\\]', ttype),
+ # unhandled string formatting sign
+ (r'%', ttype),
+ # newlines are an error (use "nl" state)
+ ]
+
+ tokens = {
+ 'root': [
+ (r'\n', Text),
+ (r'^(\s*)([rRuUbB]{,2})("""(?:.|\n)*?""")',
+ bygroups(Text, String.Affix, String.Doc)),
+ (r"^(\s*)([rRuUbB]{,2})('''(?:.|\n)*?''')",
+ bygroups(Text, String.Affix, String.Doc)),
+ (r'[^\S\n]+', Text),
+ (r'\A#!.+$', Comment.Hashbang),
+ (r'#.*$', Comment.Single),
+ (r'[]{}:(),;[]', Punctuation),
+ (r'\\\n', Text),
+ (r'\\', Text),
+ (r'(in|is|and|or|not)\b', Operator.Word),
+ (r'!=|==|<<|>>|[-~+/*%=<>&^|.]', Operator),
+ include('keywords'),
+ (r'(def)((?:\s|\\\s)+)', bygroups(Keyword, Text), 'funcname'),
+ (r'(class)((?:\s|\\\s)+)', bygroups(Keyword, Text), 'classname'),
+ (r'(from)((?:\s|\\\s)+)', bygroups(Keyword.Namespace, Text),
+ 'fromimport'),
+ (r'(import)((?:\s|\\\s)+)', bygroups(Keyword.Namespace, Text),
+ 'import'),
+ include('builtins'),
+ include('magicfuncs'),
+ include('magicvars'),
+ include('backtick'),
+ ('([rR]|[uUbB][rR]|[rR][uUbB])(""")',
+ bygroups(String.Affix, String.Double), 'tdqs'),
+ ("([rR]|[uUbB][rR]|[rR][uUbB])(''')",
+ bygroups(String.Affix, String.Single), 'tsqs'),
+ ('([rR]|[uUbB][rR]|[rR][uUbB])(")',
+ bygroups(String.Affix, String.Double), 'dqs'),
+ ("([rR]|[uUbB][rR]|[rR][uUbB])(')",
+ bygroups(String.Affix, String.Single), 'sqs'),
+ ('([uUbB]?)(""")', bygroups(String.Affix, String.Double),
+ combined('stringescape', 'tdqs')),
+ ("([uUbB]?)(''')", bygroups(String.Affix, String.Single),
+ combined('stringescape', 'tsqs')),
+ ('([uUbB]?)(")', bygroups(String.Affix, String.Double),
+ combined('stringescape', 'dqs')),
+ ("([uUbB]?)(')", bygroups(String.Affix, String.Single),
+ combined('stringescape', 'sqs')),
+ include('name'),
+ include('numbers'),
+ ],
+ 'keywords': [
+ (words((
+ 'assert', 'break', 'continue', 'del', 'elif', 'else', 'except',
+ 'exec', 'finally', 'for', 'global', 'if', 'lambda', 'pass',
+ 'print', 'raise', 'return', 'try', 'while', 'yield',
+ 'yield from', 'as', 'with'), suffix=r'\b'),
+ Keyword),
+ ],
+ 'builtins': [
+ (words((
+ '__import__', 'abs', 'all', 'any', 'apply', 'basestring', 'bin',
+ 'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod',
+ 'cmp', 'coerce', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod',
+ 'enumerate', 'eval', 'execfile', 'exit', 'file', 'filter', 'float',
+ 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'hex', 'id',
+ 'input', 'int', 'intern', 'isinstance', 'issubclass', 'iter', 'len',
+ 'list', 'locals', 'long', 'map', 'max', 'min', 'next', 'object',
+ 'oct', 'open', 'ord', 'pow', 'property', 'range', 'raw_input', 'reduce',
+ 'reload', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice',
+ 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type',
+ 'unichr', 'unicode', 'vars', 'xrange', 'zip'),
+ prefix=r'(?<!\.)', suffix=r'\b'),
+ Name.Builtin),
+ (r'(?<!\.)(self|None|Ellipsis|NotImplemented|False|True|cls'
+ r')\b', Name.Builtin.Pseudo),
+ (words((
+ 'ArithmeticError', 'AssertionError', 'AttributeError',
+ 'BaseException', 'DeprecationWarning', 'EOFError', 'EnvironmentError',
+ 'Exception', 'FloatingPointError', 'FutureWarning', 'GeneratorExit',
+ 'IOError', 'ImportError', 'ImportWarning', 'IndentationError',
+ 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError',
+ 'MemoryError', 'NameError',
+ 'NotImplementedError', 'OSError', 'OverflowError', 'OverflowWarning',
+ 'PendingDeprecationWarning', 'ReferenceError',
+ 'RuntimeError', 'RuntimeWarning', 'StandardError', 'StopIteration',
+ 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit',
+ 'TabError', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError',
+ 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError',
+ 'UnicodeWarning', 'UserWarning', 'ValueError', 'VMSError', 'Warning',
+ 'WindowsError', 'ZeroDivisionError'), prefix=r'(?<!\.)', suffix=r'\b'),
+ Name.Exception),
+ ],
+ 'magicfuncs': [
+ (words((
+ '__abs__', '__add__', '__and__', '__call__', '__cmp__', '__coerce__',
+ '__complex__', '__contains__', '__del__', '__delattr__', '__delete__',
+ '__delitem__', '__delslice__', '__div__', '__divmod__', '__enter__',
+ '__eq__', '__exit__', '__float__', '__floordiv__', '__ge__', '__get__',
+ '__getattr__', '__getattribute__', '__getitem__', '__getslice__', '__gt__',
+ '__hash__', '__hex__', '__iadd__', '__iand__', '__idiv__', '__ifloordiv__',
+ '__ilshift__', '__imod__', '__imul__', '__index__', '__init__',
+ '__instancecheck__', '__int__', '__invert__', '__iop__', '__ior__',
+ '__ipow__', '__irshift__', '__isub__', '__iter__', '__itruediv__',
+ '__ixor__', '__le__', '__len__', '__long__', '__lshift__', '__lt__',
+ '__missing__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__',
+ '__nonzero__', '__oct__', '__op__', '__or__', '__pos__', '__pow__',
+ '__radd__', '__rand__', '__rcmp__', '__rdiv__', '__rdivmod__', '__repr__',
+ '__reversed__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__',
+ '__rop__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__',
+ '__rtruediv__', '__rxor__', '__set__', '__setattr__', '__setitem__',
+ '__setslice__', '__str__', '__sub__', '__subclasscheck__', '__truediv__',
+ '__unicode__', '__xor__'), suffix=r'\b'),
+ Name.Function.Magic),
+ ],
+ 'magicvars': [
+ (words((
+ '__bases__', '__class__', '__closure__', '__code__', '__defaults__',
+ '__dict__', '__doc__', '__file__', '__func__', '__globals__',
+ '__metaclass__', '__module__', '__mro__', '__name__', '__self__',
+ '__slots__', '__weakref__'),
+ suffix=r'\b'),
+ Name.Variable.Magic),
+ ],
+ 'numbers': [
+ (r'(\d+\.\d*|\d*\.\d+)([eE][+-]?[0-9]+)?j?', Number.Float),
+ (r'\d+[eE][+-]?[0-9]+j?', Number.Float),
+ (r'0[0-7]+j?', Number.Oct),
+ (r'0[bB][01]+', Number.Bin),
+ (r'0[xX][a-fA-F0-9]+', Number.Hex),
+ (r'\d+L', Number.Integer.Long),
+ (r'\d+j?', Number.Integer)
+ ],
+ 'backtick': [
+ ('`.*?`', String.Backtick),
+ ],
+ 'name': [
+ (r'@[\w.]+', Name.Decorator),
+ (r'[a-zA-Z_]\w*', Name),
+ ],
+ 'funcname': [
+ include('magicfuncs'),
+ (r'[a-zA-Z_]\w*', Name.Function, '#pop'),
+ default('#pop'),
+ ],
+ 'classname': [
+ (r'[a-zA-Z_]\w*', Name.Class, '#pop')
+ ],
+ 'import': [
+ (r'(?:[ \t]|\\\n)+', Text),
+ (r'as\b', Keyword.Namespace),
+ (r',', Operator),
+ (r'[a-zA-Z_][\w.]*', Name.Namespace),
+ default('#pop') # all else: go back
+ ],
+ 'fromimport': [
+ (r'(?:[ \t]|\\\n)+', Text),
+ (r'import\b', Keyword.Namespace, '#pop'),
+ # if None occurs here, it's "raise x from None", since None can
+ # never be a module name
+ (r'None\b', Name.Builtin.Pseudo, '#pop'),
+ # sadly, in "raise x from y" y will be highlighted as namespace too
+ (r'[a-zA-Z_.][\w.]*', Name.Namespace),
+ # anything else here also means "raise x from y" and is therefore
+ # not an error
+ default('#pop'),
+ ],
+ 'stringescape': [
+ (r'\\([\\abfnrtv"\']|\n|N\{.*?\}|u[a-fA-F0-9]{4}|'
+ r'U[a-fA-F0-9]{8}|x[a-fA-F0-9]{2}|[0-7]{1,3})', String.Escape)
+ ],
+ 'strings-single': innerstring_rules(String.Single),
+ 'strings-double': innerstring_rules(String.Double),
+ 'dqs': [
+ (r'"', String.Double, '#pop'),
+ (r'\\\\|\\"|\\\n', String.Escape), # included here for raw strings
+ include('strings-double')
+ ],
+ 'sqs': [
+ (r"'", String.Single, '#pop'),
+ (r"\\\\|\\'|\\\n", String.Escape), # included here for raw strings
+ include('strings-single')
+ ],
+ 'tdqs': [
+ (r'"""', String.Double, '#pop'),
+ include('strings-double'),
+ (r'\n', String.Double)
+ ],
+ 'tsqs': [
+ (r"'''", String.Single, '#pop'),
+ include('strings-single'),
+ (r'\n', String.Single)
+ ],
+ }
+
+ def analyse_text(text):
+ return shebang_matches(text, r'pythonw?2(\.\d)?')
+
+
+class PythonConsoleLexer(Lexer):
+ """
+ For Python console output or doctests, such as:
+
+ .. sourcecode:: pycon
+
+ >>> a = 'foo'
+ >>> print a
+ foo
+ >>> 1 / 0
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ ZeroDivisionError: integer division or modulo by zero
+
+ Additional options:
+
+ `python3`
+ Use Python 3 lexer for code. Default is ``True``.
+
+ .. versionadded:: 1.0
+ .. versionchanged:: 2.5
+ Now defaults to ``True``.
+ """
+ name = 'Python console session'
+ aliases = ['pycon']
+ mimetypes = ['text/x-python-doctest']
+
+ def __init__(self, **options):
+ self.python3 = get_bool_opt(options, 'python3', True)
+ Lexer.__init__(self, **options)
+
+ def get_tokens_unprocessed(self, text):
+ if self.python3:
+ pylexer = PythonLexer(**self.options)
+ tblexer = PythonTracebackLexer(**self.options)
+ else:
+ pylexer = Python2Lexer(**self.options)
+ tblexer = Python2TracebackLexer(**self.options)
+
+ curcode = ''
+ insertions = []
+ curtb = ''
+ tbindex = 0
+ tb = 0
+ for match in line_re.finditer(text):
+ line = match.group()
+ if line.startswith('>>> ') or line.startswith('... '):
+ tb = 0
+ insertions.append((len(curcode),
+ [(0, Generic.Prompt, line[:4])]))
+ curcode += line[4:]
+ elif line.rstrip() == '...' and not tb:
+ # only a new >>> prompt can end an exception block
+ # otherwise an ellipsis in place of the traceback frames
+ # will be mishandled
+ insertions.append((len(curcode),
+ [(0, Generic.Prompt, '...')]))
+ curcode += line[3:]
+ else:
+ if curcode:
+ yield from do_insertions(
+ insertions, pylexer.get_tokens_unprocessed(curcode))
+ curcode = ''
+ insertions = []
+ if (line.startswith('Traceback (most recent call last):') or
+ re.match(' File "[^"]+", line \\d+\\n$', line)):
+ tb = 1
+ curtb = line
+ tbindex = match.start()
+ elif line == 'KeyboardInterrupt\n':
+ yield match.start(), Name.Class, line
+ elif tb:
+ curtb += line
+ if not (line.startswith(' ') or line.strip() == '...'):
+ tb = 0
+ for i, t, v in tblexer.get_tokens_unprocessed(curtb):
+ yield tbindex+i, t, v
+ curtb = ''
+ else:
+ yield match.start(), Generic.Output, line
+ if curcode:
+ yield from do_insertions(insertions,
+ pylexer.get_tokens_unprocessed(curcode))
+ if curtb:
+ for i, t, v in tblexer.get_tokens_unprocessed(curtb):
+ yield tbindex+i, t, v
+
+
+class PythonTracebackLexer(RegexLexer):
+ """
+ For Python 3.x tracebacks, with support for chained exceptions.
+
+ .. versionadded:: 1.0
+
+ .. versionchanged:: 2.5
+ This is now the default ``PythonTracebackLexer``. It is still available
+ as the alias ``Python3TracebackLexer``.
+ """
+
+ name = 'Python Traceback'
+ aliases = ['pytb', 'py3tb']
+ filenames = ['*.pytb', '*.py3tb']
+ mimetypes = ['text/x-python-traceback', 'text/x-python3-traceback']
+
+ tokens = {
+ 'root': [
+ (r'\n', Text),
+ (r'^Traceback \(most recent call last\):\n', Generic.Traceback, 'intb'),
+ (r'^During handling of the above exception, another '
+ r'exception occurred:\n\n', Generic.Traceback),
+ (r'^The above exception was the direct cause of the '
+ r'following exception:\n\n', Generic.Traceback),
+ (r'^(?= File "[^"]+", line \d+)', Generic.Traceback, 'intb'),
+ (r'^.*\n', Other),
+ ],
+ 'intb': [
+ (r'^( File )("[^"]+")(, line )(\d+)(, in )(.+)(\n)',
+ bygroups(Text, Name.Builtin, Text, Number, Text, Name, Text)),
+ (r'^( File )("[^"]+")(, line )(\d+)(\n)',
+ bygroups(Text, Name.Builtin, Text, Number, Text)),
+ (r'^( )(.+)(\n)',
+ bygroups(Text, using(PythonLexer), Text), 'markers'),
+ (r'^([ \t]*)(\.\.\.)(\n)',
+ bygroups(Text, Comment, Text)), # for doctests...
+ (r'^([^:]+)(: )(.+)(\n)',
+ bygroups(Generic.Error, Text, Name, Text), '#pop'),
+ (r'^([a-zA-Z_][\w.]*)(:?\n)',
+ bygroups(Generic.Error, Text), '#pop')
+ ],
+ 'markers': [
+ # Either `PEP 657 <https://www.python.org/dev/peps/pep-0657/>`
+ # error locations in Python 3.11+, or single-caret markers
+ # for syntax errors before that.
+ (r'^( {4,})([~^]+)(\n)',
+ bygroups(Text, Punctuation.Marker, Text),
+ '#pop'),
+ default('#pop'),
+ ],
+ }
+
+
+Python3TracebackLexer = PythonTracebackLexer
+
+
+class Python2TracebackLexer(RegexLexer):
+ """
+ For Python tracebacks.
+
+ .. versionadded:: 0.7
+
+ .. versionchanged:: 2.5
+ This class has been renamed from ``PythonTracebackLexer``.
+ ``PythonTracebackLexer`` now refers to the Python 3 variant.
+ """
+
+ name = 'Python 2.x Traceback'
+ aliases = ['py2tb']
+ filenames = ['*.py2tb']
+ mimetypes = ['text/x-python2-traceback']
+
+ tokens = {
+ 'root': [
+ # Cover both (most recent call last) and (innermost last)
+ # The optional ^C allows us to catch keyboard interrupt signals.
+ (r'^(\^C)?(Traceback.*\n)',
+ bygroups(Text, Generic.Traceback), 'intb'),
+ # SyntaxError starts with this.
+ (r'^(?= File "[^"]+", line \d+)', Generic.Traceback, 'intb'),
+ (r'^.*\n', Other),
+ ],
+ 'intb': [
+ (r'^( File )("[^"]+")(, line )(\d+)(, in )(.+)(\n)',
+ bygroups(Text, Name.Builtin, Text, Number, Text, Name, Text)),
+ (r'^( File )("[^"]+")(, line )(\d+)(\n)',
+ bygroups(Text, Name.Builtin, Text, Number, Text)),
+ (r'^( )(.+)(\n)',
+ bygroups(Text, using(Python2Lexer), Text), 'marker'),
+ (r'^([ \t]*)(\.\.\.)(\n)',
+ bygroups(Text, Comment, Text)), # for doctests...
+ (r'^([^:]+)(: )(.+)(\n)',
+ bygroups(Generic.Error, Text, Name, Text), '#pop'),
+ (r'^([a-zA-Z_]\w*)(:?\n)',
+ bygroups(Generic.Error, Text), '#pop')
+ ],
+ 'marker': [
+ # For syntax errors.
+ (r'( {4,})(\^)', bygroups(Text, Punctuation.Marker), '#pop'),
+ default('#pop'),
+ ],
+ }
+
+
+class CythonLexer(RegexLexer):
+ """
+ For Pyrex and Cython source code.
+
+ .. versionadded:: 1.1
+ """
+
+ name = 'Cython'
+ url = 'http://cython.org'
+ aliases = ['cython', 'pyx', 'pyrex']
+ filenames = ['*.pyx', '*.pxd', '*.pxi']
+ mimetypes = ['text/x-cython', 'application/x-cython']
+
+ tokens = {
+ 'root': [
+ (r'\n', Text),
+ (r'^(\s*)("""(?:.|\n)*?""")', bygroups(Text, String.Doc)),
+ (r"^(\s*)('''(?:.|\n)*?''')", bygroups(Text, String.Doc)),
+ (r'[^\S\n]+', Text),
+ (r'#.*$', Comment),
+ (r'[]{}:(),;[]', Punctuation),
+ (r'\\\n', Text),
+ (r'\\', Text),
+ (r'(in|is|and|or|not)\b', Operator.Word),
+ (r'(<)([a-zA-Z0-9.?]+)(>)',
+ bygroups(Punctuation, Keyword.Type, Punctuation)),
+ (r'!=|==|<<|>>|[-~+/*%=<>&^|.?]', Operator),
+ (r'(from)(\d+)(<=)(\s+)(<)(\d+)(:)',
+ bygroups(Keyword, Number.Integer, Operator, Name, Operator,
+ Name, Punctuation)),
+ include('keywords'),
+ (r'(def|property)(\s+)', bygroups(Keyword, Text), 'funcname'),
+ (r'(cp?def)(\s+)', bygroups(Keyword, Text), 'cdef'),
+ # (should actually start a block with only cdefs)
+ (r'(cdef)(:)', bygroups(Keyword, Punctuation)),
+ (r'(class|struct)(\s+)', bygroups(Keyword, Text), 'classname'),
+ (r'(from)(\s+)', bygroups(Keyword, Text), 'fromimport'),
+ (r'(c?import)(\s+)', bygroups(Keyword, Text), 'import'),
+ include('builtins'),
+ include('backtick'),
+ ('(?:[rR]|[uU][rR]|[rR][uU])"""', String, 'tdqs'),
+ ("(?:[rR]|[uU][rR]|[rR][uU])'''", String, 'tsqs'),
+ ('(?:[rR]|[uU][rR]|[rR][uU])"', String, 'dqs'),
+ ("(?:[rR]|[uU][rR]|[rR][uU])'", String, 'sqs'),
+ ('[uU]?"""', String, combined('stringescape', 'tdqs')),
+ ("[uU]?'''", String, combined('stringescape', 'tsqs')),
+ ('[uU]?"', String, combined('stringescape', 'dqs')),
+ ("[uU]?'", String, combined('stringescape', 'sqs')),
+ include('name'),
+ include('numbers'),
+ ],
+ 'keywords': [
+ (words((
+ 'assert', 'async', 'await', 'break', 'by', 'continue', 'ctypedef', 'del', 'elif',
+ 'else', 'except', 'except?', 'exec', 'finally', 'for', 'fused', 'gil',
+ 'global', 'if', 'include', 'lambda', 'nogil', 'pass', 'print',
+ 'raise', 'return', 'try', 'while', 'yield', 'as', 'with'), suffix=r'\b'),
+ Keyword),
+ (r'(DEF|IF|ELIF|ELSE)\b', Comment.Preproc),
+ ],
+ 'builtins': [
+ (words((
+ '__import__', 'abs', 'all', 'any', 'apply', 'basestring', 'bin', 'bint',
+ 'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr',
+ 'classmethod', 'cmp', 'coerce', 'compile', 'complex', 'delattr',
+ 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'execfile', 'exit',
+ 'file', 'filter', 'float', 'frozenset', 'getattr', 'globals',
+ 'hasattr', 'hash', 'hex', 'id', 'input', 'int', 'intern', 'isinstance',
+ 'issubclass', 'iter', 'len', 'list', 'locals', 'long', 'map', 'max',
+ 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'property', 'Py_ssize_t',
+ 'range', 'raw_input', 'reduce', 'reload', 'repr', 'reversed',
+ 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod',
+ 'str', 'sum', 'super', 'tuple', 'type', 'unichr', 'unicode', 'unsigned',
+ 'vars', 'xrange', 'zip'), prefix=r'(?<!\.)', suffix=r'\b'),
+ Name.Builtin),
+ (r'(?<!\.)(self|None|Ellipsis|NotImplemented|False|True|NULL'
+ r')\b', Name.Builtin.Pseudo),
+ (words((
+ 'ArithmeticError', 'AssertionError', 'AttributeError',
+ 'BaseException', 'DeprecationWarning', 'EOFError', 'EnvironmentError',
+ 'Exception', 'FloatingPointError', 'FutureWarning', 'GeneratorExit',
+ 'IOError', 'ImportError', 'ImportWarning', 'IndentationError',
+ 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError',
+ 'MemoryError', 'NameError', 'NotImplemented', 'NotImplementedError',
+ 'OSError', 'OverflowError', 'OverflowWarning',
+ 'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError',
+ 'RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError',
+ 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError',
+ 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError',
+ 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError',
+ 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning',
+ 'ZeroDivisionError'), prefix=r'(?<!\.)', suffix=r'\b'),
+ Name.Exception),
+ ],
+ 'numbers': [
+ (r'(\d+\.?\d*|\d*\.\d+)([eE][+-]?[0-9]+)?', Number.Float),
+ (r'0\d+', Number.Oct),
+ (r'0[xX][a-fA-F0-9]+', Number.Hex),
+ (r'\d+L', Number.Integer.Long),
+ (r'\d+', Number.Integer)
+ ],
+ 'backtick': [
+ ('`.*?`', String.Backtick),
+ ],
+ 'name': [
+ (r'@\w+', Name.Decorator),
+ (r'[a-zA-Z_]\w*', Name),
+ ],
+ 'funcname': [
+ (r'[a-zA-Z_]\w*', Name.Function, '#pop')
+ ],
+ 'cdef': [
+ (r'(public|readonly|extern|api|inline)\b', Keyword.Reserved),
+ (r'(struct|enum|union|class)\b', Keyword),
+ (r'([a-zA-Z_]\w*)(\s*)(?=[(:#=]|$)',
+ bygroups(Name.Function, Text), '#pop'),
+ (r'([a-zA-Z_]\w*)(\s*)(,)',
+ bygroups(Name.Function, Text, Punctuation)),
+ (r'from\b', Keyword, '#pop'),
+ (r'as\b', Keyword),
+ (r':', Punctuation, '#pop'),
+ (r'(?=["\'])', Text, '#pop'),
+ (r'[a-zA-Z_]\w*', Keyword.Type),
+ (r'.', Text),
+ ],
+ 'classname': [
+ (r'[a-zA-Z_]\w*', Name.Class, '#pop')
+ ],
+ 'import': [
+ (r'(\s+)(as)(\s+)', bygroups(Text, Keyword, Text)),
+ (r'[a-zA-Z_][\w.]*', Name.Namespace),
+ (r'(\s*)(,)(\s*)', bygroups(Text, Operator, Text)),
+ default('#pop') # all else: go back
+ ],
+ 'fromimport': [
+ (r'(\s+)(c?import)\b', bygroups(Text, Keyword), '#pop'),
+ (r'[a-zA-Z_.][\w.]*', Name.Namespace),
+ # ``cdef foo from "header"``, or ``for foo from 0 < i < 10``
+ default('#pop'),
+ ],
+ 'stringescape': [
+ (r'\\([\\abfnrtv"\']|\n|N\{.*?\}|u[a-fA-F0-9]{4}|'
+ r'U[a-fA-F0-9]{8}|x[a-fA-F0-9]{2}|[0-7]{1,3})', String.Escape)
+ ],
+ 'strings': [
+ (r'%(\([a-zA-Z0-9]+\))?[-#0 +]*([0-9]+|[*])?(\.([0-9]+|[*]))?'
+ '[hlL]?[E-GXc-giorsux%]', String.Interpol),
+ (r'[^\\\'"%\n]+', String),
+ # quotes, percents and backslashes must be parsed one at a time
+ (r'[\'"\\]', String),
+ # unhandled string formatting sign
+ (r'%', String)
+ # newlines are an error (use "nl" state)
+ ],
+ 'nl': [
+ (r'\n', String)
+ ],
+ 'dqs': [
+ (r'"', String, '#pop'),
+ (r'\\\\|\\"|\\\n', String.Escape), # included here again for raw strings
+ include('strings')
+ ],
+ 'sqs': [
+ (r"'", String, '#pop'),
+ (r"\\\\|\\'|\\\n", String.Escape), # included here again for raw strings
+ include('strings')
+ ],
+ 'tdqs': [
+ (r'"""', String, '#pop'),
+ include('strings'),
+ include('nl')
+ ],
+ 'tsqs': [
+ (r"'''", String, '#pop'),
+ include('strings'),
+ include('nl')
+ ],
+ }
+
+
+class DgLexer(RegexLexer):
+ """
+ Lexer for dg,
+ a functional and object-oriented programming language
+ running on the CPython 3 VM.
+
+ .. versionadded:: 1.6
+ """
+ name = 'dg'
+ aliases = ['dg']
+ filenames = ['*.dg']
+ mimetypes = ['text/x-dg']
+
+ tokens = {
+ 'root': [
+ (r'\s+', Text),
+ (r'#.*?$', Comment.Single),
+
+ (r'(?i)0b[01]+', Number.Bin),
+ (r'(?i)0o[0-7]+', Number.Oct),
+ (r'(?i)0x[0-9a-f]+', Number.Hex),
+ (r'(?i)[+-]?[0-9]+\.[0-9]+(e[+-]?[0-9]+)?j?', Number.Float),
+ (r'(?i)[+-]?[0-9]+e[+-]?\d+j?', Number.Float),
+ (r'(?i)[+-]?[0-9]+j?', Number.Integer),
+
+ (r"(?i)(br|r?b?)'''", String, combined('stringescape', 'tsqs', 'string')),
+ (r'(?i)(br|r?b?)"""', String, combined('stringescape', 'tdqs', 'string')),
+ (r"(?i)(br|r?b?)'", String, combined('stringescape', 'sqs', 'string')),
+ (r'(?i)(br|r?b?)"', String, combined('stringescape', 'dqs', 'string')),
+
+ (r"`\w+'*`", Operator),
+ (r'\b(and|in|is|or|where)\b', Operator.Word),
+ (r'[!$%&*+\-./:<-@\\^|~;,]+', Operator),
+
+ (words((
+ 'bool', 'bytearray', 'bytes', 'classmethod', 'complex', 'dict', 'dict\'',
+ 'float', 'frozenset', 'int', 'list', 'list\'', 'memoryview', 'object',
+ 'property', 'range', 'set', 'set\'', 'slice', 'staticmethod', 'str',
+ 'super', 'tuple', 'tuple\'', 'type'),
+ prefix=r'(?<!\.)', suffix=r'(?![\'\w])'),
+ Name.Builtin),
+ (words((
+ '__import__', 'abs', 'all', 'any', 'bin', 'bind', 'chr', 'cmp', 'compile',
+ 'complex', 'delattr', 'dir', 'divmod', 'drop', 'dropwhile', 'enumerate',
+ 'eval', 'exhaust', 'filter', 'flip', 'foldl1?', 'format', 'fst',
+ 'getattr', 'globals', 'hasattr', 'hash', 'head', 'hex', 'id', 'init',
+ 'input', 'isinstance', 'issubclass', 'iter', 'iterate', 'last', 'len',
+ 'locals', 'map', 'max', 'min', 'next', 'oct', 'open', 'ord', 'pow',
+ 'print', 'repr', 'reversed', 'round', 'setattr', 'scanl1?', 'snd',
+ 'sorted', 'sum', 'tail', 'take', 'takewhile', 'vars', 'zip'),
+ prefix=r'(?<!\.)', suffix=r'(?![\'\w])'),
+ Name.Builtin),
+ (r"(?<!\.)(self|Ellipsis|NotImplemented|None|True|False)(?!['\w])",
+ Name.Builtin.Pseudo),
+
+ (r"(?<!\.)[A-Z]\w*(Error|Exception|Warning)'*(?!['\w])",
+ Name.Exception),
+ (r"(?<!\.)(Exception|GeneratorExit|KeyboardInterrupt|StopIteration|"
+ r"SystemExit)(?!['\w])", Name.Exception),
+
+ (r"(?<![\w.])(except|finally|for|if|import|not|otherwise|raise|"
+ r"subclass|while|with|yield)(?!['\w])", Keyword.Reserved),
+
+ (r"[A-Z_]+'*(?!['\w])", Name),
+ (r"[A-Z]\w+'*(?!['\w])", Keyword.Type),
+ (r"\w+'*", Name),
+
+ (r'[()]', Punctuation),
+ (r'.', Error),
+ ],
+ 'stringescape': [
+ (r'\\([\\abfnrtv"\']|\n|N\{.*?\}|u[a-fA-F0-9]{4}|'
+ r'U[a-fA-F0-9]{8}|x[a-fA-F0-9]{2}|[0-7]{1,3})', String.Escape)
+ ],
+ 'string': [
+ (r'%(\(\w+\))?[-#0 +]*([0-9]+|[*])?(\.([0-9]+|[*]))?'
+ '[hlL]?[E-GXc-giorsux%]', String.Interpol),
+ (r'[^\\\'"%\n]+', String),
+ # quotes, percents and backslashes must be parsed one at a time
+ (r'[\'"\\]', String),
+ # unhandled string formatting sign
+ (r'%', String),
+ (r'\n', String)
+ ],
+ 'dqs': [
+ (r'"', String, '#pop')
+ ],
+ 'sqs': [
+ (r"'", String, '#pop')
+ ],
+ 'tdqs': [
+ (r'"""', String, '#pop')
+ ],
+ 'tsqs': [
+ (r"'''", String, '#pop')
+ ],
+ }
+
+
+class NumPyLexer(PythonLexer):
+ """
+ A Python lexer recognizing Numerical Python builtins.
+
+ .. versionadded:: 0.10
+ """
+
+ name = 'NumPy'
+ url = 'https://numpy.org/'
+ aliases = ['numpy']
+
+ # override the mimetypes to not inherit them from python
+ mimetypes = []
+ filenames = []
+
+ EXTRA_KEYWORDS = {
+ 'abs', 'absolute', 'accumulate', 'add', 'alen', 'all', 'allclose',
+ 'alltrue', 'alterdot', 'amax', 'amin', 'angle', 'any', 'append',
+ 'apply_along_axis', 'apply_over_axes', 'arange', 'arccos', 'arccosh',
+ 'arcsin', 'arcsinh', 'arctan', 'arctan2', 'arctanh', 'argmax', 'argmin',
+ 'argsort', 'argwhere', 'around', 'array', 'array2string', 'array_equal',
+ 'array_equiv', 'array_repr', 'array_split', 'array_str', 'arrayrange',
+ 'asanyarray', 'asarray', 'asarray_chkfinite', 'ascontiguousarray',
+ 'asfarray', 'asfortranarray', 'asmatrix', 'asscalar', 'astype',
+ 'atleast_1d', 'atleast_2d', 'atleast_3d', 'average', 'bartlett',
+ 'base_repr', 'beta', 'binary_repr', 'bincount', 'binomial',
+ 'bitwise_and', 'bitwise_not', 'bitwise_or', 'bitwise_xor', 'blackman',
+ 'bmat', 'broadcast', 'byte_bounds', 'bytes', 'byteswap', 'c_',
+ 'can_cast', 'ceil', 'choose', 'clip', 'column_stack', 'common_type',
+ 'compare_chararrays', 'compress', 'concatenate', 'conj', 'conjugate',
+ 'convolve', 'copy', 'corrcoef', 'correlate', 'cos', 'cosh', 'cov',
+ 'cross', 'cumprod', 'cumproduct', 'cumsum', 'delete', 'deprecate',
+ 'diag', 'diagflat', 'diagonal', 'diff', 'digitize', 'disp', 'divide',
+ 'dot', 'dsplit', 'dstack', 'dtype', 'dump', 'dumps', 'ediff1d', 'empty',
+ 'empty_like', 'equal', 'exp', 'expand_dims', 'expm1', 'extract', 'eye',
+ 'fabs', 'fastCopyAndTranspose', 'fft', 'fftfreq', 'fftshift', 'fill',
+ 'finfo', 'fix', 'flat', 'flatnonzero', 'flatten', 'fliplr', 'flipud',
+ 'floor', 'floor_divide', 'fmod', 'frexp', 'fromarrays', 'frombuffer',
+ 'fromfile', 'fromfunction', 'fromiter', 'frompyfunc', 'fromstring',
+ 'generic', 'get_array_wrap', 'get_include', 'get_numarray_include',
+ 'get_numpy_include', 'get_printoptions', 'getbuffer', 'getbufsize',
+ 'geterr', 'geterrcall', 'geterrobj', 'getfield', 'gradient', 'greater',
+ 'greater_equal', 'gumbel', 'hamming', 'hanning', 'histogram',
+ 'histogram2d', 'histogramdd', 'hsplit', 'hstack', 'hypot', 'i0',
+ 'identity', 'ifft', 'imag', 'index_exp', 'indices', 'inf', 'info',
+ 'inner', 'insert', 'int_asbuffer', 'interp', 'intersect1d',
+ 'intersect1d_nu', 'inv', 'invert', 'iscomplex', 'iscomplexobj',
+ 'isfinite', 'isfortran', 'isinf', 'isnan', 'isneginf', 'isposinf',
+ 'isreal', 'isrealobj', 'isscalar', 'issctype', 'issubclass_',
+ 'issubdtype', 'issubsctype', 'item', 'itemset', 'iterable', 'ix_',
+ 'kaiser', 'kron', 'ldexp', 'left_shift', 'less', 'less_equal', 'lexsort',
+ 'linspace', 'load', 'loads', 'loadtxt', 'log', 'log10', 'log1p', 'log2',
+ 'logical_and', 'logical_not', 'logical_or', 'logical_xor', 'logspace',
+ 'lstsq', 'mat', 'matrix', 'max', 'maximum', 'maximum_sctype',
+ 'may_share_memory', 'mean', 'median', 'meshgrid', 'mgrid', 'min',
+ 'minimum', 'mintypecode', 'mod', 'modf', 'msort', 'multiply', 'nan',
+ 'nan_to_num', 'nanargmax', 'nanargmin', 'nanmax', 'nanmin', 'nansum',
+ 'ndenumerate', 'ndim', 'ndindex', 'negative', 'newaxis', 'newbuffer',
+ 'newbyteorder', 'nonzero', 'not_equal', 'obj2sctype', 'ogrid', 'ones',
+ 'ones_like', 'outer', 'permutation', 'piecewise', 'pinv', 'pkgload',
+ 'place', 'poisson', 'poly', 'poly1d', 'polyadd', 'polyder', 'polydiv',
+ 'polyfit', 'polyint', 'polymul', 'polysub', 'polyval', 'power', 'prod',
+ 'product', 'ptp', 'put', 'putmask', 'r_', 'randint', 'random_integers',
+ 'random_sample', 'ranf', 'rank', 'ravel', 'real', 'real_if_close',
+ 'recarray', 'reciprocal', 'reduce', 'remainder', 'repeat', 'require',
+ 'reshape', 'resize', 'restoredot', 'right_shift', 'rint', 'roll',
+ 'rollaxis', 'roots', 'rot90', 'round', 'round_', 'row_stack', 's_',
+ 'sample', 'savetxt', 'sctype2char', 'searchsorted', 'seed', 'select',
+ 'set_numeric_ops', 'set_printoptions', 'set_string_function',
+ 'setbufsize', 'setdiff1d', 'seterr', 'seterrcall', 'seterrobj',
+ 'setfield', 'setflags', 'setmember1d', 'setxor1d', 'shape',
+ 'show_config', 'shuffle', 'sign', 'signbit', 'sin', 'sinc', 'sinh',
+ 'size', 'slice', 'solve', 'sometrue', 'sort', 'sort_complex', 'source',
+ 'split', 'sqrt', 'square', 'squeeze', 'standard_normal', 'std',
+ 'subtract', 'sum', 'svd', 'swapaxes', 'take', 'tan', 'tanh', 'tensordot',
+ 'test', 'tile', 'tofile', 'tolist', 'tostring', 'trace', 'transpose',
+ 'trapz', 'tri', 'tril', 'trim_zeros', 'triu', 'true_divide', 'typeDict',
+ 'typename', 'uniform', 'union1d', 'unique', 'unique1d', 'unravel_index',
+ 'unwrap', 'vander', 'var', 'vdot', 'vectorize', 'view', 'vonmises',
+ 'vsplit', 'vstack', 'weibull', 'where', 'who', 'zeros', 'zeros_like'
+ }
+
+ def get_tokens_unprocessed(self, text):
+ for index, token, value in \
+ PythonLexer.get_tokens_unprocessed(self, text):
+ if token is Name and value in self.EXTRA_KEYWORDS:
+ yield index, Keyword.Pseudo, value
+ else:
+ yield index, token, value
+
+ def analyse_text(text):
+ ltext = text[:1000]
+ return (shebang_matches(text, r'pythonw?(3(\.\d)?)?') or
+ 'import ' in ltext) \
+ and ('import numpy' in ltext or 'from numpy import' in ltext)
diff --git a/src/pip/_vendor/pygments/modeline.py b/src/pip/_vendor/pygments/modeline.py
new file mode 100644
index 000000000..43630835c
--- /dev/null
+++ b/src/pip/_vendor/pygments/modeline.py
@@ -0,0 +1,43 @@
+"""
+ pygments.modeline
+ ~~~~~~~~~~~~~~~~~
+
+ A simple modeline parser (based on pymodeline).
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+__all__ = ['get_filetype_from_buffer']
+
+
+modeline_re = re.compile(r'''
+ (?: vi | vim | ex ) (?: [<=>]? \d* )? :
+ .* (?: ft | filetype | syn | syntax ) = ( [^:\s]+ )
+''', re.VERBOSE)
+
+
+def get_filetype_from_line(l):
+ m = modeline_re.search(l)
+ if m:
+ return m.group(1)
+
+
+def get_filetype_from_buffer(buf, max_lines=5):
+ """
+ Scan the buffer for modelines and return filetype if one is found.
+ """
+ lines = buf.splitlines()
+ for l in lines[-1:-max_lines-1:-1]:
+ ret = get_filetype_from_line(l)
+ if ret:
+ return ret
+ for i in range(max_lines, -1, -1):
+ if i < len(lines):
+ ret = get_filetype_from_line(lines[i])
+ if ret:
+ return ret
+
+ return None
diff --git a/src/pip/_vendor/pygments/plugin.py b/src/pip/_vendor/pygments/plugin.py
new file mode 100644
index 000000000..a0431bf72
--- /dev/null
+++ b/src/pip/_vendor/pygments/plugin.py
@@ -0,0 +1,69 @@
+"""
+ pygments.plugin
+ ~~~~~~~~~~~~~~~
+
+ Pygments setuptools plugin interface. The methods defined
+ here also work if setuptools isn't installed but they just
+ return nothing.
+
+ lexer plugins::
+
+ [pygments.lexers]
+ yourlexer = yourmodule:YourLexer
+
+ formatter plugins::
+
+ [pygments.formatters]
+ yourformatter = yourformatter:YourFormatter
+ /.ext = yourformatter:YourFormatter
+
+ As you can see, you can define extensions for the formatter
+ with a leading slash.
+
+ syntax plugins::
+
+ [pygments.styles]
+ yourstyle = yourstyle:YourStyle
+
+ filter plugin::
+
+ [pygments.filter]
+ yourfilter = yourfilter:YourFilter
+
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+LEXER_ENTRY_POINT = 'pygments.lexers'
+FORMATTER_ENTRY_POINT = 'pygments.formatters'
+STYLE_ENTRY_POINT = 'pygments.styles'
+FILTER_ENTRY_POINT = 'pygments.filters'
+
+
+def iter_entry_points(group_name):
+ try:
+ from pip._vendor import pkg_resources
+ except (ImportError, OSError):
+ return []
+
+ return pkg_resources.iter_entry_points(group_name)
+
+
+def find_plugin_lexers():
+ for entrypoint in iter_entry_points(LEXER_ENTRY_POINT):
+ yield entrypoint.load()
+
+
+def find_plugin_formatters():
+ for entrypoint in iter_entry_points(FORMATTER_ENTRY_POINT):
+ yield entrypoint.name, entrypoint.load()
+
+
+def find_plugin_styles():
+ for entrypoint in iter_entry_points(STYLE_ENTRY_POINT):
+ yield entrypoint.name, entrypoint.load()
+
+
+def find_plugin_filters():
+ for entrypoint in iter_entry_points(FILTER_ENTRY_POINT):
+ yield entrypoint.name, entrypoint.load()
diff --git a/src/pip/_vendor/pygments/regexopt.py b/src/pip/_vendor/pygments/regexopt.py
new file mode 100644
index 000000000..ae0079199
--- /dev/null
+++ b/src/pip/_vendor/pygments/regexopt.py
@@ -0,0 +1,91 @@
+"""
+ pygments.regexopt
+ ~~~~~~~~~~~~~~~~~
+
+ An algorithm that generates optimized regexes for matching long lists of
+ literal strings.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import re
+from re import escape
+from os.path import commonprefix
+from itertools import groupby
+from operator import itemgetter
+
+CS_ESCAPE = re.compile(r'[\[\^\\\-\]]')
+FIRST_ELEMENT = itemgetter(0)
+
+
+def make_charset(letters):
+ return '[' + CS_ESCAPE.sub(lambda m: '\\' + m.group(), ''.join(letters)) + ']'
+
+
+def regex_opt_inner(strings, open_paren):
+ """Return a regex that matches any string in the sorted list of strings."""
+ close_paren = open_paren and ')' or ''
+ # print strings, repr(open_paren)
+ if not strings:
+ # print '-> nothing left'
+ return ''
+ first = strings[0]
+ if len(strings) == 1:
+ # print '-> only 1 string'
+ return open_paren + escape(first) + close_paren
+ if not first:
+ # print '-> first string empty'
+ return open_paren + regex_opt_inner(strings[1:], '(?:') \
+ + '?' + close_paren
+ if len(first) == 1:
+ # multiple one-char strings? make a charset
+ oneletter = []
+ rest = []
+ for s in strings:
+ if len(s) == 1:
+ oneletter.append(s)
+ else:
+ rest.append(s)
+ if len(oneletter) > 1: # do we have more than one oneletter string?
+ if rest:
+ # print '-> 1-character + rest'
+ return open_paren + regex_opt_inner(rest, '') + '|' \
+ + make_charset(oneletter) + close_paren
+ # print '-> only 1-character'
+ return open_paren + make_charset(oneletter) + close_paren
+ prefix = commonprefix(strings)
+ if prefix:
+ plen = len(prefix)
+ # we have a prefix for all strings
+ # print '-> prefix:', prefix
+ return open_paren + escape(prefix) \
+ + regex_opt_inner([s[plen:] for s in strings], '(?:') \
+ + close_paren
+ # is there a suffix?
+ strings_rev = [s[::-1] for s in strings]
+ suffix = commonprefix(strings_rev)
+ if suffix:
+ slen = len(suffix)
+ # print '-> suffix:', suffix[::-1]
+ return open_paren \
+ + regex_opt_inner(sorted(s[:-slen] for s in strings), '(?:') \
+ + escape(suffix[::-1]) + close_paren
+ # recurse on common 1-string prefixes
+ # print '-> last resort'
+ return open_paren + \
+ '|'.join(regex_opt_inner(list(group[1]), '')
+ for group in groupby(strings, lambda s: s[0] == first[0])) \
+ + close_paren
+
+
+def regex_opt(strings, prefix='', suffix=''):
+ """Return a compiled regex that matches any string in the given list.
+
+ The strings to match must be literal strings, not regexes. They will be
+ regex-escaped.
+
+ *prefix* and *suffix* are pre- and appended to the final regex.
+ """
+ strings = sorted(strings)
+ return prefix + regex_opt_inner(strings, '(') + suffix
diff --git a/src/pip/_vendor/pygments/scanner.py b/src/pip/_vendor/pygments/scanner.py
new file mode 100644
index 000000000..d47ed4828
--- /dev/null
+++ b/src/pip/_vendor/pygments/scanner.py
@@ -0,0 +1,104 @@
+"""
+ pygments.scanner
+ ~~~~~~~~~~~~~~~~
+
+ This library implements a regex based scanner. Some languages
+ like Pascal are easy to parse but have some keywords that
+ depend on the context. Because of this it's impossible to lex
+ that just by using a regular expression lexer like the
+ `RegexLexer`.
+
+ Have a look at the `DelphiLexer` to get an idea of how to use
+ this scanner.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+import re
+
+
+class EndOfText(RuntimeError):
+ """
+ Raise if end of text is reached and the user
+ tried to call a match function.
+ """
+
+
+class Scanner:
+ """
+ Simple scanner
+
+ All method patterns are regular expression strings (not
+ compiled expressions!)
+ """
+
+ def __init__(self, text, flags=0):
+ """
+ :param text: The text which should be scanned
+ :param flags: default regular expression flags
+ """
+ self.data = text
+ self.data_length = len(text)
+ self.start_pos = 0
+ self.pos = 0
+ self.flags = flags
+ self.last = None
+ self.match = None
+ self._re_cache = {}
+
+ def eos(self):
+ """`True` if the scanner reached the end of text."""
+ return self.pos >= self.data_length
+ eos = property(eos, eos.__doc__)
+
+ def check(self, pattern):
+ """
+ Apply `pattern` on the current position and return
+ the match object. (Doesn't touch pos). Use this for
+ lookahead.
+ """
+ if self.eos:
+ raise EndOfText()
+ if pattern not in self._re_cache:
+ self._re_cache[pattern] = re.compile(pattern, self.flags)
+ return self._re_cache[pattern].match(self.data, self.pos)
+
+ def test(self, pattern):
+ """Apply a pattern on the current position and check
+ if it patches. Doesn't touch pos.
+ """
+ return self.check(pattern) is not None
+
+ def scan(self, pattern):
+ """
+ Scan the text for the given pattern and update pos/match
+ and related fields. The return value is a boolean that
+ indicates if the pattern matched. The matched value is
+ stored on the instance as ``match``, the last value is
+ stored as ``last``. ``start_pos`` is the position of the
+ pointer before the pattern was matched, ``pos`` is the
+ end position.
+ """
+ if self.eos:
+ raise EndOfText()
+ if pattern not in self._re_cache:
+ self._re_cache[pattern] = re.compile(pattern, self.flags)
+ self.last = self.match
+ m = self._re_cache[pattern].match(self.data, self.pos)
+ if m is None:
+ return False
+ self.start_pos = m.start()
+ self.pos = m.end()
+ self.match = m.group()
+ return True
+
+ def get_char(self):
+ """Scan exactly one char."""
+ self.scan('.')
+
+ def __repr__(self):
+ return '<%s %d/%d>' % (
+ self.__class__.__name__,
+ self.pos,
+ self.data_length
+ )
diff --git a/src/pip/_vendor/pygments/sphinxext.py b/src/pip/_vendor/pygments/sphinxext.py
new file mode 100644
index 000000000..c41bd49dd
--- /dev/null
+++ b/src/pip/_vendor/pygments/sphinxext.py
@@ -0,0 +1,155 @@
+"""
+ pygments.sphinxext
+ ~~~~~~~~~~~~~~~~~~
+
+ Sphinx extension to generate automatic documentation of lexers,
+ formatters and filters.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import sys
+
+from docutils import nodes
+from docutils.statemachine import ViewList
+from docutils.parsers.rst import Directive
+from sphinx.util.nodes import nested_parse_with_titles
+
+
+MODULEDOC = '''
+.. module:: %s
+
+%s
+%s
+'''
+
+LEXERDOC = '''
+.. class:: %s
+
+ :Short names: %s
+ :Filenames: %s
+ :MIME types: %s
+
+ %s
+
+'''
+
+FMTERDOC = '''
+.. class:: %s
+
+ :Short names: %s
+ :Filenames: %s
+
+ %s
+
+'''
+
+FILTERDOC = '''
+.. class:: %s
+
+ :Name: %s
+
+ %s
+
+'''
+
+
+class PygmentsDoc(Directive):
+ """
+ A directive to collect all lexers/formatters/filters and generate
+ autoclass directives for them.
+ """
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec = {}
+
+ def run(self):
+ self.filenames = set()
+ if self.arguments[0] == 'lexers':
+ out = self.document_lexers()
+ elif self.arguments[0] == 'formatters':
+ out = self.document_formatters()
+ elif self.arguments[0] == 'filters':
+ out = self.document_filters()
+ else:
+ raise Exception('invalid argument for "pygmentsdoc" directive')
+ node = nodes.compound()
+ vl = ViewList(out.split('\n'), source='')
+ nested_parse_with_titles(self.state, vl, node)
+ for fn in self.filenames:
+ self.state.document.settings.record_dependencies.add(fn)
+ return node.children
+
+ def document_lexers(self):
+ from pip._vendor.pygments.lexers._mapping import LEXERS
+ out = []
+ modules = {}
+ moduledocstrings = {}
+ for classname, data in sorted(LEXERS.items(), key=lambda x: x[0]):
+ module = data[0]
+ mod = __import__(module, None, None, [classname])
+ self.filenames.add(mod.__file__)
+ cls = getattr(mod, classname)
+ if not cls.__doc__:
+ print("Warning: %s does not have a docstring." % classname)
+ docstring = cls.__doc__
+ if isinstance(docstring, bytes):
+ docstring = docstring.decode('utf8')
+ modules.setdefault(module, []).append((
+ classname,
+ ', '.join(data[2]) or 'None',
+ ', '.join(data[3]).replace('*', '\\*').replace('_', '\\') or 'None',
+ ', '.join(data[4]) or 'None',
+ docstring))
+ if module not in moduledocstrings:
+ moddoc = mod.__doc__
+ if isinstance(moddoc, bytes):
+ moddoc = moddoc.decode('utf8')
+ moduledocstrings[module] = moddoc
+
+ for module, lexers in sorted(modules.items(), key=lambda x: x[0]):
+ if moduledocstrings[module] is None:
+ raise Exception("Missing docstring for %s" % (module,))
+ heading = moduledocstrings[module].splitlines()[4].strip().rstrip('.')
+ out.append(MODULEDOC % (module, heading, '-'*len(heading)))
+ for data in lexers:
+ out.append(LEXERDOC % data)
+
+ return ''.join(out)
+
+ def document_formatters(self):
+ from pip._vendor.pygments.formatters import FORMATTERS
+
+ out = []
+ for classname, data in sorted(FORMATTERS.items(), key=lambda x: x[0]):
+ module = data[0]
+ mod = __import__(module, None, None, [classname])
+ self.filenames.add(mod.__file__)
+ cls = getattr(mod, classname)
+ docstring = cls.__doc__
+ if isinstance(docstring, bytes):
+ docstring = docstring.decode('utf8')
+ heading = cls.__name__
+ out.append(FMTERDOC % (heading, ', '.join(data[2]) or 'None',
+ ', '.join(data[3]).replace('*', '\\*') or 'None',
+ docstring))
+ return ''.join(out)
+
+ def document_filters(self):
+ from pip._vendor.pygments.filters import FILTERS
+
+ out = []
+ for name, cls in FILTERS.items():
+ self.filenames.add(sys.modules[cls.__module__].__file__)
+ docstring = cls.__doc__
+ if isinstance(docstring, bytes):
+ docstring = docstring.decode('utf8')
+ out.append(FILTERDOC % (cls.__name__, name, docstring))
+ return ''.join(out)
+
+
+def setup(app):
+ app.add_directive('pygmentsdoc', PygmentsDoc)
diff --git a/src/pip/_vendor/pygments/style.py b/src/pip/_vendor/pygments/style.py
new file mode 100644
index 000000000..84abbc205
--- /dev/null
+++ b/src/pip/_vendor/pygments/style.py
@@ -0,0 +1,197 @@
+"""
+ pygments.style
+ ~~~~~~~~~~~~~~
+
+ Basic style object.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+from pip._vendor.pygments.token import Token, STANDARD_TYPES
+
+# Default mapping of ansixxx to RGB colors.
+_ansimap = {
+ # dark
+ 'ansiblack': '000000',
+ 'ansired': '7f0000',
+ 'ansigreen': '007f00',
+ 'ansiyellow': '7f7fe0',
+ 'ansiblue': '00007f',
+ 'ansimagenta': '7f007f',
+ 'ansicyan': '007f7f',
+ 'ansigray': 'e5e5e5',
+ # normal
+ 'ansibrightblack': '555555',
+ 'ansibrightred': 'ff0000',
+ 'ansibrightgreen': '00ff00',
+ 'ansibrightyellow': 'ffff00',
+ 'ansibrightblue': '0000ff',
+ 'ansibrightmagenta': 'ff00ff',
+ 'ansibrightcyan': '00ffff',
+ 'ansiwhite': 'ffffff',
+}
+# mapping of deprecated #ansixxx colors to new color names
+_deprecated_ansicolors = {
+ # dark
+ '#ansiblack': 'ansiblack',
+ '#ansidarkred': 'ansired',
+ '#ansidarkgreen': 'ansigreen',
+ '#ansibrown': 'ansiyellow',
+ '#ansidarkblue': 'ansiblue',
+ '#ansipurple': 'ansimagenta',
+ '#ansiteal': 'ansicyan',
+ '#ansilightgray': 'ansigray',
+ # normal
+ '#ansidarkgray': 'ansibrightblack',
+ '#ansired': 'ansibrightred',
+ '#ansigreen': 'ansibrightgreen',
+ '#ansiyellow': 'ansibrightyellow',
+ '#ansiblue': 'ansibrightblue',
+ '#ansifuchsia': 'ansibrightmagenta',
+ '#ansiturquoise': 'ansibrightcyan',
+ '#ansiwhite': 'ansiwhite',
+}
+ansicolors = set(_ansimap)
+
+
+class StyleMeta(type):
+
+ def __new__(mcs, name, bases, dct):
+ obj = type.__new__(mcs, name, bases, dct)
+ for token in STANDARD_TYPES:
+ if token not in obj.styles:
+ obj.styles[token] = ''
+
+ def colorformat(text):
+ if text in ansicolors:
+ return text
+ if text[0:1] == '#':
+ col = text[1:]
+ if len(col) == 6:
+ return col
+ elif len(col) == 3:
+ return col[0] * 2 + col[1] * 2 + col[2] * 2
+ elif text == '':
+ return ''
+ elif text.startswith('var') or text.startswith('calc'):
+ return text
+ assert False, "wrong color format %r" % text
+
+ _styles = obj._styles = {}
+
+ for ttype in obj.styles:
+ for token in ttype.split():
+ if token in _styles:
+ continue
+ ndef = _styles.get(token.parent, None)
+ styledefs = obj.styles.get(token, '').split()
+ if not ndef or token is None:
+ ndef = ['', 0, 0, 0, '', '', 0, 0, 0]
+ elif 'noinherit' in styledefs and token is not Token:
+ ndef = _styles[Token][:]
+ else:
+ ndef = ndef[:]
+ _styles[token] = ndef
+ for styledef in obj.styles.get(token, '').split():
+ if styledef == 'noinherit':
+ pass
+ elif styledef == 'bold':
+ ndef[1] = 1
+ elif styledef == 'nobold':
+ ndef[1] = 0
+ elif styledef == 'italic':
+ ndef[2] = 1
+ elif styledef == 'noitalic':
+ ndef[2] = 0
+ elif styledef == 'underline':
+ ndef[3] = 1
+ elif styledef == 'nounderline':
+ ndef[3] = 0
+ elif styledef[:3] == 'bg:':
+ ndef[4] = colorformat(styledef[3:])
+ elif styledef[:7] == 'border:':
+ ndef[5] = colorformat(styledef[7:])
+ elif styledef == 'roman':
+ ndef[6] = 1
+ elif styledef == 'sans':
+ ndef[7] = 1
+ elif styledef == 'mono':
+ ndef[8] = 1
+ else:
+ ndef[0] = colorformat(styledef)
+
+ return obj
+
+ def style_for_token(cls, token):
+ t = cls._styles[token]
+ ansicolor = bgansicolor = None
+ color = t[0]
+ if color in _deprecated_ansicolors:
+ color = _deprecated_ansicolors[color]
+ if color in ansicolors:
+ ansicolor = color
+ color = _ansimap[color]
+ bgcolor = t[4]
+ if bgcolor in _deprecated_ansicolors:
+ bgcolor = _deprecated_ansicolors[bgcolor]
+ if bgcolor in ansicolors:
+ bgansicolor = bgcolor
+ bgcolor = _ansimap[bgcolor]
+
+ return {
+ 'color': color or None,
+ 'bold': bool(t[1]),
+ 'italic': bool(t[2]),
+ 'underline': bool(t[3]),
+ 'bgcolor': bgcolor or None,
+ 'border': t[5] or None,
+ 'roman': bool(t[6]) or None,
+ 'sans': bool(t[7]) or None,
+ 'mono': bool(t[8]) or None,
+ 'ansicolor': ansicolor,
+ 'bgansicolor': bgansicolor,
+ }
+
+ def list_styles(cls):
+ return list(cls)
+
+ def styles_token(cls, ttype):
+ return ttype in cls._styles
+
+ def __iter__(cls):
+ for token in cls._styles:
+ yield token, cls.style_for_token(token)
+
+ def __len__(cls):
+ return len(cls._styles)
+
+
+class Style(metaclass=StyleMeta):
+
+ #: overall background color (``None`` means transparent)
+ background_color = '#ffffff'
+
+ #: highlight background color
+ highlight_color = '#ffffcc'
+
+ #: line number font color
+ line_number_color = 'inherit'
+
+ #: line number background color
+ line_number_background_color = 'transparent'
+
+ #: special line number font color
+ line_number_special_color = '#000000'
+
+ #: special line number background color
+ line_number_special_background_color = '#ffffc0'
+
+ #: Style definitions for individual token types.
+ styles = {}
+
+ # Attribute for lexers defined within Pygments. If set
+ # to True, the style is not shown in the style gallery
+ # on the website. This is intended for language-specific
+ # styles.
+ web_style_gallery_exclude = False
diff --git a/src/pip/_vendor/pygments/styles/__init__.py b/src/pip/_vendor/pygments/styles/__init__.py
new file mode 100644
index 000000000..951ca1794
--- /dev/null
+++ b/src/pip/_vendor/pygments/styles/__init__.py
@@ -0,0 +1,93 @@
+"""
+ pygments.styles
+ ~~~~~~~~~~~~~~~
+
+ Contains built-in styles.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+from pip._vendor.pygments.plugin import find_plugin_styles
+from pip._vendor.pygments.util import ClassNotFound
+
+
+#: Maps style names to 'submodule::classname'.
+STYLE_MAP = {
+ 'default': 'default::DefaultStyle',
+ 'emacs': 'emacs::EmacsStyle',
+ 'friendly': 'friendly::FriendlyStyle',
+ 'friendly_grayscale': 'friendly_grayscale::FriendlyGrayscaleStyle',
+ 'colorful': 'colorful::ColorfulStyle',
+ 'autumn': 'autumn::AutumnStyle',
+ 'murphy': 'murphy::MurphyStyle',
+ 'manni': 'manni::ManniStyle',
+ 'material': 'material::MaterialStyle',
+ 'monokai': 'monokai::MonokaiStyle',
+ 'perldoc': 'perldoc::PerldocStyle',
+ 'pastie': 'pastie::PastieStyle',
+ 'borland': 'borland::BorlandStyle',
+ 'trac': 'trac::TracStyle',
+ 'native': 'native::NativeStyle',
+ 'fruity': 'fruity::FruityStyle',
+ 'bw': 'bw::BlackWhiteStyle',
+ 'vim': 'vim::VimStyle',
+ 'vs': 'vs::VisualStudioStyle',
+ 'tango': 'tango::TangoStyle',
+ 'rrt': 'rrt::RrtStyle',
+ 'xcode': 'xcode::XcodeStyle',
+ 'igor': 'igor::IgorStyle',
+ 'paraiso-light': 'paraiso_light::ParaisoLightStyle',
+ 'paraiso-dark': 'paraiso_dark::ParaisoDarkStyle',
+ 'lovelace': 'lovelace::LovelaceStyle',
+ 'algol': 'algol::AlgolStyle',
+ 'algol_nu': 'algol_nu::Algol_NuStyle',
+ 'arduino': 'arduino::ArduinoStyle',
+ 'rainbow_dash': 'rainbow_dash::RainbowDashStyle',
+ 'abap': 'abap::AbapStyle',
+ 'solarized-dark': 'solarized::SolarizedDarkStyle',
+ 'solarized-light': 'solarized::SolarizedLightStyle',
+ 'sas': 'sas::SasStyle',
+ 'stata': 'stata_light::StataLightStyle',
+ 'stata-light': 'stata_light::StataLightStyle',
+ 'stata-dark': 'stata_dark::StataDarkStyle',
+ 'inkpot': 'inkpot::InkPotStyle',
+ 'zenburn': 'zenburn::ZenburnStyle',
+ 'gruvbox-dark': 'gruvbox::GruvboxDarkStyle',
+ 'gruvbox-light': 'gruvbox::GruvboxLightStyle',
+ 'dracula': 'dracula::DraculaStyle',
+ 'one-dark': 'onedark::OneDarkStyle',
+ 'lilypond' : 'lilypond::LilyPondStyle',
+}
+
+
+def get_style_by_name(name):
+ if name in STYLE_MAP:
+ mod, cls = STYLE_MAP[name].split('::')
+ builtin = "yes"
+ else:
+ for found_name, style in find_plugin_styles():
+ if name == found_name:
+ return style
+ # perhaps it got dropped into our styles package
+ builtin = ""
+ mod = name
+ cls = name.title() + "Style"
+
+ try:
+ mod = __import__('pygments.styles.' + mod, None, None, [cls])
+ except ImportError:
+ raise ClassNotFound("Could not find style module %r" % mod +
+ (builtin and ", though it should be builtin") + ".")
+ try:
+ return getattr(mod, cls)
+ except AttributeError:
+ raise ClassNotFound("Could not find style class %r in style module." % cls)
+
+
+def get_all_styles():
+ """Return a generator for all styles by name,
+ both builtin and plugin."""
+ yield from STYLE_MAP
+ for name, _ in find_plugin_styles():
+ yield name
diff --git a/src/pip/_vendor/pygments/token.py b/src/pip/_vendor/pygments/token.py
new file mode 100644
index 000000000..8aee88a83
--- /dev/null
+++ b/src/pip/_vendor/pygments/token.py
@@ -0,0 +1,212 @@
+"""
+ pygments.token
+ ~~~~~~~~~~~~~~
+
+ Basic token types and the standard tokens.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+
+class _TokenType(tuple):
+ parent = None
+
+ def split(self):
+ buf = []
+ node = self
+ while node is not None:
+ buf.append(node)
+ node = node.parent
+ buf.reverse()
+ return buf
+
+ def __init__(self, *args):
+ # no need to call super.__init__
+ self.subtypes = set()
+
+ def __contains__(self, val):
+ return self is val or (
+ type(val) is self.__class__ and
+ val[:len(self)] == self
+ )
+
+ def __getattr__(self, val):
+ if not val or not val[0].isupper():
+ return tuple.__getattribute__(self, val)
+ new = _TokenType(self + (val,))
+ setattr(self, val, new)
+ self.subtypes.add(new)
+ new.parent = self
+ return new
+
+ def __repr__(self):
+ return 'Token' + (self and '.' or '') + '.'.join(self)
+
+ def __copy__(self):
+ # These instances are supposed to be singletons
+ return self
+
+ def __deepcopy__(self, memo):
+ # These instances are supposed to be singletons
+ return self
+
+
+Token = _TokenType()
+
+# Special token types
+Text = Token.Text
+Whitespace = Text.Whitespace
+Escape = Token.Escape
+Error = Token.Error
+# Text that doesn't belong to this lexer (e.g. HTML in PHP)
+Other = Token.Other
+
+# Common token types for source code
+Keyword = Token.Keyword
+Name = Token.Name
+Literal = Token.Literal
+String = Literal.String
+Number = Literal.Number
+Punctuation = Token.Punctuation
+Operator = Token.Operator
+Comment = Token.Comment
+
+# Generic types for non-source code
+Generic = Token.Generic
+
+# String and some others are not direct children of Token.
+# alias them:
+Token.Token = Token
+Token.String = String
+Token.Number = Number
+
+
+def is_token_subtype(ttype, other):
+ """
+ Return True if ``ttype`` is a subtype of ``other``.
+
+ exists for backwards compatibility. use ``ttype in other`` now.
+ """
+ return ttype in other
+
+
+def string_to_tokentype(s):
+ """
+ Convert a string into a token type::
+
+ >>> string_to_token('String.Double')
+ Token.Literal.String.Double
+ >>> string_to_token('Token.Literal.Number')
+ Token.Literal.Number
+ >>> string_to_token('')
+ Token
+
+ Tokens that are already tokens are returned unchanged:
+
+ >>> string_to_token(String)
+ Token.Literal.String
+ """
+ if isinstance(s, _TokenType):
+ return s
+ if not s:
+ return Token
+ node = Token
+ for item in s.split('.'):
+ node = getattr(node, item)
+ return node
+
+
+# Map standard token types to short names, used in CSS class naming.
+# If you add a new item, please be sure to run this file to perform
+# a consistency check for duplicate values.
+STANDARD_TYPES = {
+ Token: '',
+
+ Text: '',
+ Whitespace: 'w',
+ Escape: 'esc',
+ Error: 'err',
+ Other: 'x',
+
+ Keyword: 'k',
+ Keyword.Constant: 'kc',
+ Keyword.Declaration: 'kd',
+ Keyword.Namespace: 'kn',
+ Keyword.Pseudo: 'kp',
+ Keyword.Reserved: 'kr',
+ Keyword.Type: 'kt',
+
+ Name: 'n',
+ Name.Attribute: 'na',
+ Name.Builtin: 'nb',
+ Name.Builtin.Pseudo: 'bp',
+ Name.Class: 'nc',
+ Name.Constant: 'no',
+ Name.Decorator: 'nd',
+ Name.Entity: 'ni',
+ Name.Exception: 'ne',
+ Name.Function: 'nf',
+ Name.Function.Magic: 'fm',
+ Name.Property: 'py',
+ Name.Label: 'nl',
+ Name.Namespace: 'nn',
+ Name.Other: 'nx',
+ Name.Tag: 'nt',
+ Name.Variable: 'nv',
+ Name.Variable.Class: 'vc',
+ Name.Variable.Global: 'vg',
+ Name.Variable.Instance: 'vi',
+ Name.Variable.Magic: 'vm',
+
+ Literal: 'l',
+ Literal.Date: 'ld',
+
+ String: 's',
+ String.Affix: 'sa',
+ String.Backtick: 'sb',
+ String.Char: 'sc',
+ String.Delimiter: 'dl',
+ String.Doc: 'sd',
+ String.Double: 's2',
+ String.Escape: 'se',
+ String.Heredoc: 'sh',
+ String.Interpol: 'si',
+ String.Other: 'sx',
+ String.Regex: 'sr',
+ String.Single: 's1',
+ String.Symbol: 'ss',
+
+ Number: 'm',
+ Number.Bin: 'mb',
+ Number.Float: 'mf',
+ Number.Hex: 'mh',
+ Number.Integer: 'mi',
+ Number.Integer.Long: 'il',
+ Number.Oct: 'mo',
+
+ Operator: 'o',
+ Operator.Word: 'ow',
+
+ Punctuation: 'p',
+
+ Comment: 'c',
+ Comment.Hashbang: 'ch',
+ Comment.Multiline: 'cm',
+ Comment.Preproc: 'cp',
+ Comment.PreprocFile: 'cpf',
+ Comment.Single: 'c1',
+ Comment.Special: 'cs',
+
+ Generic: 'g',
+ Generic.Deleted: 'gd',
+ Generic.Emph: 'ge',
+ Generic.Error: 'gr',
+ Generic.Heading: 'gh',
+ Generic.Inserted: 'gi',
+ Generic.Output: 'go',
+ Generic.Prompt: 'gp',
+ Generic.Strong: 'gs',
+ Generic.Subheading: 'gu',
+ Generic.Traceback: 'gt',
+}
diff --git a/src/pip/_vendor/pygments/unistring.py b/src/pip/_vendor/pygments/unistring.py
new file mode 100644
index 000000000..2e3c80869
--- /dev/null
+++ b/src/pip/_vendor/pygments/unistring.py
@@ -0,0 +1,153 @@
+"""
+ pygments.unistring
+ ~~~~~~~~~~~~~~~~~~
+
+ Strings of all Unicode characters of a certain category.
+ Used for matching in Unicode-aware languages. Run to regenerate.
+
+ Inspired by chartypes_create.py from the MoinMoin project.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+Cc = '\x00-\x1f\x7f-\x9f'
+
+Cf = '\xad\u0600-\u0605\u061c\u06dd\u070f\u08e2\u180e\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u206f\ufeff\ufff9-\ufffb\U000110bd\U000110cd\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f'
+
+Cn = '\u0378-\u0379\u0380-\u0383\u038b\u038d\u03a2\u0530\u0557-\u0558\u058b-\u058c\u0590\u05c8-\u05cf\u05eb-\u05ee\u05f5-\u05ff\u061d\u070e\u074b-\u074c\u07b2-\u07bf\u07fb-\u07fc\u082e-\u082f\u083f\u085c-\u085d\u085f\u086b-\u089f\u08b5\u08be-\u08d2\u0984\u098d-\u098e\u0991-\u0992\u09a9\u09b1\u09b3-\u09b5\u09ba-\u09bb\u09c5-\u09c6\u09c9-\u09ca\u09cf-\u09d6\u09d8-\u09db\u09de\u09e4-\u09e5\u09ff-\u0a00\u0a04\u0a0b-\u0a0e\u0a11-\u0a12\u0a29\u0a31\u0a34\u0a37\u0a3a-\u0a3b\u0a3d\u0a43-\u0a46\u0a49-\u0a4a\u0a4e-\u0a50\u0a52-\u0a58\u0a5d\u0a5f-\u0a65\u0a77-\u0a80\u0a84\u0a8e\u0a92\u0aa9\u0ab1\u0ab4\u0aba-\u0abb\u0ac6\u0aca\u0ace-\u0acf\u0ad1-\u0adf\u0ae4-\u0ae5\u0af2-\u0af8\u0b00\u0b04\u0b0d-\u0b0e\u0b11-\u0b12\u0b29\u0b31\u0b34\u0b3a-\u0b3b\u0b45-\u0b46\u0b49-\u0b4a\u0b4e-\u0b55\u0b58-\u0b5b\u0b5e\u0b64-\u0b65\u0b78-\u0b81\u0b84\u0b8b-\u0b8d\u0b91\u0b96-\u0b98\u0b9b\u0b9d\u0ba0-\u0ba2\u0ba5-\u0ba7\u0bab-\u0bad\u0bba-\u0bbd\u0bc3-\u0bc5\u0bc9\u0bce-\u0bcf\u0bd1-\u0bd6\u0bd8-\u0be5\u0bfb-\u0bff\u0c0d\u0c11\u0c29\u0c3a-\u0c3c\u0c45\u0c49\u0c4e-\u0c54\u0c57\u0c5b-\u0c5f\u0c64-\u0c65\u0c70-\u0c77\u0c8d\u0c91\u0ca9\u0cb4\u0cba-\u0cbb\u0cc5\u0cc9\u0cce-\u0cd4\u0cd7-\u0cdd\u0cdf\u0ce4-\u0ce5\u0cf0\u0cf3-\u0cff\u0d04\u0d0d\u0d11\u0d45\u0d49\u0d50-\u0d53\u0d64-\u0d65\u0d80-\u0d81\u0d84\u0d97-\u0d99\u0db2\u0dbc\u0dbe-\u0dbf\u0dc7-\u0dc9\u0dcb-\u0dce\u0dd5\u0dd7\u0de0-\u0de5\u0df0-\u0df1\u0df5-\u0e00\u0e3b-\u0e3e\u0e5c-\u0e80\u0e83\u0e85-\u0e86\u0e89\u0e8b-\u0e8c\u0e8e-\u0e93\u0e98\u0ea0\u0ea4\u0ea6\u0ea8-\u0ea9\u0eac\u0eba\u0ebe-\u0ebf\u0ec5\u0ec7\u0ece-\u0ecf\u0eda-\u0edb\u0ee0-\u0eff\u0f48\u0f6d-\u0f70\u0f98\u0fbd\u0fcd\u0fdb-\u0fff\u10c6\u10c8-\u10cc\u10ce-\u10cf\u1249\u124e-\u124f\u1257\u1259\u125e-\u125f\u1289\u128e-\u128f\u12b1\u12b6-\u12b7\u12bf\u12c1\u12c6-\u12c7\u12d7\u1311\u1316-\u1317\u135b-\u135c\u137d-\u137f\u139a-\u139f\u13f6-\u13f7\u13fe-\u13ff\u169d-\u169f\u16f9-\u16ff\u170d\u1715-\u171f\u1737-\u173f\u1754-\u175f\u176d\u1771\u1774-\u177f\u17de-\u17df\u17ea-\u17ef\u17fa-\u17ff\u180f\u181a-\u181f\u1879-\u187f\u18ab-\u18af\u18f6-\u18ff\u191f\u192c-\u192f\u193c-\u193f\u1941-\u1943\u196e-\u196f\u1975-\u197f\u19ac-\u19af\u19ca-\u19cf\u19db-\u19dd\u1a1c-\u1a1d\u1a5f\u1a7d-\u1a7e\u1a8a-\u1a8f\u1a9a-\u1a9f\u1aae-\u1aaf\u1abf-\u1aff\u1b4c-\u1b4f\u1b7d-\u1b7f\u1bf4-\u1bfb\u1c38-\u1c3a\u1c4a-\u1c4c\u1c89-\u1c8f\u1cbb-\u1cbc\u1cc8-\u1ccf\u1cfa-\u1cff\u1dfa\u1f16-\u1f17\u1f1e-\u1f1f\u1f46-\u1f47\u1f4e-\u1f4f\u1f58\u1f5a\u1f5c\u1f5e\u1f7e-\u1f7f\u1fb5\u1fc5\u1fd4-\u1fd5\u1fdc\u1ff0-\u1ff1\u1ff5\u1fff\u2065\u2072-\u2073\u208f\u209d-\u209f\u20c0-\u20cf\u20f1-\u20ff\u218c-\u218f\u2427-\u243f\u244b-\u245f\u2b74-\u2b75\u2b96-\u2b97\u2bc9\u2bff\u2c2f\u2c5f\u2cf4-\u2cf8\u2d26\u2d28-\u2d2c\u2d2e-\u2d2f\u2d68-\u2d6e\u2d71-\u2d7e\u2d97-\u2d9f\u2da7\u2daf\u2db7\u2dbf\u2dc7\u2dcf\u2dd7\u2ddf\u2e4f-\u2e7f\u2e9a\u2ef4-\u2eff\u2fd6-\u2fef\u2ffc-\u2fff\u3040\u3097-\u3098\u3100-\u3104\u3130\u318f\u31bb-\u31bf\u31e4-\u31ef\u321f\u32ff\u4db6-\u4dbf\u9ff0-\u9fff\ua48d-\ua48f\ua4c7-\ua4cf\ua62c-\ua63f\ua6f8-\ua6ff\ua7ba-\ua7f6\ua82c-\ua82f\ua83a-\ua83f\ua878-\ua87f\ua8c6-\ua8cd\ua8da-\ua8df\ua954-\ua95e\ua97d-\ua97f\ua9ce\ua9da-\ua9dd\ua9ff\uaa37-\uaa3f\uaa4e-\uaa4f\uaa5a-\uaa5b\uaac3-\uaada\uaaf7-\uab00\uab07-\uab08\uab0f-\uab10\uab17-\uab1f\uab27\uab2f\uab66-\uab6f\uabee-\uabef\uabfa-\uabff\ud7a4-\ud7af\ud7c7-\ud7ca\ud7fc-\ud7ff\ufa6e-\ufa6f\ufada-\ufaff\ufb07-\ufb12\ufb18-\ufb1c\ufb37\ufb3d\ufb3f\ufb42\ufb45\ufbc2-\ufbd2\ufd40-\ufd4f\ufd90-\ufd91\ufdc8-\ufdef\ufdfe-\ufdff\ufe1a-\ufe1f\ufe53\ufe67\ufe6c-\ufe6f\ufe75\ufefd-\ufefe\uff00\uffbf-\uffc1\uffc8-\uffc9\uffd0-\uffd1\uffd8-\uffd9\uffdd-\uffdf\uffe7\uffef-\ufff8\ufffe-\uffff\U0001000c\U00010027\U0001003b\U0001003e\U0001004e-\U0001004f\U0001005e-\U0001007f\U000100fb-\U000100ff\U00010103-\U00010106\U00010134-\U00010136\U0001018f\U0001019c-\U0001019f\U000101a1-\U000101cf\U000101fe-\U0001027f\U0001029d-\U0001029f\U000102d1-\U000102df\U000102fc-\U000102ff\U00010324-\U0001032c\U0001034b-\U0001034f\U0001037b-\U0001037f\U0001039e\U000103c4-\U000103c7\U000103d6-\U000103ff\U0001049e-\U0001049f\U000104aa-\U000104af\U000104d4-\U000104d7\U000104fc-\U000104ff\U00010528-\U0001052f\U00010564-\U0001056e\U00010570-\U000105ff\U00010737-\U0001073f\U00010756-\U0001075f\U00010768-\U000107ff\U00010806-\U00010807\U00010809\U00010836\U00010839-\U0001083b\U0001083d-\U0001083e\U00010856\U0001089f-\U000108a6\U000108b0-\U000108df\U000108f3\U000108f6-\U000108fa\U0001091c-\U0001091e\U0001093a-\U0001093e\U00010940-\U0001097f\U000109b8-\U000109bb\U000109d0-\U000109d1\U00010a04\U00010a07-\U00010a0b\U00010a14\U00010a18\U00010a36-\U00010a37\U00010a3b-\U00010a3e\U00010a49-\U00010a4f\U00010a59-\U00010a5f\U00010aa0-\U00010abf\U00010ae7-\U00010aea\U00010af7-\U00010aff\U00010b36-\U00010b38\U00010b56-\U00010b57\U00010b73-\U00010b77\U00010b92-\U00010b98\U00010b9d-\U00010ba8\U00010bb0-\U00010bff\U00010c49-\U00010c7f\U00010cb3-\U00010cbf\U00010cf3-\U00010cf9\U00010d28-\U00010d2f\U00010d3a-\U00010e5f\U00010e7f-\U00010eff\U00010f28-\U00010f2f\U00010f5a-\U00010fff\U0001104e-\U00011051\U00011070-\U0001107e\U000110c2-\U000110cc\U000110ce-\U000110cf\U000110e9-\U000110ef\U000110fa-\U000110ff\U00011135\U00011147-\U0001114f\U00011177-\U0001117f\U000111ce-\U000111cf\U000111e0\U000111f5-\U000111ff\U00011212\U0001123f-\U0001127f\U00011287\U00011289\U0001128e\U0001129e\U000112aa-\U000112af\U000112eb-\U000112ef\U000112fa-\U000112ff\U00011304\U0001130d-\U0001130e\U00011311-\U00011312\U00011329\U00011331\U00011334\U0001133a\U00011345-\U00011346\U00011349-\U0001134a\U0001134e-\U0001134f\U00011351-\U00011356\U00011358-\U0001135c\U00011364-\U00011365\U0001136d-\U0001136f\U00011375-\U000113ff\U0001145a\U0001145c\U0001145f-\U0001147f\U000114c8-\U000114cf\U000114da-\U0001157f\U000115b6-\U000115b7\U000115de-\U000115ff\U00011645-\U0001164f\U0001165a-\U0001165f\U0001166d-\U0001167f\U000116b8-\U000116bf\U000116ca-\U000116ff\U0001171b-\U0001171c\U0001172c-\U0001172f\U00011740-\U000117ff\U0001183c-\U0001189f\U000118f3-\U000118fe\U00011900-\U000119ff\U00011a48-\U00011a4f\U00011a84-\U00011a85\U00011aa3-\U00011abf\U00011af9-\U00011bff\U00011c09\U00011c37\U00011c46-\U00011c4f\U00011c6d-\U00011c6f\U00011c90-\U00011c91\U00011ca8\U00011cb7-\U00011cff\U00011d07\U00011d0a\U00011d37-\U00011d39\U00011d3b\U00011d3e\U00011d48-\U00011d4f\U00011d5a-\U00011d5f\U00011d66\U00011d69\U00011d8f\U00011d92\U00011d99-\U00011d9f\U00011daa-\U00011edf\U00011ef9-\U00011fff\U0001239a-\U000123ff\U0001246f\U00012475-\U0001247f\U00012544-\U00012fff\U0001342f-\U000143ff\U00014647-\U000167ff\U00016a39-\U00016a3f\U00016a5f\U00016a6a-\U00016a6d\U00016a70-\U00016acf\U00016aee-\U00016aef\U00016af6-\U00016aff\U00016b46-\U00016b4f\U00016b5a\U00016b62\U00016b78-\U00016b7c\U00016b90-\U00016e3f\U00016e9b-\U00016eff\U00016f45-\U00016f4f\U00016f7f-\U00016f8e\U00016fa0-\U00016fdf\U00016fe2-\U00016fff\U000187f2-\U000187ff\U00018af3-\U0001afff\U0001b11f-\U0001b16f\U0001b2fc-\U0001bbff\U0001bc6b-\U0001bc6f\U0001bc7d-\U0001bc7f\U0001bc89-\U0001bc8f\U0001bc9a-\U0001bc9b\U0001bca4-\U0001cfff\U0001d0f6-\U0001d0ff\U0001d127-\U0001d128\U0001d1e9-\U0001d1ff\U0001d246-\U0001d2df\U0001d2f4-\U0001d2ff\U0001d357-\U0001d35f\U0001d379-\U0001d3ff\U0001d455\U0001d49d\U0001d4a0-\U0001d4a1\U0001d4a3-\U0001d4a4\U0001d4a7-\U0001d4a8\U0001d4ad\U0001d4ba\U0001d4bc\U0001d4c4\U0001d506\U0001d50b-\U0001d50c\U0001d515\U0001d51d\U0001d53a\U0001d53f\U0001d545\U0001d547-\U0001d549\U0001d551\U0001d6a6-\U0001d6a7\U0001d7cc-\U0001d7cd\U0001da8c-\U0001da9a\U0001daa0\U0001dab0-\U0001dfff\U0001e007\U0001e019-\U0001e01a\U0001e022\U0001e025\U0001e02b-\U0001e7ff\U0001e8c5-\U0001e8c6\U0001e8d7-\U0001e8ff\U0001e94b-\U0001e94f\U0001e95a-\U0001e95d\U0001e960-\U0001ec70\U0001ecb5-\U0001edff\U0001ee04\U0001ee20\U0001ee23\U0001ee25-\U0001ee26\U0001ee28\U0001ee33\U0001ee38\U0001ee3a\U0001ee3c-\U0001ee41\U0001ee43-\U0001ee46\U0001ee48\U0001ee4a\U0001ee4c\U0001ee50\U0001ee53\U0001ee55-\U0001ee56\U0001ee58\U0001ee5a\U0001ee5c\U0001ee5e\U0001ee60\U0001ee63\U0001ee65-\U0001ee66\U0001ee6b\U0001ee73\U0001ee78\U0001ee7d\U0001ee7f\U0001ee8a\U0001ee9c-\U0001eea0\U0001eea4\U0001eeaa\U0001eebc-\U0001eeef\U0001eef2-\U0001efff\U0001f02c-\U0001f02f\U0001f094-\U0001f09f\U0001f0af-\U0001f0b0\U0001f0c0\U0001f0d0\U0001f0f6-\U0001f0ff\U0001f10d-\U0001f10f\U0001f16c-\U0001f16f\U0001f1ad-\U0001f1e5\U0001f203-\U0001f20f\U0001f23c-\U0001f23f\U0001f249-\U0001f24f\U0001f252-\U0001f25f\U0001f266-\U0001f2ff\U0001f6d5-\U0001f6df\U0001f6ed-\U0001f6ef\U0001f6fa-\U0001f6ff\U0001f774-\U0001f77f\U0001f7d9-\U0001f7ff\U0001f80c-\U0001f80f\U0001f848-\U0001f84f\U0001f85a-\U0001f85f\U0001f888-\U0001f88f\U0001f8ae-\U0001f8ff\U0001f90c-\U0001f90f\U0001f93f\U0001f971-\U0001f972\U0001f977-\U0001f979\U0001f97b\U0001f9a3-\U0001f9af\U0001f9ba-\U0001f9bf\U0001f9c3-\U0001f9cf\U0001fa00-\U0001fa5f\U0001fa6e-\U0001ffff\U0002a6d7-\U0002a6ff\U0002b735-\U0002b73f\U0002b81e-\U0002b81f\U0002cea2-\U0002ceaf\U0002ebe1-\U0002f7ff\U0002fa1e-\U000e0000\U000e0002-\U000e001f\U000e0080-\U000e00ff\U000e01f0-\U000effff\U000ffffe-\U000fffff\U0010fffe-\U0010ffff'
+
+Co = '\ue000-\uf8ff\U000f0000-\U000ffffd\U00100000-\U0010fffd'
+
+Cs = '\ud800-\udbff\\\udc00\udc01-\udfff'
+
+Ll = 'a-z\xb5\xdf-\xf6\xf8-\xff\u0101\u0103\u0105\u0107\u0109\u010b\u010d\u010f\u0111\u0113\u0115\u0117\u0119\u011b\u011d\u011f\u0121\u0123\u0125\u0127\u0129\u012b\u012d\u012f\u0131\u0133\u0135\u0137-\u0138\u013a\u013c\u013e\u0140\u0142\u0144\u0146\u0148-\u0149\u014b\u014d\u014f\u0151\u0153\u0155\u0157\u0159\u015b\u015d\u015f\u0161\u0163\u0165\u0167\u0169\u016b\u016d\u016f\u0171\u0173\u0175\u0177\u017a\u017c\u017e-\u0180\u0183\u0185\u0188\u018c-\u018d\u0192\u0195\u0199-\u019b\u019e\u01a1\u01a3\u01a5\u01a8\u01aa-\u01ab\u01ad\u01b0\u01b4\u01b6\u01b9-\u01ba\u01bd-\u01bf\u01c6\u01c9\u01cc\u01ce\u01d0\u01d2\u01d4\u01d6\u01d8\u01da\u01dc-\u01dd\u01df\u01e1\u01e3\u01e5\u01e7\u01e9\u01eb\u01ed\u01ef-\u01f0\u01f3\u01f5\u01f9\u01fb\u01fd\u01ff\u0201\u0203\u0205\u0207\u0209\u020b\u020d\u020f\u0211\u0213\u0215\u0217\u0219\u021b\u021d\u021f\u0221\u0223\u0225\u0227\u0229\u022b\u022d\u022f\u0231\u0233-\u0239\u023c\u023f-\u0240\u0242\u0247\u0249\u024b\u024d\u024f-\u0293\u0295-\u02af\u0371\u0373\u0377\u037b-\u037d\u0390\u03ac-\u03ce\u03d0-\u03d1\u03d5-\u03d7\u03d9\u03db\u03dd\u03df\u03e1\u03e3\u03e5\u03e7\u03e9\u03eb\u03ed\u03ef-\u03f3\u03f5\u03f8\u03fb-\u03fc\u0430-\u045f\u0461\u0463\u0465\u0467\u0469\u046b\u046d\u046f\u0471\u0473\u0475\u0477\u0479\u047b\u047d\u047f\u0481\u048b\u048d\u048f\u0491\u0493\u0495\u0497\u0499\u049b\u049d\u049f\u04a1\u04a3\u04a5\u04a7\u04a9\u04ab\u04ad\u04af\u04b1\u04b3\u04b5\u04b7\u04b9\u04bb\u04bd\u04bf\u04c2\u04c4\u04c6\u04c8\u04ca\u04cc\u04ce-\u04cf\u04d1\u04d3\u04d5\u04d7\u04d9\u04db\u04dd\u04df\u04e1\u04e3\u04e5\u04e7\u04e9\u04eb\u04ed\u04ef\u04f1\u04f3\u04f5\u04f7\u04f9\u04fb\u04fd\u04ff\u0501\u0503\u0505\u0507\u0509\u050b\u050d\u050f\u0511\u0513\u0515\u0517\u0519\u051b\u051d\u051f\u0521\u0523\u0525\u0527\u0529\u052b\u052d\u052f\u0560-\u0588\u10d0-\u10fa\u10fd-\u10ff\u13f8-\u13fd\u1c80-\u1c88\u1d00-\u1d2b\u1d6b-\u1d77\u1d79-\u1d9a\u1e01\u1e03\u1e05\u1e07\u1e09\u1e0b\u1e0d\u1e0f\u1e11\u1e13\u1e15\u1e17\u1e19\u1e1b\u1e1d\u1e1f\u1e21\u1e23\u1e25\u1e27\u1e29\u1e2b\u1e2d\u1e2f\u1e31\u1e33\u1e35\u1e37\u1e39\u1e3b\u1e3d\u1e3f\u1e41\u1e43\u1e45\u1e47\u1e49\u1e4b\u1e4d\u1e4f\u1e51\u1e53\u1e55\u1e57\u1e59\u1e5b\u1e5d\u1e5f\u1e61\u1e63\u1e65\u1e67\u1e69\u1e6b\u1e6d\u1e6f\u1e71\u1e73\u1e75\u1e77\u1e79\u1e7b\u1e7d\u1e7f\u1e81\u1e83\u1e85\u1e87\u1e89\u1e8b\u1e8d\u1e8f\u1e91\u1e93\u1e95-\u1e9d\u1e9f\u1ea1\u1ea3\u1ea5\u1ea7\u1ea9\u1eab\u1ead\u1eaf\u1eb1\u1eb3\u1eb5\u1eb7\u1eb9\u1ebb\u1ebd\u1ebf\u1ec1\u1ec3\u1ec5\u1ec7\u1ec9\u1ecb\u1ecd\u1ecf\u1ed1\u1ed3\u1ed5\u1ed7\u1ed9\u1edb\u1edd\u1edf\u1ee1\u1ee3\u1ee5\u1ee7\u1ee9\u1eeb\u1eed\u1eef\u1ef1\u1ef3\u1ef5\u1ef7\u1ef9\u1efb\u1efd\u1eff-\u1f07\u1f10-\u1f15\u1f20-\u1f27\u1f30-\u1f37\u1f40-\u1f45\u1f50-\u1f57\u1f60-\u1f67\u1f70-\u1f7d\u1f80-\u1f87\u1f90-\u1f97\u1fa0-\u1fa7\u1fb0-\u1fb4\u1fb6-\u1fb7\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fc7\u1fd0-\u1fd3\u1fd6-\u1fd7\u1fe0-\u1fe7\u1ff2-\u1ff4\u1ff6-\u1ff7\u210a\u210e-\u210f\u2113\u212f\u2134\u2139\u213c-\u213d\u2146-\u2149\u214e\u2184\u2c30-\u2c5e\u2c61\u2c65-\u2c66\u2c68\u2c6a\u2c6c\u2c71\u2c73-\u2c74\u2c76-\u2c7b\u2c81\u2c83\u2c85\u2c87\u2c89\u2c8b\u2c8d\u2c8f\u2c91\u2c93\u2c95\u2c97\u2c99\u2c9b\u2c9d\u2c9f\u2ca1\u2ca3\u2ca5\u2ca7\u2ca9\u2cab\u2cad\u2caf\u2cb1\u2cb3\u2cb5\u2cb7\u2cb9\u2cbb\u2cbd\u2cbf\u2cc1\u2cc3\u2cc5\u2cc7\u2cc9\u2ccb\u2ccd\u2ccf\u2cd1\u2cd3\u2cd5\u2cd7\u2cd9\u2cdb\u2cdd\u2cdf\u2ce1\u2ce3-\u2ce4\u2cec\u2cee\u2cf3\u2d00-\u2d25\u2d27\u2d2d\ua641\ua643\ua645\ua647\ua649\ua64b\ua64d\ua64f\ua651\ua653\ua655\ua657\ua659\ua65b\ua65d\ua65f\ua661\ua663\ua665\ua667\ua669\ua66b\ua66d\ua681\ua683\ua685\ua687\ua689\ua68b\ua68d\ua68f\ua691\ua693\ua695\ua697\ua699\ua69b\ua723\ua725\ua727\ua729\ua72b\ua72d\ua72f-\ua731\ua733\ua735\ua737\ua739\ua73b\ua73d\ua73f\ua741\ua743\ua745\ua747\ua749\ua74b\ua74d\ua74f\ua751\ua753\ua755\ua757\ua759\ua75b\ua75d\ua75f\ua761\ua763\ua765\ua767\ua769\ua76b\ua76d\ua76f\ua771-\ua778\ua77a\ua77c\ua77f\ua781\ua783\ua785\ua787\ua78c\ua78e\ua791\ua793-\ua795\ua797\ua799\ua79b\ua79d\ua79f\ua7a1\ua7a3\ua7a5\ua7a7\ua7a9\ua7af\ua7b5\ua7b7\ua7b9\ua7fa\uab30-\uab5a\uab60-\uab65\uab70-\uabbf\ufb00-\ufb06\ufb13-\ufb17\uff41-\uff5a\U00010428-\U0001044f\U000104d8-\U000104fb\U00010cc0-\U00010cf2\U000118c0-\U000118df\U00016e60-\U00016e7f\U0001d41a-\U0001d433\U0001d44e-\U0001d454\U0001d456-\U0001d467\U0001d482-\U0001d49b\U0001d4b6-\U0001d4b9\U0001d4bb\U0001d4bd-\U0001d4c3\U0001d4c5-\U0001d4cf\U0001d4ea-\U0001d503\U0001d51e-\U0001d537\U0001d552-\U0001d56b\U0001d586-\U0001d59f\U0001d5ba-\U0001d5d3\U0001d5ee-\U0001d607\U0001d622-\U0001d63b\U0001d656-\U0001d66f\U0001d68a-\U0001d6a5\U0001d6c2-\U0001d6da\U0001d6dc-\U0001d6e1\U0001d6fc-\U0001d714\U0001d716-\U0001d71b\U0001d736-\U0001d74e\U0001d750-\U0001d755\U0001d770-\U0001d788\U0001d78a-\U0001d78f\U0001d7aa-\U0001d7c2\U0001d7c4-\U0001d7c9\U0001d7cb\U0001e922-\U0001e943'
+
+Lm = '\u02b0-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0374\u037a\u0559\u0640\u06e5-\u06e6\u07f4-\u07f5\u07fa\u081a\u0824\u0828\u0971\u0e46\u0ec6\u10fc\u17d7\u1843\u1aa7\u1c78-\u1c7d\u1d2c-\u1d6a\u1d78\u1d9b-\u1dbf\u2071\u207f\u2090-\u209c\u2c7c-\u2c7d\u2d6f\u2e2f\u3005\u3031-\u3035\u303b\u309d-\u309e\u30fc-\u30fe\ua015\ua4f8-\ua4fd\ua60c\ua67f\ua69c-\ua69d\ua717-\ua71f\ua770\ua788\ua7f8-\ua7f9\ua9cf\ua9e6\uaa70\uaadd\uaaf3-\uaaf4\uab5c-\uab5f\uff70\uff9e-\uff9f\U00016b40-\U00016b43\U00016f93-\U00016f9f\U00016fe0-\U00016fe1'
+
+Lo = '\xaa\xba\u01bb\u01c0-\u01c3\u0294\u05d0-\u05ea\u05ef-\u05f2\u0620-\u063f\u0641-\u064a\u066e-\u066f\u0671-\u06d3\u06d5\u06ee-\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u0800-\u0815\u0840-\u0858\u0860-\u086a\u08a0-\u08b4\u08b6-\u08bd\u0904-\u0939\u093d\u0950\u0958-\u0961\u0972-\u0980\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc-\u09dd\u09df-\u09e1\u09f0-\u09f1\u09fc\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0-\u0ae1\u0af9\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c60-\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0-\u0ce1\u0cf1-\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32-\u0e33\u0e40-\u0e45\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065-\u1066\u106e-\u1070\u1075-\u1081\u108e\u1100-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16f1-\u16f8\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17dc\u1820-\u1842\u1844-\u1878\u1880-\u1884\u1887-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae-\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c77\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5-\u1cf6\u2135-\u2138\u2d30-\u2d67\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3006\u303c\u3041-\u3096\u309f\u30a1-\u30fa\u30ff\u3105-\u312f\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fef\ua000-\ua014\ua016-\ua48c\ua4d0-\ua4f7\ua500-\ua60b\ua610-\ua61f\ua62a-\ua62b\ua66e\ua6a0-\ua6e5\ua78f\ua7f7\ua7fb-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd-\ua8fe\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9e0-\ua9e4\ua9e7-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa6f\uaa71-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5-\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadc\uaae0-\uaaea\uaaf2\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff66-\uff6f\uff71-\uff9d\uffa0-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc\U00010000-\U0001000b\U0001000d-\U00010026\U00010028-\U0001003a\U0001003c-\U0001003d\U0001003f-\U0001004d\U00010050-\U0001005d\U00010080-\U000100fa\U00010280-\U0001029c\U000102a0-\U000102d0\U00010300-\U0001031f\U0001032d-\U00010340\U00010342-\U00010349\U00010350-\U00010375\U00010380-\U0001039d\U000103a0-\U000103c3\U000103c8-\U000103cf\U00010450-\U0001049d\U00010500-\U00010527\U00010530-\U00010563\U00010600-\U00010736\U00010740-\U00010755\U00010760-\U00010767\U00010800-\U00010805\U00010808\U0001080a-\U00010835\U00010837-\U00010838\U0001083c\U0001083f-\U00010855\U00010860-\U00010876\U00010880-\U0001089e\U000108e0-\U000108f2\U000108f4-\U000108f5\U00010900-\U00010915\U00010920-\U00010939\U00010980-\U000109b7\U000109be-\U000109bf\U00010a00\U00010a10-\U00010a13\U00010a15-\U00010a17\U00010a19-\U00010a35\U00010a60-\U00010a7c\U00010a80-\U00010a9c\U00010ac0-\U00010ac7\U00010ac9-\U00010ae4\U00010b00-\U00010b35\U00010b40-\U00010b55\U00010b60-\U00010b72\U00010b80-\U00010b91\U00010c00-\U00010c48\U00010d00-\U00010d23\U00010f00-\U00010f1c\U00010f27\U00010f30-\U00010f45\U00011003-\U00011037\U00011083-\U000110af\U000110d0-\U000110e8\U00011103-\U00011126\U00011144\U00011150-\U00011172\U00011176\U00011183-\U000111b2\U000111c1-\U000111c4\U000111da\U000111dc\U00011200-\U00011211\U00011213-\U0001122b\U00011280-\U00011286\U00011288\U0001128a-\U0001128d\U0001128f-\U0001129d\U0001129f-\U000112a8\U000112b0-\U000112de\U00011305-\U0001130c\U0001130f-\U00011310\U00011313-\U00011328\U0001132a-\U00011330\U00011332-\U00011333\U00011335-\U00011339\U0001133d\U00011350\U0001135d-\U00011361\U00011400-\U00011434\U00011447-\U0001144a\U00011480-\U000114af\U000114c4-\U000114c5\U000114c7\U00011580-\U000115ae\U000115d8-\U000115db\U00011600-\U0001162f\U00011644\U00011680-\U000116aa\U00011700-\U0001171a\U00011800-\U0001182b\U000118ff\U00011a00\U00011a0b-\U00011a32\U00011a3a\U00011a50\U00011a5c-\U00011a83\U00011a86-\U00011a89\U00011a9d\U00011ac0-\U00011af8\U00011c00-\U00011c08\U00011c0a-\U00011c2e\U00011c40\U00011c72-\U00011c8f\U00011d00-\U00011d06\U00011d08-\U00011d09\U00011d0b-\U00011d30\U00011d46\U00011d60-\U00011d65\U00011d67-\U00011d68\U00011d6a-\U00011d89\U00011d98\U00011ee0-\U00011ef2\U00012000-\U00012399\U00012480-\U00012543\U00013000-\U0001342e\U00014400-\U00014646\U00016800-\U00016a38\U00016a40-\U00016a5e\U00016ad0-\U00016aed\U00016b00-\U00016b2f\U00016b63-\U00016b77\U00016b7d-\U00016b8f\U00016f00-\U00016f44\U00016f50\U00017000-\U000187f1\U00018800-\U00018af2\U0001b000-\U0001b11e\U0001b170-\U0001b2fb\U0001bc00-\U0001bc6a\U0001bc70-\U0001bc7c\U0001bc80-\U0001bc88\U0001bc90-\U0001bc99\U0001e800-\U0001e8c4\U0001ee00-\U0001ee03\U0001ee05-\U0001ee1f\U0001ee21-\U0001ee22\U0001ee24\U0001ee27\U0001ee29-\U0001ee32\U0001ee34-\U0001ee37\U0001ee39\U0001ee3b\U0001ee42\U0001ee47\U0001ee49\U0001ee4b\U0001ee4d-\U0001ee4f\U0001ee51-\U0001ee52\U0001ee54\U0001ee57\U0001ee59\U0001ee5b\U0001ee5d\U0001ee5f\U0001ee61-\U0001ee62\U0001ee64\U0001ee67-\U0001ee6a\U0001ee6c-\U0001ee72\U0001ee74-\U0001ee77\U0001ee79-\U0001ee7c\U0001ee7e\U0001ee80-\U0001ee89\U0001ee8b-\U0001ee9b\U0001eea1-\U0001eea3\U0001eea5-\U0001eea9\U0001eeab-\U0001eebb\U00020000-\U0002a6d6\U0002a700-\U0002b734\U0002b740-\U0002b81d\U0002b820-\U0002cea1\U0002ceb0-\U0002ebe0\U0002f800-\U0002fa1d'
+
+Lt = '\u01c5\u01c8\u01cb\u01f2\u1f88-\u1f8f\u1f98-\u1f9f\u1fa8-\u1faf\u1fbc\u1fcc\u1ffc'
+
+Lu = 'A-Z\xc0-\xd6\xd8-\xde\u0100\u0102\u0104\u0106\u0108\u010a\u010c\u010e\u0110\u0112\u0114\u0116\u0118\u011a\u011c\u011e\u0120\u0122\u0124\u0126\u0128\u012a\u012c\u012e\u0130\u0132\u0134\u0136\u0139\u013b\u013d\u013f\u0141\u0143\u0145\u0147\u014a\u014c\u014e\u0150\u0152\u0154\u0156\u0158\u015a\u015c\u015e\u0160\u0162\u0164\u0166\u0168\u016a\u016c\u016e\u0170\u0172\u0174\u0176\u0178-\u0179\u017b\u017d\u0181-\u0182\u0184\u0186-\u0187\u0189-\u018b\u018e-\u0191\u0193-\u0194\u0196-\u0198\u019c-\u019d\u019f-\u01a0\u01a2\u01a4\u01a6-\u01a7\u01a9\u01ac\u01ae-\u01af\u01b1-\u01b3\u01b5\u01b7-\u01b8\u01bc\u01c4\u01c7\u01ca\u01cd\u01cf\u01d1\u01d3\u01d5\u01d7\u01d9\u01db\u01de\u01e0\u01e2\u01e4\u01e6\u01e8\u01ea\u01ec\u01ee\u01f1\u01f4\u01f6-\u01f8\u01fa\u01fc\u01fe\u0200\u0202\u0204\u0206\u0208\u020a\u020c\u020e\u0210\u0212\u0214\u0216\u0218\u021a\u021c\u021e\u0220\u0222\u0224\u0226\u0228\u022a\u022c\u022e\u0230\u0232\u023a-\u023b\u023d-\u023e\u0241\u0243-\u0246\u0248\u024a\u024c\u024e\u0370\u0372\u0376\u037f\u0386\u0388-\u038a\u038c\u038e-\u038f\u0391-\u03a1\u03a3-\u03ab\u03cf\u03d2-\u03d4\u03d8\u03da\u03dc\u03de\u03e0\u03e2\u03e4\u03e6\u03e8\u03ea\u03ec\u03ee\u03f4\u03f7\u03f9-\u03fa\u03fd-\u042f\u0460\u0462\u0464\u0466\u0468\u046a\u046c\u046e\u0470\u0472\u0474\u0476\u0478\u047a\u047c\u047e\u0480\u048a\u048c\u048e\u0490\u0492\u0494\u0496\u0498\u049a\u049c\u049e\u04a0\u04a2\u04a4\u04a6\u04a8\u04aa\u04ac\u04ae\u04b0\u04b2\u04b4\u04b6\u04b8\u04ba\u04bc\u04be\u04c0-\u04c1\u04c3\u04c5\u04c7\u04c9\u04cb\u04cd\u04d0\u04d2\u04d4\u04d6\u04d8\u04da\u04dc\u04de\u04e0\u04e2\u04e4\u04e6\u04e8\u04ea\u04ec\u04ee\u04f0\u04f2\u04f4\u04f6\u04f8\u04fa\u04fc\u04fe\u0500\u0502\u0504\u0506\u0508\u050a\u050c\u050e\u0510\u0512\u0514\u0516\u0518\u051a\u051c\u051e\u0520\u0522\u0524\u0526\u0528\u052a\u052c\u052e\u0531-\u0556\u10a0-\u10c5\u10c7\u10cd\u13a0-\u13f5\u1c90-\u1cba\u1cbd-\u1cbf\u1e00\u1e02\u1e04\u1e06\u1e08\u1e0a\u1e0c\u1e0e\u1e10\u1e12\u1e14\u1e16\u1e18\u1e1a\u1e1c\u1e1e\u1e20\u1e22\u1e24\u1e26\u1e28\u1e2a\u1e2c\u1e2e\u1e30\u1e32\u1e34\u1e36\u1e38\u1e3a\u1e3c\u1e3e\u1e40\u1e42\u1e44\u1e46\u1e48\u1e4a\u1e4c\u1e4e\u1e50\u1e52\u1e54\u1e56\u1e58\u1e5a\u1e5c\u1e5e\u1e60\u1e62\u1e64\u1e66\u1e68\u1e6a\u1e6c\u1e6e\u1e70\u1e72\u1e74\u1e76\u1e78\u1e7a\u1e7c\u1e7e\u1e80\u1e82\u1e84\u1e86\u1e88\u1e8a\u1e8c\u1e8e\u1e90\u1e92\u1e94\u1e9e\u1ea0\u1ea2\u1ea4\u1ea6\u1ea8\u1eaa\u1eac\u1eae\u1eb0\u1eb2\u1eb4\u1eb6\u1eb8\u1eba\u1ebc\u1ebe\u1ec0\u1ec2\u1ec4\u1ec6\u1ec8\u1eca\u1ecc\u1ece\u1ed0\u1ed2\u1ed4\u1ed6\u1ed8\u1eda\u1edc\u1ede\u1ee0\u1ee2\u1ee4\u1ee6\u1ee8\u1eea\u1eec\u1eee\u1ef0\u1ef2\u1ef4\u1ef6\u1ef8\u1efa\u1efc\u1efe\u1f08-\u1f0f\u1f18-\u1f1d\u1f28-\u1f2f\u1f38-\u1f3f\u1f48-\u1f4d\u1f59\u1f5b\u1f5d\u1f5f\u1f68-\u1f6f\u1fb8-\u1fbb\u1fc8-\u1fcb\u1fd8-\u1fdb\u1fe8-\u1fec\u1ff8-\u1ffb\u2102\u2107\u210b-\u210d\u2110-\u2112\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u2130-\u2133\u213e-\u213f\u2145\u2183\u2c00-\u2c2e\u2c60\u2c62-\u2c64\u2c67\u2c69\u2c6b\u2c6d-\u2c70\u2c72\u2c75\u2c7e-\u2c80\u2c82\u2c84\u2c86\u2c88\u2c8a\u2c8c\u2c8e\u2c90\u2c92\u2c94\u2c96\u2c98\u2c9a\u2c9c\u2c9e\u2ca0\u2ca2\u2ca4\u2ca6\u2ca8\u2caa\u2cac\u2cae\u2cb0\u2cb2\u2cb4\u2cb6\u2cb8\u2cba\u2cbc\u2cbe\u2cc0\u2cc2\u2cc4\u2cc6\u2cc8\u2cca\u2ccc\u2cce\u2cd0\u2cd2\u2cd4\u2cd6\u2cd8\u2cda\u2cdc\u2cde\u2ce0\u2ce2\u2ceb\u2ced\u2cf2\ua640\ua642\ua644\ua646\ua648\ua64a\ua64c\ua64e\ua650\ua652\ua654\ua656\ua658\ua65a\ua65c\ua65e\ua660\ua662\ua664\ua666\ua668\ua66a\ua66c\ua680\ua682\ua684\ua686\ua688\ua68a\ua68c\ua68e\ua690\ua692\ua694\ua696\ua698\ua69a\ua722\ua724\ua726\ua728\ua72a\ua72c\ua72e\ua732\ua734\ua736\ua738\ua73a\ua73c\ua73e\ua740\ua742\ua744\ua746\ua748\ua74a\ua74c\ua74e\ua750\ua752\ua754\ua756\ua758\ua75a\ua75c\ua75e\ua760\ua762\ua764\ua766\ua768\ua76a\ua76c\ua76e\ua779\ua77b\ua77d-\ua77e\ua780\ua782\ua784\ua786\ua78b\ua78d\ua790\ua792\ua796\ua798\ua79a\ua79c\ua79e\ua7a0\ua7a2\ua7a4\ua7a6\ua7a8\ua7aa-\ua7ae\ua7b0-\ua7b4\ua7b6\ua7b8\uff21-\uff3a\U00010400-\U00010427\U000104b0-\U000104d3\U00010c80-\U00010cb2\U000118a0-\U000118bf\U00016e40-\U00016e5f\U0001d400-\U0001d419\U0001d434-\U0001d44d\U0001d468-\U0001d481\U0001d49c\U0001d49e-\U0001d49f\U0001d4a2\U0001d4a5-\U0001d4a6\U0001d4a9-\U0001d4ac\U0001d4ae-\U0001d4b5\U0001d4d0-\U0001d4e9\U0001d504-\U0001d505\U0001d507-\U0001d50a\U0001d50d-\U0001d514\U0001d516-\U0001d51c\U0001d538-\U0001d539\U0001d53b-\U0001d53e\U0001d540-\U0001d544\U0001d546\U0001d54a-\U0001d550\U0001d56c-\U0001d585\U0001d5a0-\U0001d5b9\U0001d5d4-\U0001d5ed\U0001d608-\U0001d621\U0001d63c-\U0001d655\U0001d670-\U0001d689\U0001d6a8-\U0001d6c0\U0001d6e2-\U0001d6fa\U0001d71c-\U0001d734\U0001d756-\U0001d76e\U0001d790-\U0001d7a8\U0001d7ca\U0001e900-\U0001e921'
+
+Mc = '\u0903\u093b\u093e-\u0940\u0949-\u094c\u094e-\u094f\u0982-\u0983\u09be-\u09c0\u09c7-\u09c8\u09cb-\u09cc\u09d7\u0a03\u0a3e-\u0a40\u0a83\u0abe-\u0ac0\u0ac9\u0acb-\u0acc\u0b02-\u0b03\u0b3e\u0b40\u0b47-\u0b48\u0b4b-\u0b4c\u0b57\u0bbe-\u0bbf\u0bc1-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcc\u0bd7\u0c01-\u0c03\u0c41-\u0c44\u0c82-\u0c83\u0cbe\u0cc0-\u0cc4\u0cc7-\u0cc8\u0cca-\u0ccb\u0cd5-\u0cd6\u0d02-\u0d03\u0d3e-\u0d40\u0d46-\u0d48\u0d4a-\u0d4c\u0d57\u0d82-\u0d83\u0dcf-\u0dd1\u0dd8-\u0ddf\u0df2-\u0df3\u0f3e-\u0f3f\u0f7f\u102b-\u102c\u1031\u1038\u103b-\u103c\u1056-\u1057\u1062-\u1064\u1067-\u106d\u1083-\u1084\u1087-\u108c\u108f\u109a-\u109c\u17b6\u17be-\u17c5\u17c7-\u17c8\u1923-\u1926\u1929-\u192b\u1930-\u1931\u1933-\u1938\u1a19-\u1a1a\u1a55\u1a57\u1a61\u1a63-\u1a64\u1a6d-\u1a72\u1b04\u1b35\u1b3b\u1b3d-\u1b41\u1b43-\u1b44\u1b82\u1ba1\u1ba6-\u1ba7\u1baa\u1be7\u1bea-\u1bec\u1bee\u1bf2-\u1bf3\u1c24-\u1c2b\u1c34-\u1c35\u1ce1\u1cf2-\u1cf3\u1cf7\u302e-\u302f\ua823-\ua824\ua827\ua880-\ua881\ua8b4-\ua8c3\ua952-\ua953\ua983\ua9b4-\ua9b5\ua9ba-\ua9bb\ua9bd-\ua9c0\uaa2f-\uaa30\uaa33-\uaa34\uaa4d\uaa7b\uaa7d\uaaeb\uaaee-\uaaef\uaaf5\uabe3-\uabe4\uabe6-\uabe7\uabe9-\uabea\uabec\U00011000\U00011002\U00011082\U000110b0-\U000110b2\U000110b7-\U000110b8\U0001112c\U00011145-\U00011146\U00011182\U000111b3-\U000111b5\U000111bf-\U000111c0\U0001122c-\U0001122e\U00011232-\U00011233\U00011235\U000112e0-\U000112e2\U00011302-\U00011303\U0001133e-\U0001133f\U00011341-\U00011344\U00011347-\U00011348\U0001134b-\U0001134d\U00011357\U00011362-\U00011363\U00011435-\U00011437\U00011440-\U00011441\U00011445\U000114b0-\U000114b2\U000114b9\U000114bb-\U000114be\U000114c1\U000115af-\U000115b1\U000115b8-\U000115bb\U000115be\U00011630-\U00011632\U0001163b-\U0001163c\U0001163e\U000116ac\U000116ae-\U000116af\U000116b6\U00011720-\U00011721\U00011726\U0001182c-\U0001182e\U00011838\U00011a39\U00011a57-\U00011a58\U00011a97\U00011c2f\U00011c3e\U00011ca9\U00011cb1\U00011cb4\U00011d8a-\U00011d8e\U00011d93-\U00011d94\U00011d96\U00011ef5-\U00011ef6\U00016f51-\U00016f7e\U0001d165-\U0001d166\U0001d16d-\U0001d172'
+
+Me = '\u0488-\u0489\u1abe\u20dd-\u20e0\u20e2-\u20e4\ua670-\ua672'
+
+Mn = '\u0300-\u036f\u0483-\u0487\u0591-\u05bd\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05c7\u0610-\u061a\u064b-\u065f\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u07fd\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u08d3-\u08e1\u08e3-\u0902\u093a\u093c\u0941-\u0948\u094d\u0951-\u0957\u0962-\u0963\u0981\u09bc\u09c1-\u09c4\u09cd\u09e2-\u09e3\u09fe\u0a01-\u0a02\u0a3c\u0a41-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a51\u0a70-\u0a71\u0a75\u0a81-\u0a82\u0abc\u0ac1-\u0ac5\u0ac7-\u0ac8\u0acd\u0ae2-\u0ae3\u0afa-\u0aff\u0b01\u0b3c\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b62-\u0b63\u0b82\u0bc0\u0bcd\u0c00\u0c04\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c62-\u0c63\u0c81\u0cbc\u0cbf\u0cc6\u0ccc-\u0ccd\u0ce2-\u0ce3\u0d00-\u0d01\u0d3b-\u0d3c\u0d41-\u0d44\u0d4d\u0d62-\u0d63\u0dca\u0dd2-\u0dd4\u0dd6\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb-\u0ebc\u0ec8-\u0ecd\u0f18-\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86-\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039-\u103a\u103d-\u103e\u1058-\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085-\u1086\u108d\u109d\u135d-\u135f\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17b4-\u17b5\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u1885-\u1886\u18a9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193b\u1a17-\u1a18\u1a1b\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1ab0-\u1abd\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80-\u1b81\u1ba2-\u1ba5\u1ba8-\u1ba9\u1bab-\u1bad\u1be6\u1be8-\u1be9\u1bed\u1bef-\u1bf1\u1c2c-\u1c33\u1c36-\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1cf4\u1cf8-\u1cf9\u1dc0-\u1df9\u1dfb-\u1dff\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302d\u3099-\u309a\ua66f\ua674-\ua67d\ua69e-\ua69f\ua6f0-\ua6f1\ua802\ua806\ua80b\ua825-\ua826\ua8c4-\ua8c5\ua8e0-\ua8f1\ua8ff\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\ua9e5\uaa29-\uaa2e\uaa31-\uaa32\uaa35-\uaa36\uaa43\uaa4c\uaa7c\uaab0\uaab2-\uaab4\uaab7-\uaab8\uaabe-\uaabf\uaac1\uaaec-\uaaed\uaaf6\uabe5\uabe8\uabed\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\U000101fd\U000102e0\U00010376-\U0001037a\U00010a01-\U00010a03\U00010a05-\U00010a06\U00010a0c-\U00010a0f\U00010a38-\U00010a3a\U00010a3f\U00010ae5-\U00010ae6\U00010d24-\U00010d27\U00010f46-\U00010f50\U00011001\U00011038-\U00011046\U0001107f-\U00011081\U000110b3-\U000110b6\U000110b9-\U000110ba\U00011100-\U00011102\U00011127-\U0001112b\U0001112d-\U00011134\U00011173\U00011180-\U00011181\U000111b6-\U000111be\U000111c9-\U000111cc\U0001122f-\U00011231\U00011234\U00011236-\U00011237\U0001123e\U000112df\U000112e3-\U000112ea\U00011300-\U00011301\U0001133b-\U0001133c\U00011340\U00011366-\U0001136c\U00011370-\U00011374\U00011438-\U0001143f\U00011442-\U00011444\U00011446\U0001145e\U000114b3-\U000114b8\U000114ba\U000114bf-\U000114c0\U000114c2-\U000114c3\U000115b2-\U000115b5\U000115bc-\U000115bd\U000115bf-\U000115c0\U000115dc-\U000115dd\U00011633-\U0001163a\U0001163d\U0001163f-\U00011640\U000116ab\U000116ad\U000116b0-\U000116b5\U000116b7\U0001171d-\U0001171f\U00011722-\U00011725\U00011727-\U0001172b\U0001182f-\U00011837\U00011839-\U0001183a\U00011a01-\U00011a0a\U00011a33-\U00011a38\U00011a3b-\U00011a3e\U00011a47\U00011a51-\U00011a56\U00011a59-\U00011a5b\U00011a8a-\U00011a96\U00011a98-\U00011a99\U00011c30-\U00011c36\U00011c38-\U00011c3d\U00011c3f\U00011c92-\U00011ca7\U00011caa-\U00011cb0\U00011cb2-\U00011cb3\U00011cb5-\U00011cb6\U00011d31-\U00011d36\U00011d3a\U00011d3c-\U00011d3d\U00011d3f-\U00011d45\U00011d47\U00011d90-\U00011d91\U00011d95\U00011d97\U00011ef3-\U00011ef4\U00016af0-\U00016af4\U00016b30-\U00016b36\U00016f8f-\U00016f92\U0001bc9d-\U0001bc9e\U0001d167-\U0001d169\U0001d17b-\U0001d182\U0001d185-\U0001d18b\U0001d1aa-\U0001d1ad\U0001d242-\U0001d244\U0001da00-\U0001da36\U0001da3b-\U0001da6c\U0001da75\U0001da84\U0001da9b-\U0001da9f\U0001daa1-\U0001daaf\U0001e000-\U0001e006\U0001e008-\U0001e018\U0001e01b-\U0001e021\U0001e023-\U0001e024\U0001e026-\U0001e02a\U0001e8d0-\U0001e8d6\U0001e944-\U0001e94a\U000e0100-\U000e01ef'
+
+Nd = '0-9\u0660-\u0669\u06f0-\u06f9\u07c0-\u07c9\u0966-\u096f\u09e6-\u09ef\u0a66-\u0a6f\u0ae6-\u0aef\u0b66-\u0b6f\u0be6-\u0bef\u0c66-\u0c6f\u0ce6-\u0cef\u0d66-\u0d6f\u0de6-\u0def\u0e50-\u0e59\u0ed0-\u0ed9\u0f20-\u0f29\u1040-\u1049\u1090-\u1099\u17e0-\u17e9\u1810-\u1819\u1946-\u194f\u19d0-\u19d9\u1a80-\u1a89\u1a90-\u1a99\u1b50-\u1b59\u1bb0-\u1bb9\u1c40-\u1c49\u1c50-\u1c59\ua620-\ua629\ua8d0-\ua8d9\ua900-\ua909\ua9d0-\ua9d9\ua9f0-\ua9f9\uaa50-\uaa59\uabf0-\uabf9\uff10-\uff19\U000104a0-\U000104a9\U00010d30-\U00010d39\U00011066-\U0001106f\U000110f0-\U000110f9\U00011136-\U0001113f\U000111d0-\U000111d9\U000112f0-\U000112f9\U00011450-\U00011459\U000114d0-\U000114d9\U00011650-\U00011659\U000116c0-\U000116c9\U00011730-\U00011739\U000118e0-\U000118e9\U00011c50-\U00011c59\U00011d50-\U00011d59\U00011da0-\U00011da9\U00016a60-\U00016a69\U00016b50-\U00016b59\U0001d7ce-\U0001d7ff\U0001e950-\U0001e959'
+
+Nl = '\u16ee-\u16f0\u2160-\u2182\u2185-\u2188\u3007\u3021-\u3029\u3038-\u303a\ua6e6-\ua6ef\U00010140-\U00010174\U00010341\U0001034a\U000103d1-\U000103d5\U00012400-\U0001246e'
+
+No = '\xb2-\xb3\xb9\xbc-\xbe\u09f4-\u09f9\u0b72-\u0b77\u0bf0-\u0bf2\u0c78-\u0c7e\u0d58-\u0d5e\u0d70-\u0d78\u0f2a-\u0f33\u1369-\u137c\u17f0-\u17f9\u19da\u2070\u2074-\u2079\u2080-\u2089\u2150-\u215f\u2189\u2460-\u249b\u24ea-\u24ff\u2776-\u2793\u2cfd\u3192-\u3195\u3220-\u3229\u3248-\u324f\u3251-\u325f\u3280-\u3289\u32b1-\u32bf\ua830-\ua835\U00010107-\U00010133\U00010175-\U00010178\U0001018a-\U0001018b\U000102e1-\U000102fb\U00010320-\U00010323\U00010858-\U0001085f\U00010879-\U0001087f\U000108a7-\U000108af\U000108fb-\U000108ff\U00010916-\U0001091b\U000109bc-\U000109bd\U000109c0-\U000109cf\U000109d2-\U000109ff\U00010a40-\U00010a48\U00010a7d-\U00010a7e\U00010a9d-\U00010a9f\U00010aeb-\U00010aef\U00010b58-\U00010b5f\U00010b78-\U00010b7f\U00010ba9-\U00010baf\U00010cfa-\U00010cff\U00010e60-\U00010e7e\U00010f1d-\U00010f26\U00010f51-\U00010f54\U00011052-\U00011065\U000111e1-\U000111f4\U0001173a-\U0001173b\U000118ea-\U000118f2\U00011c5a-\U00011c6c\U00016b5b-\U00016b61\U00016e80-\U00016e96\U0001d2e0-\U0001d2f3\U0001d360-\U0001d378\U0001e8c7-\U0001e8cf\U0001ec71-\U0001ecab\U0001ecad-\U0001ecaf\U0001ecb1-\U0001ecb4\U0001f100-\U0001f10c'
+
+Pc = '_\u203f-\u2040\u2054\ufe33-\ufe34\ufe4d-\ufe4f\uff3f'
+
+Pd = '\\-\u058a\u05be\u1400\u1806\u2010-\u2015\u2e17\u2e1a\u2e3a-\u2e3b\u2e40\u301c\u3030\u30a0\ufe31-\ufe32\ufe58\ufe63\uff0d'
+
+Pe = ')\\]}\u0f3b\u0f3d\u169c\u2046\u207e\u208e\u2309\u230b\u232a\u2769\u276b\u276d\u276f\u2771\u2773\u2775\u27c6\u27e7\u27e9\u27eb\u27ed\u27ef\u2984\u2986\u2988\u298a\u298c\u298e\u2990\u2992\u2994\u2996\u2998\u29d9\u29db\u29fd\u2e23\u2e25\u2e27\u2e29\u3009\u300b\u300d\u300f\u3011\u3015\u3017\u3019\u301b\u301e-\u301f\ufd3e\ufe18\ufe36\ufe38\ufe3a\ufe3c\ufe3e\ufe40\ufe42\ufe44\ufe48\ufe5a\ufe5c\ufe5e\uff09\uff3d\uff5d\uff60\uff63'
+
+Pf = '\xbb\u2019\u201d\u203a\u2e03\u2e05\u2e0a\u2e0d\u2e1d\u2e21'
+
+Pi = '\xab\u2018\u201b-\u201c\u201f\u2039\u2e02\u2e04\u2e09\u2e0c\u2e1c\u2e20'
+
+Po = "!-#%-'*,.-/:-;?-@\\\\\xa1\xa7\xb6-\xb7\xbf\u037e\u0387\u055a-\u055f\u0589\u05c0\u05c3\u05c6\u05f3-\u05f4\u0609-\u060a\u060c-\u060d\u061b\u061e-\u061f\u066a-\u066d\u06d4\u0700-\u070d\u07f7-\u07f9\u0830-\u083e\u085e\u0964-\u0965\u0970\u09fd\u0a76\u0af0\u0c84\u0df4\u0e4f\u0e5a-\u0e5b\u0f04-\u0f12\u0f14\u0f85\u0fd0-\u0fd4\u0fd9-\u0fda\u104a-\u104f\u10fb\u1360-\u1368\u166d-\u166e\u16eb-\u16ed\u1735-\u1736\u17d4-\u17d6\u17d8-\u17da\u1800-\u1805\u1807-\u180a\u1944-\u1945\u1a1e-\u1a1f\u1aa0-\u1aa6\u1aa8-\u1aad\u1b5a-\u1b60\u1bfc-\u1bff\u1c3b-\u1c3f\u1c7e-\u1c7f\u1cc0-\u1cc7\u1cd3\u2016-\u2017\u2020-\u2027\u2030-\u2038\u203b-\u203e\u2041-\u2043\u2047-\u2051\u2053\u2055-\u205e\u2cf9-\u2cfc\u2cfe-\u2cff\u2d70\u2e00-\u2e01\u2e06-\u2e08\u2e0b\u2e0e-\u2e16\u2e18-\u2e19\u2e1b\u2e1e-\u2e1f\u2e2a-\u2e2e\u2e30-\u2e39\u2e3c-\u2e3f\u2e41\u2e43-\u2e4e\u3001-\u3003\u303d\u30fb\ua4fe-\ua4ff\ua60d-\ua60f\ua673\ua67e\ua6f2-\ua6f7\ua874-\ua877\ua8ce-\ua8cf\ua8f8-\ua8fa\ua8fc\ua92e-\ua92f\ua95f\ua9c1-\ua9cd\ua9de-\ua9df\uaa5c-\uaa5f\uaade-\uaadf\uaaf0-\uaaf1\uabeb\ufe10-\ufe16\ufe19\ufe30\ufe45-\ufe46\ufe49-\ufe4c\ufe50-\ufe52\ufe54-\ufe57\ufe5f-\ufe61\ufe68\ufe6a-\ufe6b\uff01-\uff03\uff05-\uff07\uff0a\uff0c\uff0e-\uff0f\uff1a-\uff1b\uff1f-\uff20\uff3c\uff61\uff64-\uff65\U00010100-\U00010102\U0001039f\U000103d0\U0001056f\U00010857\U0001091f\U0001093f\U00010a50-\U00010a58\U00010a7f\U00010af0-\U00010af6\U00010b39-\U00010b3f\U00010b99-\U00010b9c\U00010f55-\U00010f59\U00011047-\U0001104d\U000110bb-\U000110bc\U000110be-\U000110c1\U00011140-\U00011143\U00011174-\U00011175\U000111c5-\U000111c8\U000111cd\U000111db\U000111dd-\U000111df\U00011238-\U0001123d\U000112a9\U0001144b-\U0001144f\U0001145b\U0001145d\U000114c6\U000115c1-\U000115d7\U00011641-\U00011643\U00011660-\U0001166c\U0001173c-\U0001173e\U0001183b\U00011a3f-\U00011a46\U00011a9a-\U00011a9c\U00011a9e-\U00011aa2\U00011c41-\U00011c45\U00011c70-\U00011c71\U00011ef7-\U00011ef8\U00012470-\U00012474\U00016a6e-\U00016a6f\U00016af5\U00016b37-\U00016b3b\U00016b44\U00016e97-\U00016e9a\U0001bc9f\U0001da87-\U0001da8b\U0001e95e-\U0001e95f"
+
+Ps = '(\\[{\u0f3a\u0f3c\u169b\u201a\u201e\u2045\u207d\u208d\u2308\u230a\u2329\u2768\u276a\u276c\u276e\u2770\u2772\u2774\u27c5\u27e6\u27e8\u27ea\u27ec\u27ee\u2983\u2985\u2987\u2989\u298b\u298d\u298f\u2991\u2993\u2995\u2997\u29d8\u29da\u29fc\u2e22\u2e24\u2e26\u2e28\u2e42\u3008\u300a\u300c\u300e\u3010\u3014\u3016\u3018\u301a\u301d\ufd3f\ufe17\ufe35\ufe37\ufe39\ufe3b\ufe3d\ufe3f\ufe41\ufe43\ufe47\ufe59\ufe5b\ufe5d\uff08\uff3b\uff5b\uff5f\uff62'
+
+Sc = '$\xa2-\xa5\u058f\u060b\u07fe-\u07ff\u09f2-\u09f3\u09fb\u0af1\u0bf9\u0e3f\u17db\u20a0-\u20bf\ua838\ufdfc\ufe69\uff04\uffe0-\uffe1\uffe5-\uffe6\U0001ecb0'
+
+Sk = '\\^`\xa8\xaf\xb4\xb8\u02c2-\u02c5\u02d2-\u02df\u02e5-\u02eb\u02ed\u02ef-\u02ff\u0375\u0384-\u0385\u1fbd\u1fbf-\u1fc1\u1fcd-\u1fcf\u1fdd-\u1fdf\u1fed-\u1fef\u1ffd-\u1ffe\u309b-\u309c\ua700-\ua716\ua720-\ua721\ua789-\ua78a\uab5b\ufbb2-\ufbc1\uff3e\uff40\uffe3\U0001f3fb-\U0001f3ff'
+
+Sm = '+<->|~\xac\xb1\xd7\xf7\u03f6\u0606-\u0608\u2044\u2052\u207a-\u207c\u208a-\u208c\u2118\u2140-\u2144\u214b\u2190-\u2194\u219a-\u219b\u21a0\u21a3\u21a6\u21ae\u21ce-\u21cf\u21d2\u21d4\u21f4-\u22ff\u2320-\u2321\u237c\u239b-\u23b3\u23dc-\u23e1\u25b7\u25c1\u25f8-\u25ff\u266f\u27c0-\u27c4\u27c7-\u27e5\u27f0-\u27ff\u2900-\u2982\u2999-\u29d7\u29dc-\u29fb\u29fe-\u2aff\u2b30-\u2b44\u2b47-\u2b4c\ufb29\ufe62\ufe64-\ufe66\uff0b\uff1c-\uff1e\uff5c\uff5e\uffe2\uffe9-\uffec\U0001d6c1\U0001d6db\U0001d6fb\U0001d715\U0001d735\U0001d74f\U0001d76f\U0001d789\U0001d7a9\U0001d7c3\U0001eef0-\U0001eef1'
+
+So = '\xa6\xa9\xae\xb0\u0482\u058d-\u058e\u060e-\u060f\u06de\u06e9\u06fd-\u06fe\u07f6\u09fa\u0b70\u0bf3-\u0bf8\u0bfa\u0c7f\u0d4f\u0d79\u0f01-\u0f03\u0f13\u0f15-\u0f17\u0f1a-\u0f1f\u0f34\u0f36\u0f38\u0fbe-\u0fc5\u0fc7-\u0fcc\u0fce-\u0fcf\u0fd5-\u0fd8\u109e-\u109f\u1390-\u1399\u1940\u19de-\u19ff\u1b61-\u1b6a\u1b74-\u1b7c\u2100-\u2101\u2103-\u2106\u2108-\u2109\u2114\u2116-\u2117\u211e-\u2123\u2125\u2127\u2129\u212e\u213a-\u213b\u214a\u214c-\u214d\u214f\u218a-\u218b\u2195-\u2199\u219c-\u219f\u21a1-\u21a2\u21a4-\u21a5\u21a7-\u21ad\u21af-\u21cd\u21d0-\u21d1\u21d3\u21d5-\u21f3\u2300-\u2307\u230c-\u231f\u2322-\u2328\u232b-\u237b\u237d-\u239a\u23b4-\u23db\u23e2-\u2426\u2440-\u244a\u249c-\u24e9\u2500-\u25b6\u25b8-\u25c0\u25c2-\u25f7\u2600-\u266e\u2670-\u2767\u2794-\u27bf\u2800-\u28ff\u2b00-\u2b2f\u2b45-\u2b46\u2b4d-\u2b73\u2b76-\u2b95\u2b98-\u2bc8\u2bca-\u2bfe\u2ce5-\u2cea\u2e80-\u2e99\u2e9b-\u2ef3\u2f00-\u2fd5\u2ff0-\u2ffb\u3004\u3012-\u3013\u3020\u3036-\u3037\u303e-\u303f\u3190-\u3191\u3196-\u319f\u31c0-\u31e3\u3200-\u321e\u322a-\u3247\u3250\u3260-\u327f\u328a-\u32b0\u32c0-\u32fe\u3300-\u33ff\u4dc0-\u4dff\ua490-\ua4c6\ua828-\ua82b\ua836-\ua837\ua839\uaa77-\uaa79\ufdfd\uffe4\uffe8\uffed-\uffee\ufffc-\ufffd\U00010137-\U0001013f\U00010179-\U00010189\U0001018c-\U0001018e\U00010190-\U0001019b\U000101a0\U000101d0-\U000101fc\U00010877-\U00010878\U00010ac8\U0001173f\U00016b3c-\U00016b3f\U00016b45\U0001bc9c\U0001d000-\U0001d0f5\U0001d100-\U0001d126\U0001d129-\U0001d164\U0001d16a-\U0001d16c\U0001d183-\U0001d184\U0001d18c-\U0001d1a9\U0001d1ae-\U0001d1e8\U0001d200-\U0001d241\U0001d245\U0001d300-\U0001d356\U0001d800-\U0001d9ff\U0001da37-\U0001da3a\U0001da6d-\U0001da74\U0001da76-\U0001da83\U0001da85-\U0001da86\U0001ecac\U0001f000-\U0001f02b\U0001f030-\U0001f093\U0001f0a0-\U0001f0ae\U0001f0b1-\U0001f0bf\U0001f0c1-\U0001f0cf\U0001f0d1-\U0001f0f5\U0001f110-\U0001f16b\U0001f170-\U0001f1ac\U0001f1e6-\U0001f202\U0001f210-\U0001f23b\U0001f240-\U0001f248\U0001f250-\U0001f251\U0001f260-\U0001f265\U0001f300-\U0001f3fa\U0001f400-\U0001f6d4\U0001f6e0-\U0001f6ec\U0001f6f0-\U0001f6f9\U0001f700-\U0001f773\U0001f780-\U0001f7d8\U0001f800-\U0001f80b\U0001f810-\U0001f847\U0001f850-\U0001f859\U0001f860-\U0001f887\U0001f890-\U0001f8ad\U0001f900-\U0001f90b\U0001f910-\U0001f93e\U0001f940-\U0001f970\U0001f973-\U0001f976\U0001f97a\U0001f97c-\U0001f9a2\U0001f9b0-\U0001f9b9\U0001f9c0-\U0001f9c2\U0001f9d0-\U0001f9ff\U0001fa60-\U0001fa6d'
+
+Zl = '\u2028'
+
+Zp = '\u2029'
+
+Zs = ' \xa0\u1680\u2000-\u200a\u202f\u205f\u3000'
+
+xid_continue = '0-9A-Z_a-z\xaa\xb5\xb7\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0300-\u0374\u0376-\u0377\u037b-\u037d\u037f\u0386-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u0483-\u0487\u048a-\u052f\u0531-\u0556\u0559\u0560-\u0588\u0591-\u05bd\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05c7\u05d0-\u05ea\u05ef-\u05f2\u0610-\u061a\u0620-\u0669\u066e-\u06d3\u06d5-\u06dc\u06df-\u06e8\u06ea-\u06fc\u06ff\u0710-\u074a\u074d-\u07b1\u07c0-\u07f5\u07fa\u07fd\u0800-\u082d\u0840-\u085b\u0860-\u086a\u08a0-\u08b4\u08b6-\u08bd\u08d3-\u08e1\u08e3-\u0963\u0966-\u096f\u0971-\u0983\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bc-\u09c4\u09c7-\u09c8\u09cb-\u09ce\u09d7\u09dc-\u09dd\u09df-\u09e3\u09e6-\u09f1\u09fc\u09fe\u0a01-\u0a03\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a3c\u0a3e-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a51\u0a59-\u0a5c\u0a5e\u0a66-\u0a75\u0a81-\u0a83\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abc-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ad0\u0ae0-\u0ae3\u0ae6-\u0aef\u0af9-\u0aff\u0b01-\u0b03\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3c-\u0b44\u0b47-\u0b48\u0b4b-\u0b4d\u0b56-\u0b57\u0b5c-\u0b5d\u0b5f-\u0b63\u0b66-\u0b6f\u0b71\u0b82-\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd0\u0bd7\u0be6-\u0bef\u0c00-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c58-\u0c5a\u0c60-\u0c63\u0c66-\u0c6f\u0c80-\u0c83\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbc-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5-\u0cd6\u0cde\u0ce0-\u0ce3\u0ce6-\u0cef\u0cf1-\u0cf2\u0d00-\u0d03\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d44\u0d46-\u0d48\u0d4a-\u0d4e\u0d54-\u0d57\u0d5f-\u0d63\u0d66-\u0d6f\u0d7a-\u0d7f\u0d82-\u0d83\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0de6-\u0def\u0df2-\u0df3\u0e01-\u0e3a\u0e40-\u0e4e\u0e50-\u0e59\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eb9\u0ebb-\u0ebd\u0ec0-\u0ec4\u0ec6\u0ec8-\u0ecd\u0ed0-\u0ed9\u0edc-\u0edf\u0f00\u0f18-\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e-\u0f47\u0f49-\u0f6c\u0f71-\u0f84\u0f86-\u0f97\u0f99-\u0fbc\u0fc6\u1000-\u1049\u1050-\u109d\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u135d-\u135f\u1369-\u1371\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u170c\u170e-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176c\u176e-\u1770\u1772-\u1773\u1780-\u17d3\u17d7\u17dc-\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u1820-\u1878\u1880-\u18aa\u18b0-\u18f5\u1900-\u191e\u1920-\u192b\u1930-\u193b\u1946-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u19d0-\u19da\u1a00-\u1a1b\u1a20-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1aa7\u1ab0-\u1abd\u1b00-\u1b4b\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1bf3\u1c00-\u1c37\u1c40-\u1c49\u1c4d-\u1c7d\u1c80-\u1c88\u1c90-\u1cba\u1cbd-\u1cbf\u1cd0-\u1cd2\u1cd4-\u1cf9\u1d00-\u1df9\u1dfb-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u203f-\u2040\u2054\u2071\u207f\u2090-\u209c\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2102\u2107\u210a-\u2113\u2115\u2118-\u211d\u2124\u2126\u2128\u212a-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d7f-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2de0-\u2dff\u3005-\u3007\u3021-\u302f\u3031-\u3035\u3038-\u303c\u3041-\u3096\u3099-\u309a\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312f\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fef\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua62b\ua640-\ua66f\ua674-\ua67d\ua67f-\ua6f1\ua717-\ua71f\ua722-\ua788\ua78b-\ua7b9\ua7f7-\ua827\ua840-\ua873\ua880-\ua8c5\ua8d0-\ua8d9\ua8e0-\ua8f7\ua8fb\ua8fd-\ua92d\ua930-\ua953\ua960-\ua97c\ua980-\ua9c0\ua9cf-\ua9d9\ua9e0-\ua9fe\uaa00-\uaa36\uaa40-\uaa4d\uaa50-\uaa59\uaa60-\uaa76\uaa7a-\uaac2\uaadb-\uaadd\uaae0-\uaaef\uaaf2-\uaaf6\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab65\uab70-\uabea\uabec-\uabed\uabf0-\uabf9\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufc5d\ufc64-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdf9\ufe00-\ufe0f\ufe20-\ufe2f\ufe33-\ufe34\ufe4d-\ufe4f\ufe71\ufe73\ufe77\ufe79\ufe7b\ufe7d\ufe7f-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc\U00010000-\U0001000b\U0001000d-\U00010026\U00010028-\U0001003a\U0001003c-\U0001003d\U0001003f-\U0001004d\U00010050-\U0001005d\U00010080-\U000100fa\U00010140-\U00010174\U000101fd\U00010280-\U0001029c\U000102a0-\U000102d0\U000102e0\U00010300-\U0001031f\U0001032d-\U0001034a\U00010350-\U0001037a\U00010380-\U0001039d\U000103a0-\U000103c3\U000103c8-\U000103cf\U000103d1-\U000103d5\U00010400-\U0001049d\U000104a0-\U000104a9\U000104b0-\U000104d3\U000104d8-\U000104fb\U00010500-\U00010527\U00010530-\U00010563\U00010600-\U00010736\U00010740-\U00010755\U00010760-\U00010767\U00010800-\U00010805\U00010808\U0001080a-\U00010835\U00010837-\U00010838\U0001083c\U0001083f-\U00010855\U00010860-\U00010876\U00010880-\U0001089e\U000108e0-\U000108f2\U000108f4-\U000108f5\U00010900-\U00010915\U00010920-\U00010939\U00010980-\U000109b7\U000109be-\U000109bf\U00010a00-\U00010a03\U00010a05-\U00010a06\U00010a0c-\U00010a13\U00010a15-\U00010a17\U00010a19-\U00010a35\U00010a38-\U00010a3a\U00010a3f\U00010a60-\U00010a7c\U00010a80-\U00010a9c\U00010ac0-\U00010ac7\U00010ac9-\U00010ae6\U00010b00-\U00010b35\U00010b40-\U00010b55\U00010b60-\U00010b72\U00010b80-\U00010b91\U00010c00-\U00010c48\U00010c80-\U00010cb2\U00010cc0-\U00010cf2\U00010d00-\U00010d27\U00010d30-\U00010d39\U00010f00-\U00010f1c\U00010f27\U00010f30-\U00010f50\U00011000-\U00011046\U00011066-\U0001106f\U0001107f-\U000110ba\U000110d0-\U000110e8\U000110f0-\U000110f9\U00011100-\U00011134\U00011136-\U0001113f\U00011144-\U00011146\U00011150-\U00011173\U00011176\U00011180-\U000111c4\U000111c9-\U000111cc\U000111d0-\U000111da\U000111dc\U00011200-\U00011211\U00011213-\U00011237\U0001123e\U00011280-\U00011286\U00011288\U0001128a-\U0001128d\U0001128f-\U0001129d\U0001129f-\U000112a8\U000112b0-\U000112ea\U000112f0-\U000112f9\U00011300-\U00011303\U00011305-\U0001130c\U0001130f-\U00011310\U00011313-\U00011328\U0001132a-\U00011330\U00011332-\U00011333\U00011335-\U00011339\U0001133b-\U00011344\U00011347-\U00011348\U0001134b-\U0001134d\U00011350\U00011357\U0001135d-\U00011363\U00011366-\U0001136c\U00011370-\U00011374\U00011400-\U0001144a\U00011450-\U00011459\U0001145e\U00011480-\U000114c5\U000114c7\U000114d0-\U000114d9\U00011580-\U000115b5\U000115b8-\U000115c0\U000115d8-\U000115dd\U00011600-\U00011640\U00011644\U00011650-\U00011659\U00011680-\U000116b7\U000116c0-\U000116c9\U00011700-\U0001171a\U0001171d-\U0001172b\U00011730-\U00011739\U00011800-\U0001183a\U000118a0-\U000118e9\U000118ff\U00011a00-\U00011a3e\U00011a47\U00011a50-\U00011a83\U00011a86-\U00011a99\U00011a9d\U00011ac0-\U00011af8\U00011c00-\U00011c08\U00011c0a-\U00011c36\U00011c38-\U00011c40\U00011c50-\U00011c59\U00011c72-\U00011c8f\U00011c92-\U00011ca7\U00011ca9-\U00011cb6\U00011d00-\U00011d06\U00011d08-\U00011d09\U00011d0b-\U00011d36\U00011d3a\U00011d3c-\U00011d3d\U00011d3f-\U00011d47\U00011d50-\U00011d59\U00011d60-\U00011d65\U00011d67-\U00011d68\U00011d6a-\U00011d8e\U00011d90-\U00011d91\U00011d93-\U00011d98\U00011da0-\U00011da9\U00011ee0-\U00011ef6\U00012000-\U00012399\U00012400-\U0001246e\U00012480-\U00012543\U00013000-\U0001342e\U00014400-\U00014646\U00016800-\U00016a38\U00016a40-\U00016a5e\U00016a60-\U00016a69\U00016ad0-\U00016aed\U00016af0-\U00016af4\U00016b00-\U00016b36\U00016b40-\U00016b43\U00016b50-\U00016b59\U00016b63-\U00016b77\U00016b7d-\U00016b8f\U00016e40-\U00016e7f\U00016f00-\U00016f44\U00016f50-\U00016f7e\U00016f8f-\U00016f9f\U00016fe0-\U00016fe1\U00017000-\U000187f1\U00018800-\U00018af2\U0001b000-\U0001b11e\U0001b170-\U0001b2fb\U0001bc00-\U0001bc6a\U0001bc70-\U0001bc7c\U0001bc80-\U0001bc88\U0001bc90-\U0001bc99\U0001bc9d-\U0001bc9e\U0001d165-\U0001d169\U0001d16d-\U0001d172\U0001d17b-\U0001d182\U0001d185-\U0001d18b\U0001d1aa-\U0001d1ad\U0001d242-\U0001d244\U0001d400-\U0001d454\U0001d456-\U0001d49c\U0001d49e-\U0001d49f\U0001d4a2\U0001d4a5-\U0001d4a6\U0001d4a9-\U0001d4ac\U0001d4ae-\U0001d4b9\U0001d4bb\U0001d4bd-\U0001d4c3\U0001d4c5-\U0001d505\U0001d507-\U0001d50a\U0001d50d-\U0001d514\U0001d516-\U0001d51c\U0001d51e-\U0001d539\U0001d53b-\U0001d53e\U0001d540-\U0001d544\U0001d546\U0001d54a-\U0001d550\U0001d552-\U0001d6a5\U0001d6a8-\U0001d6c0\U0001d6c2-\U0001d6da\U0001d6dc-\U0001d6fa\U0001d6fc-\U0001d714\U0001d716-\U0001d734\U0001d736-\U0001d74e\U0001d750-\U0001d76e\U0001d770-\U0001d788\U0001d78a-\U0001d7a8\U0001d7aa-\U0001d7c2\U0001d7c4-\U0001d7cb\U0001d7ce-\U0001d7ff\U0001da00-\U0001da36\U0001da3b-\U0001da6c\U0001da75\U0001da84\U0001da9b-\U0001da9f\U0001daa1-\U0001daaf\U0001e000-\U0001e006\U0001e008-\U0001e018\U0001e01b-\U0001e021\U0001e023-\U0001e024\U0001e026-\U0001e02a\U0001e800-\U0001e8c4\U0001e8d0-\U0001e8d6\U0001e900-\U0001e94a\U0001e950-\U0001e959\U0001ee00-\U0001ee03\U0001ee05-\U0001ee1f\U0001ee21-\U0001ee22\U0001ee24\U0001ee27\U0001ee29-\U0001ee32\U0001ee34-\U0001ee37\U0001ee39\U0001ee3b\U0001ee42\U0001ee47\U0001ee49\U0001ee4b\U0001ee4d-\U0001ee4f\U0001ee51-\U0001ee52\U0001ee54\U0001ee57\U0001ee59\U0001ee5b\U0001ee5d\U0001ee5f\U0001ee61-\U0001ee62\U0001ee64\U0001ee67-\U0001ee6a\U0001ee6c-\U0001ee72\U0001ee74-\U0001ee77\U0001ee79-\U0001ee7c\U0001ee7e\U0001ee80-\U0001ee89\U0001ee8b-\U0001ee9b\U0001eea1-\U0001eea3\U0001eea5-\U0001eea9\U0001eeab-\U0001eebb\U00020000-\U0002a6d6\U0002a700-\U0002b734\U0002b740-\U0002b81d\U0002b820-\U0002cea1\U0002ceb0-\U0002ebe0\U0002f800-\U0002fa1d\U000e0100-\U000e01ef'
+
+xid_start = 'A-Z_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376-\u0377\u037b-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u052f\u0531-\u0556\u0559\u0560-\u0588\u05d0-\u05ea\u05ef-\u05f2\u0620-\u064a\u066e-\u066f\u0671-\u06d3\u06d5\u06e5-\u06e6\u06ee-\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4-\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0860-\u086a\u08a0-\u08b4\u08b6-\u08bd\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc-\u09dd\u09df-\u09e1\u09f0-\u09f1\u09fc\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0-\u0ae1\u0af9\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c60-\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0-\u0ce1\u0cf1-\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e40-\u0e46\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eb0\u0eb2\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065-\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1878\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae-\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1c80-\u1c88\u1c90-\u1cba\u1cbd-\u1cbf\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5-\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2118-\u211d\u2124\u2126\u2128\u212a-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2-\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312f\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fef\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a-\ua62b\ua640-\ua66e\ua67f-\ua69d\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua7b9\ua7f7-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd-\ua8fe\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\ua9e0-\ua9e4\ua9e6-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5-\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab65\uab70-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufc5d\ufc64-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdf9\ufe71\ufe73\ufe77\ufe79\ufe7b\ufe7d\ufe7f-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uff9d\uffa0-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc\U00010000-\U0001000b\U0001000d-\U00010026\U00010028-\U0001003a\U0001003c-\U0001003d\U0001003f-\U0001004d\U00010050-\U0001005d\U00010080-\U000100fa\U00010140-\U00010174\U00010280-\U0001029c\U000102a0-\U000102d0\U00010300-\U0001031f\U0001032d-\U0001034a\U00010350-\U00010375\U00010380-\U0001039d\U000103a0-\U000103c3\U000103c8-\U000103cf\U000103d1-\U000103d5\U00010400-\U0001049d\U000104b0-\U000104d3\U000104d8-\U000104fb\U00010500-\U00010527\U00010530-\U00010563\U00010600-\U00010736\U00010740-\U00010755\U00010760-\U00010767\U00010800-\U00010805\U00010808\U0001080a-\U00010835\U00010837-\U00010838\U0001083c\U0001083f-\U00010855\U00010860-\U00010876\U00010880-\U0001089e\U000108e0-\U000108f2\U000108f4-\U000108f5\U00010900-\U00010915\U00010920-\U00010939\U00010980-\U000109b7\U000109be-\U000109bf\U00010a00\U00010a10-\U00010a13\U00010a15-\U00010a17\U00010a19-\U00010a35\U00010a60-\U00010a7c\U00010a80-\U00010a9c\U00010ac0-\U00010ac7\U00010ac9-\U00010ae4\U00010b00-\U00010b35\U00010b40-\U00010b55\U00010b60-\U00010b72\U00010b80-\U00010b91\U00010c00-\U00010c48\U00010c80-\U00010cb2\U00010cc0-\U00010cf2\U00010d00-\U00010d23\U00010f00-\U00010f1c\U00010f27\U00010f30-\U00010f45\U00011003-\U00011037\U00011083-\U000110af\U000110d0-\U000110e8\U00011103-\U00011126\U00011144\U00011150-\U00011172\U00011176\U00011183-\U000111b2\U000111c1-\U000111c4\U000111da\U000111dc\U00011200-\U00011211\U00011213-\U0001122b\U00011280-\U00011286\U00011288\U0001128a-\U0001128d\U0001128f-\U0001129d\U0001129f-\U000112a8\U000112b0-\U000112de\U00011305-\U0001130c\U0001130f-\U00011310\U00011313-\U00011328\U0001132a-\U00011330\U00011332-\U00011333\U00011335-\U00011339\U0001133d\U00011350\U0001135d-\U00011361\U00011400-\U00011434\U00011447-\U0001144a\U00011480-\U000114af\U000114c4-\U000114c5\U000114c7\U00011580-\U000115ae\U000115d8-\U000115db\U00011600-\U0001162f\U00011644\U00011680-\U000116aa\U00011700-\U0001171a\U00011800-\U0001182b\U000118a0-\U000118df\U000118ff\U00011a00\U00011a0b-\U00011a32\U00011a3a\U00011a50\U00011a5c-\U00011a83\U00011a86-\U00011a89\U00011a9d\U00011ac0-\U00011af8\U00011c00-\U00011c08\U00011c0a-\U00011c2e\U00011c40\U00011c72-\U00011c8f\U00011d00-\U00011d06\U00011d08-\U00011d09\U00011d0b-\U00011d30\U00011d46\U00011d60-\U00011d65\U00011d67-\U00011d68\U00011d6a-\U00011d89\U00011d98\U00011ee0-\U00011ef2\U00012000-\U00012399\U00012400-\U0001246e\U00012480-\U00012543\U00013000-\U0001342e\U00014400-\U00014646\U00016800-\U00016a38\U00016a40-\U00016a5e\U00016ad0-\U00016aed\U00016b00-\U00016b2f\U00016b40-\U00016b43\U00016b63-\U00016b77\U00016b7d-\U00016b8f\U00016e40-\U00016e7f\U00016f00-\U00016f44\U00016f50\U00016f93-\U00016f9f\U00016fe0-\U00016fe1\U00017000-\U000187f1\U00018800-\U00018af2\U0001b000-\U0001b11e\U0001b170-\U0001b2fb\U0001bc00-\U0001bc6a\U0001bc70-\U0001bc7c\U0001bc80-\U0001bc88\U0001bc90-\U0001bc99\U0001d400-\U0001d454\U0001d456-\U0001d49c\U0001d49e-\U0001d49f\U0001d4a2\U0001d4a5-\U0001d4a6\U0001d4a9-\U0001d4ac\U0001d4ae-\U0001d4b9\U0001d4bb\U0001d4bd-\U0001d4c3\U0001d4c5-\U0001d505\U0001d507-\U0001d50a\U0001d50d-\U0001d514\U0001d516-\U0001d51c\U0001d51e-\U0001d539\U0001d53b-\U0001d53e\U0001d540-\U0001d544\U0001d546\U0001d54a-\U0001d550\U0001d552-\U0001d6a5\U0001d6a8-\U0001d6c0\U0001d6c2-\U0001d6da\U0001d6dc-\U0001d6fa\U0001d6fc-\U0001d714\U0001d716-\U0001d734\U0001d736-\U0001d74e\U0001d750-\U0001d76e\U0001d770-\U0001d788\U0001d78a-\U0001d7a8\U0001d7aa-\U0001d7c2\U0001d7c4-\U0001d7cb\U0001e800-\U0001e8c4\U0001e900-\U0001e943\U0001ee00-\U0001ee03\U0001ee05-\U0001ee1f\U0001ee21-\U0001ee22\U0001ee24\U0001ee27\U0001ee29-\U0001ee32\U0001ee34-\U0001ee37\U0001ee39\U0001ee3b\U0001ee42\U0001ee47\U0001ee49\U0001ee4b\U0001ee4d-\U0001ee4f\U0001ee51-\U0001ee52\U0001ee54\U0001ee57\U0001ee59\U0001ee5b\U0001ee5d\U0001ee5f\U0001ee61-\U0001ee62\U0001ee64\U0001ee67-\U0001ee6a\U0001ee6c-\U0001ee72\U0001ee74-\U0001ee77\U0001ee79-\U0001ee7c\U0001ee7e\U0001ee80-\U0001ee89\U0001ee8b-\U0001ee9b\U0001eea1-\U0001eea3\U0001eea5-\U0001eea9\U0001eeab-\U0001eebb\U00020000-\U0002a6d6\U0002a700-\U0002b734\U0002b740-\U0002b81d\U0002b820-\U0002cea1\U0002ceb0-\U0002ebe0\U0002f800-\U0002fa1d'
+
+cats = ['Cc', 'Cf', 'Cn', 'Co', 'Cs', 'Ll', 'Lm', 'Lo', 'Lt', 'Lu', 'Mc', 'Me', 'Mn', 'Nd', 'Nl', 'No', 'Pc', 'Pd', 'Pe', 'Pf', 'Pi', 'Po', 'Ps', 'Sc', 'Sk', 'Sm', 'So', 'Zl', 'Zp', 'Zs']
+
+# Generated from unidata 11.0.0
+
+def combine(*args):
+ return ''.join(globals()[cat] for cat in args)
+
+
+def allexcept(*args):
+ newcats = cats[:]
+ for arg in args:
+ newcats.remove(arg)
+ return ''.join(globals()[cat] for cat in newcats)
+
+
+def _handle_runs(char_list): # pragma: no cover
+ buf = []
+ for c in char_list:
+ if len(c) == 1:
+ if buf and buf[-1][1] == chr(ord(c)-1):
+ buf[-1] = (buf[-1][0], c)
+ else:
+ buf.append((c, c))
+ else:
+ buf.append((c, c))
+ for a, b in buf:
+ if a == b:
+ yield a
+ else:
+ yield '%s-%s' % (a, b)
+
+
+if __name__ == '__main__': # pragma: no cover
+ import unicodedata
+
+ categories = {'xid_start': [], 'xid_continue': []}
+
+ with open(__file__) as fp:
+ content = fp.read()
+
+ header = content[:content.find('Cc =')]
+ footer = content[content.find("def combine("):]
+
+ for code in range(0x110000):
+ c = chr(code)
+ cat = unicodedata.category(c)
+ if ord(c) == 0xdc00:
+ # Hack to avoid combining this combining with the preceding high
+ # surrogate, 0xdbff, when doing a repr.
+ c = '\\' + c
+ elif ord(c) in (0x2d, 0x5b, 0x5c, 0x5d, 0x5e):
+ # Escape regex metachars.
+ c = '\\' + c
+ categories.setdefault(cat, []).append(c)
+ # XID_START and XID_CONTINUE are special categories used for matching
+ # identifiers in Python 3.
+ if c.isidentifier():
+ categories['xid_start'].append(c)
+ if ('a' + c).isidentifier():
+ categories['xid_continue'].append(c)
+
+ with open(__file__, 'w') as fp:
+ fp.write(header)
+
+ for cat in sorted(categories):
+ val = ''.join(_handle_runs(categories[cat]))
+ fp.write('%s = %a\n\n' % (cat, val))
+
+ cats = sorted(categories)
+ cats.remove('xid_start')
+ cats.remove('xid_continue')
+ fp.write('cats = %r\n\n' % cats)
+
+ fp.write('# Generated from unidata %s\n\n' % (unicodedata.unidata_version,))
+
+ fp.write(footer)
diff --git a/src/pip/_vendor/pygments/util.py b/src/pip/_vendor/pygments/util.py
new file mode 100644
index 000000000..8032962dc
--- /dev/null
+++ b/src/pip/_vendor/pygments/util.py
@@ -0,0 +1,308 @@
+"""
+ pygments.util
+ ~~~~~~~~~~~~~
+
+ Utility functions.
+
+ :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS.
+ :license: BSD, see LICENSE for details.
+"""
+
+import re
+from io import TextIOWrapper
+
+
+split_path_re = re.compile(r'[/\\ ]')
+doctype_lookup_re = re.compile(r'''
+ <!DOCTYPE\s+(
+ [a-zA-Z_][a-zA-Z0-9]*
+ (?: \s+ # optional in HTML5
+ [a-zA-Z_][a-zA-Z0-9]*\s+
+ "[^"]*")?
+ )
+ [^>]*>
+''', re.DOTALL | re.MULTILINE | re.VERBOSE)
+tag_re = re.compile(r'<(.+?)(\s.*?)?>.*?</.+?>',
+ re.IGNORECASE | re.DOTALL | re.MULTILINE)
+xml_decl_re = re.compile(r'\s*<\?xml[^>]*\?>', re.I)
+
+
+class ClassNotFound(ValueError):
+ """Raised if one of the lookup functions didn't find a matching class."""
+
+
+class OptionError(Exception):
+ pass
+
+
+def get_choice_opt(options, optname, allowed, default=None, normcase=False):
+ string = options.get(optname, default)
+ if normcase:
+ string = string.lower()
+ if string not in allowed:
+ raise OptionError('Value for option %s must be one of %s' %
+ (optname, ', '.join(map(str, allowed))))
+ return string
+
+
+def get_bool_opt(options, optname, default=None):
+ string = options.get(optname, default)
+ if isinstance(string, bool):
+ return string
+ elif isinstance(string, int):
+ return bool(string)
+ elif not isinstance(string, str):
+ raise OptionError('Invalid type %r for option %s; use '
+ '1/0, yes/no, true/false, on/off' % (
+ string, optname))
+ elif string.lower() in ('1', 'yes', 'true', 'on'):
+ return True
+ elif string.lower() in ('0', 'no', 'false', 'off'):
+ return False
+ else:
+ raise OptionError('Invalid value %r for option %s; use '
+ '1/0, yes/no, true/false, on/off' % (
+ string, optname))
+
+
+def get_int_opt(options, optname, default=None):
+ string = options.get(optname, default)
+ try:
+ return int(string)
+ except TypeError:
+ raise OptionError('Invalid type %r for option %s; you '
+ 'must give an integer value' % (
+ string, optname))
+ except ValueError:
+ raise OptionError('Invalid value %r for option %s; you '
+ 'must give an integer value' % (
+ string, optname))
+
+
+def get_list_opt(options, optname, default=None):
+ val = options.get(optname, default)
+ if isinstance(val, str):
+ return val.split()
+ elif isinstance(val, (list, tuple)):
+ return list(val)
+ else:
+ raise OptionError('Invalid type %r for option %s; you '
+ 'must give a list value' % (
+ val, optname))
+
+
+def docstring_headline(obj):
+ if not obj.__doc__:
+ return ''
+ res = []
+ for line in obj.__doc__.strip().splitlines():
+ if line.strip():
+ res.append(" " + line.strip())
+ else:
+ break
+ return ''.join(res).lstrip()
+
+
+def make_analysator(f):
+ """Return a static text analyser function that returns float values."""
+ def text_analyse(text):
+ try:
+ rv = f(text)
+ except Exception:
+ return 0.0
+ if not rv:
+ return 0.0
+ try:
+ return min(1.0, max(0.0, float(rv)))
+ except (ValueError, TypeError):
+ return 0.0
+ text_analyse.__doc__ = f.__doc__
+ return staticmethod(text_analyse)
+
+
+def shebang_matches(text, regex):
+ r"""Check if the given regular expression matches the last part of the
+ shebang if one exists.
+
+ >>> from pygments.util import shebang_matches
+ >>> shebang_matches('#!/usr/bin/env python', r'python(2\.\d)?')
+ True
+ >>> shebang_matches('#!/usr/bin/python2.4', r'python(2\.\d)?')
+ True
+ >>> shebang_matches('#!/usr/bin/python-ruby', r'python(2\.\d)?')
+ False
+ >>> shebang_matches('#!/usr/bin/python/ruby', r'python(2\.\d)?')
+ False
+ >>> shebang_matches('#!/usr/bin/startsomethingwith python',
+ ... r'python(2\.\d)?')
+ True
+
+ It also checks for common windows executable file extensions::
+
+ >>> shebang_matches('#!C:\\Python2.4\\Python.exe', r'python(2\.\d)?')
+ True
+
+ Parameters (``'-f'`` or ``'--foo'`` are ignored so ``'perl'`` does
+ the same as ``'perl -e'``)
+
+ Note that this method automatically searches the whole string (eg:
+ the regular expression is wrapped in ``'^$'``)
+ """
+ index = text.find('\n')
+ if index >= 0:
+ first_line = text[:index].lower()
+ else:
+ first_line = text.lower()
+ if first_line.startswith('#!'):
+ try:
+ found = [x for x in split_path_re.split(first_line[2:].strip())
+ if x and not x.startswith('-')][-1]
+ except IndexError:
+ return False
+ regex = re.compile(r'^%s(\.(exe|cmd|bat|bin))?$' % regex, re.IGNORECASE)
+ if regex.search(found) is not None:
+ return True
+ return False
+
+
+def doctype_matches(text, regex):
+ """Check if the doctype matches a regular expression (if present).
+
+ Note that this method only checks the first part of a DOCTYPE.
+ eg: 'html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'
+ """
+ m = doctype_lookup_re.search(text)
+ if m is None:
+ return False
+ doctype = m.group(1)
+ return re.compile(regex, re.I).match(doctype.strip()) is not None
+
+
+def html_doctype_matches(text):
+ """Check if the file looks like it has a html doctype."""
+ return doctype_matches(text, r'html')
+
+
+_looks_like_xml_cache = {}
+
+
+def looks_like_xml(text):
+ """Check if a doctype exists or if we have some tags."""
+ if xml_decl_re.match(text):
+ return True
+ key = hash(text)
+ try:
+ return _looks_like_xml_cache[key]
+ except KeyError:
+ m = doctype_lookup_re.search(text)
+ if m is not None:
+ return True
+ rv = tag_re.search(text[:1000]) is not None
+ _looks_like_xml_cache[key] = rv
+ return rv
+
+
+def surrogatepair(c):
+ """Given a unicode character code with length greater than 16 bits,
+ return the two 16 bit surrogate pair.
+ """
+ # From example D28 of:
+ # http://www.unicode.org/book/ch03.pdf
+ return (0xd7c0 + (c >> 10), (0xdc00 + (c & 0x3ff)))
+
+
+def format_lines(var_name, seq, raw=False, indent_level=0):
+ """Formats a sequence of strings for output."""
+ lines = []
+ base_indent = ' ' * indent_level * 4
+ inner_indent = ' ' * (indent_level + 1) * 4
+ lines.append(base_indent + var_name + ' = (')
+ if raw:
+ # These should be preformatted reprs of, say, tuples.
+ for i in seq:
+ lines.append(inner_indent + i + ',')
+ else:
+ for i in seq:
+ # Force use of single quotes
+ r = repr(i + '"')
+ lines.append(inner_indent + r[:-2] + r[-1] + ',')
+ lines.append(base_indent + ')')
+ return '\n'.join(lines)
+
+
+def duplicates_removed(it, already_seen=()):
+ """
+ Returns a list with duplicates removed from the iterable `it`.
+
+ Order is preserved.
+ """
+ lst = []
+ seen = set()
+ for i in it:
+ if i in seen or i in already_seen:
+ continue
+ lst.append(i)
+ seen.add(i)
+ return lst
+
+
+class Future:
+ """Generic class to defer some work.
+
+ Handled specially in RegexLexerMeta, to support regex string construction at
+ first use.
+ """
+ def get(self):
+ raise NotImplementedError
+
+
+def guess_decode(text):
+ """Decode *text* with guessed encoding.
+
+ First try UTF-8; this should fail for non-UTF-8 encodings.
+ Then try the preferred locale encoding.
+ Fall back to latin-1, which always works.
+ """
+ try:
+ text = text.decode('utf-8')
+ return text, 'utf-8'
+ except UnicodeDecodeError:
+ try:
+ import locale
+ prefencoding = locale.getpreferredencoding()
+ text = text.decode()
+ return text, prefencoding
+ except (UnicodeDecodeError, LookupError):
+ text = text.decode('latin1')
+ return text, 'latin1'
+
+
+def guess_decode_from_terminal(text, term):
+ """Decode *text* coming from terminal *term*.
+
+ First try the terminal encoding, if given.
+ Then try UTF-8. Then try the preferred locale encoding.
+ Fall back to latin-1, which always works.
+ """
+ if getattr(term, 'encoding', None):
+ try:
+ text = text.decode(term.encoding)
+ except UnicodeDecodeError:
+ pass
+ else:
+ return text, term.encoding
+ return guess_decode(text)
+
+
+def terminal_encoding(term):
+ """Return our best guess of encoding for the given *term*."""
+ if getattr(term, 'encoding', None):
+ return term.encoding
+ import locale
+ return locale.getpreferredencoding()
+
+
+class UnclosingTextIOWrapper(TextIOWrapper):
+ # Don't close underlying buffer on destruction.
+ def close(self):
+ self.flush()
diff --git a/src/pip/_vendor/pyparsing.py b/src/pip/_vendor/pyparsing.py
deleted file mode 100644
index 7ebc7eb99..000000000
--- a/src/pip/_vendor/pyparsing.py
+++ /dev/null
@@ -1,7107 +0,0 @@
-# -*- coding: utf-8 -*-
-# module pyparsing.py
-#
-# Copyright (c) 2003-2019 Paul T. McGuire
-#
-# 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.
-#
-
-__doc__ = \
-"""
-pyparsing module - Classes and methods to define and execute parsing grammars
-=============================================================================
-
-The pyparsing module is an alternative approach to creating and
-executing simple grammars, vs. the traditional lex/yacc approach, or the
-use of regular expressions. With pyparsing, you don't need to learn
-a new syntax for defining grammars or matching expressions - the parsing
-module provides a library of classes that you use to construct the
-grammar directly in Python.
-
-Here is a program to parse "Hello, World!" (or any greeting of the form
-``"<salutation>, <addressee>!"``), built up using :class:`Word`,
-:class:`Literal`, and :class:`And` elements
-(the :class:`'+'<ParserElement.__add__>` operators create :class:`And` expressions,
-and the strings are auto-converted to :class:`Literal` expressions)::
-
- from pip._vendor.pyparsing import Word, alphas
-
- # define grammar of a greeting
- greet = Word(alphas) + "," + Word(alphas) + "!"
-
- hello = "Hello, World!"
- print (hello, "->", greet.parseString(hello))
-
-The program outputs the following::
-
- Hello, World! -> ['Hello', ',', 'World', '!']
-
-The Python representation of the grammar is quite readable, owing to the
-self-explanatory class names, and the use of '+', '|' and '^' operators.
-
-The :class:`ParseResults` object returned from
-:class:`ParserElement.parseString` can be
-accessed as a nested list, a dictionary, or an object with named
-attributes.
-
-The pyparsing module handles some of the problems that are typically
-vexing when writing text parsers:
-
- - extra or missing whitespace (the above program will also handle
- "Hello,World!", "Hello , World !", etc.)
- - quoted strings
- - embedded comments
-
-
-Getting Started -
------------------
-Visit the classes :class:`ParserElement` and :class:`ParseResults` to
-see the base classes that most other pyparsing
-classes inherit from. Use the docstrings for examples of how to:
-
- - construct literal match expressions from :class:`Literal` and
- :class:`CaselessLiteral` classes
- - construct character word-group expressions using the :class:`Word`
- class
- - see how to create repetitive expressions using :class:`ZeroOrMore`
- and :class:`OneOrMore` classes
- - use :class:`'+'<And>`, :class:`'|'<MatchFirst>`, :class:`'^'<Or>`,
- and :class:`'&'<Each>` operators to combine simple expressions into
- more complex ones
- - associate names with your parsed results using
- :class:`ParserElement.setResultsName`
- - access the parsed data, which is returned as a :class:`ParseResults`
- object
- - find some helpful expression short-cuts like :class:`delimitedList`
- and :class:`oneOf`
- - find more useful common expressions in the :class:`pyparsing_common`
- namespace class
-"""
-
-__version__ = "2.4.7"
-__versionTime__ = "30 Mar 2020 00:43 UTC"
-__author__ = "Paul McGuire <ptmcg@users.sourceforge.net>"
-
-import string
-from weakref import ref as wkref
-import copy
-import sys
-import warnings
-import re
-import sre_constants
-import collections
-import pprint
-import traceback
-import types
-from datetime import datetime
-from operator import itemgetter
-import itertools
-from functools import wraps
-from contextlib import contextmanager
-
-try:
- # Python 3
- from itertools import filterfalse
-except ImportError:
- from itertools import ifilterfalse as filterfalse
-
-try:
- from _thread import RLock
-except ImportError:
- from threading import RLock
-
-try:
- # Python 3
- from collections.abc import Iterable
- from collections.abc import MutableMapping, Mapping
-except ImportError:
- # Python 2.7
- from collections import Iterable
- from collections import MutableMapping, Mapping
-
-try:
- from collections import OrderedDict as _OrderedDict
-except ImportError:
- try:
- from ordereddict import OrderedDict as _OrderedDict
- except ImportError:
- _OrderedDict = None
-
-try:
- from types import SimpleNamespace
-except ImportError:
- class SimpleNamespace: pass
-
-# version compatibility configuration
-__compat__ = SimpleNamespace()
-__compat__.__doc__ = """
- A cross-version compatibility configuration for pyparsing features that will be
- released in a future version. By setting values in this configuration to True,
- those features can be enabled in prior versions for compatibility development
- and testing.
-
- - collect_all_And_tokens - flag to enable fix for Issue #63 that fixes erroneous grouping
- of results names when an And expression is nested within an Or or MatchFirst; set to
- True to enable bugfix released in pyparsing 2.3.0, or False to preserve
- pre-2.3.0 handling of named results
-"""
-__compat__.collect_all_And_tokens = True
-
-__diag__ = SimpleNamespace()
-__diag__.__doc__ = """
-Diagnostic configuration (all default to False)
- - warn_multiple_tokens_in_named_alternation - flag to enable warnings when a results
- name is defined on a MatchFirst or Or expression with one or more And subexpressions
- (only warns if __compat__.collect_all_And_tokens is False)
- - warn_ungrouped_named_tokens_in_collection - flag to enable warnings when a results
- name is defined on a containing expression with ungrouped subexpressions that also
- have results names
- - warn_name_set_on_empty_Forward - flag to enable warnings whan a Forward is defined
- with a results name, but has no contents defined
- - warn_on_multiple_string_args_to_oneof - flag to enable warnings whan oneOf is
- incorrectly called with multiple str arguments
- - enable_debug_on_named_expressions - flag to auto-enable debug on all subsequent
- calls to ParserElement.setName()
-"""
-__diag__.warn_multiple_tokens_in_named_alternation = False
-__diag__.warn_ungrouped_named_tokens_in_collection = False
-__diag__.warn_name_set_on_empty_Forward = False
-__diag__.warn_on_multiple_string_args_to_oneof = False
-__diag__.enable_debug_on_named_expressions = False
-__diag__._all_names = [nm for nm in vars(__diag__) if nm.startswith("enable_") or nm.startswith("warn_")]
-
-def _enable_all_warnings():
- __diag__.warn_multiple_tokens_in_named_alternation = True
- __diag__.warn_ungrouped_named_tokens_in_collection = True
- __diag__.warn_name_set_on_empty_Forward = True
- __diag__.warn_on_multiple_string_args_to_oneof = True
-__diag__.enable_all_warnings = _enable_all_warnings
-
-
-__all__ = ['__version__', '__versionTime__', '__author__', '__compat__', '__diag__',
- 'And', 'CaselessKeyword', 'CaselessLiteral', 'CharsNotIn', 'Combine', 'Dict', 'Each', 'Empty',
- 'FollowedBy', 'Forward', 'GoToColumn', 'Group', 'Keyword', 'LineEnd', 'LineStart', 'Literal',
- 'PrecededBy', 'MatchFirst', 'NoMatch', 'NotAny', 'OneOrMore', 'OnlyOnce', 'Optional', 'Or',
- 'ParseBaseException', 'ParseElementEnhance', 'ParseException', 'ParseExpression', 'ParseFatalException',
- 'ParseResults', 'ParseSyntaxException', 'ParserElement', 'QuotedString', 'RecursiveGrammarException',
- 'Regex', 'SkipTo', 'StringEnd', 'StringStart', 'Suppress', 'Token', 'TokenConverter',
- 'White', 'Word', 'WordEnd', 'WordStart', 'ZeroOrMore', 'Char',
- 'alphanums', 'alphas', 'alphas8bit', 'anyCloseTag', 'anyOpenTag', 'cStyleComment', 'col',
- 'commaSeparatedList', 'commonHTMLEntity', 'countedArray', 'cppStyleComment', 'dblQuotedString',
- 'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'hexnums',
- 'htmlComment', 'javaStyleComment', 'line', 'lineEnd', 'lineStart', 'lineno',
- 'makeHTMLTags', 'makeXMLTags', 'matchOnlyAtCol', 'matchPreviousExpr', 'matchPreviousLiteral',
- 'nestedExpr', 'nullDebugAction', 'nums', 'oneOf', 'opAssoc', 'operatorPrecedence', 'printables',
- 'punc8bit', 'pythonStyleComment', 'quotedString', 'removeQuotes', 'replaceHTMLEntity',
- 'replaceWith', 'restOfLine', 'sglQuotedString', 'srange', 'stringEnd',
- 'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute',
- 'indentedBlock', 'originalTextFor', 'ungroup', 'infixNotation', 'locatedExpr', 'withClass',
- 'CloseMatch', 'tokenMap', 'pyparsing_common', 'pyparsing_unicode', 'unicode_set',
- 'conditionAsParseAction', 're',
- ]
-
-system_version = tuple(sys.version_info)[:3]
-PY_3 = system_version[0] == 3
-if PY_3:
- _MAX_INT = sys.maxsize
- basestring = str
- unichr = chr
- unicode = str
- _ustr = str
-
- # build list of single arg builtins, that can be used as parse actions
- singleArgBuiltins = [sum, len, sorted, reversed, list, tuple, set, any, all, min, max]
-
-else:
- _MAX_INT = sys.maxint
- range = xrange
-
- def _ustr(obj):
- """Drop-in replacement for str(obj) that tries to be Unicode
- friendly. It first tries str(obj). If that fails with
- a UnicodeEncodeError, then it tries unicode(obj). It then
- < returns the unicode object | encodes it with the default
- encoding | ... >.
- """
- if isinstance(obj, unicode):
- return obj
-
- try:
- # If this works, then _ustr(obj) has the same behaviour as str(obj), so
- # it won't break any existing code.
- return str(obj)
-
- except UnicodeEncodeError:
- # Else encode it
- ret = unicode(obj).encode(sys.getdefaultencoding(), 'xmlcharrefreplace')
- xmlcharref = Regex(r'&#\d+;')
- xmlcharref.setParseAction(lambda t: '\\u' + hex(int(t[0][2:-1]))[2:])
- return xmlcharref.transformString(ret)
-
- # build list of single arg builtins, tolerant of Python version, that can be used as parse actions
- singleArgBuiltins = []
- import __builtin__
-
- for fname in "sum len sorted reversed list tuple set any all min max".split():
- try:
- singleArgBuiltins.append(getattr(__builtin__, fname))
- except AttributeError:
- continue
-
-_generatorType = type((y for y in range(1)))
-
-def _xml_escape(data):
- """Escape &, <, >, ", ', etc. in a string of data."""
-
- # ampersand must be replaced first
- from_symbols = '&><"\''
- to_symbols = ('&' + s + ';' for s in "amp gt lt quot apos".split())
- for from_, to_ in zip(from_symbols, to_symbols):
- data = data.replace(from_, to_)
- return data
-
-alphas = string.ascii_uppercase + string.ascii_lowercase
-nums = "0123456789"
-hexnums = nums + "ABCDEFabcdef"
-alphanums = alphas + nums
-_bslash = chr(92)
-printables = "".join(c for c in string.printable if c not in string.whitespace)
-
-
-def conditionAsParseAction(fn, message=None, fatal=False):
- msg = message if message is not None else "failed user-defined condition"
- exc_type = ParseFatalException if fatal else ParseException
- fn = _trim_arity(fn)
-
- @wraps(fn)
- def pa(s, l, t):
- if not bool(fn(s, l, t)):
- raise exc_type(s, l, msg)
-
- return pa
-
-class ParseBaseException(Exception):
- """base exception class for all parsing runtime exceptions"""
- # Performance tuning: we construct a *lot* of these, so keep this
- # constructor as small and fast as possible
- def __init__(self, pstr, loc=0, msg=None, elem=None):
- self.loc = loc
- if msg is None:
- self.msg = pstr
- self.pstr = ""
- else:
- self.msg = msg
- self.pstr = pstr
- self.parserElement = elem
- self.args = (pstr, loc, msg)
-
- @classmethod
- def _from_exception(cls, pe):
- """
- internal factory method to simplify creating one type of ParseException
- from another - avoids having __init__ signature conflicts among subclasses
- """
- return cls(pe.pstr, pe.loc, pe.msg, pe.parserElement)
-
- def __getattr__(self, aname):
- """supported attributes by name are:
- - lineno - returns the line number of the exception text
- - col - returns the column number of the exception text
- - line - returns the line containing the exception text
- """
- if aname == "lineno":
- return lineno(self.loc, self.pstr)
- elif aname in ("col", "column"):
- return col(self.loc, self.pstr)
- elif aname == "line":
- return line(self.loc, self.pstr)
- else:
- raise AttributeError(aname)
-
- def __str__(self):
- if self.pstr:
- if self.loc >= len(self.pstr):
- foundstr = ', found end of text'
- else:
- foundstr = (', found %r' % self.pstr[self.loc:self.loc + 1]).replace(r'\\', '\\')
- else:
- foundstr = ''
- return ("%s%s (at char %d), (line:%d, col:%d)" %
- (self.msg, foundstr, self.loc, self.lineno, self.column))
- def __repr__(self):
- return _ustr(self)
- def markInputline(self, markerString=">!<"):
- """Extracts the exception line from the input string, and marks
- the location of the exception with a special symbol.
- """
- line_str = self.line
- line_column = self.column - 1
- if markerString:
- line_str = "".join((line_str[:line_column],
- markerString, line_str[line_column:]))
- return line_str.strip()
- def __dir__(self):
- return "lineno col line".split() + dir(type(self))
-
-class ParseException(ParseBaseException):
- """
- Exception thrown when parse expressions don't match class;
- supported attributes by name are:
- - lineno - returns the line number of the exception text
- - col - returns the column number of the exception text
- - line - returns the line containing the exception text
-
- Example::
-
- try:
- Word(nums).setName("integer").parseString("ABC")
- except ParseException as pe:
- print(pe)
- print("column: {}".format(pe.col))
-
- prints::
-
- Expected integer (at char 0), (line:1, col:1)
- column: 1
-
- """
-
- @staticmethod
- def explain(exc, depth=16):
- """
- Method to take an exception and translate the Python internal traceback into a list
- of the pyparsing expressions that caused the exception to be raised.
-
- Parameters:
-
- - exc - exception raised during parsing (need not be a ParseException, in support
- of Python exceptions that might be raised in a parse action)
- - depth (default=16) - number of levels back in the stack trace to list expression
- and function names; if None, the full stack trace names will be listed; if 0, only
- the failing input line, marker, and exception string will be shown
-
- Returns a multi-line string listing the ParserElements and/or function names in the
- exception's stack trace.
-
- Note: the diagnostic output will include string representations of the expressions
- that failed to parse. These representations will be more helpful if you use `setName` to
- give identifiable names to your expressions. Otherwise they will use the default string
- forms, which may be cryptic to read.
-
- explain() is only supported under Python 3.
- """
- import inspect
-
- if depth is None:
- depth = sys.getrecursionlimit()
- ret = []
- if isinstance(exc, ParseBaseException):
- ret.append(exc.line)
- ret.append(' ' * (exc.col - 1) + '^')
- ret.append("{0}: {1}".format(type(exc).__name__, exc))
-
- if depth > 0:
- callers = inspect.getinnerframes(exc.__traceback__, context=depth)
- seen = set()
- for i, ff in enumerate(callers[-depth:]):
- frm = ff[0]
-
- f_self = frm.f_locals.get('self', None)
- if isinstance(f_self, ParserElement):
- if frm.f_code.co_name not in ('parseImpl', '_parseNoCache'):
- continue
- if f_self in seen:
- continue
- seen.add(f_self)
-
- self_type = type(f_self)
- ret.append("{0}.{1} - {2}".format(self_type.__module__,
- self_type.__name__,
- f_self))
- elif f_self is not None:
- self_type = type(f_self)
- ret.append("{0}.{1}".format(self_type.__module__,
- self_type.__name__))
- else:
- code = frm.f_code
- if code.co_name in ('wrapper', '<module>'):
- continue
-
- ret.append("{0}".format(code.co_name))
-
- depth -= 1
- if not depth:
- break
-
- return '\n'.join(ret)
-
-
-class ParseFatalException(ParseBaseException):
- """user-throwable exception thrown when inconsistent parse content
- is found; stops all parsing immediately"""
- pass
-
-class ParseSyntaxException(ParseFatalException):
- """just like :class:`ParseFatalException`, but thrown internally
- when an :class:`ErrorStop<And._ErrorStop>` ('-' operator) indicates
- that parsing is to stop immediately because an unbacktrackable
- syntax error has been found.
- """
- pass
-
-#~ class ReparseException(ParseBaseException):
- #~ """Experimental class - parse actions can raise this exception to cause
- #~ pyparsing to reparse the input string:
- #~ - with a modified input string, and/or
- #~ - with a modified start location
- #~ Set the values of the ReparseException in the constructor, and raise the
- #~ exception in a parse action to cause pyparsing to use the new string/location.
- #~ Setting the values as None causes no change to be made.
- #~ """
- #~ def __init_( self, newstring, restartLoc ):
- #~ self.newParseText = newstring
- #~ self.reparseLoc = restartLoc
-
-class RecursiveGrammarException(Exception):
- """exception thrown by :class:`ParserElement.validate` if the
- grammar could be improperly recursive
- """
- def __init__(self, parseElementList):
- self.parseElementTrace = parseElementList
-
- def __str__(self):
- return "RecursiveGrammarException: %s" % self.parseElementTrace
-
-class _ParseResultsWithOffset(object):
- def __init__(self, p1, p2):
- self.tup = (p1, p2)
- def __getitem__(self, i):
- return self.tup[i]
- def __repr__(self):
- return repr(self.tup[0])
- def setOffset(self, i):
- self.tup = (self.tup[0], i)
-
-class ParseResults(object):
- """Structured parse results, to provide multiple means of access to
- the parsed data:
-
- - as a list (``len(results)``)
- - by list index (``results[0], results[1]``, etc.)
- - by attribute (``results.<resultsName>`` - see :class:`ParserElement.setResultsName`)
-
- Example::
-
- integer = Word(nums)
- date_str = (integer.setResultsName("year") + '/'
- + integer.setResultsName("month") + '/'
- + integer.setResultsName("day"))
- # equivalent form:
- # date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
-
- # parseString returns a ParseResults object
- result = date_str.parseString("1999/12/31")
-
- def test(s, fn=repr):
- print("%s -> %s" % (s, fn(eval(s))))
- test("list(result)")
- test("result[0]")
- test("result['month']")
- test("result.day")
- test("'month' in result")
- test("'minutes' in result")
- test("result.dump()", str)
-
- prints::
-
- list(result) -> ['1999', '/', '12', '/', '31']
- result[0] -> '1999'
- result['month'] -> '12'
- result.day -> '31'
- 'month' in result -> True
- 'minutes' in result -> False
- result.dump() -> ['1999', '/', '12', '/', '31']
- - day: 31
- - month: 12
- - year: 1999
- """
- def __new__(cls, toklist=None, name=None, asList=True, modal=True):
- if isinstance(toklist, cls):
- return toklist
- retobj = object.__new__(cls)
- retobj.__doinit = True
- return retobj
-
- # Performance tuning: we construct a *lot* of these, so keep this
- # constructor as small and fast as possible
- def __init__(self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance):
- if self.__doinit:
- self.__doinit = False
- self.__name = None
- self.__parent = None
- self.__accumNames = {}
- self.__asList = asList
- self.__modal = modal
- if toklist is None:
- toklist = []
- if isinstance(toklist, list):
- self.__toklist = toklist[:]
- elif isinstance(toklist, _generatorType):
- self.__toklist = list(toklist)
- else:
- self.__toklist = [toklist]
- self.__tokdict = dict()
-
- if name is not None and name:
- if not modal:
- self.__accumNames[name] = 0
- if isinstance(name, int):
- name = _ustr(name) # will always return a str, but use _ustr for consistency
- self.__name = name
- if not (isinstance(toklist, (type(None), basestring, list)) and toklist in (None, '', [])):
- if isinstance(toklist, basestring):
- toklist = [toklist]
- if asList:
- if isinstance(toklist, ParseResults):
- self[name] = _ParseResultsWithOffset(ParseResults(toklist.__toklist), 0)
- else:
- self[name] = _ParseResultsWithOffset(ParseResults(toklist[0]), 0)
- self[name].__name = name
- else:
- try:
- self[name] = toklist[0]
- except (KeyError, TypeError, IndexError):
- self[name] = toklist
-
- def __getitem__(self, i):
- if isinstance(i, (int, slice)):
- return self.__toklist[i]
- else:
- if i not in self.__accumNames:
- return self.__tokdict[i][-1][0]
- else:
- return ParseResults([v[0] for v in self.__tokdict[i]])
-
- def __setitem__(self, k, v, isinstance=isinstance):
- if isinstance(v, _ParseResultsWithOffset):
- self.__tokdict[k] = self.__tokdict.get(k, list()) + [v]
- sub = v[0]
- elif isinstance(k, (int, slice)):
- self.__toklist[k] = v
- sub = v
- else:
- self.__tokdict[k] = self.__tokdict.get(k, list()) + [_ParseResultsWithOffset(v, 0)]
- sub = v
- if isinstance(sub, ParseResults):
- sub.__parent = wkref(self)
-
- def __delitem__(self, i):
- if isinstance(i, (int, slice)):
- mylen = len(self.__toklist)
- del self.__toklist[i]
-
- # convert int to slice
- if isinstance(i, int):
- if i < 0:
- i += mylen
- i = slice(i, i + 1)
- # get removed indices
- removed = list(range(*i.indices(mylen)))
- removed.reverse()
- # fixup indices in token dictionary
- for name, occurrences in self.__tokdict.items():
- for j in removed:
- for k, (value, position) in enumerate(occurrences):
- occurrences[k] = _ParseResultsWithOffset(value, position - (position > j))
- else:
- del self.__tokdict[i]
-
- def __contains__(self, k):
- return k in self.__tokdict
-
- def __len__(self):
- return len(self.__toklist)
-
- def __bool__(self):
- return (not not self.__toklist)
- __nonzero__ = __bool__
-
- def __iter__(self):
- return iter(self.__toklist)
-
- def __reversed__(self):
- return iter(self.__toklist[::-1])
-
- def _iterkeys(self):
- if hasattr(self.__tokdict, "iterkeys"):
- return self.__tokdict.iterkeys()
- else:
- return iter(self.__tokdict)
-
- def _itervalues(self):
- return (self[k] for k in self._iterkeys())
-
- def _iteritems(self):
- return ((k, self[k]) for k in self._iterkeys())
-
- if PY_3:
- keys = _iterkeys
- """Returns an iterator of all named result keys."""
-
- values = _itervalues
- """Returns an iterator of all named result values."""
-
- items = _iteritems
- """Returns an iterator of all named result key-value tuples."""
-
- else:
- iterkeys = _iterkeys
- """Returns an iterator of all named result keys (Python 2.x only)."""
-
- itervalues = _itervalues
- """Returns an iterator of all named result values (Python 2.x only)."""
-
- iteritems = _iteritems
- """Returns an iterator of all named result key-value tuples (Python 2.x only)."""
-
- def keys(self):
- """Returns all named result keys (as a list in Python 2.x, as an iterator in Python 3.x)."""
- return list(self.iterkeys())
-
- def values(self):
- """Returns all named result values (as a list in Python 2.x, as an iterator in Python 3.x)."""
- return list(self.itervalues())
-
- def items(self):
- """Returns all named result key-values (as a list of tuples in Python 2.x, as an iterator in Python 3.x)."""
- return list(self.iteritems())
-
- def haskeys(self):
- """Since keys() returns an iterator, this method is helpful in bypassing
- code that looks for the existence of any defined results names."""
- return bool(self.__tokdict)
-
- def pop(self, *args, **kwargs):
- """
- Removes and returns item at specified index (default= ``last``).
- Supports both ``list`` and ``dict`` semantics for ``pop()``. If
- passed no argument or an integer argument, it will use ``list``
- semantics and pop tokens from the list of parsed tokens. If passed
- a non-integer argument (most likely a string), it will use ``dict``
- semantics and pop the corresponding value from any defined results
- names. A second default return value argument is supported, just as in
- ``dict.pop()``.
-
- Example::
-
- def remove_first(tokens):
- tokens.pop(0)
- print(OneOrMore(Word(nums)).parseString("0 123 321")) # -> ['0', '123', '321']
- print(OneOrMore(Word(nums)).addParseAction(remove_first).parseString("0 123 321")) # -> ['123', '321']
-
- label = Word(alphas)
- patt = label("LABEL") + OneOrMore(Word(nums))
- print(patt.parseString("AAB 123 321").dump())
-
- # Use pop() in a parse action to remove named result (note that corresponding value is not
- # removed from list form of results)
- def remove_LABEL(tokens):
- tokens.pop("LABEL")
- return tokens
- patt.addParseAction(remove_LABEL)
- print(patt.parseString("AAB 123 321").dump())
-
- prints::
-
- ['AAB', '123', '321']
- - LABEL: AAB
-
- ['AAB', '123', '321']
- """
- if not args:
- args = [-1]
- for k, v in kwargs.items():
- if k == 'default':
- args = (args[0], v)
- else:
- raise TypeError("pop() got an unexpected keyword argument '%s'" % k)
- if (isinstance(args[0], int)
- or len(args) == 1
- or args[0] in self):
- index = args[0]
- ret = self[index]
- del self[index]
- return ret
- else:
- defaultvalue = args[1]
- return defaultvalue
-
- def get(self, key, defaultValue=None):
- """
- Returns named result matching the given key, or if there is no
- such name, then returns the given ``defaultValue`` or ``None`` if no
- ``defaultValue`` is specified.
-
- Similar to ``dict.get()``.
-
- Example::
-
- integer = Word(nums)
- date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
-
- result = date_str.parseString("1999/12/31")
- print(result.get("year")) # -> '1999'
- print(result.get("hour", "not specified")) # -> 'not specified'
- print(result.get("hour")) # -> None
- """
- if key in self:
- return self[key]
- else:
- return defaultValue
-
- def insert(self, index, insStr):
- """
- Inserts new element at location index in the list of parsed tokens.
-
- Similar to ``list.insert()``.
-
- Example::
-
- print(OneOrMore(Word(nums)).parseString("0 123 321")) # -> ['0', '123', '321']
-
- # use a parse action to insert the parse location in the front of the parsed results
- def insert_locn(locn, tokens):
- tokens.insert(0, locn)
- print(OneOrMore(Word(nums)).addParseAction(insert_locn).parseString("0 123 321")) # -> [0, '0', '123', '321']
- """
- self.__toklist.insert(index, insStr)
- # fixup indices in token dictionary
- for name, occurrences in self.__tokdict.items():
- for k, (value, position) in enumerate(occurrences):
- occurrences[k] = _ParseResultsWithOffset(value, position + (position > index))
-
- def append(self, item):
- """
- Add single element to end of ParseResults list of elements.
-
- Example::
-
- print(OneOrMore(Word(nums)).parseString("0 123 321")) # -> ['0', '123', '321']
-
- # use a parse action to compute the sum of the parsed integers, and add it to the end
- def append_sum(tokens):
- tokens.append(sum(map(int, tokens)))
- print(OneOrMore(Word(nums)).addParseAction(append_sum).parseString("0 123 321")) # -> ['0', '123', '321', 444]
- """
- self.__toklist.append(item)
-
- def extend(self, itemseq):
- """
- Add sequence of elements to end of ParseResults list of elements.
-
- Example::
-
- patt = OneOrMore(Word(alphas))
-
- # use a parse action to append the reverse of the matched strings, to make a palindrome
- def make_palindrome(tokens):
- tokens.extend(reversed([t[::-1] for t in tokens]))
- return ''.join(tokens)
- print(patt.addParseAction(make_palindrome).parseString("lskdj sdlkjf lksd")) # -> 'lskdjsdlkjflksddsklfjkldsjdksl'
- """
- if isinstance(itemseq, ParseResults):
- self.__iadd__(itemseq)
- else:
- self.__toklist.extend(itemseq)
-
- def clear(self):
- """
- Clear all elements and results names.
- """
- del self.__toklist[:]
- self.__tokdict.clear()
-
- def __getattr__(self, name):
- try:
- return self[name]
- except KeyError:
- return ""
-
- def __add__(self, other):
- ret = self.copy()
- ret += other
- return ret
-
- def __iadd__(self, other):
- if other.__tokdict:
- offset = len(self.__toklist)
- addoffset = lambda a: offset if a < 0 else a + offset
- otheritems = other.__tokdict.items()
- otherdictitems = [(k, _ParseResultsWithOffset(v[0], addoffset(v[1])))
- for k, vlist in otheritems for v in vlist]
- for k, v in otherdictitems:
- self[k] = v
- if isinstance(v[0], ParseResults):
- v[0].__parent = wkref(self)
-
- self.__toklist += other.__toklist
- self.__accumNames.update(other.__accumNames)
- return self
-
- def __radd__(self, other):
- if isinstance(other, int) and other == 0:
- # useful for merging many ParseResults using sum() builtin
- return self.copy()
- else:
- # this may raise a TypeError - so be it
- return other + self
-
- def __repr__(self):
- return "(%s, %s)" % (repr(self.__toklist), repr(self.__tokdict))
-
- def __str__(self):
- return '[' + ', '.join(_ustr(i) if isinstance(i, ParseResults) else repr(i) for i in self.__toklist) + ']'
-
- def _asStringList(self, sep=''):
- out = []
- for item in self.__toklist:
- if out and sep:
- out.append(sep)
- if isinstance(item, ParseResults):
- out += item._asStringList()
- else:
- out.append(_ustr(item))
- return out
-
- def asList(self):
- """
- Returns the parse results as a nested list of matching tokens, all converted to strings.
-
- Example::
-
- patt = OneOrMore(Word(alphas))
- result = patt.parseString("sldkj lsdkj sldkj")
- # even though the result prints in string-like form, it is actually a pyparsing ParseResults
- print(type(result), result) # -> <class 'pyparsing.ParseResults'> ['sldkj', 'lsdkj', 'sldkj']
-
- # Use asList() to create an actual list
- result_list = result.asList()
- print(type(result_list), result_list) # -> <class 'list'> ['sldkj', 'lsdkj', 'sldkj']
- """
- return [res.asList() if isinstance(res, ParseResults) else res for res in self.__toklist]
-
- def asDict(self):
- """
- Returns the named parse results as a nested dictionary.
-
- Example::
-
- integer = Word(nums)
- date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
-
- result = date_str.parseString('12/31/1999')
- print(type(result), repr(result)) # -> <class 'pyparsing.ParseResults'> (['12', '/', '31', '/', '1999'], {'day': [('1999', 4)], 'year': [('12', 0)], 'month': [('31', 2)]})
-
- result_dict = result.asDict()
- print(type(result_dict), repr(result_dict)) # -> <class 'dict'> {'day': '1999', 'year': '12', 'month': '31'}
-
- # even though a ParseResults supports dict-like access, sometime you just need to have a dict
- import json
- print(json.dumps(result)) # -> Exception: TypeError: ... is not JSON serializable
- print(json.dumps(result.asDict())) # -> {"month": "31", "day": "1999", "year": "12"}
- """
- if PY_3:
- item_fn = self.items
- else:
- item_fn = self.iteritems
-
- def toItem(obj):
- if isinstance(obj, ParseResults):
- if obj.haskeys():
- return obj.asDict()
- else:
- return [toItem(v) for v in obj]
- else:
- return obj
-
- return dict((k, toItem(v)) for k, v in item_fn())
-
- def copy(self):
- """
- Returns a new copy of a :class:`ParseResults` object.
- """
- ret = ParseResults(self.__toklist)
- ret.__tokdict = dict(self.__tokdict.items())
- ret.__parent = self.__parent
- ret.__accumNames.update(self.__accumNames)
- ret.__name = self.__name
- return ret
-
- def asXML(self, doctag=None, namedItemsOnly=False, indent="", formatted=True):
- """
- (Deprecated) Returns the parse results as XML. Tags are created for tokens and lists that have defined results names.
- """
- nl = "\n"
- out = []
- namedItems = dict((v[1], k) for (k, vlist) in self.__tokdict.items()
- for v in vlist)
- nextLevelIndent = indent + " "
-
- # collapse out indents if formatting is not desired
- if not formatted:
- indent = ""
- nextLevelIndent = ""
- nl = ""
-
- selfTag = None
- if doctag is not None:
- selfTag = doctag
- else:
- if self.__name:
- selfTag = self.__name
-
- if not selfTag:
- if namedItemsOnly:
- return ""
- else:
- selfTag = "ITEM"
-
- out += [nl, indent, "<", selfTag, ">"]
-
- for i, res in enumerate(self.__toklist):
- if isinstance(res, ParseResults):
- if i in namedItems:
- out += [res.asXML(namedItems[i],
- namedItemsOnly and doctag is None,
- nextLevelIndent,
- formatted)]
- else:
- out += [res.asXML(None,
- namedItemsOnly and doctag is None,
- nextLevelIndent,
- formatted)]
- else:
- # individual token, see if there is a name for it
- resTag = None
- if i in namedItems:
- resTag = namedItems[i]
- if not resTag:
- if namedItemsOnly:
- continue
- else:
- resTag = "ITEM"
- xmlBodyText = _xml_escape(_ustr(res))
- out += [nl, nextLevelIndent, "<", resTag, ">",
- xmlBodyText,
- "</", resTag, ">"]
-
- out += [nl, indent, "</", selfTag, ">"]
- return "".join(out)
-
- def __lookup(self, sub):
- for k, vlist in self.__tokdict.items():
- for v, loc in vlist:
- if sub is v:
- return k
- return None
-
- def getName(self):
- r"""
- Returns the results name for this token expression. Useful when several
- different expressions might match at a particular location.
-
- Example::
-
- integer = Word(nums)
- ssn_expr = Regex(r"\d\d\d-\d\d-\d\d\d\d")
- house_number_expr = Suppress('#') + Word(nums, alphanums)
- user_data = (Group(house_number_expr)("house_number")
- | Group(ssn_expr)("ssn")
- | Group(integer)("age"))
- user_info = OneOrMore(user_data)
-
- result = user_info.parseString("22 111-22-3333 #221B")
- for item in result:
- print(item.getName(), ':', item[0])
-
- prints::
-
- age : 22
- ssn : 111-22-3333
- house_number : 221B
- """
- if self.__name:
- return self.__name
- elif self.__parent:
- par = self.__parent()
- if par:
- return par.__lookup(self)
- else:
- return None
- elif (len(self) == 1
- and len(self.__tokdict) == 1
- and next(iter(self.__tokdict.values()))[0][1] in (0, -1)):
- return next(iter(self.__tokdict.keys()))
- else:
- return None
-
- def dump(self, indent='', full=True, include_list=True, _depth=0):
- """
- Diagnostic method for listing out the contents of
- a :class:`ParseResults`. Accepts an optional ``indent`` argument so
- that this string can be embedded in a nested display of other data.
-
- Example::
-
- integer = Word(nums)
- date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
-
- result = date_str.parseString('12/31/1999')
- print(result.dump())
-
- prints::
-
- ['12', '/', '31', '/', '1999']
- - day: 1999
- - month: 31
- - year: 12
- """
- out = []
- NL = '\n'
- if include_list:
- out.append(indent + _ustr(self.asList()))
- else:
- out.append('')
-
- if full:
- if self.haskeys():
- items = sorted((str(k), v) for k, v in self.items())
- for k, v in items:
- if out:
- out.append(NL)
- out.append("%s%s- %s: " % (indent, (' ' * _depth), k))
- if isinstance(v, ParseResults):
- if v:
- out.append(v.dump(indent=indent, full=full, include_list=include_list, _depth=_depth + 1))
- else:
- out.append(_ustr(v))
- else:
- out.append(repr(v))
- elif any(isinstance(vv, ParseResults) for vv in self):
- v = self
- for i, vv in enumerate(v):
- if isinstance(vv, ParseResults):
- out.append("\n%s%s[%d]:\n%s%s%s" % (indent,
- (' ' * (_depth)),
- i,
- indent,
- (' ' * (_depth + 1)),
- vv.dump(indent=indent,
- full=full,
- include_list=include_list,
- _depth=_depth + 1)))
- else:
- out.append("\n%s%s[%d]:\n%s%s%s" % (indent,
- (' ' * (_depth)),
- i,
- indent,
- (' ' * (_depth + 1)),
- _ustr(vv)))
-
- return "".join(out)
-
- def pprint(self, *args, **kwargs):
- """
- Pretty-printer for parsed results as a list, using the
- `pprint <https://docs.python.org/3/library/pprint.html>`_ module.
- Accepts additional positional or keyword args as defined for
- `pprint.pprint <https://docs.python.org/3/library/pprint.html#pprint.pprint>`_ .
-
- Example::
-
- ident = Word(alphas, alphanums)
- num = Word(nums)
- func = Forward()
- term = ident | num | Group('(' + func + ')')
- func <<= ident + Group(Optional(delimitedList(term)))
- result = func.parseString("fna a,b,(fnb c,d,200),100")
- result.pprint(width=40)
-
- prints::
-
- ['fna',
- ['a',
- 'b',
- ['(', 'fnb', ['c', 'd', '200'], ')'],
- '100']]
- """
- pprint.pprint(self.asList(), *args, **kwargs)
-
- # add support for pickle protocol
- def __getstate__(self):
- return (self.__toklist,
- (self.__tokdict.copy(),
- self.__parent is not None and self.__parent() or None,
- self.__accumNames,
- self.__name))
-
- def __setstate__(self, state):
- self.__toklist = state[0]
- self.__tokdict, par, inAccumNames, self.__name = state[1]
- self.__accumNames = {}
- self.__accumNames.update(inAccumNames)
- if par is not None:
- self.__parent = wkref(par)
- else:
- self.__parent = None
-
- def __getnewargs__(self):
- return self.__toklist, self.__name, self.__asList, self.__modal
-
- def __dir__(self):
- return dir(type(self)) + list(self.keys())
-
- @classmethod
- def from_dict(cls, other, name=None):
- """
- Helper classmethod to construct a ParseResults from a dict, preserving the
- name-value relations as results names. If an optional 'name' argument is
- given, a nested ParseResults will be returned
- """
- def is_iterable(obj):
- try:
- iter(obj)
- except Exception:
- return False
- else:
- if PY_3:
- return not isinstance(obj, (str, bytes))
- else:
- return not isinstance(obj, basestring)
-
- ret = cls([])
- for k, v in other.items():
- if isinstance(v, Mapping):
- ret += cls.from_dict(v, name=k)
- else:
- ret += cls([v], name=k, asList=is_iterable(v))
- if name is not None:
- ret = cls([ret], name=name)
- return ret
-
-MutableMapping.register(ParseResults)
-
-def col (loc, strg):
- """Returns current column within a string, counting newlines as line separators.
- The first column is number 1.
-
- Note: the default parsing behavior is to expand tabs in the input string
- before starting the parsing process. See
- :class:`ParserElement.parseString` for more
- information on parsing strings containing ``<TAB>`` s, and suggested
- methods to maintain a consistent view of the parsed string, the parse
- location, and line and column positions within the parsed string.
- """
- s = strg
- return 1 if 0 < loc < len(s) and s[loc-1] == '\n' else loc - s.rfind("\n", 0, loc)
-
-def lineno(loc, strg):
- """Returns current line number within a string, counting newlines as line separators.
- The first line is number 1.
-
- Note - the default parsing behavior is to expand tabs in the input string
- before starting the parsing process. See :class:`ParserElement.parseString`
- for more information on parsing strings containing ``<TAB>`` s, and
- suggested methods to maintain a consistent view of the parsed string, the
- parse location, and line and column positions within the parsed string.
- """
- return strg.count("\n", 0, loc) + 1
-
-def line(loc, strg):
- """Returns the line of text containing loc within a string, counting newlines as line separators.
- """
- lastCR = strg.rfind("\n", 0, loc)
- nextCR = strg.find("\n", loc)
- if nextCR >= 0:
- return strg[lastCR + 1:nextCR]
- else:
- return strg[lastCR + 1:]
-
-def _defaultStartDebugAction(instring, loc, expr):
- print(("Match " + _ustr(expr) + " at loc " + _ustr(loc) + "(%d,%d)" % (lineno(loc, instring), col(loc, instring))))
-
-def _defaultSuccessDebugAction(instring, startloc, endloc, expr, toks):
- print("Matched " + _ustr(expr) + " -> " + str(toks.asList()))
-
-def _defaultExceptionDebugAction(instring, loc, expr, exc):
- print("Exception raised:" + _ustr(exc))
-
-def nullDebugAction(*args):
- """'Do-nothing' debug action, to suppress debugging output during parsing."""
- pass
-
-# Only works on Python 3.x - nonlocal is toxic to Python 2 installs
-#~ 'decorator to trim function calls to match the arity of the target'
-#~ def _trim_arity(func, maxargs=3):
- #~ if func in singleArgBuiltins:
- #~ return lambda s,l,t: func(t)
- #~ limit = 0
- #~ foundArity = False
- #~ def wrapper(*args):
- #~ nonlocal limit,foundArity
- #~ while 1:
- #~ try:
- #~ ret = func(*args[limit:])
- #~ foundArity = True
- #~ return ret
- #~ except TypeError:
- #~ if limit == maxargs or foundArity:
- #~ raise
- #~ limit += 1
- #~ continue
- #~ return wrapper
-
-# this version is Python 2.x-3.x cross-compatible
-'decorator to trim function calls to match the arity of the target'
-def _trim_arity(func, maxargs=2):
- if func in singleArgBuiltins:
- return lambda s, l, t: func(t)
- limit = [0]
- foundArity = [False]
-
- # traceback return data structure changed in Py3.5 - normalize back to plain tuples
- if system_version[:2] >= (3, 5):
- def extract_stack(limit=0):
- # special handling for Python 3.5.0 - extra deep call stack by 1
- offset = -3 if system_version == (3, 5, 0) else -2
- frame_summary = traceback.extract_stack(limit=-offset + limit - 1)[offset]
- return [frame_summary[:2]]
- def extract_tb(tb, limit=0):
- frames = traceback.extract_tb(tb, limit=limit)
- frame_summary = frames[-1]
- return [frame_summary[:2]]
- else:
- extract_stack = traceback.extract_stack
- extract_tb = traceback.extract_tb
-
- # synthesize what would be returned by traceback.extract_stack at the call to
- # user's parse action 'func', so that we don't incur call penalty at parse time
-
- LINE_DIFF = 6
- # IF ANY CODE CHANGES, EVEN JUST COMMENTS OR BLANK LINES, BETWEEN THE NEXT LINE AND
- # THE CALL TO FUNC INSIDE WRAPPER, LINE_DIFF MUST BE MODIFIED!!!!
- this_line = extract_stack(limit=2)[-1]
- pa_call_line_synth = (this_line[0], this_line[1] + LINE_DIFF)
-
- def wrapper(*args):
- while 1:
- try:
- ret = func(*args[limit[0]:])
- foundArity[0] = True
- return ret
- except TypeError:
- # re-raise TypeErrors if they did not come from our arity testing
- if foundArity[0]:
- raise
- else:
- try:
- tb = sys.exc_info()[-1]
- if not extract_tb(tb, limit=2)[-1][:2] == pa_call_line_synth:
- raise
- finally:
- try:
- del tb
- except NameError:
- pass
-
- if limit[0] <= maxargs:
- limit[0] += 1
- continue
- raise
-
- # copy func name to wrapper for sensible debug output
- func_name = "<parse action>"
- try:
- func_name = getattr(func, '__name__',
- getattr(func, '__class__').__name__)
- except Exception:
- func_name = str(func)
- wrapper.__name__ = func_name
-
- return wrapper
-
-
-class ParserElement(object):
- """Abstract base level parser element class."""
- DEFAULT_WHITE_CHARS = " \n\t\r"
- verbose_stacktrace = False
-
- @staticmethod
- def setDefaultWhitespaceChars(chars):
- r"""
- Overrides the default whitespace chars
-
- Example::
-
- # default whitespace chars are space, <TAB> and newline
- OneOrMore(Word(alphas)).parseString("abc def\nghi jkl") # -> ['abc', 'def', 'ghi', 'jkl']
-
- # change to just treat newline as significant
- ParserElement.setDefaultWhitespaceChars(" \t")
- OneOrMore(Word(alphas)).parseString("abc def\nghi jkl") # -> ['abc', 'def']
- """
- ParserElement.DEFAULT_WHITE_CHARS = chars
-
- @staticmethod
- def inlineLiteralsUsing(cls):
- """
- Set class to be used for inclusion of string literals into a parser.
-
- Example::
-
- # default literal class used is Literal
- integer = Word(nums)
- date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
-
- date_str.parseString("1999/12/31") # -> ['1999', '/', '12', '/', '31']
-
-
- # change to Suppress
- ParserElement.inlineLiteralsUsing(Suppress)
- date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
-
- date_str.parseString("1999/12/31") # -> ['1999', '12', '31']
- """
- ParserElement._literalStringClass = cls
-
- @classmethod
- def _trim_traceback(cls, tb):
- while tb.tb_next:
- tb = tb.tb_next
- return tb
-
- def __init__(self, savelist=False):
- self.parseAction = list()
- self.failAction = None
- # ~ self.name = "<unknown>" # don't define self.name, let subclasses try/except upcall
- self.strRepr = None
- self.resultsName = None
- self.saveAsList = savelist
- self.skipWhitespace = True
- self.whiteChars = set(ParserElement.DEFAULT_WHITE_CHARS)
- self.copyDefaultWhiteChars = True
- self.mayReturnEmpty = False # used when checking for left-recursion
- self.keepTabs = False
- self.ignoreExprs = list()
- self.debug = False
- self.streamlined = False
- self.mayIndexError = True # used to optimize exception handling for subclasses that don't advance parse index
- self.errmsg = ""
- self.modalResults = True # used to mark results names as modal (report only last) or cumulative (list all)
- self.debugActions = (None, None, None) # custom debug actions
- self.re = None
- self.callPreparse = True # used to avoid redundant calls to preParse
- self.callDuringTry = False
-
- def copy(self):
- """
- Make a copy of this :class:`ParserElement`. Useful for defining
- different parse actions for the same parsing pattern, using copies of
- the original parse element.
-
- Example::
-
- integer = Word(nums).setParseAction(lambda toks: int(toks[0]))
- integerK = integer.copy().addParseAction(lambda toks: toks[0] * 1024) + Suppress("K")
- integerM = integer.copy().addParseAction(lambda toks: toks[0] * 1024 * 1024) + Suppress("M")
-
- print(OneOrMore(integerK | integerM | integer).parseString("5K 100 640K 256M"))
-
- prints::
-
- [5120, 100, 655360, 268435456]
-
- Equivalent form of ``expr.copy()`` is just ``expr()``::
-
- integerM = integer().addParseAction(lambda toks: toks[0] * 1024 * 1024) + Suppress("M")
- """
- cpy = copy.copy(self)
- cpy.parseAction = self.parseAction[:]
- cpy.ignoreExprs = self.ignoreExprs[:]
- if self.copyDefaultWhiteChars:
- cpy.whiteChars = ParserElement.DEFAULT_WHITE_CHARS
- return cpy
-
- def setName(self, name):
- """
- Define name for this expression, makes debugging and exception messages clearer.
-
- Example::
-
- Word(nums).parseString("ABC") # -> Exception: Expected W:(0123...) (at char 0), (line:1, col:1)
- Word(nums).setName("integer").parseString("ABC") # -> Exception: Expected integer (at char 0), (line:1, col:1)
- """
- self.name = name
- self.errmsg = "Expected " + self.name
- if __diag__.enable_debug_on_named_expressions:
- self.setDebug()
- return self
-
- def setResultsName(self, name, listAllMatches=False):
- """
- Define name for referencing matching tokens as a nested attribute
- of the returned parse results.
- NOTE: this returns a *copy* of the original :class:`ParserElement` object;
- this is so that the client can define a basic element, such as an
- integer, and reference it in multiple places with different names.
-
- You can also set results names using the abbreviated syntax,
- ``expr("name")`` in place of ``expr.setResultsName("name")``
- - see :class:`__call__`.
-
- Example::
-
- date_str = (integer.setResultsName("year") + '/'
- + integer.setResultsName("month") + '/'
- + integer.setResultsName("day"))
-
- # equivalent form:
- date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
- """
- return self._setResultsName(name, listAllMatches)
-
- def _setResultsName(self, name, listAllMatches=False):
- newself = self.copy()
- if name.endswith("*"):
- name = name[:-1]
- listAllMatches = True
- newself.resultsName = name
- newself.modalResults = not listAllMatches
- return newself
-
- def setBreak(self, breakFlag=True):
- """Method to invoke the Python pdb debugger when this element is
- about to be parsed. Set ``breakFlag`` to True to enable, False to
- disable.
- """
- if breakFlag:
- _parseMethod = self._parse
- def breaker(instring, loc, doActions=True, callPreParse=True):
- import pdb
- # this call to pdb.set_trace() is intentional, not a checkin error
- pdb.set_trace()
- return _parseMethod(instring, loc, doActions, callPreParse)
- breaker._originalParseMethod = _parseMethod
- self._parse = breaker
- else:
- if hasattr(self._parse, "_originalParseMethod"):
- self._parse = self._parse._originalParseMethod
- return self
-
- def setParseAction(self, *fns, **kwargs):
- """
- Define one or more actions to perform when successfully matching parse element definition.
- Parse action fn is a callable method with 0-3 arguments, called as ``fn(s, loc, toks)`` ,
- ``fn(loc, toks)`` , ``fn(toks)`` , or just ``fn()`` , where:
-
- - s = the original string being parsed (see note below)
- - loc = the location of the matching substring
- - toks = a list of the matched tokens, packaged as a :class:`ParseResults` object
-
- If the functions in fns modify the tokens, they can return them as the return
- value from fn, and the modified list of tokens will replace the original.
- Otherwise, fn does not need to return any value.
-
- If None is passed as the parse action, all previously added parse actions for this
- expression are cleared.
-
- Optional keyword arguments:
- - callDuringTry = (default= ``False``) indicate if parse action should be run during lookaheads and alternate testing
-
- Note: the default parsing behavior is to expand tabs in the input string
- before starting the parsing process. See :class:`parseString for more
- information on parsing strings containing ``<TAB>`` s, and suggested
- methods to maintain a consistent view of the parsed string, the parse
- location, and line and column positions within the parsed string.
-
- Example::
-
- integer = Word(nums)
- date_str = integer + '/' + integer + '/' + integer
-
- date_str.parseString("1999/12/31") # -> ['1999', '/', '12', '/', '31']
-
- # use parse action to convert to ints at parse time
- integer = Word(nums).setParseAction(lambda toks: int(toks[0]))
- date_str = integer + '/' + integer + '/' + integer
-
- # note that integer fields are now ints, not strings
- date_str.parseString("1999/12/31") # -> [1999, '/', 12, '/', 31]
- """
- if list(fns) == [None,]:
- self.parseAction = []
- else:
- if not all(callable(fn) for fn in fns):
- raise TypeError("parse actions must be callable")
- self.parseAction = list(map(_trim_arity, list(fns)))
- self.callDuringTry = kwargs.get("callDuringTry", False)
- return self
-
- def addParseAction(self, *fns, **kwargs):
- """
- Add one or more parse actions to expression's list of parse actions. See :class:`setParseAction`.
-
- See examples in :class:`copy`.
- """
- self.parseAction += list(map(_trim_arity, list(fns)))
- self.callDuringTry = self.callDuringTry or kwargs.get("callDuringTry", False)
- return self
-
- def addCondition(self, *fns, **kwargs):
- """Add a boolean predicate function to expression's list of parse actions. See
- :class:`setParseAction` for function call signatures. Unlike ``setParseAction``,
- functions passed to ``addCondition`` need to return boolean success/fail of the condition.
-
- Optional keyword arguments:
- - message = define a custom message to be used in the raised exception
- - fatal = if True, will raise ParseFatalException to stop parsing immediately; otherwise will raise ParseException
-
- Example::
-
- integer = Word(nums).setParseAction(lambda toks: int(toks[0]))
- year_int = integer.copy()
- year_int.addCondition(lambda toks: toks[0] >= 2000, message="Only support years 2000 and later")
- date_str = year_int + '/' + integer + '/' + integer
-
- result = date_str.parseString("1999/12/31") # -> Exception: Only support years 2000 and later (at char 0), (line:1, col:1)
- """
- for fn in fns:
- self.parseAction.append(conditionAsParseAction(fn, message=kwargs.get('message'),
- fatal=kwargs.get('fatal', False)))
-
- self.callDuringTry = self.callDuringTry or kwargs.get("callDuringTry", False)
- return self
-
- def setFailAction(self, fn):
- """Define action to perform if parsing fails at this expression.
- Fail acton fn is a callable function that takes the arguments
- ``fn(s, loc, expr, err)`` where:
- - s = string being parsed
- - loc = location where expression match was attempted and failed
- - expr = the parse expression that failed
- - err = the exception thrown
- The function returns no value. It may throw :class:`ParseFatalException`
- if it is desired to stop parsing immediately."""
- self.failAction = fn
- return self
-
- def _skipIgnorables(self, instring, loc):
- exprsFound = True
- while exprsFound:
- exprsFound = False
- for e in self.ignoreExprs:
- try:
- while 1:
- loc, dummy = e._parse(instring, loc)
- exprsFound = True
- except ParseException:
- pass
- return loc
-
- def preParse(self, instring, loc):
- if self.ignoreExprs:
- loc = self._skipIgnorables(instring, loc)
-
- if self.skipWhitespace:
- wt = self.whiteChars
- instrlen = len(instring)
- while loc < instrlen and instring[loc] in wt:
- loc += 1
-
- return loc
-
- def parseImpl(self, instring, loc, doActions=True):
- return loc, []
-
- def postParse(self, instring, loc, tokenlist):
- return tokenlist
-
- # ~ @profile
- def _parseNoCache(self, instring, loc, doActions=True, callPreParse=True):
- TRY, MATCH, FAIL = 0, 1, 2
- debugging = (self.debug) # and doActions)
-
- if debugging or self.failAction:
- # ~ print ("Match", self, "at loc", loc, "(%d, %d)" % (lineno(loc, instring), col(loc, instring)))
- if self.debugActions[TRY]:
- self.debugActions[TRY](instring, loc, self)
- try:
- if callPreParse and self.callPreparse:
- preloc = self.preParse(instring, loc)
- else:
- preloc = loc
- tokensStart = preloc
- if self.mayIndexError or preloc >= len(instring):
- try:
- loc, tokens = self.parseImpl(instring, preloc, doActions)
- except IndexError:
- raise ParseException(instring, len(instring), self.errmsg, self)
- else:
- loc, tokens = self.parseImpl(instring, preloc, doActions)
- except Exception as err:
- # ~ print ("Exception raised:", err)
- if self.debugActions[FAIL]:
- self.debugActions[FAIL](instring, tokensStart, self, err)
- if self.failAction:
- self.failAction(instring, tokensStart, self, err)
- raise
- else:
- if callPreParse and self.callPreparse:
- preloc = self.preParse(instring, loc)
- else:
- preloc = loc
- tokensStart = preloc
- if self.mayIndexError or preloc >= len(instring):
- try:
- loc, tokens = self.parseImpl(instring, preloc, doActions)
- except IndexError:
- raise ParseException(instring, len(instring), self.errmsg, self)
- else:
- loc, tokens = self.parseImpl(instring, preloc, doActions)
-
- tokens = self.postParse(instring, loc, tokens)
-
- retTokens = ParseResults(tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults)
- if self.parseAction and (doActions or self.callDuringTry):
- if debugging:
- try:
- for fn in self.parseAction:
- try:
- tokens = fn(instring, tokensStart, retTokens)
- except IndexError as parse_action_exc:
- exc = ParseException("exception raised in parse action")
- exc.__cause__ = parse_action_exc
- raise exc
-
- if tokens is not None and tokens is not retTokens:
- retTokens = ParseResults(tokens,
- self.resultsName,
- asList=self.saveAsList and isinstance(tokens, (ParseResults, list)),
- modal=self.modalResults)
- except Exception as err:
- # ~ print "Exception raised in user parse action:", err
- if self.debugActions[FAIL]:
- self.debugActions[FAIL](instring, tokensStart, self, err)
- raise
- else:
- for fn in self.parseAction:
- try:
- tokens = fn(instring, tokensStart, retTokens)
- except IndexError as parse_action_exc:
- exc = ParseException("exception raised in parse action")
- exc.__cause__ = parse_action_exc
- raise exc
-
- if tokens is not None and tokens is not retTokens:
- retTokens = ParseResults(tokens,
- self.resultsName,
- asList=self.saveAsList and isinstance(tokens, (ParseResults, list)),
- modal=self.modalResults)
- if debugging:
- # ~ print ("Matched", self, "->", retTokens.asList())
- if self.debugActions[MATCH]:
- self.debugActions[MATCH](instring, tokensStart, loc, self, retTokens)
-
- return loc, retTokens
-
- def tryParse(self, instring, loc):
- try:
- return self._parse(instring, loc, doActions=False)[0]
- except ParseFatalException:
- raise ParseException(instring, loc, self.errmsg, self)
-
- def canParseNext(self, instring, loc):
- try:
- self.tryParse(instring, loc)
- except (ParseException, IndexError):
- return False
- else:
- return True
-
- class _UnboundedCache(object):
- def __init__(self):
- cache = {}
- self.not_in_cache = not_in_cache = object()
-
- def get(self, key):
- return cache.get(key, not_in_cache)
-
- def set(self, key, value):
- cache[key] = value
-
- def clear(self):
- cache.clear()
-
- def cache_len(self):
- return len(cache)
-
- self.get = types.MethodType(get, self)
- self.set = types.MethodType(set, self)
- self.clear = types.MethodType(clear, self)
- self.__len__ = types.MethodType(cache_len, self)
-
- if _OrderedDict is not None:
- class _FifoCache(object):
- def __init__(self, size):
- self.not_in_cache = not_in_cache = object()
-
- cache = _OrderedDict()
-
- def get(self, key):
- return cache.get(key, not_in_cache)
-
- def set(self, key, value):
- cache[key] = value
- while len(cache) > size:
- try:
- cache.popitem(False)
- except KeyError:
- pass
-
- def clear(self):
- cache.clear()
-
- def cache_len(self):
- return len(cache)
-
- self.get = types.MethodType(get, self)
- self.set = types.MethodType(set, self)
- self.clear = types.MethodType(clear, self)
- self.__len__ = types.MethodType(cache_len, self)
-
- else:
- class _FifoCache(object):
- def __init__(self, size):
- self.not_in_cache = not_in_cache = object()
-
- cache = {}
- key_fifo = collections.deque([], size)
-
- def get(self, key):
- return cache.get(key, not_in_cache)
-
- def set(self, key, value):
- cache[key] = value
- while len(key_fifo) > size:
- cache.pop(key_fifo.popleft(), None)
- key_fifo.append(key)
-
- def clear(self):
- cache.clear()
- key_fifo.clear()
-
- def cache_len(self):
- return len(cache)
-
- self.get = types.MethodType(get, self)
- self.set = types.MethodType(set, self)
- self.clear = types.MethodType(clear, self)
- self.__len__ = types.MethodType(cache_len, self)
-
- # argument cache for optimizing repeated calls when backtracking through recursive expressions
- packrat_cache = {} # this is set later by enabledPackrat(); this is here so that resetCache() doesn't fail
- packrat_cache_lock = RLock()
- packrat_cache_stats = [0, 0]
-
- # this method gets repeatedly called during backtracking with the same arguments -
- # we can cache these arguments and save ourselves the trouble of re-parsing the contained expression
- def _parseCache(self, instring, loc, doActions=True, callPreParse=True):
- HIT, MISS = 0, 1
- lookup = (self, instring, loc, callPreParse, doActions)
- with ParserElement.packrat_cache_lock:
- cache = ParserElement.packrat_cache
- value = cache.get(lookup)
- if value is cache.not_in_cache:
- ParserElement.packrat_cache_stats[MISS] += 1
- try:
- value = self._parseNoCache(instring, loc, doActions, callPreParse)
- except ParseBaseException as pe:
- # cache a copy of the exception, without the traceback
- cache.set(lookup, pe.__class__(*pe.args))
- raise
- else:
- cache.set(lookup, (value[0], value[1].copy()))
- return value
- else:
- ParserElement.packrat_cache_stats[HIT] += 1
- if isinstance(value, Exception):
- raise value
- return value[0], value[1].copy()
-
- _parse = _parseNoCache
-
- @staticmethod
- def resetCache():
- ParserElement.packrat_cache.clear()
- ParserElement.packrat_cache_stats[:] = [0] * len(ParserElement.packrat_cache_stats)
-
- _packratEnabled = False
- @staticmethod
- def enablePackrat(cache_size_limit=128):
- """Enables "packrat" parsing, which adds memoizing to the parsing logic.
- Repeated parse attempts at the same string location (which happens
- often in many complex grammars) can immediately return a cached value,
- instead of re-executing parsing/validating code. Memoizing is done of
- both valid results and parsing exceptions.
-
- Parameters:
-
- - cache_size_limit - (default= ``128``) - if an integer value is provided
- will limit the size of the packrat cache; if None is passed, then
- the cache size will be unbounded; if 0 is passed, the cache will
- be effectively disabled.
-
- This speedup may break existing programs that use parse actions that
- have side-effects. For this reason, packrat parsing is disabled when
- you first import pyparsing. To activate the packrat feature, your
- program must call the class method :class:`ParserElement.enablePackrat`.
- For best results, call ``enablePackrat()`` immediately after
- importing pyparsing.
-
- Example::
-
- from pip._vendor import pyparsing
- pyparsing.ParserElement.enablePackrat()
- """
- if not ParserElement._packratEnabled:
- ParserElement._packratEnabled = True
- if cache_size_limit is None:
- ParserElement.packrat_cache = ParserElement._UnboundedCache()
- else:
- ParserElement.packrat_cache = ParserElement._FifoCache(cache_size_limit)
- ParserElement._parse = ParserElement._parseCache
-
- def parseString(self, instring, parseAll=False):
- """
- Execute the parse expression with the given string.
- This is the main interface to the client code, once the complete
- expression has been built.
-
- Returns the parsed data as a :class:`ParseResults` object, which may be
- accessed as a list, or as a dict or object with attributes if the given parser
- includes results names.
-
- If you want the grammar to require that the entire input string be
- successfully parsed, then set ``parseAll`` to True (equivalent to ending
- the grammar with ``StringEnd()``).
-
- Note: ``parseString`` implicitly calls ``expandtabs()`` on the input string,
- in order to report proper column numbers in parse actions.
- If the input string contains tabs and
- the grammar uses parse actions that use the ``loc`` argument to index into the
- string being parsed, you can ensure you have a consistent view of the input
- string by:
-
- - calling ``parseWithTabs`` on your grammar before calling ``parseString``
- (see :class:`parseWithTabs`)
- - define your parse action using the full ``(s, loc, toks)`` signature, and
- reference the input string using the parse action's ``s`` argument
- - explictly expand the tabs in your input string before calling
- ``parseString``
-
- Example::
-
- Word('a').parseString('aaaaabaaa') # -> ['aaaaa']
- Word('a').parseString('aaaaabaaa', parseAll=True) # -> Exception: Expected end of text
- """
- ParserElement.resetCache()
- if not self.streamlined:
- self.streamline()
- # ~ self.saveAsList = True
- for e in self.ignoreExprs:
- e.streamline()
- if not self.keepTabs:
- instring = instring.expandtabs()
- try:
- loc, tokens = self._parse(instring, 0)
- if parseAll:
- loc = self.preParse(instring, loc)
- se = Empty() + StringEnd()
- se._parse(instring, loc)
- except ParseBaseException as exc:
- if ParserElement.verbose_stacktrace:
- raise
- else:
- # catch and re-raise exception from here, clearing out pyparsing internal stack trace
- if getattr(exc, '__traceback__', None) is not None:
- exc.__traceback__ = self._trim_traceback(exc.__traceback__)
- raise exc
- else:
- return tokens
-
- def scanString(self, instring, maxMatches=_MAX_INT, overlap=False):
- """
- Scan the input string for expression matches. Each match will return the
- matching tokens, start location, and end location. May be called with optional
- ``maxMatches`` argument, to clip scanning after 'n' matches are found. If
- ``overlap`` is specified, then overlapping matches will be reported.
-
- Note that the start and end locations are reported relative to the string
- being parsed. See :class:`parseString` for more information on parsing
- strings with embedded tabs.
-
- Example::
-
- source = "sldjf123lsdjjkf345sldkjf879lkjsfd987"
- print(source)
- for tokens, start, end in Word(alphas).scanString(source):
- print(' '*start + '^'*(end-start))
- print(' '*start + tokens[0])
-
- prints::
-
- sldjf123lsdjjkf345sldkjf879lkjsfd987
- ^^^^^
- sldjf
- ^^^^^^^
- lsdjjkf
- ^^^^^^
- sldkjf
- ^^^^^^
- lkjsfd
- """
- if not self.streamlined:
- self.streamline()
- for e in self.ignoreExprs:
- e.streamline()
-
- if not self.keepTabs:
- instring = _ustr(instring).expandtabs()
- instrlen = len(instring)
- loc = 0
- preparseFn = self.preParse
- parseFn = self._parse
- ParserElement.resetCache()
- matches = 0
- try:
- while loc <= instrlen and matches < maxMatches:
- try:
- preloc = preparseFn(instring, loc)
- nextLoc, tokens = parseFn(instring, preloc, callPreParse=False)
- except ParseException:
- loc = preloc + 1
- else:
- if nextLoc > loc:
- matches += 1
- yield tokens, preloc, nextLoc
- if overlap:
- nextloc = preparseFn(instring, loc)
- if nextloc > loc:
- loc = nextLoc
- else:
- loc += 1
- else:
- loc = nextLoc
- else:
- loc = preloc + 1
- except ParseBaseException as exc:
- if ParserElement.verbose_stacktrace:
- raise
- else:
- # catch and re-raise exception from here, clearing out pyparsing internal stack trace
- if getattr(exc, '__traceback__', None) is not None:
- exc.__traceback__ = self._trim_traceback(exc.__traceback__)
- raise exc
-
- def transformString(self, instring):
- """
- Extension to :class:`scanString`, to modify matching text with modified tokens that may
- be returned from a parse action. To use ``transformString``, define a grammar and
- attach a parse action to it that modifies the returned token list.
- Invoking ``transformString()`` on a target string will then scan for matches,
- and replace the matched text patterns according to the logic in the parse
- action. ``transformString()`` returns the resulting transformed string.
-
- Example::
-
- wd = Word(alphas)
- wd.setParseAction(lambda toks: toks[0].title())
-
- print(wd.transformString("now is the winter of our discontent made glorious summer by this sun of york."))
-
- prints::
-
- Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York.
- """
- out = []
- lastE = 0
- # force preservation of <TAB>s, to minimize unwanted transformation of string, and to
- # keep string locs straight between transformString and scanString
- self.keepTabs = True
- try:
- for t, s, e in self.scanString(instring):
- out.append(instring[lastE:s])
- if t:
- if isinstance(t, ParseResults):
- out += t.asList()
- elif isinstance(t, list):
- out += t
- else:
- out.append(t)
- lastE = e
- out.append(instring[lastE:])
- out = [o for o in out if o]
- return "".join(map(_ustr, _flatten(out)))
- except ParseBaseException as exc:
- if ParserElement.verbose_stacktrace:
- raise
- else:
- # catch and re-raise exception from here, clearing out pyparsing internal stack trace
- if getattr(exc, '__traceback__', None) is not None:
- exc.__traceback__ = self._trim_traceback(exc.__traceback__)
- raise exc
-
- def searchString(self, instring, maxMatches=_MAX_INT):
- """
- Another extension to :class:`scanString`, simplifying the access to the tokens found
- to match the given parse expression. May be called with optional
- ``maxMatches`` argument, to clip searching after 'n' matches are found.
-
- Example::
-
- # a capitalized word starts with an uppercase letter, followed by zero or more lowercase letters
- cap_word = Word(alphas.upper(), alphas.lower())
-
- print(cap_word.searchString("More than Iron, more than Lead, more than Gold I need Electricity"))
-
- # the sum() builtin can be used to merge results into a single ParseResults object
- print(sum(cap_word.searchString("More than Iron, more than Lead, more than Gold I need Electricity")))
-
- prints::
-
- [['More'], ['Iron'], ['Lead'], ['Gold'], ['I'], ['Electricity']]
- ['More', 'Iron', 'Lead', 'Gold', 'I', 'Electricity']
- """
- try:
- return ParseResults([t for t, s, e in self.scanString(instring, maxMatches)])
- except ParseBaseException as exc:
- if ParserElement.verbose_stacktrace:
- raise
- else:
- # catch and re-raise exception from here, clearing out pyparsing internal stack trace
- if getattr(exc, '__traceback__', None) is not None:
- exc.__traceback__ = self._trim_traceback(exc.__traceback__)
- raise exc
-
- def split(self, instring, maxsplit=_MAX_INT, includeSeparators=False):
- """
- Generator method to split a string using the given expression as a separator.
- May be called with optional ``maxsplit`` argument, to limit the number of splits;
- and the optional ``includeSeparators`` argument (default= ``False``), if the separating
- matching text should be included in the split results.
-
- Example::
-
- punc = oneOf(list(".,;:/-!?"))
- print(list(punc.split("This, this?, this sentence, is badly punctuated!")))
-
- prints::
-
- ['This', ' this', '', ' this sentence', ' is badly punctuated', '']
- """
- splits = 0
- last = 0
- for t, s, e in self.scanString(instring, maxMatches=maxsplit):
- yield instring[last:s]
- if includeSeparators:
- yield t[0]
- last = e
- yield instring[last:]
-
- def __add__(self, other):
- """
- Implementation of + operator - returns :class:`And`. Adding strings to a ParserElement
- converts them to :class:`Literal`s by default.
-
- Example::
-
- greet = Word(alphas) + "," + Word(alphas) + "!"
- hello = "Hello, World!"
- print (hello, "->", greet.parseString(hello))
-
- prints::
-
- Hello, World! -> ['Hello', ',', 'World', '!']
-
- ``...`` may be used as a parse expression as a short form of :class:`SkipTo`.
-
- Literal('start') + ... + Literal('end')
-
- is equivalent to:
-
- Literal('start') + SkipTo('end')("_skipped*") + Literal('end')
-
- Note that the skipped text is returned with '_skipped' as a results name,
- and to support having multiple skips in the same parser, the value returned is
- a list of all skipped text.
- """
- if other is Ellipsis:
- return _PendingSkip(self)
-
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- if not isinstance(other, ParserElement):
- warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
- SyntaxWarning, stacklevel=2)
- return None
- return And([self, other])
-
- def __radd__(self, other):
- """
- Implementation of + operator when left operand is not a :class:`ParserElement`
- """
- if other is Ellipsis:
- return SkipTo(self)("_skipped*") + self
-
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- if not isinstance(other, ParserElement):
- warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
- SyntaxWarning, stacklevel=2)
- return None
- return other + self
-
- def __sub__(self, other):
- """
- Implementation of - operator, returns :class:`And` with error stop
- """
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- if not isinstance(other, ParserElement):
- warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
- SyntaxWarning, stacklevel=2)
- return None
- return self + And._ErrorStop() + other
-
- def __rsub__(self, other):
- """
- Implementation of - operator when left operand is not a :class:`ParserElement`
- """
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- if not isinstance(other, ParserElement):
- warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
- SyntaxWarning, stacklevel=2)
- return None
- return other - self
-
- def __mul__(self, other):
- """
- Implementation of * operator, allows use of ``expr * 3`` in place of
- ``expr + expr + expr``. Expressions may also me multiplied by a 2-integer
- tuple, similar to ``{min, max}`` multipliers in regular expressions. Tuples
- may also include ``None`` as in:
- - ``expr*(n, None)`` or ``expr*(n, )`` is equivalent
- to ``expr*n + ZeroOrMore(expr)``
- (read as "at least n instances of ``expr``")
- - ``expr*(None, n)`` is equivalent to ``expr*(0, n)``
- (read as "0 to n instances of ``expr``")
- - ``expr*(None, None)`` is equivalent to ``ZeroOrMore(expr)``
- - ``expr*(1, None)`` is equivalent to ``OneOrMore(expr)``
-
- Note that ``expr*(None, n)`` does not raise an exception if
- more than n exprs exist in the input stream; that is,
- ``expr*(None, n)`` does not enforce a maximum number of expr
- occurrences. If this behavior is desired, then write
- ``expr*(None, n) + ~expr``
- """
- if other is Ellipsis:
- other = (0, None)
- elif isinstance(other, tuple) and other[:1] == (Ellipsis,):
- other = ((0, ) + other[1:] + (None,))[:2]
-
- if isinstance(other, int):
- minElements, optElements = other, 0
- elif isinstance(other, tuple):
- other = tuple(o if o is not Ellipsis else None for o in other)
- other = (other + (None, None))[:2]
- if other[0] is None:
- other = (0, other[1])
- if isinstance(other[0], int) and other[1] is None:
- if other[0] == 0:
- return ZeroOrMore(self)
- if other[0] == 1:
- return OneOrMore(self)
- else:
- return self * other[0] + ZeroOrMore(self)
- elif isinstance(other[0], int) and isinstance(other[1], int):
- minElements, optElements = other
- optElements -= minElements
- else:
- raise TypeError("cannot multiply 'ParserElement' and ('%s', '%s') objects", type(other[0]), type(other[1]))
- else:
- raise TypeError("cannot multiply 'ParserElement' and '%s' objects", type(other))
-
- if minElements < 0:
- raise ValueError("cannot multiply ParserElement by negative value")
- if optElements < 0:
- raise ValueError("second tuple value must be greater or equal to first tuple value")
- if minElements == optElements == 0:
- raise ValueError("cannot multiply ParserElement by 0 or (0, 0)")
-
- if optElements:
- def makeOptionalList(n):
- if n > 1:
- return Optional(self + makeOptionalList(n - 1))
- else:
- return Optional(self)
- if minElements:
- if minElements == 1:
- ret = self + makeOptionalList(optElements)
- else:
- ret = And([self] * minElements) + makeOptionalList(optElements)
- else:
- ret = makeOptionalList(optElements)
- else:
- if minElements == 1:
- ret = self
- else:
- ret = And([self] * minElements)
- return ret
-
- def __rmul__(self, other):
- return self.__mul__(other)
-
- def __or__(self, other):
- """
- Implementation of | operator - returns :class:`MatchFirst`
- """
- if other is Ellipsis:
- return _PendingSkip(self, must_skip=True)
-
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- if not isinstance(other, ParserElement):
- warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
- SyntaxWarning, stacklevel=2)
- return None
- return MatchFirst([self, other])
-
- def __ror__(self, other):
- """
- Implementation of | operator when left operand is not a :class:`ParserElement`
- """
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- if not isinstance(other, ParserElement):
- warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
- SyntaxWarning, stacklevel=2)
- return None
- return other | self
-
- def __xor__(self, other):
- """
- Implementation of ^ operator - returns :class:`Or`
- """
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- if not isinstance(other, ParserElement):
- warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
- SyntaxWarning, stacklevel=2)
- return None
- return Or([self, other])
-
- def __rxor__(self, other):
- """
- Implementation of ^ operator when left operand is not a :class:`ParserElement`
- """
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- if not isinstance(other, ParserElement):
- warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
- SyntaxWarning, stacklevel=2)
- return None
- return other ^ self
-
- def __and__(self, other):
- """
- Implementation of & operator - returns :class:`Each`
- """
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- if not isinstance(other, ParserElement):
- warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
- SyntaxWarning, stacklevel=2)
- return None
- return Each([self, other])
-
- def __rand__(self, other):
- """
- Implementation of & operator when left operand is not a :class:`ParserElement`
- """
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- if not isinstance(other, ParserElement):
- warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
- SyntaxWarning, stacklevel=2)
- return None
- return other & self
-
- def __invert__(self):
- """
- Implementation of ~ operator - returns :class:`NotAny`
- """
- return NotAny(self)
-
- def __iter__(self):
- # must implement __iter__ to override legacy use of sequential access to __getitem__ to
- # iterate over a sequence
- raise TypeError('%r object is not iterable' % self.__class__.__name__)
-
- def __getitem__(self, key):
- """
- use ``[]`` indexing notation as a short form for expression repetition:
- - ``expr[n]`` is equivalent to ``expr*n``
- - ``expr[m, n]`` is equivalent to ``expr*(m, n)``
- - ``expr[n, ...]`` or ``expr[n,]`` is equivalent
- to ``expr*n + ZeroOrMore(expr)``
- (read as "at least n instances of ``expr``")
- - ``expr[..., n]`` is equivalent to ``expr*(0, n)``
- (read as "0 to n instances of ``expr``")
- - ``expr[...]`` and ``expr[0, ...]`` are equivalent to ``ZeroOrMore(expr)``
- - ``expr[1, ...]`` is equivalent to ``OneOrMore(expr)``
- ``None`` may be used in place of ``...``.
-
- Note that ``expr[..., n]`` and ``expr[m, n]``do not raise an exception
- if more than ``n`` ``expr``s exist in the input stream. If this behavior is
- desired, then write ``expr[..., n] + ~expr``.
- """
-
- # convert single arg keys to tuples
- try:
- if isinstance(key, str):
- key = (key,)
- iter(key)
- except TypeError:
- key = (key, key)
-
- if len(key) > 2:
- warnings.warn("only 1 or 2 index arguments supported ({0}{1})".format(key[:5],
- '... [{0}]'.format(len(key))
- if len(key) > 5 else ''))
-
- # clip to 2 elements
- ret = self * tuple(key[:2])
- return ret
-
- def __call__(self, name=None):
- """
- Shortcut for :class:`setResultsName`, with ``listAllMatches=False``.
-
- If ``name`` is given with a trailing ``'*'`` character, then ``listAllMatches`` will be
- passed as ``True``.
-
- If ``name` is omitted, same as calling :class:`copy`.
-
- Example::
-
- # these are equivalent
- userdata = Word(alphas).setResultsName("name") + Word(nums + "-").setResultsName("socsecno")
- userdata = Word(alphas)("name") + Word(nums + "-")("socsecno")
- """
- if name is not None:
- return self._setResultsName(name)
- else:
- return self.copy()
-
- def suppress(self):
- """
- Suppresses the output of this :class:`ParserElement`; useful to keep punctuation from
- cluttering up returned output.
- """
- return Suppress(self)
-
- def leaveWhitespace(self):
- """
- Disables the skipping of whitespace before matching the characters in the
- :class:`ParserElement`'s defined pattern. This is normally only used internally by
- the pyparsing module, but may be needed in some whitespace-sensitive grammars.
- """
- self.skipWhitespace = False
- return self
-
- def setWhitespaceChars(self, chars):
- """
- Overrides the default whitespace chars
- """
- self.skipWhitespace = True
- self.whiteChars = chars
- self.copyDefaultWhiteChars = False
- return self
-
- def parseWithTabs(self):
- """
- Overrides default behavior to expand ``<TAB>``s to spaces before parsing the input string.
- Must be called before ``parseString`` when the input grammar contains elements that
- match ``<TAB>`` characters.
- """
- self.keepTabs = True
- return self
-
- def ignore(self, other):
- """
- Define expression to be ignored (e.g., comments) while doing pattern
- matching; may be called repeatedly, to define multiple comment or other
- ignorable patterns.
-
- Example::
-
- patt = OneOrMore(Word(alphas))
- patt.parseString('ablaj /* comment */ lskjd') # -> ['ablaj']
-
- patt.ignore(cStyleComment)
- patt.parseString('ablaj /* comment */ lskjd') # -> ['ablaj', 'lskjd']
- """
- if isinstance(other, basestring):
- other = Suppress(other)
-
- if isinstance(other, Suppress):
- if other not in self.ignoreExprs:
- self.ignoreExprs.append(other)
- else:
- self.ignoreExprs.append(Suppress(other.copy()))
- return self
-
- def setDebugActions(self, startAction, successAction, exceptionAction):
- """
- Enable display of debugging messages while doing pattern matching.
- """
- self.debugActions = (startAction or _defaultStartDebugAction,
- successAction or _defaultSuccessDebugAction,
- exceptionAction or _defaultExceptionDebugAction)
- self.debug = True
- return self
-
- def setDebug(self, flag=True):
- """
- Enable display of debugging messages while doing pattern matching.
- Set ``flag`` to True to enable, False to disable.
-
- Example::
-
- wd = Word(alphas).setName("alphaword")
- integer = Word(nums).setName("numword")
- term = wd | integer
-
- # turn on debugging for wd
- wd.setDebug()
-
- OneOrMore(term).parseString("abc 123 xyz 890")
-
- prints::
-
- Match alphaword at loc 0(1,1)
- Matched alphaword -> ['abc']
- Match alphaword at loc 3(1,4)
- Exception raised:Expected alphaword (at char 4), (line:1, col:5)
- Match alphaword at loc 7(1,8)
- Matched alphaword -> ['xyz']
- Match alphaword at loc 11(1,12)
- Exception raised:Expected alphaword (at char 12), (line:1, col:13)
- Match alphaword at loc 15(1,16)
- Exception raised:Expected alphaword (at char 15), (line:1, col:16)
-
- The output shown is that produced by the default debug actions - custom debug actions can be
- specified using :class:`setDebugActions`. Prior to attempting
- to match the ``wd`` expression, the debugging message ``"Match <exprname> at loc <n>(<line>,<col>)"``
- is shown. Then if the parse succeeds, a ``"Matched"`` message is shown, or an ``"Exception raised"``
- message is shown. Also note the use of :class:`setName` to assign a human-readable name to the expression,
- which makes debugging and exception messages easier to understand - for instance, the default
- name created for the :class:`Word` expression without calling ``setName`` is ``"W:(ABCD...)"``.
- """
- if flag:
- self.setDebugActions(_defaultStartDebugAction, _defaultSuccessDebugAction, _defaultExceptionDebugAction)
- else:
- self.debug = False
- return self
-
- def __str__(self):
- return self.name
-
- def __repr__(self):
- return _ustr(self)
-
- def streamline(self):
- self.streamlined = True
- self.strRepr = None
- return self
-
- def checkRecursion(self, parseElementList):
- pass
-
- def validate(self, validateTrace=None):
- """
- Check defined expressions for valid structure, check for infinite recursive definitions.
- """
- self.checkRecursion([])
-
- def parseFile(self, file_or_filename, parseAll=False):
- """
- Execute the parse expression on the given file or filename.
- If a filename is specified (instead of a file object),
- the entire file is opened, read, and closed before parsing.
- """
- try:
- file_contents = file_or_filename.read()
- except AttributeError:
- with open(file_or_filename, "r") as f:
- file_contents = f.read()
- try:
- return self.parseString(file_contents, parseAll)
- except ParseBaseException as exc:
- if ParserElement.verbose_stacktrace:
- raise
- else:
- # catch and re-raise exception from here, clearing out pyparsing internal stack trace
- if getattr(exc, '__traceback__', None) is not None:
- exc.__traceback__ = self._trim_traceback(exc.__traceback__)
- raise exc
-
- def __eq__(self, other):
- if self is other:
- return True
- elif isinstance(other, basestring):
- return self.matches(other)
- elif isinstance(other, ParserElement):
- return vars(self) == vars(other)
- return False
-
- def __ne__(self, other):
- return not (self == other)
-
- def __hash__(self):
- return id(self)
-
- def __req__(self, other):
- return self == other
-
- def __rne__(self, other):
- return not (self == other)
-
- def matches(self, testString, parseAll=True):
- """
- Method for quick testing of a parser against a test string. Good for simple
- inline microtests of sub expressions while building up larger parser.
-
- Parameters:
- - testString - to test against this expression for a match
- - parseAll - (default= ``True``) - flag to pass to :class:`parseString` when running tests
-
- Example::
-
- expr = Word(nums)
- assert expr.matches("100")
- """
- try:
- self.parseString(_ustr(testString), parseAll=parseAll)
- return True
- except ParseBaseException:
- return False
-
- def runTests(self, tests, parseAll=True, comment='#',
- fullDump=True, printResults=True, failureTests=False, postParse=None,
- file=None):
- """
- Execute the parse expression on a series of test strings, showing each
- test, the parsed results or where the parse failed. Quick and easy way to
- run a parse expression against a list of sample strings.
-
- Parameters:
- - tests - a list of separate test strings, or a multiline string of test strings
- - parseAll - (default= ``True``) - flag to pass to :class:`parseString` when running tests
- - comment - (default= ``'#'``) - expression for indicating embedded comments in the test
- string; pass None to disable comment filtering
- - fullDump - (default= ``True``) - dump results as list followed by results names in nested outline;
- if False, only dump nested list
- - printResults - (default= ``True``) prints test output to stdout
- - failureTests - (default= ``False``) indicates if these tests are expected to fail parsing
- - postParse - (default= ``None``) optional callback for successful parse results; called as
- `fn(test_string, parse_results)` and returns a string to be added to the test output
- - file - (default=``None``) optional file-like object to which test output will be written;
- if None, will default to ``sys.stdout``
-
- Returns: a (success, results) tuple, where success indicates that all tests succeeded
- (or failed if ``failureTests`` is True), and the results contain a list of lines of each
- test's output
-
- Example::
-
- number_expr = pyparsing_common.number.copy()
-
- result = number_expr.runTests('''
- # unsigned integer
- 100
- # negative integer
- -100
- # float with scientific notation
- 6.02e23
- # integer with scientific notation
- 1e-12
- ''')
- print("Success" if result[0] else "Failed!")
-
- result = number_expr.runTests('''
- # stray character
- 100Z
- # missing leading digit before '.'
- -.100
- # too many '.'
- 3.14.159
- ''', failureTests=True)
- print("Success" if result[0] else "Failed!")
-
- prints::
-
- # unsigned integer
- 100
- [100]
-
- # negative integer
- -100
- [-100]
-
- # float with scientific notation
- 6.02e23
- [6.02e+23]
-
- # integer with scientific notation
- 1e-12
- [1e-12]
-
- Success
-
- # stray character
- 100Z
- ^
- FAIL: Expected end of text (at char 3), (line:1, col:4)
-
- # missing leading digit before '.'
- -.100
- ^
- FAIL: Expected {real number with scientific notation | real number | signed integer} (at char 0), (line:1, col:1)
-
- # too many '.'
- 3.14.159
- ^
- FAIL: Expected end of text (at char 4), (line:1, col:5)
-
- Success
-
- Each test string must be on a single line. If you want to test a string that spans multiple
- lines, create a test like this::
-
- expr.runTest(r"this is a test\\n of strings that spans \\n 3 lines")
-
- (Note that this is a raw string literal, you must include the leading 'r'.)
- """
- if isinstance(tests, basestring):
- tests = list(map(str.strip, tests.rstrip().splitlines()))
- if isinstance(comment, basestring):
- comment = Literal(comment)
- if file is None:
- file = sys.stdout
- print_ = file.write
-
- allResults = []
- comments = []
- success = True
- NL = Literal(r'\n').addParseAction(replaceWith('\n')).ignore(quotedString)
- BOM = u'\ufeff'
- for t in tests:
- if comment is not None and comment.matches(t, False) or comments and not t:
- comments.append(t)
- continue
- if not t:
- continue
- out = ['\n' + '\n'.join(comments) if comments else '', t]
- comments = []
- try:
- # convert newline marks to actual newlines, and strip leading BOM if present
- t = NL.transformString(t.lstrip(BOM))
- result = self.parseString(t, parseAll=parseAll)
- except ParseBaseException as pe:
- fatal = "(FATAL)" if isinstance(pe, ParseFatalException) else ""
- if '\n' in t:
- out.append(line(pe.loc, t))
- out.append(' ' * (col(pe.loc, t) - 1) + '^' + fatal)
- else:
- out.append(' ' * pe.loc + '^' + fatal)
- out.append("FAIL: " + str(pe))
- success = success and failureTests
- result = pe
- except Exception as exc:
- out.append("FAIL-EXCEPTION: " + str(exc))
- success = success and failureTests
- result = exc
- else:
- success = success and not failureTests
- if postParse is not None:
- try:
- pp_value = postParse(t, result)
- if pp_value is not None:
- if isinstance(pp_value, ParseResults):
- out.append(pp_value.dump())
- else:
- out.append(str(pp_value))
- else:
- out.append(result.dump())
- except Exception as e:
- out.append(result.dump(full=fullDump))
- out.append("{0} failed: {1}: {2}".format(postParse.__name__, type(e).__name__, e))
- else:
- out.append(result.dump(full=fullDump))
-
- if printResults:
- if fullDump:
- out.append('')
- print_('\n'.join(out))
-
- allResults.append((t, result))
-
- return success, allResults
-
-
-class _PendingSkip(ParserElement):
- # internal placeholder class to hold a place were '...' is added to a parser element,
- # once another ParserElement is added, this placeholder will be replaced with a SkipTo
- def __init__(self, expr, must_skip=False):
- super(_PendingSkip, self).__init__()
- self.strRepr = str(expr + Empty()).replace('Empty', '...')
- self.name = self.strRepr
- self.anchor = expr
- self.must_skip = must_skip
-
- def __add__(self, other):
- skipper = SkipTo(other).setName("...")("_skipped*")
- if self.must_skip:
- def must_skip(t):
- if not t._skipped or t._skipped.asList() == ['']:
- del t[0]
- t.pop("_skipped", None)
- def show_skip(t):
- if t._skipped.asList()[-1:] == ['']:
- skipped = t.pop('_skipped')
- t['_skipped'] = 'missing <' + repr(self.anchor) + '>'
- return (self.anchor + skipper().addParseAction(must_skip)
- | skipper().addParseAction(show_skip)) + other
-
- return self.anchor + skipper + other
-
- def __repr__(self):
- return self.strRepr
-
- def parseImpl(self, *args):
- raise Exception("use of `...` expression without following SkipTo target expression")
-
-
-class Token(ParserElement):
- """Abstract :class:`ParserElement` subclass, for defining atomic
- matching patterns.
- """
- def __init__(self):
- super(Token, self).__init__(savelist=False)
-
-
-class Empty(Token):
- """An empty token, will always match.
- """
- def __init__(self):
- super(Empty, self).__init__()
- self.name = "Empty"
- self.mayReturnEmpty = True
- self.mayIndexError = False
-
-
-class NoMatch(Token):
- """A token that will never match.
- """
- def __init__(self):
- super(NoMatch, self).__init__()
- self.name = "NoMatch"
- self.mayReturnEmpty = True
- self.mayIndexError = False
- self.errmsg = "Unmatchable token"
-
- def parseImpl(self, instring, loc, doActions=True):
- raise ParseException(instring, loc, self.errmsg, self)
-
-
-class Literal(Token):
- """Token to exactly match a specified string.
-
- Example::
-
- Literal('blah').parseString('blah') # -> ['blah']
- Literal('blah').parseString('blahfooblah') # -> ['blah']
- Literal('blah').parseString('bla') # -> Exception: Expected "blah"
-
- For case-insensitive matching, use :class:`CaselessLiteral`.
-
- For keyword matching (force word break before and after the matched string),
- use :class:`Keyword` or :class:`CaselessKeyword`.
- """
- def __init__(self, matchString):
- super(Literal, self).__init__()
- self.match = matchString
- self.matchLen = len(matchString)
- try:
- self.firstMatchChar = matchString[0]
- except IndexError:
- warnings.warn("null string passed to Literal; use Empty() instead",
- SyntaxWarning, stacklevel=2)
- self.__class__ = Empty
- self.name = '"%s"' % _ustr(self.match)
- self.errmsg = "Expected " + self.name
- self.mayReturnEmpty = False
- self.mayIndexError = False
-
- # Performance tuning: modify __class__ to select
- # a parseImpl optimized for single-character check
- if self.matchLen == 1 and type(self) is Literal:
- self.__class__ = _SingleCharLiteral
-
- def parseImpl(self, instring, loc, doActions=True):
- if instring[loc] == self.firstMatchChar and instring.startswith(self.match, loc):
- return loc + self.matchLen, self.match
- raise ParseException(instring, loc, self.errmsg, self)
-
-class _SingleCharLiteral(Literal):
- def parseImpl(self, instring, loc, doActions=True):
- if instring[loc] == self.firstMatchChar:
- return loc + 1, self.match
- raise ParseException(instring, loc, self.errmsg, self)
-
-_L = Literal
-ParserElement._literalStringClass = Literal
-
-class Keyword(Token):
- """Token to exactly match a specified string as a keyword, that is,
- it must be immediately followed by a non-keyword character. Compare
- with :class:`Literal`:
-
- - ``Literal("if")`` will match the leading ``'if'`` in
- ``'ifAndOnlyIf'``.
- - ``Keyword("if")`` will not; it will only match the leading
- ``'if'`` in ``'if x=1'``, or ``'if(y==2)'``
-
- Accepts two optional constructor arguments in addition to the
- keyword string:
-
- - ``identChars`` is a string of characters that would be valid
- identifier characters, defaulting to all alphanumerics + "_" and
- "$"
- - ``caseless`` allows case-insensitive matching, default is ``False``.
-
- Example::
-
- Keyword("start").parseString("start") # -> ['start']
- Keyword("start").parseString("starting") # -> Exception
-
- For case-insensitive matching, use :class:`CaselessKeyword`.
- """
- DEFAULT_KEYWORD_CHARS = alphanums + "_$"
-
- def __init__(self, matchString, identChars=None, caseless=False):
- super(Keyword, self).__init__()
- if identChars is None:
- identChars = Keyword.DEFAULT_KEYWORD_CHARS
- self.match = matchString
- self.matchLen = len(matchString)
- try:
- self.firstMatchChar = matchString[0]
- except IndexError:
- warnings.warn("null string passed to Keyword; use Empty() instead",
- SyntaxWarning, stacklevel=2)
- self.name = '"%s"' % self.match
- self.errmsg = "Expected " + self.name
- self.mayReturnEmpty = False
- self.mayIndexError = False
- self.caseless = caseless
- if caseless:
- self.caselessmatch = matchString.upper()
- identChars = identChars.upper()
- self.identChars = set(identChars)
-
- def parseImpl(self, instring, loc, doActions=True):
- if self.caseless:
- if ((instring[loc:loc + self.matchLen].upper() == self.caselessmatch)
- and (loc >= len(instring) - self.matchLen
- or instring[loc + self.matchLen].upper() not in self.identChars)
- and (loc == 0
- or instring[loc - 1].upper() not in self.identChars)):
- return loc + self.matchLen, self.match
-
- else:
- if instring[loc] == self.firstMatchChar:
- if ((self.matchLen == 1 or instring.startswith(self.match, loc))
- and (loc >= len(instring) - self.matchLen
- or instring[loc + self.matchLen] not in self.identChars)
- and (loc == 0 or instring[loc - 1] not in self.identChars)):
- return loc + self.matchLen, self.match
-
- raise ParseException(instring, loc, self.errmsg, self)
-
- def copy(self):
- c = super(Keyword, self).copy()
- c.identChars = Keyword.DEFAULT_KEYWORD_CHARS
- return c
-
- @staticmethod
- def setDefaultKeywordChars(chars):
- """Overrides the default Keyword chars
- """
- Keyword.DEFAULT_KEYWORD_CHARS = chars
-
-class CaselessLiteral(Literal):
- """Token to match a specified string, ignoring case of letters.
- Note: the matched results will always be in the case of the given
- match string, NOT the case of the input text.
-
- Example::
-
- OneOrMore(CaselessLiteral("CMD")).parseString("cmd CMD Cmd10") # -> ['CMD', 'CMD', 'CMD']
-
- (Contrast with example for :class:`CaselessKeyword`.)
- """
- def __init__(self, matchString):
- super(CaselessLiteral, self).__init__(matchString.upper())
- # Preserve the defining literal.
- self.returnString = matchString
- self.name = "'%s'" % self.returnString
- self.errmsg = "Expected " + self.name
-
- def parseImpl(self, instring, loc, doActions=True):
- if instring[loc:loc + self.matchLen].upper() == self.match:
- return loc + self.matchLen, self.returnString
- raise ParseException(instring, loc, self.errmsg, self)
-
-class CaselessKeyword(Keyword):
- """
- Caseless version of :class:`Keyword`.
-
- Example::
-
- OneOrMore(CaselessKeyword("CMD")).parseString("cmd CMD Cmd10") # -> ['CMD', 'CMD']
-
- (Contrast with example for :class:`CaselessLiteral`.)
- """
- def __init__(self, matchString, identChars=None):
- super(CaselessKeyword, self).__init__(matchString, identChars, caseless=True)
-
-class CloseMatch(Token):
- """A variation on :class:`Literal` which matches "close" matches,
- that is, strings with at most 'n' mismatching characters.
- :class:`CloseMatch` takes parameters:
-
- - ``match_string`` - string to be matched
- - ``maxMismatches`` - (``default=1``) maximum number of
- mismatches allowed to count as a match
-
- The results from a successful parse will contain the matched text
- from the input string and the following named results:
-
- - ``mismatches`` - a list of the positions within the
- match_string where mismatches were found
- - ``original`` - the original match_string used to compare
- against the input string
-
- If ``mismatches`` is an empty list, then the match was an exact
- match.
-
- Example::
-
- patt = CloseMatch("ATCATCGAATGGA")
- patt.parseString("ATCATCGAAXGGA") # -> (['ATCATCGAAXGGA'], {'mismatches': [[9]], 'original': ['ATCATCGAATGGA']})
- patt.parseString("ATCAXCGAAXGGA") # -> Exception: Expected 'ATCATCGAATGGA' (with up to 1 mismatches) (at char 0), (line:1, col:1)
-
- # exact match
- patt.parseString("ATCATCGAATGGA") # -> (['ATCATCGAATGGA'], {'mismatches': [[]], 'original': ['ATCATCGAATGGA']})
-
- # close match allowing up to 2 mismatches
- patt = CloseMatch("ATCATCGAATGGA", maxMismatches=2)
- patt.parseString("ATCAXCGAAXGGA") # -> (['ATCAXCGAAXGGA'], {'mismatches': [[4, 9]], 'original': ['ATCATCGAATGGA']})
- """
- def __init__(self, match_string, maxMismatches=1):
- super(CloseMatch, self).__init__()
- self.name = match_string
- self.match_string = match_string
- self.maxMismatches = maxMismatches
- self.errmsg = "Expected %r (with up to %d mismatches)" % (self.match_string, self.maxMismatches)
- self.mayIndexError = False
- self.mayReturnEmpty = False
-
- def parseImpl(self, instring, loc, doActions=True):
- start = loc
- instrlen = len(instring)
- maxloc = start + len(self.match_string)
-
- if maxloc <= instrlen:
- match_string = self.match_string
- match_stringloc = 0
- mismatches = []
- maxMismatches = self.maxMismatches
-
- for match_stringloc, s_m in enumerate(zip(instring[loc:maxloc], match_string)):
- src, mat = s_m
- if src != mat:
- mismatches.append(match_stringloc)
- if len(mismatches) > maxMismatches:
- break
- else:
- loc = match_stringloc + 1
- results = ParseResults([instring[start:loc]])
- results['original'] = match_string
- results['mismatches'] = mismatches
- return loc, results
-
- raise ParseException(instring, loc, self.errmsg, self)
-
-
-class Word(Token):
- """Token for matching words composed of allowed character sets.
- Defined with string containing all allowed initial characters, an
- optional string containing allowed body characters (if omitted,
- defaults to the initial character set), and an optional minimum,
- maximum, and/or exact length. The default value for ``min`` is
- 1 (a minimum value < 1 is not valid); the default values for
- ``max`` and ``exact`` are 0, meaning no maximum or exact
- length restriction. An optional ``excludeChars`` parameter can
- list characters that might be found in the input ``bodyChars``
- string; useful to define a word of all printables except for one or
- two characters, for instance.
-
- :class:`srange` is useful for defining custom character set strings
- for defining ``Word`` expressions, using range notation from
- regular expression character sets.
-
- A common mistake is to use :class:`Word` to match a specific literal
- string, as in ``Word("Address")``. Remember that :class:`Word`
- uses the string argument to define *sets* of matchable characters.
- This expression would match "Add", "AAA", "dAred", or any other word
- made up of the characters 'A', 'd', 'r', 'e', and 's'. To match an
- exact literal string, use :class:`Literal` or :class:`Keyword`.
-
- pyparsing includes helper strings for building Words:
-
- - :class:`alphas`
- - :class:`nums`
- - :class:`alphanums`
- - :class:`hexnums`
- - :class:`alphas8bit` (alphabetic characters in ASCII range 128-255
- - accented, tilded, umlauted, etc.)
- - :class:`punc8bit` (non-alphabetic characters in ASCII range
- 128-255 - currency, symbols, superscripts, diacriticals, etc.)
- - :class:`printables` (any non-whitespace character)
-
- Example::
-
- # a word composed of digits
- integer = Word(nums) # equivalent to Word("0123456789") or Word(srange("0-9"))
-
- # a word with a leading capital, and zero or more lowercase
- capital_word = Word(alphas.upper(), alphas.lower())
-
- # hostnames are alphanumeric, with leading alpha, and '-'
- hostname = Word(alphas, alphanums + '-')
-
- # roman numeral (not a strict parser, accepts invalid mix of characters)
- roman = Word("IVXLCDM")
-
- # any string of non-whitespace characters, except for ','
- csv_value = Word(printables, excludeChars=",")
- """
- def __init__(self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword=False, excludeChars=None):
- super(Word, self).__init__()
- if excludeChars:
- excludeChars = set(excludeChars)
- initChars = ''.join(c for c in initChars if c not in excludeChars)
- if bodyChars:
- bodyChars = ''.join(c for c in bodyChars if c not in excludeChars)
- self.initCharsOrig = initChars
- self.initChars = set(initChars)
- if bodyChars:
- self.bodyCharsOrig = bodyChars
- self.bodyChars = set(bodyChars)
- else:
- self.bodyCharsOrig = initChars
- self.bodyChars = set(initChars)
-
- self.maxSpecified = max > 0
-
- if min < 1:
- raise ValueError("cannot specify a minimum length < 1; use Optional(Word()) if zero-length word is permitted")
-
- self.minLen = min
-
- if max > 0:
- self.maxLen = max
- else:
- self.maxLen = _MAX_INT
-
- if exact > 0:
- self.maxLen = exact
- self.minLen = exact
-
- self.name = _ustr(self)
- self.errmsg = "Expected " + self.name
- self.mayIndexError = False
- self.asKeyword = asKeyword
-
- if ' ' not in self.initCharsOrig + self.bodyCharsOrig and (min == 1 and max == 0 and exact == 0):
- if self.bodyCharsOrig == self.initCharsOrig:
- self.reString = "[%s]+" % _escapeRegexRangeChars(self.initCharsOrig)
- elif len(self.initCharsOrig) == 1:
- self.reString = "%s[%s]*" % (re.escape(self.initCharsOrig),
- _escapeRegexRangeChars(self.bodyCharsOrig),)
- else:
- self.reString = "[%s][%s]*" % (_escapeRegexRangeChars(self.initCharsOrig),
- _escapeRegexRangeChars(self.bodyCharsOrig),)
- if self.asKeyword:
- self.reString = r"\b" + self.reString + r"\b"
-
- try:
- self.re = re.compile(self.reString)
- except Exception:
- self.re = None
- else:
- self.re_match = self.re.match
- self.__class__ = _WordRegex
-
- def parseImpl(self, instring, loc, doActions=True):
- if instring[loc] not in self.initChars:
- raise ParseException(instring, loc, self.errmsg, self)
-
- start = loc
- loc += 1
- instrlen = len(instring)
- bodychars = self.bodyChars
- maxloc = start + self.maxLen
- maxloc = min(maxloc, instrlen)
- while loc < maxloc and instring[loc] in bodychars:
- loc += 1
-
- throwException = False
- if loc - start < self.minLen:
- throwException = True
- elif self.maxSpecified and loc < instrlen and instring[loc] in bodychars:
- throwException = True
- elif self.asKeyword:
- if (start > 0 and instring[start - 1] in bodychars
- or loc < instrlen and instring[loc] in bodychars):
- throwException = True
-
- if throwException:
- raise ParseException(instring, loc, self.errmsg, self)
-
- return loc, instring[start:loc]
-
- def __str__(self):
- try:
- return super(Word, self).__str__()
- except Exception:
- pass
-
- if self.strRepr is None:
-
- def charsAsStr(s):
- if len(s) > 4:
- return s[:4] + "..."
- else:
- return s
-
- if self.initCharsOrig != self.bodyCharsOrig:
- self.strRepr = "W:(%s, %s)" % (charsAsStr(self.initCharsOrig), charsAsStr(self.bodyCharsOrig))
- else:
- self.strRepr = "W:(%s)" % charsAsStr(self.initCharsOrig)
-
- return self.strRepr
-
-class _WordRegex(Word):
- def parseImpl(self, instring, loc, doActions=True):
- result = self.re_match(instring, loc)
- if not result:
- raise ParseException(instring, loc, self.errmsg, self)
-
- loc = result.end()
- return loc, result.group()
-
-
-class Char(_WordRegex):
- """A short-cut class for defining ``Word(characters, exact=1)``,
- when defining a match of any single character in a string of
- characters.
- """
- def __init__(self, charset, asKeyword=False, excludeChars=None):
- super(Char, self).__init__(charset, exact=1, asKeyword=asKeyword, excludeChars=excludeChars)
- self.reString = "[%s]" % _escapeRegexRangeChars(''.join(self.initChars))
- if asKeyword:
- self.reString = r"\b%s\b" % self.reString
- self.re = re.compile(self.reString)
- self.re_match = self.re.match
-
-
-class Regex(Token):
- r"""Token for matching strings that match a given regular
- expression. Defined with string specifying the regular expression in
- a form recognized by the stdlib Python `re module <https://docs.python.org/3/library/re.html>`_.
- If the given regex contains named groups (defined using ``(?P<name>...)``),
- these will be preserved as named parse results.
-
- If instead of the Python stdlib re module you wish to use a different RE module
- (such as the `regex` module), you can replace it by either building your
- Regex object with a compiled RE that was compiled using regex:
-
- Example::
-
- realnum = Regex(r"[+-]?\d+\.\d*")
- date = Regex(r'(?P<year>\d{4})-(?P<month>\d\d?)-(?P<day>\d\d?)')
- # ref: https://stackoverflow.com/questions/267399/how-do-you-match-only-valid-roman-numerals-with-a-regular-expression
- roman = Regex(r"M{0,4}(CM|CD|D?{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})")
-
- # use regex module instead of stdlib re module to construct a Regex using
- # a compiled regular expression
- import regex
- parser = pp.Regex(regex.compile(r'[0-9]'))
-
- """
- def __init__(self, pattern, flags=0, asGroupList=False, asMatch=False):
- """The parameters ``pattern`` and ``flags`` are passed
- to the ``re.compile()`` function as-is. See the Python
- `re module <https://docs.python.org/3/library/re.html>`_ module for an
- explanation of the acceptable patterns and flags.
- """
- super(Regex, self).__init__()
-
- if isinstance(pattern, basestring):
- if not pattern:
- warnings.warn("null string passed to Regex; use Empty() instead",
- SyntaxWarning, stacklevel=2)
-
- self.pattern = pattern
- self.flags = flags
-
- try:
- self.re = re.compile(self.pattern, self.flags)
- self.reString = self.pattern
- except sre_constants.error:
- warnings.warn("invalid pattern (%s) passed to Regex" % pattern,
- SyntaxWarning, stacklevel=2)
- raise
-
- elif hasattr(pattern, 'pattern') and hasattr(pattern, 'match'):
- self.re = pattern
- self.pattern = self.reString = pattern.pattern
- self.flags = flags
-
- else:
- raise TypeError("Regex may only be constructed with a string or a compiled RE object")
-
- self.re_match = self.re.match
-
- self.name = _ustr(self)
- self.errmsg = "Expected " + self.name
- self.mayIndexError = False
- self.mayReturnEmpty = self.re_match("") is not None
- self.asGroupList = asGroupList
- self.asMatch = asMatch
- if self.asGroupList:
- self.parseImpl = self.parseImplAsGroupList
- if self.asMatch:
- self.parseImpl = self.parseImplAsMatch
-
- def parseImpl(self, instring, loc, doActions=True):
- result = self.re_match(instring, loc)
- if not result:
- raise ParseException(instring, loc, self.errmsg, self)
-
- loc = result.end()
- ret = ParseResults(result.group())
- d = result.groupdict()
- if d:
- for k, v in d.items():
- ret[k] = v
- return loc, ret
-
- def parseImplAsGroupList(self, instring, loc, doActions=True):
- result = self.re_match(instring, loc)
- if not result:
- raise ParseException(instring, loc, self.errmsg, self)
-
- loc = result.end()
- ret = result.groups()
- return loc, ret
-
- def parseImplAsMatch(self, instring, loc, doActions=True):
- result = self.re_match(instring, loc)
- if not result:
- raise ParseException(instring, loc, self.errmsg, self)
-
- loc = result.end()
- ret = result
- return loc, ret
-
- def __str__(self):
- try:
- return super(Regex, self).__str__()
- except Exception:
- pass
-
- if self.strRepr is None:
- self.strRepr = "Re:(%s)" % repr(self.pattern)
-
- return self.strRepr
-
- def sub(self, repl):
- r"""
- Return Regex with an attached parse action to transform the parsed
- result as if called using `re.sub(expr, repl, string) <https://docs.python.org/3/library/re.html#re.sub>`_.
-
- Example::
-
- make_html = Regex(r"(\w+):(.*?):").sub(r"<\1>\2</\1>")
- print(make_html.transformString("h1:main title:"))
- # prints "<h1>main title</h1>"
- """
- if self.asGroupList:
- warnings.warn("cannot use sub() with Regex(asGroupList=True)",
- SyntaxWarning, stacklevel=2)
- raise SyntaxError()
-
- if self.asMatch and callable(repl):
- warnings.warn("cannot use sub() with a callable with Regex(asMatch=True)",
- SyntaxWarning, stacklevel=2)
- raise SyntaxError()
-
- if self.asMatch:
- def pa(tokens):
- return tokens[0].expand(repl)
- else:
- def pa(tokens):
- return self.re.sub(repl, tokens[0])
- return self.addParseAction(pa)
-
-class QuotedString(Token):
- r"""
- Token for matching strings that are delimited by quoting characters.
-
- Defined with the following parameters:
-
- - quoteChar - string of one or more characters defining the
- quote delimiting string
- - escChar - character to escape quotes, typically backslash
- (default= ``None``)
- - escQuote - special quote sequence to escape an embedded quote
- string (such as SQL's ``""`` to escape an embedded ``"``)
- (default= ``None``)
- - multiline - boolean indicating whether quotes can span
- multiple lines (default= ``False``)
- - unquoteResults - boolean indicating whether the matched text
- should be unquoted (default= ``True``)
- - endQuoteChar - string of one or more characters defining the
- end of the quote delimited string (default= ``None`` => same as
- quoteChar)
- - convertWhitespaceEscapes - convert escaped whitespace
- (``'\t'``, ``'\n'``, etc.) to actual whitespace
- (default= ``True``)
-
- Example::
-
- qs = QuotedString('"')
- print(qs.searchString('lsjdf "This is the quote" sldjf'))
- complex_qs = QuotedString('{{', endQuoteChar='}}')
- print(complex_qs.searchString('lsjdf {{This is the "quote"}} sldjf'))
- sql_qs = QuotedString('"', escQuote='""')
- print(sql_qs.searchString('lsjdf "This is the quote with ""embedded"" quotes" sldjf'))
-
- prints::
-
- [['This is the quote']]
- [['This is the "quote"']]
- [['This is the quote with "embedded" quotes']]
- """
- def __init__(self, quoteChar, escChar=None, escQuote=None, multiline=False,
- unquoteResults=True, endQuoteChar=None, convertWhitespaceEscapes=True):
- super(QuotedString, self).__init__()
-
- # remove white space from quote chars - wont work anyway
- quoteChar = quoteChar.strip()
- if not quoteChar:
- warnings.warn("quoteChar cannot be the empty string", SyntaxWarning, stacklevel=2)
- raise SyntaxError()
-
- if endQuoteChar is None:
- endQuoteChar = quoteChar
- else:
- endQuoteChar = endQuoteChar.strip()
- if not endQuoteChar:
- warnings.warn("endQuoteChar cannot be the empty string", SyntaxWarning, stacklevel=2)
- raise SyntaxError()
-
- self.quoteChar = quoteChar
- self.quoteCharLen = len(quoteChar)
- self.firstQuoteChar = quoteChar[0]
- self.endQuoteChar = endQuoteChar
- self.endQuoteCharLen = len(endQuoteChar)
- self.escChar = escChar
- self.escQuote = escQuote
- self.unquoteResults = unquoteResults
- self.convertWhitespaceEscapes = convertWhitespaceEscapes
-
- if multiline:
- self.flags = re.MULTILINE | re.DOTALL
- self.pattern = r'%s(?:[^%s%s]' % (re.escape(self.quoteChar),
- _escapeRegexRangeChars(self.endQuoteChar[0]),
- (escChar is not None and _escapeRegexRangeChars(escChar) or ''))
- else:
- self.flags = 0
- self.pattern = r'%s(?:[^%s\n\r%s]' % (re.escape(self.quoteChar),
- _escapeRegexRangeChars(self.endQuoteChar[0]),
- (escChar is not None and _escapeRegexRangeChars(escChar) or ''))
- if len(self.endQuoteChar) > 1:
- self.pattern += (
- '|(?:' + ')|(?:'.join("%s[^%s]" % (re.escape(self.endQuoteChar[:i]),
- _escapeRegexRangeChars(self.endQuoteChar[i]))
- for i in range(len(self.endQuoteChar) - 1, 0, -1)) + ')')
-
- if escQuote:
- self.pattern += (r'|(?:%s)' % re.escape(escQuote))
- if escChar:
- self.pattern += (r'|(?:%s.)' % re.escape(escChar))
- self.escCharReplacePattern = re.escape(self.escChar) + "(.)"
- self.pattern += (r')*%s' % re.escape(self.endQuoteChar))
-
- try:
- self.re = re.compile(self.pattern, self.flags)
- self.reString = self.pattern
- self.re_match = self.re.match
- except sre_constants.error:
- warnings.warn("invalid pattern (%s) passed to Regex" % self.pattern,
- SyntaxWarning, stacklevel=2)
- raise
-
- self.name = _ustr(self)
- self.errmsg = "Expected " + self.name
- self.mayIndexError = False
- self.mayReturnEmpty = True
-
- def parseImpl(self, instring, loc, doActions=True):
- result = instring[loc] == self.firstQuoteChar and self.re_match(instring, loc) or None
- if not result:
- raise ParseException(instring, loc, self.errmsg, self)
-
- loc = result.end()
- ret = result.group()
-
- if self.unquoteResults:
-
- # strip off quotes
- ret = ret[self.quoteCharLen: -self.endQuoteCharLen]
-
- if isinstance(ret, basestring):
- # replace escaped whitespace
- if '\\' in ret and self.convertWhitespaceEscapes:
- ws_map = {
- r'\t': '\t',
- r'\n': '\n',
- r'\f': '\f',
- r'\r': '\r',
- }
- for wslit, wschar in ws_map.items():
- ret = ret.replace(wslit, wschar)
-
- # replace escaped characters
- if self.escChar:
- ret = re.sub(self.escCharReplacePattern, r"\g<1>", ret)
-
- # replace escaped quotes
- if self.escQuote:
- ret = ret.replace(self.escQuote, self.endQuoteChar)
-
- return loc, ret
-
- def __str__(self):
- try:
- return super(QuotedString, self).__str__()
- except Exception:
- pass
-
- if self.strRepr is None:
- self.strRepr = "quoted string, starting with %s ending with %s" % (self.quoteChar, self.endQuoteChar)
-
- return self.strRepr
-
-
-class CharsNotIn(Token):
- """Token for matching words composed of characters *not* in a given
- set (will include whitespace in matched characters if not listed in
- the provided exclusion set - see example). Defined with string
- containing all disallowed characters, and an optional minimum,
- maximum, and/or exact length. The default value for ``min`` is
- 1 (a minimum value < 1 is not valid); the default values for
- ``max`` and ``exact`` are 0, meaning no maximum or exact
- length restriction.
-
- Example::
-
- # define a comma-separated-value as anything that is not a ','
- csv_value = CharsNotIn(',')
- print(delimitedList(csv_value).parseString("dkls,lsdkjf,s12 34,@!#,213"))
-
- prints::
-
- ['dkls', 'lsdkjf', 's12 34', '@!#', '213']
- """
- def __init__(self, notChars, min=1, max=0, exact=0):
- super(CharsNotIn, self).__init__()
- self.skipWhitespace = False
- self.notChars = notChars
-
- if min < 1:
- raise ValueError("cannot specify a minimum length < 1; use "
- "Optional(CharsNotIn()) if zero-length char group is permitted")
-
- self.minLen = min
-
- if max > 0:
- self.maxLen = max
- else:
- self.maxLen = _MAX_INT
-
- if exact > 0:
- self.maxLen = exact
- self.minLen = exact
-
- self.name = _ustr(self)
- self.errmsg = "Expected " + self.name
- self.mayReturnEmpty = (self.minLen == 0)
- self.mayIndexError = False
-
- def parseImpl(self, instring, loc, doActions=True):
- if instring[loc] in self.notChars:
- raise ParseException(instring, loc, self.errmsg, self)
-
- start = loc
- loc += 1
- notchars = self.notChars
- maxlen = min(start + self.maxLen, len(instring))
- while loc < maxlen and instring[loc] not in notchars:
- loc += 1
-
- if loc - start < self.minLen:
- raise ParseException(instring, loc, self.errmsg, self)
-
- return loc, instring[start:loc]
-
- def __str__(self):
- try:
- return super(CharsNotIn, self).__str__()
- except Exception:
- pass
-
- if self.strRepr is None:
- if len(self.notChars) > 4:
- self.strRepr = "!W:(%s...)" % self.notChars[:4]
- else:
- self.strRepr = "!W:(%s)" % self.notChars
-
- return self.strRepr
-
-class White(Token):
- """Special matching class for matching whitespace. Normally,
- whitespace is ignored by pyparsing grammars. This class is included
- when some whitespace structures are significant. Define with
- a string containing the whitespace characters to be matched; default
- is ``" \\t\\r\\n"``. Also takes optional ``min``,
- ``max``, and ``exact`` arguments, as defined for the
- :class:`Word` class.
- """
- whiteStrs = {
- ' ' : '<SP>',
- '\t': '<TAB>',
- '\n': '<LF>',
- '\r': '<CR>',
- '\f': '<FF>',
- u'\u00A0': '<NBSP>',
- u'\u1680': '<OGHAM_SPACE_MARK>',
- u'\u180E': '<MONGOLIAN_VOWEL_SEPARATOR>',
- u'\u2000': '<EN_QUAD>',
- u'\u2001': '<EM_QUAD>',
- u'\u2002': '<EN_SPACE>',
- u'\u2003': '<EM_SPACE>',
- u'\u2004': '<THREE-PER-EM_SPACE>',
- u'\u2005': '<FOUR-PER-EM_SPACE>',
- u'\u2006': '<SIX-PER-EM_SPACE>',
- u'\u2007': '<FIGURE_SPACE>',
- u'\u2008': '<PUNCTUATION_SPACE>',
- u'\u2009': '<THIN_SPACE>',
- u'\u200A': '<HAIR_SPACE>',
- u'\u200B': '<ZERO_WIDTH_SPACE>',
- u'\u202F': '<NNBSP>',
- u'\u205F': '<MMSP>',
- u'\u3000': '<IDEOGRAPHIC_SPACE>',
- }
- def __init__(self, ws=" \t\r\n", min=1, max=0, exact=0):
- super(White, self).__init__()
- self.matchWhite = ws
- self.setWhitespaceChars("".join(c for c in self.whiteChars if c not in self.matchWhite))
- # ~ self.leaveWhitespace()
- self.name = ("".join(White.whiteStrs[c] for c in self.matchWhite))
- self.mayReturnEmpty = True
- self.errmsg = "Expected " + self.name
-
- self.minLen = min
-
- if max > 0:
- self.maxLen = max
- else:
- self.maxLen = _MAX_INT
-
- if exact > 0:
- self.maxLen = exact
- self.minLen = exact
-
- def parseImpl(self, instring, loc, doActions=True):
- if instring[loc] not in self.matchWhite:
- raise ParseException(instring, loc, self.errmsg, self)
- start = loc
- loc += 1
- maxloc = start + self.maxLen
- maxloc = min(maxloc, len(instring))
- while loc < maxloc and instring[loc] in self.matchWhite:
- loc += 1
-
- if loc - start < self.minLen:
- raise ParseException(instring, loc, self.errmsg, self)
-
- return loc, instring[start:loc]
-
-
-class _PositionToken(Token):
- def __init__(self):
- super(_PositionToken, self).__init__()
- self.name = self.__class__.__name__
- self.mayReturnEmpty = True
- self.mayIndexError = False
-
-class GoToColumn(_PositionToken):
- """Token to advance to a specific column of input text; useful for
- tabular report scraping.
- """
- def __init__(self, colno):
- super(GoToColumn, self).__init__()
- self.col = colno
-
- def preParse(self, instring, loc):
- if col(loc, instring) != self.col:
- instrlen = len(instring)
- if self.ignoreExprs:
- loc = self._skipIgnorables(instring, loc)
- while loc < instrlen and instring[loc].isspace() and col(loc, instring) != self.col:
- loc += 1
- return loc
-
- def parseImpl(self, instring, loc, doActions=True):
- thiscol = col(loc, instring)
- if thiscol > self.col:
- raise ParseException(instring, loc, "Text not in expected column", self)
- newloc = loc + self.col - thiscol
- ret = instring[loc: newloc]
- return newloc, ret
-
-
-class LineStart(_PositionToken):
- r"""Matches if current position is at the beginning of a line within
- the parse string
-
- Example::
-
- test = '''\
- AAA this line
- AAA and this line
- AAA but not this one
- B AAA and definitely not this one
- '''
-
- for t in (LineStart() + 'AAA' + restOfLine).searchString(test):
- print(t)
-
- prints::
-
- ['AAA', ' this line']
- ['AAA', ' and this line']
-
- """
- def __init__(self):
- super(LineStart, self).__init__()
- self.errmsg = "Expected start of line"
-
- def parseImpl(self, instring, loc, doActions=True):
- if col(loc, instring) == 1:
- return loc, []
- raise ParseException(instring, loc, self.errmsg, self)
-
-class LineEnd(_PositionToken):
- """Matches if current position is at the end of a line within the
- parse string
- """
- def __init__(self):
- super(LineEnd, self).__init__()
- self.setWhitespaceChars(ParserElement.DEFAULT_WHITE_CHARS.replace("\n", ""))
- self.errmsg = "Expected end of line"
-
- def parseImpl(self, instring, loc, doActions=True):
- if loc < len(instring):
- if instring[loc] == "\n":
- return loc + 1, "\n"
- else:
- raise ParseException(instring, loc, self.errmsg, self)
- elif loc == len(instring):
- return loc + 1, []
- else:
- raise ParseException(instring, loc, self.errmsg, self)
-
-class StringStart(_PositionToken):
- """Matches if current position is at the beginning of the parse
- string
- """
- def __init__(self):
- super(StringStart, self).__init__()
- self.errmsg = "Expected start of text"
-
- def parseImpl(self, instring, loc, doActions=True):
- if loc != 0:
- # see if entire string up to here is just whitespace and ignoreables
- if loc != self.preParse(instring, 0):
- raise ParseException(instring, loc, self.errmsg, self)
- return loc, []
-
-class StringEnd(_PositionToken):
- """Matches if current position is at the end of the parse string
- """
- def __init__(self):
- super(StringEnd, self).__init__()
- self.errmsg = "Expected end of text"
-
- def parseImpl(self, instring, loc, doActions=True):
- if loc < len(instring):
- raise ParseException(instring, loc, self.errmsg, self)
- elif loc == len(instring):
- return loc + 1, []
- elif loc > len(instring):
- return loc, []
- else:
- raise ParseException(instring, loc, self.errmsg, self)
-
-class WordStart(_PositionToken):
- """Matches if the current position is at the beginning of a Word,
- and is not preceded by any character in a given set of
- ``wordChars`` (default= ``printables``). To emulate the
- ``\b`` behavior of regular expressions, use
- ``WordStart(alphanums)``. ``WordStart`` will also match at
- the beginning of the string being parsed, or at the beginning of
- a line.
- """
- def __init__(self, wordChars=printables):
- super(WordStart, self).__init__()
- self.wordChars = set(wordChars)
- self.errmsg = "Not at the start of a word"
-
- def parseImpl(self, instring, loc, doActions=True):
- if loc != 0:
- if (instring[loc - 1] in self.wordChars
- or instring[loc] not in self.wordChars):
- raise ParseException(instring, loc, self.errmsg, self)
- return loc, []
-
-class WordEnd(_PositionToken):
- """Matches if the current position is at the end of a Word, and is
- not followed by any character in a given set of ``wordChars``
- (default= ``printables``). To emulate the ``\b`` behavior of
- regular expressions, use ``WordEnd(alphanums)``. ``WordEnd``
- will also match at the end of the string being parsed, or at the end
- of a line.
- """
- def __init__(self, wordChars=printables):
- super(WordEnd, self).__init__()
- self.wordChars = set(wordChars)
- self.skipWhitespace = False
- self.errmsg = "Not at the end of a word"
-
- def parseImpl(self, instring, loc, doActions=True):
- instrlen = len(instring)
- if instrlen > 0 and loc < instrlen:
- if (instring[loc] in self.wordChars or
- instring[loc - 1] not in self.wordChars):
- raise ParseException(instring, loc, self.errmsg, self)
- return loc, []
-
-
-class ParseExpression(ParserElement):
- """Abstract subclass of ParserElement, for combining and
- post-processing parsed tokens.
- """
- def __init__(self, exprs, savelist=False):
- super(ParseExpression, self).__init__(savelist)
- if isinstance(exprs, _generatorType):
- exprs = list(exprs)
-
- if isinstance(exprs, basestring):
- self.exprs = [self._literalStringClass(exprs)]
- elif isinstance(exprs, ParserElement):
- self.exprs = [exprs]
- elif isinstance(exprs, Iterable):
- exprs = list(exprs)
- # if sequence of strings provided, wrap with Literal
- if any(isinstance(expr, basestring) for expr in exprs):
- exprs = (self._literalStringClass(e) if isinstance(e, basestring) else e for e in exprs)
- self.exprs = list(exprs)
- else:
- try:
- self.exprs = list(exprs)
- except TypeError:
- self.exprs = [exprs]
- self.callPreparse = False
-
- def append(self, other):
- self.exprs.append(other)
- self.strRepr = None
- return self
-
- def leaveWhitespace(self):
- """Extends ``leaveWhitespace`` defined in base class, and also invokes ``leaveWhitespace`` on
- all contained expressions."""
- self.skipWhitespace = False
- self.exprs = [e.copy() for e in self.exprs]
- for e in self.exprs:
- e.leaveWhitespace()
- return self
-
- def ignore(self, other):
- if isinstance(other, Suppress):
- if other not in self.ignoreExprs:
- super(ParseExpression, self).ignore(other)
- for e in self.exprs:
- e.ignore(self.ignoreExprs[-1])
- else:
- super(ParseExpression, self).ignore(other)
- for e in self.exprs:
- e.ignore(self.ignoreExprs[-1])
- return self
-
- def __str__(self):
- try:
- return super(ParseExpression, self).__str__()
- except Exception:
- pass
-
- if self.strRepr is None:
- self.strRepr = "%s:(%s)" % (self.__class__.__name__, _ustr(self.exprs))
- return self.strRepr
-
- def streamline(self):
- super(ParseExpression, self).streamline()
-
- for e in self.exprs:
- e.streamline()
-
- # collapse nested And's of the form And(And(And(a, b), c), d) to And(a, b, c, d)
- # but only if there are no parse actions or resultsNames on the nested And's
- # (likewise for Or's and MatchFirst's)
- if len(self.exprs) == 2:
- other = self.exprs[0]
- if (isinstance(other, self.__class__)
- and not other.parseAction
- and other.resultsName is None
- and not other.debug):
- self.exprs = other.exprs[:] + [self.exprs[1]]
- self.strRepr = None
- self.mayReturnEmpty |= other.mayReturnEmpty
- self.mayIndexError |= other.mayIndexError
-
- other = self.exprs[-1]
- if (isinstance(other, self.__class__)
- and not other.parseAction
- and other.resultsName is None
- and not other.debug):
- self.exprs = self.exprs[:-1] + other.exprs[:]
- self.strRepr = None
- self.mayReturnEmpty |= other.mayReturnEmpty
- self.mayIndexError |= other.mayIndexError
-
- self.errmsg = "Expected " + _ustr(self)
-
- return self
-
- def validate(self, validateTrace=None):
- tmp = (validateTrace if validateTrace is not None else [])[:] + [self]
- for e in self.exprs:
- e.validate(tmp)
- self.checkRecursion([])
-
- def copy(self):
- ret = super(ParseExpression, self).copy()
- ret.exprs = [e.copy() for e in self.exprs]
- return ret
-
- def _setResultsName(self, name, listAllMatches=False):
- if __diag__.warn_ungrouped_named_tokens_in_collection:
- for e in self.exprs:
- if isinstance(e, ParserElement) and e.resultsName:
- warnings.warn("{0}: setting results name {1!r} on {2} expression "
- "collides with {3!r} on contained expression".format("warn_ungrouped_named_tokens_in_collection",
- name,
- type(self).__name__,
- e.resultsName),
- stacklevel=3)
-
- return super(ParseExpression, self)._setResultsName(name, listAllMatches)
-
-
-class And(ParseExpression):
- """
- Requires all given :class:`ParseExpression` s to be found in the given order.
- Expressions may be separated by whitespace.
- May be constructed using the ``'+'`` operator.
- May also be constructed using the ``'-'`` operator, which will
- suppress backtracking.
-
- Example::
-
- integer = Word(nums)
- name_expr = OneOrMore(Word(alphas))
-
- expr = And([integer("id"), name_expr("name"), integer("age")])
- # more easily written as:
- expr = integer("id") + name_expr("name") + integer("age")
- """
-
- class _ErrorStop(Empty):
- def __init__(self, *args, **kwargs):
- super(And._ErrorStop, self).__init__(*args, **kwargs)
- self.name = '-'
- self.leaveWhitespace()
-
- def __init__(self, exprs, savelist=True):
- exprs = list(exprs)
- if exprs and Ellipsis in exprs:
- tmp = []
- for i, expr in enumerate(exprs):
- if expr is Ellipsis:
- if i < len(exprs) - 1:
- skipto_arg = (Empty() + exprs[i + 1]).exprs[-1]
- tmp.append(SkipTo(skipto_arg)("_skipped*"))
- else:
- raise Exception("cannot construct And with sequence ending in ...")
- else:
- tmp.append(expr)
- exprs[:] = tmp
- super(And, self).__init__(exprs, savelist)
- self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
- self.setWhitespaceChars(self.exprs[0].whiteChars)
- self.skipWhitespace = self.exprs[0].skipWhitespace
- self.callPreparse = True
-
- def streamline(self):
- # collapse any _PendingSkip's
- if self.exprs:
- if any(isinstance(e, ParseExpression) and e.exprs and isinstance(e.exprs[-1], _PendingSkip)
- for e in self.exprs[:-1]):
- for i, e in enumerate(self.exprs[:-1]):
- if e is None:
- continue
- if (isinstance(e, ParseExpression)
- and e.exprs and isinstance(e.exprs[-1], _PendingSkip)):
- e.exprs[-1] = e.exprs[-1] + self.exprs[i + 1]
- self.exprs[i + 1] = None
- self.exprs = [e for e in self.exprs if e is not None]
-
- super(And, self).streamline()
- self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
- return self
-
- def parseImpl(self, instring, loc, doActions=True):
- # pass False as last arg to _parse for first element, since we already
- # pre-parsed the string as part of our And pre-parsing
- loc, resultlist = self.exprs[0]._parse(instring, loc, doActions, callPreParse=False)
- errorStop = False
- for e in self.exprs[1:]:
- if isinstance(e, And._ErrorStop):
- errorStop = True
- continue
- if errorStop:
- try:
- loc, exprtokens = e._parse(instring, loc, doActions)
- except ParseSyntaxException:
- raise
- except ParseBaseException as pe:
- pe.__traceback__ = None
- raise ParseSyntaxException._from_exception(pe)
- except IndexError:
- raise ParseSyntaxException(instring, len(instring), self.errmsg, self)
- else:
- loc, exprtokens = e._parse(instring, loc, doActions)
- if exprtokens or exprtokens.haskeys():
- resultlist += exprtokens
- return loc, resultlist
-
- def __iadd__(self, other):
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- return self.append(other) # And([self, other])
-
- def checkRecursion(self, parseElementList):
- subRecCheckList = parseElementList[:] + [self]
- for e in self.exprs:
- e.checkRecursion(subRecCheckList)
- if not e.mayReturnEmpty:
- break
-
- def __str__(self):
- if hasattr(self, "name"):
- return self.name
-
- if self.strRepr is None:
- self.strRepr = "{" + " ".join(_ustr(e) for e in self.exprs) + "}"
-
- return self.strRepr
-
-
-class Or(ParseExpression):
- """Requires that at least one :class:`ParseExpression` is found. If
- two expressions match, the expression that matches the longest
- string will be used. May be constructed using the ``'^'``
- operator.
-
- Example::
-
- # construct Or using '^' operator
-
- number = Word(nums) ^ Combine(Word(nums) + '.' + Word(nums))
- print(number.searchString("123 3.1416 789"))
-
- prints::
-
- [['123'], ['3.1416'], ['789']]
- """
- def __init__(self, exprs, savelist=False):
- super(Or, self).__init__(exprs, savelist)
- if self.exprs:
- self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)
- else:
- self.mayReturnEmpty = True
-
- def streamline(self):
- super(Or, self).streamline()
- if __compat__.collect_all_And_tokens:
- self.saveAsList = any(e.saveAsList for e in self.exprs)
- return self
-
- def parseImpl(self, instring, loc, doActions=True):
- maxExcLoc = -1
- maxException = None
- matches = []
- for e in self.exprs:
- try:
- loc2 = e.tryParse(instring, loc)
- except ParseException as err:
- err.__traceback__ = None
- if err.loc > maxExcLoc:
- maxException = err
- maxExcLoc = err.loc
- except IndexError:
- if len(instring) > maxExcLoc:
- maxException = ParseException(instring, len(instring), e.errmsg, self)
- maxExcLoc = len(instring)
- else:
- # save match among all matches, to retry longest to shortest
- matches.append((loc2, e))
-
- if matches:
- # re-evaluate all matches in descending order of length of match, in case attached actions
- # might change whether or how much they match of the input.
- matches.sort(key=itemgetter(0), reverse=True)
-
- if not doActions:
- # no further conditions or parse actions to change the selection of
- # alternative, so the first match will be the best match
- best_expr = matches[0][1]
- return best_expr._parse(instring, loc, doActions)
-
- longest = -1, None
- for loc1, expr1 in matches:
- if loc1 <= longest[0]:
- # already have a longer match than this one will deliver, we are done
- return longest
-
- try:
- loc2, toks = expr1._parse(instring, loc, doActions)
- except ParseException as err:
- err.__traceback__ = None
- if err.loc > maxExcLoc:
- maxException = err
- maxExcLoc = err.loc
- else:
- if loc2 >= loc1:
- return loc2, toks
- # didn't match as much as before
- elif loc2 > longest[0]:
- longest = loc2, toks
-
- if longest != (-1, None):
- return longest
-
- if maxException is not None:
- maxException.msg = self.errmsg
- raise maxException
- else:
- raise ParseException(instring, loc, "no defined alternatives to match", self)
-
-
- def __ixor__(self, other):
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- return self.append(other) # Or([self, other])
-
- def __str__(self):
- if hasattr(self, "name"):
- return self.name
-
- if self.strRepr is None:
- self.strRepr = "{" + " ^ ".join(_ustr(e) for e in self.exprs) + "}"
-
- return self.strRepr
-
- def checkRecursion(self, parseElementList):
- subRecCheckList = parseElementList[:] + [self]
- for e in self.exprs:
- e.checkRecursion(subRecCheckList)
-
- def _setResultsName(self, name, listAllMatches=False):
- if (not __compat__.collect_all_And_tokens
- and __diag__.warn_multiple_tokens_in_named_alternation):
- if any(isinstance(e, And) for e in self.exprs):
- warnings.warn("{0}: setting results name {1!r} on {2} expression "
- "may only return a single token for an And alternative, "
- "in future will return the full list of tokens".format(
- "warn_multiple_tokens_in_named_alternation", name, type(self).__name__),
- stacklevel=3)
-
- return super(Or, self)._setResultsName(name, listAllMatches)
-
-
-class MatchFirst(ParseExpression):
- """Requires that at least one :class:`ParseExpression` is found. If
- two expressions match, the first one listed is the one that will
- match. May be constructed using the ``'|'`` operator.
-
- Example::
-
- # construct MatchFirst using '|' operator
-
- # watch the order of expressions to match
- number = Word(nums) | Combine(Word(nums) + '.' + Word(nums))
- print(number.searchString("123 3.1416 789")) # Fail! -> [['123'], ['3'], ['1416'], ['789']]
-
- # put more selective expression first
- number = Combine(Word(nums) + '.' + Word(nums)) | Word(nums)
- print(number.searchString("123 3.1416 789")) # Better -> [['123'], ['3.1416'], ['789']]
- """
- def __init__(self, exprs, savelist=False):
- super(MatchFirst, self).__init__(exprs, savelist)
- if self.exprs:
- self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)
- else:
- self.mayReturnEmpty = True
-
- def streamline(self):
- super(MatchFirst, self).streamline()
- if __compat__.collect_all_And_tokens:
- self.saveAsList = any(e.saveAsList for e in self.exprs)
- return self
-
- def parseImpl(self, instring, loc, doActions=True):
- maxExcLoc = -1
- maxException = None
- for e in self.exprs:
- try:
- ret = e._parse(instring, loc, doActions)
- return ret
- except ParseException as err:
- if err.loc > maxExcLoc:
- maxException = err
- maxExcLoc = err.loc
- except IndexError:
- if len(instring) > maxExcLoc:
- maxException = ParseException(instring, len(instring), e.errmsg, self)
- maxExcLoc = len(instring)
-
- # only got here if no expression matched, raise exception for match that made it the furthest
- else:
- if maxException is not None:
- maxException.msg = self.errmsg
- raise maxException
- else:
- raise ParseException(instring, loc, "no defined alternatives to match", self)
-
- def __ior__(self, other):
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- return self.append(other) # MatchFirst([self, other])
-
- def __str__(self):
- if hasattr(self, "name"):
- return self.name
-
- if self.strRepr is None:
- self.strRepr = "{" + " | ".join(_ustr(e) for e in self.exprs) + "}"
-
- return self.strRepr
-
- def checkRecursion(self, parseElementList):
- subRecCheckList = parseElementList[:] + [self]
- for e in self.exprs:
- e.checkRecursion(subRecCheckList)
-
- def _setResultsName(self, name, listAllMatches=False):
- if (not __compat__.collect_all_And_tokens
- and __diag__.warn_multiple_tokens_in_named_alternation):
- if any(isinstance(e, And) for e in self.exprs):
- warnings.warn("{0}: setting results name {1!r} on {2} expression "
- "may only return a single token for an And alternative, "
- "in future will return the full list of tokens".format(
- "warn_multiple_tokens_in_named_alternation", name, type(self).__name__),
- stacklevel=3)
-
- return super(MatchFirst, self)._setResultsName(name, listAllMatches)
-
-
-class Each(ParseExpression):
- """Requires all given :class:`ParseExpression` s to be found, but in
- any order. Expressions may be separated by whitespace.
-
- May be constructed using the ``'&'`` operator.
-
- Example::
-
- color = oneOf("RED ORANGE YELLOW GREEN BLUE PURPLE BLACK WHITE BROWN")
- shape_type = oneOf("SQUARE CIRCLE TRIANGLE STAR HEXAGON OCTAGON")
- integer = Word(nums)
- shape_attr = "shape:" + shape_type("shape")
- posn_attr = "posn:" + Group(integer("x") + ',' + integer("y"))("posn")
- color_attr = "color:" + color("color")
- size_attr = "size:" + integer("size")
-
- # use Each (using operator '&') to accept attributes in any order
- # (shape and posn are required, color and size are optional)
- shape_spec = shape_attr & posn_attr & Optional(color_attr) & Optional(size_attr)
-
- shape_spec.runTests('''
- shape: SQUARE color: BLACK posn: 100, 120
- shape: CIRCLE size: 50 color: BLUE posn: 50,80
- color:GREEN size:20 shape:TRIANGLE posn:20,40
- '''
- )
-
- prints::
-
- shape: SQUARE color: BLACK posn: 100, 120
- ['shape:', 'SQUARE', 'color:', 'BLACK', 'posn:', ['100', ',', '120']]
- - color: BLACK
- - posn: ['100', ',', '120']
- - x: 100
- - y: 120
- - shape: SQUARE
-
-
- shape: CIRCLE size: 50 color: BLUE posn: 50,80
- ['shape:', 'CIRCLE', 'size:', '50', 'color:', 'BLUE', 'posn:', ['50', ',', '80']]
- - color: BLUE
- - posn: ['50', ',', '80']
- - x: 50
- - y: 80
- - shape: CIRCLE
- - size: 50
-
-
- color: GREEN size: 20 shape: TRIANGLE posn: 20,40
- ['color:', 'GREEN', 'size:', '20', 'shape:', 'TRIANGLE', 'posn:', ['20', ',', '40']]
- - color: GREEN
- - posn: ['20', ',', '40']
- - x: 20
- - y: 40
- - shape: TRIANGLE
- - size: 20
- """
- def __init__(self, exprs, savelist=True):
- super(Each, self).__init__(exprs, savelist)
- self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
- self.skipWhitespace = True
- self.initExprGroups = True
- self.saveAsList = True
-
- def streamline(self):
- super(Each, self).streamline()
- self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
- return self
-
- def parseImpl(self, instring, loc, doActions=True):
- if self.initExprGroups:
- self.opt1map = dict((id(e.expr), e) for e in self.exprs if isinstance(e, Optional))
- opt1 = [e.expr for e in self.exprs if isinstance(e, Optional)]
- opt2 = [e for e in self.exprs if e.mayReturnEmpty and not isinstance(e, (Optional, Regex))]
- self.optionals = opt1 + opt2
- self.multioptionals = [e.expr for e in self.exprs if isinstance(e, ZeroOrMore)]
- self.multirequired = [e.expr for e in self.exprs if isinstance(e, OneOrMore)]
- self.required = [e for e in self.exprs if not isinstance(e, (Optional, ZeroOrMore, OneOrMore))]
- self.required += self.multirequired
- self.initExprGroups = False
- tmpLoc = loc
- tmpReqd = self.required[:]
- tmpOpt = self.optionals[:]
- matchOrder = []
-
- keepMatching = True
- while keepMatching:
- tmpExprs = tmpReqd + tmpOpt + self.multioptionals + self.multirequired
- failed = []
- for e in tmpExprs:
- try:
- tmpLoc = e.tryParse(instring, tmpLoc)
- except ParseException:
- failed.append(e)
- else:
- matchOrder.append(self.opt1map.get(id(e), e))
- if e in tmpReqd:
- tmpReqd.remove(e)
- elif e in tmpOpt:
- tmpOpt.remove(e)
- if len(failed) == len(tmpExprs):
- keepMatching = False
-
- if tmpReqd:
- missing = ", ".join(_ustr(e) for e in tmpReqd)
- raise ParseException(instring, loc, "Missing one or more required elements (%s)" % missing)
-
- # add any unmatched Optionals, in case they have default values defined
- matchOrder += [e for e in self.exprs if isinstance(e, Optional) and e.expr in tmpOpt]
-
- resultlist = []
- for e in matchOrder:
- loc, results = e._parse(instring, loc, doActions)
- resultlist.append(results)
-
- finalResults = sum(resultlist, ParseResults([]))
- return loc, finalResults
-
- def __str__(self):
- if hasattr(self, "name"):
- return self.name
-
- if self.strRepr is None:
- self.strRepr = "{" + " & ".join(_ustr(e) for e in self.exprs) + "}"
-
- return self.strRepr
-
- def checkRecursion(self, parseElementList):
- subRecCheckList = parseElementList[:] + [self]
- for e in self.exprs:
- e.checkRecursion(subRecCheckList)
-
-
-class ParseElementEnhance(ParserElement):
- """Abstract subclass of :class:`ParserElement`, for combining and
- post-processing parsed tokens.
- """
- def __init__(self, expr, savelist=False):
- super(ParseElementEnhance, self).__init__(savelist)
- if isinstance(expr, basestring):
- if issubclass(self._literalStringClass, Token):
- expr = self._literalStringClass(expr)
- else:
- expr = self._literalStringClass(Literal(expr))
- self.expr = expr
- self.strRepr = None
- if expr is not None:
- self.mayIndexError = expr.mayIndexError
- self.mayReturnEmpty = expr.mayReturnEmpty
- self.setWhitespaceChars(expr.whiteChars)
- self.skipWhitespace = expr.skipWhitespace
- self.saveAsList = expr.saveAsList
- self.callPreparse = expr.callPreparse
- self.ignoreExprs.extend(expr.ignoreExprs)
-
- def parseImpl(self, instring, loc, doActions=True):
- if self.expr is not None:
- return self.expr._parse(instring, loc, doActions, callPreParse=False)
- else:
- raise ParseException("", loc, self.errmsg, self)
-
- def leaveWhitespace(self):
- self.skipWhitespace = False
- self.expr = self.expr.copy()
- if self.expr is not None:
- self.expr.leaveWhitespace()
- return self
-
- def ignore(self, other):
- if isinstance(other, Suppress):
- if other not in self.ignoreExprs:
- super(ParseElementEnhance, self).ignore(other)
- if self.expr is not None:
- self.expr.ignore(self.ignoreExprs[-1])
- else:
- super(ParseElementEnhance, self).ignore(other)
- if self.expr is not None:
- self.expr.ignore(self.ignoreExprs[-1])
- return self
-
- def streamline(self):
- super(ParseElementEnhance, self).streamline()
- if self.expr is not None:
- self.expr.streamline()
- return self
-
- def checkRecursion(self, parseElementList):
- if self in parseElementList:
- raise RecursiveGrammarException(parseElementList + [self])
- subRecCheckList = parseElementList[:] + [self]
- if self.expr is not None:
- self.expr.checkRecursion(subRecCheckList)
-
- def validate(self, validateTrace=None):
- if validateTrace is None:
- validateTrace = []
- tmp = validateTrace[:] + [self]
- if self.expr is not None:
- self.expr.validate(tmp)
- self.checkRecursion([])
-
- def __str__(self):
- try:
- return super(ParseElementEnhance, self).__str__()
- except Exception:
- pass
-
- if self.strRepr is None and self.expr is not None:
- self.strRepr = "%s:(%s)" % (self.__class__.__name__, _ustr(self.expr))
- return self.strRepr
-
-
-class FollowedBy(ParseElementEnhance):
- """Lookahead matching of the given parse expression.
- ``FollowedBy`` does *not* advance the parsing position within
- the input string, it only verifies that the specified parse
- expression matches at the current position. ``FollowedBy``
- always returns a null token list. If any results names are defined
- in the lookahead expression, those *will* be returned for access by
- name.
-
- Example::
-
- # use FollowedBy to match a label only if it is followed by a ':'
- data_word = Word(alphas)
- label = data_word + FollowedBy(':')
- attr_expr = Group(label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))
-
- OneOrMore(attr_expr).parseString("shape: SQUARE color: BLACK posn: upper left").pprint()
-
- prints::
-
- [['shape', 'SQUARE'], ['color', 'BLACK'], ['posn', 'upper left']]
- """
- def __init__(self, expr):
- super(FollowedBy, self).__init__(expr)
- self.mayReturnEmpty = True
-
- def parseImpl(self, instring, loc, doActions=True):
- # by using self._expr.parse and deleting the contents of the returned ParseResults list
- # we keep any named results that were defined in the FollowedBy expression
- _, ret = self.expr._parse(instring, loc, doActions=doActions)
- del ret[:]
-
- return loc, ret
-
-
-class PrecededBy(ParseElementEnhance):
- """Lookbehind matching of the given parse expression.
- ``PrecededBy`` does not advance the parsing position within the
- input string, it only verifies that the specified parse expression
- matches prior to the current position. ``PrecededBy`` always
- returns a null token list, but if a results name is defined on the
- given expression, it is returned.
-
- Parameters:
-
- - expr - expression that must match prior to the current parse
- location
- - retreat - (default= ``None``) - (int) maximum number of characters
- to lookbehind prior to the current parse location
-
- If the lookbehind expression is a string, Literal, Keyword, or
- a Word or CharsNotIn with a specified exact or maximum length, then
- the retreat parameter is not required. Otherwise, retreat must be
- specified to give a maximum number of characters to look back from
- the current parse position for a lookbehind match.
-
- Example::
-
- # VB-style variable names with type prefixes
- int_var = PrecededBy("#") + pyparsing_common.identifier
- str_var = PrecededBy("$") + pyparsing_common.identifier
-
- """
- def __init__(self, expr, retreat=None):
- super(PrecededBy, self).__init__(expr)
- self.expr = self.expr().leaveWhitespace()
- self.mayReturnEmpty = True
- self.mayIndexError = False
- self.exact = False
- if isinstance(expr, str):
- retreat = len(expr)
- self.exact = True
- elif isinstance(expr, (Literal, Keyword)):
- retreat = expr.matchLen
- self.exact = True
- elif isinstance(expr, (Word, CharsNotIn)) and expr.maxLen != _MAX_INT:
- retreat = expr.maxLen
- self.exact = True
- elif isinstance(expr, _PositionToken):
- retreat = 0
- self.exact = True
- self.retreat = retreat
- self.errmsg = "not preceded by " + str(expr)
- self.skipWhitespace = False
- self.parseAction.append(lambda s, l, t: t.__delitem__(slice(None, None)))
-
- def parseImpl(self, instring, loc=0, doActions=True):
- if self.exact:
- if loc < self.retreat:
- raise ParseException(instring, loc, self.errmsg)
- start = loc - self.retreat
- _, ret = self.expr._parse(instring, start)
- else:
- # retreat specified a maximum lookbehind window, iterate
- test_expr = self.expr + StringEnd()
- instring_slice = instring[max(0, loc - self.retreat):loc]
- last_expr = ParseException(instring, loc, self.errmsg)
- for offset in range(1, min(loc, self.retreat + 1)+1):
- try:
- # print('trying', offset, instring_slice, repr(instring_slice[loc - offset:]))
- _, ret = test_expr._parse(instring_slice, len(instring_slice) - offset)
- except ParseBaseException as pbe:
- last_expr = pbe
- else:
- break
- else:
- raise last_expr
- return loc, ret
-
-
-class NotAny(ParseElementEnhance):
- """Lookahead to disallow matching with the given parse expression.
- ``NotAny`` does *not* advance the parsing position within the
- input string, it only verifies that the specified parse expression
- does *not* match at the current position. Also, ``NotAny`` does
- *not* skip over leading whitespace. ``NotAny`` always returns
- a null token list. May be constructed using the '~' operator.
-
- Example::
-
- AND, OR, NOT = map(CaselessKeyword, "AND OR NOT".split())
-
- # take care not to mistake keywords for identifiers
- ident = ~(AND | OR | NOT) + Word(alphas)
- boolean_term = Optional(NOT) + ident
-
- # very crude boolean expression - to support parenthesis groups and
- # operation hierarchy, use infixNotation
- boolean_expr = boolean_term + ZeroOrMore((AND | OR) + boolean_term)
-
- # integers that are followed by "." are actually floats
- integer = Word(nums) + ~Char(".")
- """
- def __init__(self, expr):
- super(NotAny, self).__init__(expr)
- # ~ self.leaveWhitespace()
- self.skipWhitespace = False # do NOT use self.leaveWhitespace(), don't want to propagate to exprs
- self.mayReturnEmpty = True
- self.errmsg = "Found unwanted token, " + _ustr(self.expr)
-
- def parseImpl(self, instring, loc, doActions=True):
- if self.expr.canParseNext(instring, loc):
- raise ParseException(instring, loc, self.errmsg, self)
- return loc, []
-
- def __str__(self):
- if hasattr(self, "name"):
- return self.name
-
- if self.strRepr is None:
- self.strRepr = "~{" + _ustr(self.expr) + "}"
-
- return self.strRepr
-
-class _MultipleMatch(ParseElementEnhance):
- def __init__(self, expr, stopOn=None):
- super(_MultipleMatch, self).__init__(expr)
- self.saveAsList = True
- ender = stopOn
- if isinstance(ender, basestring):
- ender = self._literalStringClass(ender)
- self.stopOn(ender)
-
- def stopOn(self, ender):
- if isinstance(ender, basestring):
- ender = self._literalStringClass(ender)
- self.not_ender = ~ender if ender is not None else None
- return self
-
- def parseImpl(self, instring, loc, doActions=True):
- self_expr_parse = self.expr._parse
- self_skip_ignorables = self._skipIgnorables
- check_ender = self.not_ender is not None
- if check_ender:
- try_not_ender = self.not_ender.tryParse
-
- # must be at least one (but first see if we are the stopOn sentinel;
- # if so, fail)
- if check_ender:
- try_not_ender(instring, loc)
- loc, tokens = self_expr_parse(instring, loc, doActions, callPreParse=False)
- try:
- hasIgnoreExprs = (not not self.ignoreExprs)
- while 1:
- if check_ender:
- try_not_ender(instring, loc)
- if hasIgnoreExprs:
- preloc = self_skip_ignorables(instring, loc)
- else:
- preloc = loc
- loc, tmptokens = self_expr_parse(instring, preloc, doActions)
- if tmptokens or tmptokens.haskeys():
- tokens += tmptokens
- except (ParseException, IndexError):
- pass
-
- return loc, tokens
-
- def _setResultsName(self, name, listAllMatches=False):
- if __diag__.warn_ungrouped_named_tokens_in_collection:
- for e in [self.expr] + getattr(self.expr, 'exprs', []):
- if isinstance(e, ParserElement) and e.resultsName:
- warnings.warn("{0}: setting results name {1!r} on {2} expression "
- "collides with {3!r} on contained expression".format("warn_ungrouped_named_tokens_in_collection",
- name,
- type(self).__name__,
- e.resultsName),
- stacklevel=3)
-
- return super(_MultipleMatch, self)._setResultsName(name, listAllMatches)
-
-
-class OneOrMore(_MultipleMatch):
- """Repetition of one or more of the given expression.
-
- Parameters:
- - expr - expression that must match one or more times
- - stopOn - (default= ``None``) - expression for a terminating sentinel
- (only required if the sentinel would ordinarily match the repetition
- expression)
-
- Example::
-
- data_word = Word(alphas)
- label = data_word + FollowedBy(':')
- attr_expr = Group(label + Suppress(':') + OneOrMore(data_word).setParseAction(' '.join))
-
- text = "shape: SQUARE posn: upper left color: BLACK"
- OneOrMore(attr_expr).parseString(text).pprint() # Fail! read 'color' as data instead of next label -> [['shape', 'SQUARE color']]
-
- # use stopOn attribute for OneOrMore to avoid reading label string as part of the data
- attr_expr = Group(label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))
- OneOrMore(attr_expr).parseString(text).pprint() # Better -> [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'BLACK']]
-
- # could also be written as
- (attr_expr * (1,)).parseString(text).pprint()
- """
-
- def __str__(self):
- if hasattr(self, "name"):
- return self.name
-
- if self.strRepr is None:
- self.strRepr = "{" + _ustr(self.expr) + "}..."
-
- return self.strRepr
-
-class ZeroOrMore(_MultipleMatch):
- """Optional repetition of zero or more of the given expression.
-
- Parameters:
- - expr - expression that must match zero or more times
- - stopOn - (default= ``None``) - expression for a terminating sentinel
- (only required if the sentinel would ordinarily match the repetition
- expression)
-
- Example: similar to :class:`OneOrMore`
- """
- def __init__(self, expr, stopOn=None):
- super(ZeroOrMore, self).__init__(expr, stopOn=stopOn)
- self.mayReturnEmpty = True
-
- def parseImpl(self, instring, loc, doActions=True):
- try:
- return super(ZeroOrMore, self).parseImpl(instring, loc, doActions)
- except (ParseException, IndexError):
- return loc, []
-
- def __str__(self):
- if hasattr(self, "name"):
- return self.name
-
- if self.strRepr is None:
- self.strRepr = "[" + _ustr(self.expr) + "]..."
-
- return self.strRepr
-
-
-class _NullToken(object):
- def __bool__(self):
- return False
- __nonzero__ = __bool__
- def __str__(self):
- return ""
-
-class Optional(ParseElementEnhance):
- """Optional matching of the given expression.
-
- Parameters:
- - expr - expression that must match zero or more times
- - default (optional) - value to be returned if the optional expression is not found.
-
- Example::
-
- # US postal code can be a 5-digit zip, plus optional 4-digit qualifier
- zip = Combine(Word(nums, exact=5) + Optional('-' + Word(nums, exact=4)))
- zip.runTests('''
- # traditional ZIP code
- 12345
-
- # ZIP+4 form
- 12101-0001
-
- # invalid ZIP
- 98765-
- ''')
-
- prints::
-
- # traditional ZIP code
- 12345
- ['12345']
-
- # ZIP+4 form
- 12101-0001
- ['12101-0001']
-
- # invalid ZIP
- 98765-
- ^
- FAIL: Expected end of text (at char 5), (line:1, col:6)
- """
- __optionalNotMatched = _NullToken()
-
- def __init__(self, expr, default=__optionalNotMatched):
- super(Optional, self).__init__(expr, savelist=False)
- self.saveAsList = self.expr.saveAsList
- self.defaultValue = default
- self.mayReturnEmpty = True
-
- def parseImpl(self, instring, loc, doActions=True):
- try:
- loc, tokens = self.expr._parse(instring, loc, doActions, callPreParse=False)
- except (ParseException, IndexError):
- if self.defaultValue is not self.__optionalNotMatched:
- if self.expr.resultsName:
- tokens = ParseResults([self.defaultValue])
- tokens[self.expr.resultsName] = self.defaultValue
- else:
- tokens = [self.defaultValue]
- else:
- tokens = []
- return loc, tokens
-
- def __str__(self):
- if hasattr(self, "name"):
- return self.name
-
- if self.strRepr is None:
- self.strRepr = "[" + _ustr(self.expr) + "]"
-
- return self.strRepr
-
-class SkipTo(ParseElementEnhance):
- """Token for skipping over all undefined text until the matched
- expression is found.
-
- Parameters:
- - expr - target expression marking the end of the data to be skipped
- - include - (default= ``False``) if True, the target expression is also parsed
- (the skipped text and target expression are returned as a 2-element list).
- - ignore - (default= ``None``) used to define grammars (typically quoted strings and
- comments) that might contain false matches to the target expression
- - failOn - (default= ``None``) define expressions that are not allowed to be
- included in the skipped test; if found before the target expression is found,
- the SkipTo is not a match
-
- Example::
-
- report = '''
- Outstanding Issues Report - 1 Jan 2000
-
- # | Severity | Description | Days Open
- -----+----------+-------------------------------------------+-----------
- 101 | Critical | Intermittent system crash | 6
- 94 | Cosmetic | Spelling error on Login ('log|n') | 14
- 79 | Minor | System slow when running too many reports | 47
- '''
- integer = Word(nums)
- SEP = Suppress('|')
- # use SkipTo to simply match everything up until the next SEP
- # - ignore quoted strings, so that a '|' character inside a quoted string does not match
- # - parse action will call token.strip() for each matched token, i.e., the description body
- string_data = SkipTo(SEP, ignore=quotedString)
- string_data.setParseAction(tokenMap(str.strip))
- ticket_expr = (integer("issue_num") + SEP
- + string_data("sev") + SEP
- + string_data("desc") + SEP
- + integer("days_open"))
-
- for tkt in ticket_expr.searchString(report):
- print tkt.dump()
-
- prints::
-
- ['101', 'Critical', 'Intermittent system crash', '6']
- - days_open: 6
- - desc: Intermittent system crash
- - issue_num: 101
- - sev: Critical
- ['94', 'Cosmetic', "Spelling error on Login ('log|n')", '14']
- - days_open: 14
- - desc: Spelling error on Login ('log|n')
- - issue_num: 94
- - sev: Cosmetic
- ['79', 'Minor', 'System slow when running too many reports', '47']
- - days_open: 47
- - desc: System slow when running too many reports
- - issue_num: 79
- - sev: Minor
- """
- def __init__(self, other, include=False, ignore=None, failOn=None):
- super(SkipTo, self).__init__(other)
- self.ignoreExpr = ignore
- self.mayReturnEmpty = True
- self.mayIndexError = False
- self.includeMatch = include
- self.saveAsList = False
- if isinstance(failOn, basestring):
- self.failOn = self._literalStringClass(failOn)
- else:
- self.failOn = failOn
- self.errmsg = "No match found for " + _ustr(self.expr)
-
- def parseImpl(self, instring, loc, doActions=True):
- startloc = loc
- instrlen = len(instring)
- expr = self.expr
- expr_parse = self.expr._parse
- self_failOn_canParseNext = self.failOn.canParseNext if self.failOn is not None else None
- self_ignoreExpr_tryParse = self.ignoreExpr.tryParse if self.ignoreExpr is not None else None
-
- tmploc = loc
- while tmploc <= instrlen:
- if self_failOn_canParseNext is not None:
- # break if failOn expression matches
- if self_failOn_canParseNext(instring, tmploc):
- break
-
- if self_ignoreExpr_tryParse is not None:
- # advance past ignore expressions
- while 1:
- try:
- tmploc = self_ignoreExpr_tryParse(instring, tmploc)
- except ParseBaseException:
- break
-
- try:
- expr_parse(instring, tmploc, doActions=False, callPreParse=False)
- except (ParseException, IndexError):
- # no match, advance loc in string
- tmploc += 1
- else:
- # matched skipto expr, done
- break
-
- else:
- # ran off the end of the input string without matching skipto expr, fail
- raise ParseException(instring, loc, self.errmsg, self)
-
- # build up return values
- loc = tmploc
- skiptext = instring[startloc:loc]
- skipresult = ParseResults(skiptext)
-
- if self.includeMatch:
- loc, mat = expr_parse(instring, loc, doActions, callPreParse=False)
- skipresult += mat
-
- return loc, skipresult
-
-class Forward(ParseElementEnhance):
- """Forward declaration of an expression to be defined later -
- used for recursive grammars, such as algebraic infix notation.
- When the expression is known, it is assigned to the ``Forward``
- variable using the '<<' operator.
-
- Note: take care when assigning to ``Forward`` not to overlook
- precedence of operators.
-
- Specifically, '|' has a lower precedence than '<<', so that::
-
- fwdExpr << a | b | c
-
- will actually be evaluated as::
-
- (fwdExpr << a) | b | c
-
- thereby leaving b and c out as parseable alternatives. It is recommended that you
- explicitly group the values inserted into the ``Forward``::
-
- fwdExpr << (a | b | c)
-
- Converting to use the '<<=' operator instead will avoid this problem.
-
- See :class:`ParseResults.pprint` for an example of a recursive
- parser created using ``Forward``.
- """
- def __init__(self, other=None):
- super(Forward, self).__init__(other, savelist=False)
-
- def __lshift__(self, other):
- if isinstance(other, basestring):
- other = self._literalStringClass(other)
- self.expr = other
- self.strRepr = None
- self.mayIndexError = self.expr.mayIndexError
- self.mayReturnEmpty = self.expr.mayReturnEmpty
- self.setWhitespaceChars(self.expr.whiteChars)
- self.skipWhitespace = self.expr.skipWhitespace
- self.saveAsList = self.expr.saveAsList
- self.ignoreExprs.extend(self.expr.ignoreExprs)
- return self
-
- def __ilshift__(self, other):
- return self << other
-
- def leaveWhitespace(self):
- self.skipWhitespace = False
- return self
-
- def streamline(self):
- if not self.streamlined:
- self.streamlined = True
- if self.expr is not None:
- self.expr.streamline()
- return self
-
- def validate(self, validateTrace=None):
- if validateTrace is None:
- validateTrace = []
-
- if self not in validateTrace:
- tmp = validateTrace[:] + [self]
- if self.expr is not None:
- self.expr.validate(tmp)
- self.checkRecursion([])
-
- def __str__(self):
- if hasattr(self, "name"):
- return self.name
- if self.strRepr is not None:
- return self.strRepr
-
- # Avoid infinite recursion by setting a temporary strRepr
- self.strRepr = ": ..."
-
- # Use the string representation of main expression.
- retString = '...'
- try:
- if self.expr is not None:
- retString = _ustr(self.expr)[:1000]
- else:
- retString = "None"
- finally:
- self.strRepr = self.__class__.__name__ + ": " + retString
- return self.strRepr
-
- def copy(self):
- if self.expr is not None:
- return super(Forward, self).copy()
- else:
- ret = Forward()
- ret <<= self
- return ret
-
- def _setResultsName(self, name, listAllMatches=False):
- if __diag__.warn_name_set_on_empty_Forward:
- if self.expr is None:
- warnings.warn("{0}: setting results name {0!r} on {1} expression "
- "that has no contained expression".format("warn_name_set_on_empty_Forward",
- name,
- type(self).__name__),
- stacklevel=3)
-
- return super(Forward, self)._setResultsName(name, listAllMatches)
-
-class TokenConverter(ParseElementEnhance):
- """
- Abstract subclass of :class:`ParseExpression`, for converting parsed results.
- """
- def __init__(self, expr, savelist=False):
- super(TokenConverter, self).__init__(expr) # , savelist)
- self.saveAsList = False
-
-class Combine(TokenConverter):
- """Converter to concatenate all matching tokens to a single string.
- By default, the matching patterns must also be contiguous in the
- input string; this can be disabled by specifying
- ``'adjacent=False'`` in the constructor.
-
- Example::
-
- real = Word(nums) + '.' + Word(nums)
- print(real.parseString('3.1416')) # -> ['3', '.', '1416']
- # will also erroneously match the following
- print(real.parseString('3. 1416')) # -> ['3', '.', '1416']
-
- real = Combine(Word(nums) + '.' + Word(nums))
- print(real.parseString('3.1416')) # -> ['3.1416']
- # no match when there are internal spaces
- print(real.parseString('3. 1416')) # -> Exception: Expected W:(0123...)
- """
- def __init__(self, expr, joinString="", adjacent=True):
- super(Combine, self).__init__(expr)
- # suppress whitespace-stripping in contained parse expressions, but re-enable it on the Combine itself
- if adjacent:
- self.leaveWhitespace()
- self.adjacent = adjacent
- self.skipWhitespace = True
- self.joinString = joinString
- self.callPreparse = True
-
- def ignore(self, other):
- if self.adjacent:
- ParserElement.ignore(self, other)
- else:
- super(Combine, self).ignore(other)
- return self
-
- def postParse(self, instring, loc, tokenlist):
- retToks = tokenlist.copy()
- del retToks[:]
- retToks += ParseResults(["".join(tokenlist._asStringList(self.joinString))], modal=self.modalResults)
-
- if self.resultsName and retToks.haskeys():
- return [retToks]
- else:
- return retToks
-
-class Group(TokenConverter):
- """Converter to return the matched tokens as a list - useful for
- returning tokens of :class:`ZeroOrMore` and :class:`OneOrMore` expressions.
-
- Example::
-
- ident = Word(alphas)
- num = Word(nums)
- term = ident | num
- func = ident + Optional(delimitedList(term))
- print(func.parseString("fn a, b, 100")) # -> ['fn', 'a', 'b', '100']
-
- func = ident + Group(Optional(delimitedList(term)))
- print(func.parseString("fn a, b, 100")) # -> ['fn', ['a', 'b', '100']]
- """
- def __init__(self, expr):
- super(Group, self).__init__(expr)
- self.saveAsList = True
-
- def postParse(self, instring, loc, tokenlist):
- return [tokenlist]
-
-class Dict(TokenConverter):
- """Converter to return a repetitive expression as a list, but also
- as a dictionary. Each element can also be referenced using the first
- token in the expression as its key. Useful for tabular report
- scraping when the first column can be used as a item key.
-
- Example::
-
- data_word = Word(alphas)
- label = data_word + FollowedBy(':')
- attr_expr = Group(label + Suppress(':') + OneOrMore(data_word).setParseAction(' '.join))
-
- text = "shape: SQUARE posn: upper left color: light blue texture: burlap"
- attr_expr = (label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))
-
- # print attributes as plain groups
- print(OneOrMore(attr_expr).parseString(text).dump())
-
- # instead of OneOrMore(expr), parse using Dict(OneOrMore(Group(expr))) - Dict will auto-assign names
- result = Dict(OneOrMore(Group(attr_expr))).parseString(text)
- print(result.dump())
-
- # access named fields as dict entries, or output as dict
- print(result['shape'])
- print(result.asDict())
-
- prints::
-
- ['shape', 'SQUARE', 'posn', 'upper left', 'color', 'light blue', 'texture', 'burlap']
- [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]
- - color: light blue
- - posn: upper left
- - shape: SQUARE
- - texture: burlap
- SQUARE
- {'color': 'light blue', 'posn': 'upper left', 'texture': 'burlap', 'shape': 'SQUARE'}
-
- See more examples at :class:`ParseResults` of accessing fields by results name.
- """
- def __init__(self, expr):
- super(Dict, self).__init__(expr)
- self.saveAsList = True
-
- def postParse(self, instring, loc, tokenlist):
- for i, tok in enumerate(tokenlist):
- if len(tok) == 0:
- continue
- ikey = tok[0]
- if isinstance(ikey, int):
- ikey = _ustr(tok[0]).strip()
- if len(tok) == 1:
- tokenlist[ikey] = _ParseResultsWithOffset("", i)
- elif len(tok) == 2 and not isinstance(tok[1], ParseResults):
- tokenlist[ikey] = _ParseResultsWithOffset(tok[1], i)
- else:
- dictvalue = tok.copy() # ParseResults(i)
- del dictvalue[0]
- if len(dictvalue) != 1 or (isinstance(dictvalue, ParseResults) and dictvalue.haskeys()):
- tokenlist[ikey] = _ParseResultsWithOffset(dictvalue, i)
- else:
- tokenlist[ikey] = _ParseResultsWithOffset(dictvalue[0], i)
-
- if self.resultsName:
- return [tokenlist]
- else:
- return tokenlist
-
-
-class Suppress(TokenConverter):
- """Converter for ignoring the results of a parsed expression.
-
- Example::
-
- source = "a, b, c,d"
- wd = Word(alphas)
- wd_list1 = wd + ZeroOrMore(',' + wd)
- print(wd_list1.parseString(source))
-
- # often, delimiters that are useful during parsing are just in the
- # way afterward - use Suppress to keep them out of the parsed output
- wd_list2 = wd + ZeroOrMore(Suppress(',') + wd)
- print(wd_list2.parseString(source))
-
- prints::
-
- ['a', ',', 'b', ',', 'c', ',', 'd']
- ['a', 'b', 'c', 'd']
-
- (See also :class:`delimitedList`.)
- """
- def postParse(self, instring, loc, tokenlist):
- return []
-
- def suppress(self):
- return self
-
-
-class OnlyOnce(object):
- """Wrapper for parse actions, to ensure they are only called once.
- """
- def __init__(self, methodCall):
- self.callable = _trim_arity(methodCall)
- self.called = False
- def __call__(self, s, l, t):
- if not self.called:
- results = self.callable(s, l, t)
- self.called = True
- return results
- raise ParseException(s, l, "")
- def reset(self):
- self.called = False
-
-def traceParseAction(f):
- """Decorator for debugging parse actions.
-
- When the parse action is called, this decorator will print
- ``">> entering method-name(line:<current_source_line>, <parse_location>, <matched_tokens>)"``.
- When the parse action completes, the decorator will print
- ``"<<"`` followed by the returned value, or any exception that the parse action raised.
-
- Example::
-
- wd = Word(alphas)
-
- @traceParseAction
- def remove_duplicate_chars(tokens):
- return ''.join(sorted(set(''.join(tokens))))
-
- wds = OneOrMore(wd).setParseAction(remove_duplicate_chars)
- print(wds.parseString("slkdjs sld sldd sdlf sdljf"))
-
- prints::
-
- >>entering remove_duplicate_chars(line: 'slkdjs sld sldd sdlf sdljf', 0, (['slkdjs', 'sld', 'sldd', 'sdlf', 'sdljf'], {}))
- <<leaving remove_duplicate_chars (ret: 'dfjkls')
- ['dfjkls']
- """
- f = _trim_arity(f)
- def z(*paArgs):
- thisFunc = f.__name__
- s, l, t = paArgs[-3:]
- if len(paArgs) > 3:
- thisFunc = paArgs[0].__class__.__name__ + '.' + thisFunc
- sys.stderr.write(">>entering %s(line: '%s', %d, %r)\n" % (thisFunc, line(l, s), l, t))
- try:
- ret = f(*paArgs)
- except Exception as exc:
- sys.stderr.write("<<leaving %s (exception: %s)\n" % (thisFunc, exc))
- raise
- sys.stderr.write("<<leaving %s (ret: %r)\n" % (thisFunc, ret))
- return ret
- try:
- z.__name__ = f.__name__
- except AttributeError:
- pass
- return z
-
-#
-# global helpers
-#
-def delimitedList(expr, delim=",", combine=False):
- """Helper to define a delimited list of expressions - the delimiter
- defaults to ','. By default, the list elements and delimiters can
- have intervening whitespace, and comments, but this can be
- overridden by passing ``combine=True`` in the constructor. If
- ``combine`` is set to ``True``, the matching tokens are
- returned as a single token string, with the delimiters included;
- otherwise, the matching tokens are returned as a list of tokens,
- with the delimiters suppressed.
-
- Example::
-
- delimitedList(Word(alphas)).parseString("aa,bb,cc") # -> ['aa', 'bb', 'cc']
- delimitedList(Word(hexnums), delim=':', combine=True).parseString("AA:BB:CC:DD:EE") # -> ['AA:BB:CC:DD:EE']
- """
- dlName = _ustr(expr) + " [" + _ustr(delim) + " " + _ustr(expr) + "]..."
- if combine:
- return Combine(expr + ZeroOrMore(delim + expr)).setName(dlName)
- else:
- return (expr + ZeroOrMore(Suppress(delim) + expr)).setName(dlName)
-
-def countedArray(expr, intExpr=None):
- """Helper to define a counted list of expressions.
-
- This helper defines a pattern of the form::
-
- integer expr expr expr...
-
- where the leading integer tells how many expr expressions follow.
- The matched tokens returns the array of expr tokens as a list - the
- leading count token is suppressed.
-
- If ``intExpr`` is specified, it should be a pyparsing expression
- that produces an integer value.
-
- Example::
-
- countedArray(Word(alphas)).parseString('2 ab cd ef') # -> ['ab', 'cd']
-
- # in this parser, the leading integer value is given in binary,
- # '10' indicating that 2 values are in the array
- binaryConstant = Word('01').setParseAction(lambda t: int(t[0], 2))
- countedArray(Word(alphas), intExpr=binaryConstant).parseString('10 ab cd ef') # -> ['ab', 'cd']
- """
- arrayExpr = Forward()
- def countFieldParseAction(s, l, t):
- n = t[0]
- arrayExpr << (n and Group(And([expr] * n)) or Group(empty))
- return []
- if intExpr is None:
- intExpr = Word(nums).setParseAction(lambda t: int(t[0]))
- else:
- intExpr = intExpr.copy()
- intExpr.setName("arrayLen")
- intExpr.addParseAction(countFieldParseAction, callDuringTry=True)
- return (intExpr + arrayExpr).setName('(len) ' + _ustr(expr) + '...')
-
-def _flatten(L):
- ret = []
- for i in L:
- if isinstance(i, list):
- ret.extend(_flatten(i))
- else:
- ret.append(i)
- return ret
-
-def matchPreviousLiteral(expr):
- """Helper to define an expression that is indirectly defined from
- the tokens matched in a previous expression, that is, it looks for
- a 'repeat' of a previous expression. For example::
-
- first = Word(nums)
- second = matchPreviousLiteral(first)
- matchExpr = first + ":" + second
-
- will match ``"1:1"``, but not ``"1:2"``. Because this
- matches a previous literal, will also match the leading
- ``"1:1"`` in ``"1:10"``. If this is not desired, use
- :class:`matchPreviousExpr`. Do *not* use with packrat parsing
- enabled.
- """
- rep = Forward()
- def copyTokenToRepeater(s, l, t):
- if t:
- if len(t) == 1:
- rep << t[0]
- else:
- # flatten t tokens
- tflat = _flatten(t.asList())
- rep << And(Literal(tt) for tt in tflat)
- else:
- rep << Empty()
- expr.addParseAction(copyTokenToRepeater, callDuringTry=True)
- rep.setName('(prev) ' + _ustr(expr))
- return rep
-
-def matchPreviousExpr(expr):
- """Helper to define an expression that is indirectly defined from
- the tokens matched in a previous expression, that is, it looks for
- a 'repeat' of a previous expression. For example::
-
- first = Word(nums)
- second = matchPreviousExpr(first)
- matchExpr = first + ":" + second
-
- will match ``"1:1"``, but not ``"1:2"``. Because this
- matches by expressions, will *not* match the leading ``"1:1"``
- in ``"1:10"``; the expressions are evaluated first, and then
- compared, so ``"1"`` is compared with ``"10"``. Do *not* use
- with packrat parsing enabled.
- """
- rep = Forward()
- e2 = expr.copy()
- rep <<= e2
- def copyTokenToRepeater(s, l, t):
- matchTokens = _flatten(t.asList())
- def mustMatchTheseTokens(s, l, t):
- theseTokens = _flatten(t.asList())
- if theseTokens != matchTokens:
- raise ParseException('', 0, '')
- rep.setParseAction(mustMatchTheseTokens, callDuringTry=True)
- expr.addParseAction(copyTokenToRepeater, callDuringTry=True)
- rep.setName('(prev) ' + _ustr(expr))
- return rep
-
-def _escapeRegexRangeChars(s):
- # ~ escape these chars: ^-[]
- for c in r"\^-[]":
- s = s.replace(c, _bslash + c)
- s = s.replace("\n", r"\n")
- s = s.replace("\t", r"\t")
- return _ustr(s)
-
-def oneOf(strs, caseless=False, useRegex=True, asKeyword=False):
- """Helper to quickly define a set of alternative Literals, and makes
- sure to do longest-first testing when there is a conflict,
- regardless of the input order, but returns
- a :class:`MatchFirst` for best performance.
-
- Parameters:
-
- - strs - a string of space-delimited literals, or a collection of
- string literals
- - caseless - (default= ``False``) - treat all literals as
- caseless
- - useRegex - (default= ``True``) - as an optimization, will
- generate a Regex object; otherwise, will generate
- a :class:`MatchFirst` object (if ``caseless=True`` or ``asKeyword=True``, or if
- creating a :class:`Regex` raises an exception)
- - asKeyword - (default=``False``) - enforce Keyword-style matching on the
- generated expressions
-
- Example::
-
- comp_oper = oneOf("< = > <= >= !=")
- var = Word(alphas)
- number = Word(nums)
- term = var | number
- comparison_expr = term + comp_oper + term
- print(comparison_expr.searchString("B = 12 AA=23 B<=AA AA>12"))
-
- prints::
-
- [['B', '=', '12'], ['AA', '=', '23'], ['B', '<=', 'AA'], ['AA', '>', '12']]
- """
- if isinstance(caseless, basestring):
- warnings.warn("More than one string argument passed to oneOf, pass "
- "choices as a list or space-delimited string", stacklevel=2)
-
- if caseless:
- isequal = (lambda a, b: a.upper() == b.upper())
- masks = (lambda a, b: b.upper().startswith(a.upper()))
- parseElementClass = CaselessKeyword if asKeyword else CaselessLiteral
- else:
- isequal = (lambda a, b: a == b)
- masks = (lambda a, b: b.startswith(a))
- parseElementClass = Keyword if asKeyword else Literal
-
- symbols = []
- if isinstance(strs, basestring):
- symbols = strs.split()
- elif isinstance(strs, Iterable):
- symbols = list(strs)
- else:
- warnings.warn("Invalid argument to oneOf, expected string or iterable",
- SyntaxWarning, stacklevel=2)
- if not symbols:
- return NoMatch()
-
- if not asKeyword:
- # if not producing keywords, need to reorder to take care to avoid masking
- # longer choices with shorter ones
- i = 0
- while i < len(symbols) - 1:
- cur = symbols[i]
- for j, other in enumerate(symbols[i + 1:]):
- if isequal(other, cur):
- del symbols[i + j + 1]
- break
- elif masks(cur, other):
- del symbols[i + j + 1]
- symbols.insert(i, other)
- break
- else:
- i += 1
-
- if not (caseless or asKeyword) and useRegex:
- # ~ print (strs, "->", "|".join([_escapeRegexChars(sym) for sym in symbols]))
- try:
- if len(symbols) == len("".join(symbols)):
- return Regex("[%s]" % "".join(_escapeRegexRangeChars(sym) for sym in symbols)).setName(' | '.join(symbols))
- else:
- return Regex("|".join(re.escape(sym) for sym in symbols)).setName(' | '.join(symbols))
- except Exception:
- warnings.warn("Exception creating Regex for oneOf, building MatchFirst",
- SyntaxWarning, stacklevel=2)
-
- # last resort, just use MatchFirst
- return MatchFirst(parseElementClass(sym) for sym in symbols).setName(' | '.join(symbols))
-
-def dictOf(key, value):
- """Helper to easily and clearly define a dictionary by specifying
- the respective patterns for the key and value. Takes care of
- defining the :class:`Dict`, :class:`ZeroOrMore`, and
- :class:`Group` tokens in the proper order. The key pattern
- can include delimiting markers or punctuation, as long as they are
- suppressed, thereby leaving the significant key text. The value
- pattern can include named results, so that the :class:`Dict` results
- can include named token fields.
-
- Example::
-
- text = "shape: SQUARE posn: upper left color: light blue texture: burlap"
- attr_expr = (label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))
- print(OneOrMore(attr_expr).parseString(text).dump())
-
- attr_label = label
- attr_value = Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join)
-
- # similar to Dict, but simpler call format
- result = dictOf(attr_label, attr_value).parseString(text)
- print(result.dump())
- print(result['shape'])
- print(result.shape) # object attribute access works too
- print(result.asDict())
-
- prints::
-
- [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]
- - color: light blue
- - posn: upper left
- - shape: SQUARE
- - texture: burlap
- SQUARE
- SQUARE
- {'color': 'light blue', 'shape': 'SQUARE', 'posn': 'upper left', 'texture': 'burlap'}
- """
- return Dict(OneOrMore(Group(key + value)))
-
-def originalTextFor(expr, asString=True):
- """Helper to return the original, untokenized text for a given
- expression. Useful to restore the parsed fields of an HTML start
- tag into the raw tag text itself, or to revert separate tokens with
- intervening whitespace back to the original matching input text. By
- default, returns astring containing the original parsed text.
-
- If the optional ``asString`` argument is passed as
- ``False``, then the return value is
- a :class:`ParseResults` containing any results names that
- were originally matched, and a single token containing the original
- matched text from the input string. So if the expression passed to
- :class:`originalTextFor` contains expressions with defined
- results names, you must set ``asString`` to ``False`` if you
- want to preserve those results name values.
-
- Example::
-
- src = "this is test <b> bold <i>text</i> </b> normal text "
- for tag in ("b", "i"):
- opener, closer = makeHTMLTags(tag)
- patt = originalTextFor(opener + SkipTo(closer) + closer)
- print(patt.searchString(src)[0])
-
- prints::
-
- ['<b> bold <i>text</i> </b>']
- ['<i>text</i>']
- """
- locMarker = Empty().setParseAction(lambda s, loc, t: loc)
- endlocMarker = locMarker.copy()
- endlocMarker.callPreparse = False
- matchExpr = locMarker("_original_start") + expr + endlocMarker("_original_end")
- if asString:
- extractText = lambda s, l, t: s[t._original_start: t._original_end]
- else:
- def extractText(s, l, t):
- t[:] = [s[t.pop('_original_start'):t.pop('_original_end')]]
- matchExpr.setParseAction(extractText)
- matchExpr.ignoreExprs = expr.ignoreExprs
- return matchExpr
-
-def ungroup(expr):
- """Helper to undo pyparsing's default grouping of And expressions,
- even if all but one are non-empty.
- """
- return TokenConverter(expr).addParseAction(lambda t: t[0])
-
-def locatedExpr(expr):
- """Helper to decorate a returned token with its starting and ending
- locations in the input string.
-
- This helper adds the following results names:
-
- - locn_start = location where matched expression begins
- - locn_end = location where matched expression ends
- - value = the actual parsed results
-
- Be careful if the input text contains ``<TAB>`` characters, you
- may want to call :class:`ParserElement.parseWithTabs`
-
- Example::
-
- wd = Word(alphas)
- for match in locatedExpr(wd).searchString("ljsdf123lksdjjf123lkkjj1222"):
- print(match)
-
- prints::
-
- [[0, 'ljsdf', 5]]
- [[8, 'lksdjjf', 15]]
- [[18, 'lkkjj', 23]]
- """
- locator = Empty().setParseAction(lambda s, l, t: l)
- return Group(locator("locn_start") + expr("value") + locator.copy().leaveWhitespace()("locn_end"))
-
-
-# convenience constants for positional expressions
-empty = Empty().setName("empty")
-lineStart = LineStart().setName("lineStart")
-lineEnd = LineEnd().setName("lineEnd")
-stringStart = StringStart().setName("stringStart")
-stringEnd = StringEnd().setName("stringEnd")
-
-_escapedPunc = Word(_bslash, r"\[]-*.$+^?()~ ", exact=2).setParseAction(lambda s, l, t: t[0][1])
-_escapedHexChar = Regex(r"\\0?[xX][0-9a-fA-F]+").setParseAction(lambda s, l, t: unichr(int(t[0].lstrip(r'\0x'), 16)))
-_escapedOctChar = Regex(r"\\0[0-7]+").setParseAction(lambda s, l, t: unichr(int(t[0][1:], 8)))
-_singleChar = _escapedPunc | _escapedHexChar | _escapedOctChar | CharsNotIn(r'\]', exact=1)
-_charRange = Group(_singleChar + Suppress("-") + _singleChar)
-_reBracketExpr = Literal("[") + Optional("^").setResultsName("negate") + Group(OneOrMore(_charRange | _singleChar)).setResultsName("body") + "]"
-
-def srange(s):
- r"""Helper to easily define string ranges for use in Word
- construction. Borrows syntax from regexp '[]' string range
- definitions::
-
- srange("[0-9]") -> "0123456789"
- srange("[a-z]") -> "abcdefghijklmnopqrstuvwxyz"
- srange("[a-z$_]") -> "abcdefghijklmnopqrstuvwxyz$_"
-
- The input string must be enclosed in []'s, and the returned string
- is the expanded character set joined into a single string. The
- values enclosed in the []'s may be:
-
- - a single character
- - an escaped character with a leading backslash (such as ``\-``
- or ``\]``)
- - an escaped hex character with a leading ``'\x'``
- (``\x21``, which is a ``'!'`` character) (``\0x##``
- is also supported for backwards compatibility)
- - an escaped octal character with a leading ``'\0'``
- (``\041``, which is a ``'!'`` character)
- - a range of any of the above, separated by a dash (``'a-z'``,
- etc.)
- - any combination of the above (``'aeiouy'``,
- ``'a-zA-Z0-9_$'``, etc.)
- """
- _expanded = lambda p: p if not isinstance(p, ParseResults) else ''.join(unichr(c) for c in range(ord(p[0]), ord(p[1]) + 1))
- try:
- return "".join(_expanded(part) for part in _reBracketExpr.parseString(s).body)
- except Exception:
- return ""
-
-def matchOnlyAtCol(n):
- """Helper method for defining parse actions that require matching at
- a specific column in the input text.
- """
- def verifyCol(strg, locn, toks):
- if col(locn, strg) != n:
- raise ParseException(strg, locn, "matched token not at column %d" % n)
- return verifyCol
-
-def replaceWith(replStr):
- """Helper method for common parse actions that simply return
- a literal value. Especially useful when used with
- :class:`transformString<ParserElement.transformString>` ().
-
- Example::
-
- num = Word(nums).setParseAction(lambda toks: int(toks[0]))
- na = oneOf("N/A NA").setParseAction(replaceWith(math.nan))
- term = na | num
-
- OneOrMore(term).parseString("324 234 N/A 234") # -> [324, 234, nan, 234]
- """
- return lambda s, l, t: [replStr]
-
-def removeQuotes(s, l, t):
- """Helper parse action for removing quotation marks from parsed
- quoted strings.
-
- Example::
-
- # by default, quotation marks are included in parsed results
- quotedString.parseString("'Now is the Winter of our Discontent'") # -> ["'Now is the Winter of our Discontent'"]
-
- # use removeQuotes to strip quotation marks from parsed results
- quotedString.setParseAction(removeQuotes)
- quotedString.parseString("'Now is the Winter of our Discontent'") # -> ["Now is the Winter of our Discontent"]
- """
- return t[0][1:-1]
-
-def tokenMap(func, *args):
- """Helper to define a parse action by mapping a function to all
- elements of a ParseResults list. If any additional args are passed,
- they are forwarded to the given function as additional arguments
- after the token, as in
- ``hex_integer = Word(hexnums).setParseAction(tokenMap(int, 16))``,
- which will convert the parsed data to an integer using base 16.
-
- Example (compare the last to example in :class:`ParserElement.transformString`::
-
- hex_ints = OneOrMore(Word(hexnums)).setParseAction(tokenMap(int, 16))
- hex_ints.runTests('''
- 00 11 22 aa FF 0a 0d 1a
- ''')
-
- upperword = Word(alphas).setParseAction(tokenMap(str.upper))
- OneOrMore(upperword).runTests('''
- my kingdom for a horse
- ''')
-
- wd = Word(alphas).setParseAction(tokenMap(str.title))
- OneOrMore(wd).setParseAction(' '.join).runTests('''
- now is the winter of our discontent made glorious summer by this sun of york
- ''')
-
- prints::
-
- 00 11 22 aa FF 0a 0d 1a
- [0, 17, 34, 170, 255, 10, 13, 26]
-
- my kingdom for a horse
- ['MY', 'KINGDOM', 'FOR', 'A', 'HORSE']
-
- now is the winter of our discontent made glorious summer by this sun of york
- ['Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York']
- """
- def pa(s, l, t):
- return [func(tokn, *args) for tokn in t]
-
- try:
- func_name = getattr(func, '__name__',
- getattr(func, '__class__').__name__)
- except Exception:
- func_name = str(func)
- pa.__name__ = func_name
-
- return pa
-
-upcaseTokens = tokenMap(lambda t: _ustr(t).upper())
-"""(Deprecated) Helper parse action to convert tokens to upper case.
-Deprecated in favor of :class:`pyparsing_common.upcaseTokens`"""
-
-downcaseTokens = tokenMap(lambda t: _ustr(t).lower())
-"""(Deprecated) Helper parse action to convert tokens to lower case.
-Deprecated in favor of :class:`pyparsing_common.downcaseTokens`"""
-
-def _makeTags(tagStr, xml,
- suppress_LT=Suppress("<"),
- suppress_GT=Suppress(">")):
- """Internal helper to construct opening and closing tag expressions, given a tag name"""
- if isinstance(tagStr, basestring):
- resname = tagStr
- tagStr = Keyword(tagStr, caseless=not xml)
- else:
- resname = tagStr.name
-
- tagAttrName = Word(alphas, alphanums + "_-:")
- if xml:
- tagAttrValue = dblQuotedString.copy().setParseAction(removeQuotes)
- openTag = (suppress_LT
- + tagStr("tag")
- + Dict(ZeroOrMore(Group(tagAttrName + Suppress("=") + tagAttrValue)))
- + Optional("/", default=[False])("empty").setParseAction(lambda s, l, t: t[0] == '/')
- + suppress_GT)
- else:
- tagAttrValue = quotedString.copy().setParseAction(removeQuotes) | Word(printables, excludeChars=">")
- openTag = (suppress_LT
- + tagStr("tag")
- + Dict(ZeroOrMore(Group(tagAttrName.setParseAction(downcaseTokens)
- + Optional(Suppress("=") + tagAttrValue))))
- + Optional("/", default=[False])("empty").setParseAction(lambda s, l, t: t[0] == '/')
- + suppress_GT)
- closeTag = Combine(_L("</") + tagStr + ">", adjacent=False)
-
- openTag.setName("<%s>" % resname)
- # add start<tagname> results name in parse action now that ungrouped names are not reported at two levels
- openTag.addParseAction(lambda t: t.__setitem__("start" + "".join(resname.replace(":", " ").title().split()), t.copy()))
- closeTag = closeTag("end" + "".join(resname.replace(":", " ").title().split())).setName("</%s>" % resname)
- openTag.tag = resname
- closeTag.tag = resname
- openTag.tag_body = SkipTo(closeTag())
- return openTag, closeTag
-
-def makeHTMLTags(tagStr):
- """Helper to construct opening and closing tag expressions for HTML,
- given a tag name. Matches tags in either upper or lower case,
- attributes with namespaces and with quoted or unquoted values.
-
- Example::
-
- text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>'
- # makeHTMLTags returns pyparsing expressions for the opening and
- # closing tags as a 2-tuple
- a, a_end = makeHTMLTags("A")
- link_expr = a + SkipTo(a_end)("link_text") + a_end
-
- for link in link_expr.searchString(text):
- # attributes in the <A> tag (like "href" shown here) are
- # also accessible as named results
- print(link.link_text, '->', link.href)
-
- prints::
-
- pyparsing -> https://github.com/pyparsing/pyparsing/wiki
- """
- return _makeTags(tagStr, False)
-
-def makeXMLTags(tagStr):
- """Helper to construct opening and closing tag expressions for XML,
- given a tag name. Matches tags only in the given upper/lower case.
-
- Example: similar to :class:`makeHTMLTags`
- """
- return _makeTags(tagStr, True)
-
-def withAttribute(*args, **attrDict):
- """Helper to create a validating parse action to be used with start
- tags created with :class:`makeXMLTags` or
- :class:`makeHTMLTags`. Use ``withAttribute`` to qualify
- a starting tag with a required attribute value, to avoid false
- matches on common tags such as ``<TD>`` or ``<DIV>``.
-
- Call ``withAttribute`` with a series of attribute names and
- values. Specify the list of filter attributes names and values as:
-
- - keyword arguments, as in ``(align="right")``, or
- - as an explicit dict with ``**`` operator, when an attribute
- name is also a Python reserved word, as in ``**{"class":"Customer", "align":"right"}``
- - a list of name-value tuples, as in ``(("ns1:class", "Customer"), ("ns2:align", "right"))``
-
- For attribute names with a namespace prefix, you must use the second
- form. Attribute names are matched insensitive to upper/lower case.
-
- If just testing for ``class`` (with or without a namespace), use
- :class:`withClass`.
-
- To verify that the attribute exists, but without specifying a value,
- pass ``withAttribute.ANY_VALUE`` as the value.
-
- Example::
-
- html = '''
- <div>
- Some text
- <div type="grid">1 4 0 1 0</div>
- <div type="graph">1,3 2,3 1,1</div>
- <div>this has no type</div>
- </div>
-
- '''
- div,div_end = makeHTMLTags("div")
-
- # only match div tag having a type attribute with value "grid"
- div_grid = div().setParseAction(withAttribute(type="grid"))
- grid_expr = div_grid + SkipTo(div | div_end)("body")
- for grid_header in grid_expr.searchString(html):
- print(grid_header.body)
-
- # construct a match with any div tag having a type attribute, regardless of the value
- div_any_type = div().setParseAction(withAttribute(type=withAttribute.ANY_VALUE))
- div_expr = div_any_type + SkipTo(div | div_end)("body")
- for div_header in div_expr.searchString(html):
- print(div_header.body)
-
- prints::
-
- 1 4 0 1 0
-
- 1 4 0 1 0
- 1,3 2,3 1,1
- """
- if args:
- attrs = args[:]
- else:
- attrs = attrDict.items()
- attrs = [(k, v) for k, v in attrs]
- def pa(s, l, tokens):
- for attrName, attrValue in attrs:
- if attrName not in tokens:
- raise ParseException(s, l, "no matching attribute " + attrName)
- if attrValue != withAttribute.ANY_VALUE and tokens[attrName] != attrValue:
- raise ParseException(s, l, "attribute '%s' has value '%s', must be '%s'" %
- (attrName, tokens[attrName], attrValue))
- return pa
-withAttribute.ANY_VALUE = object()
-
-def withClass(classname, namespace=''):
- """Simplified version of :class:`withAttribute` when
- matching on a div class - made difficult because ``class`` is
- a reserved word in Python.
-
- Example::
-
- html = '''
- <div>
- Some text
- <div class="grid">1 4 0 1 0</div>
- <div class="graph">1,3 2,3 1,1</div>
- <div>this &lt;div&gt; has no class</div>
- </div>
-
- '''
- div,div_end = makeHTMLTags("div")
- div_grid = div().setParseAction(withClass("grid"))
-
- grid_expr = div_grid + SkipTo(div | div_end)("body")
- for grid_header in grid_expr.searchString(html):
- print(grid_header.body)
-
- div_any_type = div().setParseAction(withClass(withAttribute.ANY_VALUE))
- div_expr = div_any_type + SkipTo(div | div_end)("body")
- for div_header in div_expr.searchString(html):
- print(div_header.body)
-
- prints::
-
- 1 4 0 1 0
-
- 1 4 0 1 0
- 1,3 2,3 1,1
- """
- classattr = "%s:class" % namespace if namespace else "class"
- return withAttribute(**{classattr: classname})
-
-opAssoc = SimpleNamespace()
-opAssoc.LEFT = object()
-opAssoc.RIGHT = object()
-
-def infixNotation(baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')')):
- """Helper method for constructing grammars of expressions made up of
- operators working in a precedence hierarchy. Operators may be unary
- or binary, left- or right-associative. Parse actions can also be
- attached to operator expressions. The generated parser will also
- recognize the use of parentheses to override operator precedences
- (see example below).
-
- Note: if you define a deep operator list, you may see performance
- issues when using infixNotation. See
- :class:`ParserElement.enablePackrat` for a mechanism to potentially
- improve your parser performance.
-
- Parameters:
- - baseExpr - expression representing the most basic element for the
- nested
- - opList - list of tuples, one for each operator precedence level
- in the expression grammar; each tuple is of the form ``(opExpr,
- numTerms, rightLeftAssoc, parseAction)``, where:
-
- - opExpr is the pyparsing expression for the operator; may also
- be a string, which will be converted to a Literal; if numTerms
- is 3, opExpr is a tuple of two expressions, for the two
- operators separating the 3 terms
- - numTerms is the number of terms for this operator (must be 1,
- 2, or 3)
- - rightLeftAssoc is the indicator whether the operator is right
- or left associative, using the pyparsing-defined constants
- ``opAssoc.RIGHT`` and ``opAssoc.LEFT``.
- - parseAction is the parse action to be associated with
- expressions matching this operator expression (the parse action
- tuple member may be omitted); if the parse action is passed
- a tuple or list of functions, this is equivalent to calling
- ``setParseAction(*fn)``
- (:class:`ParserElement.setParseAction`)
- - lpar - expression for matching left-parentheses
- (default= ``Suppress('(')``)
- - rpar - expression for matching right-parentheses
- (default= ``Suppress(')')``)
-
- Example::
-
- # simple example of four-function arithmetic with ints and
- # variable names
- integer = pyparsing_common.signed_integer
- varname = pyparsing_common.identifier
-
- arith_expr = infixNotation(integer | varname,
- [
- ('-', 1, opAssoc.RIGHT),
- (oneOf('* /'), 2, opAssoc.LEFT),
- (oneOf('+ -'), 2, opAssoc.LEFT),
- ])
-
- arith_expr.runTests('''
- 5+3*6
- (5+3)*6
- -2--11
- ''', fullDump=False)
-
- prints::
-
- 5+3*6
- [[5, '+', [3, '*', 6]]]
-
- (5+3)*6
- [[[5, '+', 3], '*', 6]]
-
- -2--11
- [[['-', 2], '-', ['-', 11]]]
- """
- # captive version of FollowedBy that does not do parse actions or capture results names
- class _FB(FollowedBy):
- def parseImpl(self, instring, loc, doActions=True):
- self.expr.tryParse(instring, loc)
- return loc, []
-
- ret = Forward()
- lastExpr = baseExpr | (lpar + ret + rpar)
- for i, operDef in enumerate(opList):
- opExpr, arity, rightLeftAssoc, pa = (operDef + (None, ))[:4]
- termName = "%s term" % opExpr if arity < 3 else "%s%s term" % opExpr
- if arity == 3:
- if opExpr is None or len(opExpr) != 2:
- raise ValueError(
- "if numterms=3, opExpr must be a tuple or list of two expressions")
- opExpr1, opExpr2 = opExpr
- thisExpr = Forward().setName(termName)
- if rightLeftAssoc == opAssoc.LEFT:
- if arity == 1:
- matchExpr = _FB(lastExpr + opExpr) + Group(lastExpr + OneOrMore(opExpr))
- elif arity == 2:
- if opExpr is not None:
- matchExpr = _FB(lastExpr + opExpr + lastExpr) + Group(lastExpr + OneOrMore(opExpr + lastExpr))
- else:
- matchExpr = _FB(lastExpr + lastExpr) + Group(lastExpr + OneOrMore(lastExpr))
- elif arity == 3:
- matchExpr = (_FB(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr)
- + Group(lastExpr + OneOrMore(opExpr1 + lastExpr + opExpr2 + lastExpr)))
- else:
- raise ValueError("operator must be unary (1), binary (2), or ternary (3)")
- elif rightLeftAssoc == opAssoc.RIGHT:
- if arity == 1:
- # try to avoid LR with this extra test
- if not isinstance(opExpr, Optional):
- opExpr = Optional(opExpr)
- matchExpr = _FB(opExpr.expr + thisExpr) + Group(opExpr + thisExpr)
- elif arity == 2:
- if opExpr is not None:
- matchExpr = _FB(lastExpr + opExpr + thisExpr) + Group(lastExpr + OneOrMore(opExpr + thisExpr))
- else:
- matchExpr = _FB(lastExpr + thisExpr) + Group(lastExpr + OneOrMore(thisExpr))
- elif arity == 3:
- matchExpr = (_FB(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr)
- + Group(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr))
- else:
- raise ValueError("operator must be unary (1), binary (2), or ternary (3)")
- else:
- raise ValueError("operator must indicate right or left associativity")
- if pa:
- if isinstance(pa, (tuple, list)):
- matchExpr.setParseAction(*pa)
- else:
- matchExpr.setParseAction(pa)
- thisExpr <<= (matchExpr.setName(termName) | lastExpr)
- lastExpr = thisExpr
- ret <<= lastExpr
- return ret
-
-operatorPrecedence = infixNotation
-"""(Deprecated) Former name of :class:`infixNotation`, will be
-dropped in a future release."""
-
-dblQuotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*') + '"').setName("string enclosed in double quotes")
-sglQuotedString = Combine(Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*") + "'").setName("string enclosed in single quotes")
-quotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*') + '"'
- | Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*") + "'").setName("quotedString using single or double quotes")
-unicodeString = Combine(_L('u') + quotedString.copy()).setName("unicode string literal")
-
-def nestedExpr(opener="(", closer=")", content=None, ignoreExpr=quotedString.copy()):
- """Helper method for defining nested lists enclosed in opening and
- closing delimiters ("(" and ")" are the default).
-
- Parameters:
- - opener - opening character for a nested list
- (default= ``"("``); can also be a pyparsing expression
- - closer - closing character for a nested list
- (default= ``")"``); can also be a pyparsing expression
- - content - expression for items within the nested lists
- (default= ``None``)
- - ignoreExpr - expression for ignoring opening and closing
- delimiters (default= :class:`quotedString`)
-
- If an expression is not provided for the content argument, the
- nested expression will capture all whitespace-delimited content
- between delimiters as a list of separate values.
-
- Use the ``ignoreExpr`` argument to define expressions that may
- contain opening or closing characters that should not be treated as
- opening or closing characters for nesting, such as quotedString or
- a comment expression. Specify multiple expressions using an
- :class:`Or` or :class:`MatchFirst`. The default is
- :class:`quotedString`, but if no expressions are to be ignored, then
- pass ``None`` for this argument.
-
- Example::
-
- data_type = oneOf("void int short long char float double")
- decl_data_type = Combine(data_type + Optional(Word('*')))
- ident = Word(alphas+'_', alphanums+'_')
- number = pyparsing_common.number
- arg = Group(decl_data_type + ident)
- LPAR, RPAR = map(Suppress, "()")
-
- code_body = nestedExpr('{', '}', ignoreExpr=(quotedString | cStyleComment))
-
- c_function = (decl_data_type("type")
- + ident("name")
- + LPAR + Optional(delimitedList(arg), [])("args") + RPAR
- + code_body("body"))
- c_function.ignore(cStyleComment)
-
- source_code = '''
- int is_odd(int x) {
- return (x%2);
- }
-
- int dec_to_hex(char hchar) {
- if (hchar >= '0' && hchar <= '9') {
- return (ord(hchar)-ord('0'));
- } else {
- return (10+ord(hchar)-ord('A'));
- }
- }
- '''
- for func in c_function.searchString(source_code):
- print("%(name)s (%(type)s) args: %(args)s" % func)
-
-
- prints::
-
- is_odd (int) args: [['int', 'x']]
- dec_to_hex (int) args: [['char', 'hchar']]
- """
- if opener == closer:
- raise ValueError("opening and closing strings cannot be the same")
- if content is None:
- if isinstance(opener, basestring) and isinstance(closer, basestring):
- if len(opener) == 1 and len(closer) == 1:
- if ignoreExpr is not None:
- content = (Combine(OneOrMore(~ignoreExpr
- + CharsNotIn(opener
- + closer
- + ParserElement.DEFAULT_WHITE_CHARS, exact=1)
- )
- ).setParseAction(lambda t: t[0].strip()))
- else:
- content = (empty.copy() + CharsNotIn(opener
- + closer
- + ParserElement.DEFAULT_WHITE_CHARS
- ).setParseAction(lambda t: t[0].strip()))
- else:
- if ignoreExpr is not None:
- content = (Combine(OneOrMore(~ignoreExpr
- + ~Literal(opener)
- + ~Literal(closer)
- + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1))
- ).setParseAction(lambda t: t[0].strip()))
- else:
- content = (Combine(OneOrMore(~Literal(opener)
- + ~Literal(closer)
- + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1))
- ).setParseAction(lambda t: t[0].strip()))
- else:
- raise ValueError("opening and closing arguments must be strings if no content expression is given")
- ret = Forward()
- if ignoreExpr is not None:
- ret <<= Group(Suppress(opener) + ZeroOrMore(ignoreExpr | ret | content) + Suppress(closer))
- else:
- ret <<= Group(Suppress(opener) + ZeroOrMore(ret | content) + Suppress(closer))
- ret.setName('nested %s%s expression' % (opener, closer))
- return ret
-
-def indentedBlock(blockStatementExpr, indentStack, indent=True):
- """Helper method for defining space-delimited indentation blocks,
- such as those used to define block statements in Python source code.
-
- Parameters:
-
- - blockStatementExpr - expression defining syntax of statement that
- is repeated within the indented block
- - indentStack - list created by caller to manage indentation stack
- (multiple statementWithIndentedBlock expressions within a single
- grammar should share a common indentStack)
- - indent - boolean indicating whether block must be indented beyond
- the current level; set to False for block of left-most
- statements (default= ``True``)
-
- A valid block must contain at least one ``blockStatement``.
-
- Example::
-
- data = '''
- def A(z):
- A1
- B = 100
- G = A2
- A2
- A3
- B
- def BB(a,b,c):
- BB1
- def BBA():
- bba1
- bba2
- bba3
- C
- D
- def spam(x,y):
- def eggs(z):
- pass
- '''
-
-
- indentStack = [1]
- stmt = Forward()
-
- identifier = Word(alphas, alphanums)
- funcDecl = ("def" + identifier + Group("(" + Optional(delimitedList(identifier)) + ")") + ":")
- func_body = indentedBlock(stmt, indentStack)
- funcDef = Group(funcDecl + func_body)
-
- rvalue = Forward()
- funcCall = Group(identifier + "(" + Optional(delimitedList(rvalue)) + ")")
- rvalue << (funcCall | identifier | Word(nums))
- assignment = Group(identifier + "=" + rvalue)
- stmt << (funcDef | assignment | identifier)
-
- module_body = OneOrMore(stmt)
-
- parseTree = module_body.parseString(data)
- parseTree.pprint()
-
- prints::
-
- [['def',
- 'A',
- ['(', 'z', ')'],
- ':',
- [['A1'], [['B', '=', '100']], [['G', '=', 'A2']], ['A2'], ['A3']]],
- 'B',
- ['def',
- 'BB',
- ['(', 'a', 'b', 'c', ')'],
- ':',
- [['BB1'], [['def', 'BBA', ['(', ')'], ':', [['bba1'], ['bba2'], ['bba3']]]]]],
- 'C',
- 'D',
- ['def',
- 'spam',
- ['(', 'x', 'y', ')'],
- ':',
- [[['def', 'eggs', ['(', 'z', ')'], ':', [['pass']]]]]]]
- """
- backup_stack = indentStack[:]
-
- def reset_stack():
- indentStack[:] = backup_stack
-
- def checkPeerIndent(s, l, t):
- if l >= len(s): return
- curCol = col(l, s)
- if curCol != indentStack[-1]:
- if curCol > indentStack[-1]:
- raise ParseException(s, l, "illegal nesting")
- raise ParseException(s, l, "not a peer entry")
-
- def checkSubIndent(s, l, t):
- curCol = col(l, s)
- if curCol > indentStack[-1]:
- indentStack.append(curCol)
- else:
- raise ParseException(s, l, "not a subentry")
-
- def checkUnindent(s, l, t):
- if l >= len(s): return
- curCol = col(l, s)
- if not(indentStack and curCol in indentStack):
- raise ParseException(s, l, "not an unindent")
- if curCol < indentStack[-1]:
- indentStack.pop()
-
- NL = OneOrMore(LineEnd().setWhitespaceChars("\t ").suppress(), stopOn=StringEnd())
- INDENT = (Empty() + Empty().setParseAction(checkSubIndent)).setName('INDENT')
- PEER = Empty().setParseAction(checkPeerIndent).setName('')
- UNDENT = Empty().setParseAction(checkUnindent).setName('UNINDENT')
- if indent:
- smExpr = Group(Optional(NL)
- + INDENT
- + OneOrMore(PEER + Group(blockStatementExpr) + Optional(NL), stopOn=StringEnd())
- + UNDENT)
- else:
- smExpr = Group(Optional(NL)
- + OneOrMore(PEER + Group(blockStatementExpr) + Optional(NL), stopOn=StringEnd())
- + UNDENT)
- smExpr.setFailAction(lambda a, b, c, d: reset_stack())
- blockStatementExpr.ignore(_bslash + LineEnd())
- return smExpr.setName('indented block')
-
-alphas8bit = srange(r"[\0xc0-\0xd6\0xd8-\0xf6\0xf8-\0xff]")
-punc8bit = srange(r"[\0xa1-\0xbf\0xd7\0xf7]")
-
-anyOpenTag, anyCloseTag = makeHTMLTags(Word(alphas, alphanums + "_:").setName('any tag'))
-_htmlEntityMap = dict(zip("gt lt amp nbsp quot apos".split(), '><& "\''))
-commonHTMLEntity = Regex('&(?P<entity>' + '|'.join(_htmlEntityMap.keys()) +");").setName("common HTML entity")
-def replaceHTMLEntity(t):
- """Helper parser action to replace common HTML entities with their special characters"""
- return _htmlEntityMap.get(t.entity)
-
-# it's easy to get these comment structures wrong - they're very common, so may as well make them available
-cStyleComment = Combine(Regex(r"/\*(?:[^*]|\*(?!/))*") + '*/').setName("C style comment")
-"Comment of the form ``/* ... */``"
-
-htmlComment = Regex(r"<!--[\s\S]*?-->").setName("HTML comment")
-"Comment of the form ``<!-- ... -->``"
-
-restOfLine = Regex(r".*").leaveWhitespace().setName("rest of line")
-dblSlashComment = Regex(r"//(?:\\\n|[^\n])*").setName("// comment")
-"Comment of the form ``// ... (to end of line)``"
-
-cppStyleComment = Combine(Regex(r"/\*(?:[^*]|\*(?!/))*") + '*/' | dblSlashComment).setName("C++ style comment")
-"Comment of either form :class:`cStyleComment` or :class:`dblSlashComment`"
-
-javaStyleComment = cppStyleComment
-"Same as :class:`cppStyleComment`"
-
-pythonStyleComment = Regex(r"#.*").setName("Python style comment")
-"Comment of the form ``# ... (to end of line)``"
-
-_commasepitem = Combine(OneOrMore(Word(printables, excludeChars=',')
- + Optional(Word(" \t")
- + ~Literal(",") + ~LineEnd()))).streamline().setName("commaItem")
-commaSeparatedList = delimitedList(Optional(quotedString.copy() | _commasepitem, default="")).setName("commaSeparatedList")
-"""(Deprecated) Predefined expression of 1 or more printable words or
-quoted strings, separated by commas.
-
-This expression is deprecated in favor of :class:`pyparsing_common.comma_separated_list`.
-"""
-
-# some other useful expressions - using lower-case class name since we are really using this as a namespace
-class pyparsing_common:
- """Here are some common low-level expressions that may be useful in
- jump-starting parser development:
-
- - numeric forms (:class:`integers<integer>`, :class:`reals<real>`,
- :class:`scientific notation<sci_real>`)
- - common :class:`programming identifiers<identifier>`
- - network addresses (:class:`MAC<mac_address>`,
- :class:`IPv4<ipv4_address>`, :class:`IPv6<ipv6_address>`)
- - ISO8601 :class:`dates<iso8601_date>` and
- :class:`datetime<iso8601_datetime>`
- - :class:`UUID<uuid>`
- - :class:`comma-separated list<comma_separated_list>`
-
- Parse actions:
-
- - :class:`convertToInteger`
- - :class:`convertToFloat`
- - :class:`convertToDate`
- - :class:`convertToDatetime`
- - :class:`stripHTMLTags`
- - :class:`upcaseTokens`
- - :class:`downcaseTokens`
-
- Example::
-
- pyparsing_common.number.runTests('''
- # any int or real number, returned as the appropriate type
- 100
- -100
- +100
- 3.14159
- 6.02e23
- 1e-12
- ''')
-
- pyparsing_common.fnumber.runTests('''
- # any int or real number, returned as float
- 100
- -100
- +100
- 3.14159
- 6.02e23
- 1e-12
- ''')
-
- pyparsing_common.hex_integer.runTests('''
- # hex numbers
- 100
- FF
- ''')
-
- pyparsing_common.fraction.runTests('''
- # fractions
- 1/2
- -3/4
- ''')
-
- pyparsing_common.mixed_integer.runTests('''
- # mixed fractions
- 1
- 1/2
- -3/4
- 1-3/4
- ''')
-
- import uuid
- pyparsing_common.uuid.setParseAction(tokenMap(uuid.UUID))
- pyparsing_common.uuid.runTests('''
- # uuid
- 12345678-1234-5678-1234-567812345678
- ''')
-
- prints::
-
- # any int or real number, returned as the appropriate type
- 100
- [100]
-
- -100
- [-100]
-
- +100
- [100]
-
- 3.14159
- [3.14159]
-
- 6.02e23
- [6.02e+23]
-
- 1e-12
- [1e-12]
-
- # any int or real number, returned as float
- 100
- [100.0]
-
- -100
- [-100.0]
-
- +100
- [100.0]
-
- 3.14159
- [3.14159]
-
- 6.02e23
- [6.02e+23]
-
- 1e-12
- [1e-12]
-
- # hex numbers
- 100
- [256]
-
- FF
- [255]
-
- # fractions
- 1/2
- [0.5]
-
- -3/4
- [-0.75]
-
- # mixed fractions
- 1
- [1]
-
- 1/2
- [0.5]
-
- -3/4
- [-0.75]
-
- 1-3/4
- [1.75]
-
- # uuid
- 12345678-1234-5678-1234-567812345678
- [UUID('12345678-1234-5678-1234-567812345678')]
- """
-
- convertToInteger = tokenMap(int)
- """
- Parse action for converting parsed integers to Python int
- """
-
- convertToFloat = tokenMap(float)
- """
- Parse action for converting parsed numbers to Python float
- """
-
- integer = Word(nums).setName("integer").setParseAction(convertToInteger)
- """expression that parses an unsigned integer, returns an int"""
-
- hex_integer = Word(hexnums).setName("hex integer").setParseAction(tokenMap(int, 16))
- """expression that parses a hexadecimal integer, returns an int"""
-
- signed_integer = Regex(r'[+-]?\d+').setName("signed integer").setParseAction(convertToInteger)
- """expression that parses an integer with optional leading sign, returns an int"""
-
- fraction = (signed_integer().setParseAction(convertToFloat) + '/' + signed_integer().setParseAction(convertToFloat)).setName("fraction")
- """fractional expression of an integer divided by an integer, returns a float"""
- fraction.addParseAction(lambda t: t[0]/t[-1])
-
- mixed_integer = (fraction | signed_integer + Optional(Optional('-').suppress() + fraction)).setName("fraction or mixed integer-fraction")
- """mixed integer of the form 'integer - fraction', with optional leading integer, returns float"""
- mixed_integer.addParseAction(sum)
-
- real = Regex(r'[+-]?(?:\d+\.\d*|\.\d+)').setName("real number").setParseAction(convertToFloat)
- """expression that parses a floating point number and returns a float"""
-
- sci_real = Regex(r'[+-]?(?:\d+(?:[eE][+-]?\d+)|(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+)?)').setName("real number with scientific notation").setParseAction(convertToFloat)
- """expression that parses a floating point number with optional
- scientific notation and returns a float"""
-
- # streamlining this expression makes the docs nicer-looking
- number = (sci_real | real | signed_integer).streamline()
- """any numeric expression, returns the corresponding Python type"""
-
- fnumber = Regex(r'[+-]?\d+\.?\d*([eE][+-]?\d+)?').setName("fnumber").setParseAction(convertToFloat)
- """any int or real number, returned as float"""
-
- identifier = Word(alphas + '_', alphanums + '_').setName("identifier")
- """typical code identifier (leading alpha or '_', followed by 0 or more alphas, nums, or '_')"""
-
- ipv4_address = Regex(r'(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})){3}').setName("IPv4 address")
- "IPv4 address (``0.0.0.0 - 255.255.255.255``)"
-
- _ipv6_part = Regex(r'[0-9a-fA-F]{1,4}').setName("hex_integer")
- _full_ipv6_address = (_ipv6_part + (':' + _ipv6_part) * 7).setName("full IPv6 address")
- _short_ipv6_address = (Optional(_ipv6_part + (':' + _ipv6_part) * (0, 6))
- + "::"
- + Optional(_ipv6_part + (':' + _ipv6_part) * (0, 6))
- ).setName("short IPv6 address")
- _short_ipv6_address.addCondition(lambda t: sum(1 for tt in t if pyparsing_common._ipv6_part.matches(tt)) < 8)
- _mixed_ipv6_address = ("::ffff:" + ipv4_address).setName("mixed IPv6 address")
- ipv6_address = Combine((_full_ipv6_address | _mixed_ipv6_address | _short_ipv6_address).setName("IPv6 address")).setName("IPv6 address")
- "IPv6 address (long, short, or mixed form)"
-
- mac_address = Regex(r'[0-9a-fA-F]{2}([:.-])[0-9a-fA-F]{2}(?:\1[0-9a-fA-F]{2}){4}').setName("MAC address")
- "MAC address xx:xx:xx:xx:xx (may also have '-' or '.' delimiters)"
-
- @staticmethod
- def convertToDate(fmt="%Y-%m-%d"):
- """
- Helper to create a parse action for converting parsed date string to Python datetime.date
-
- Params -
- - fmt - format to be passed to datetime.strptime (default= ``"%Y-%m-%d"``)
-
- Example::
-
- date_expr = pyparsing_common.iso8601_date.copy()
- date_expr.setParseAction(pyparsing_common.convertToDate())
- print(date_expr.parseString("1999-12-31"))
-
- prints::
-
- [datetime.date(1999, 12, 31)]
- """
- def cvt_fn(s, l, t):
- try:
- return datetime.strptime(t[0], fmt).date()
- except ValueError as ve:
- raise ParseException(s, l, str(ve))
- return cvt_fn
-
- @staticmethod
- def convertToDatetime(fmt="%Y-%m-%dT%H:%M:%S.%f"):
- """Helper to create a parse action for converting parsed
- datetime string to Python datetime.datetime
-
- Params -
- - fmt - format to be passed to datetime.strptime (default= ``"%Y-%m-%dT%H:%M:%S.%f"``)
-
- Example::
-
- dt_expr = pyparsing_common.iso8601_datetime.copy()
- dt_expr.setParseAction(pyparsing_common.convertToDatetime())
- print(dt_expr.parseString("1999-12-31T23:59:59.999"))
-
- prints::
-
- [datetime.datetime(1999, 12, 31, 23, 59, 59, 999000)]
- """
- def cvt_fn(s, l, t):
- try:
- return datetime.strptime(t[0], fmt)
- except ValueError as ve:
- raise ParseException(s, l, str(ve))
- return cvt_fn
-
- iso8601_date = Regex(r'(?P<year>\d{4})(?:-(?P<month>\d\d)(?:-(?P<day>\d\d))?)?').setName("ISO8601 date")
- "ISO8601 date (``yyyy-mm-dd``)"
-
- iso8601_datetime = Regex(r'(?P<year>\d{4})-(?P<month>\d\d)-(?P<day>\d\d)[T ](?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d(\.\d*)?)?)?(?P<tz>Z|[+-]\d\d:?\d\d)?').setName("ISO8601 datetime")
- "ISO8601 datetime (``yyyy-mm-ddThh:mm:ss.s(Z|+-00:00)``) - trailing seconds, milliseconds, and timezone optional; accepts separating ``'T'`` or ``' '``"
-
- uuid = Regex(r'[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}').setName("UUID")
- "UUID (``xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx``)"
-
- _html_stripper = anyOpenTag.suppress() | anyCloseTag.suppress()
- @staticmethod
- def stripHTMLTags(s, l, tokens):
- """Parse action to remove HTML tags from web page HTML source
-
- Example::
-
- # strip HTML links from normal text
- text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>'
- td, td_end = makeHTMLTags("TD")
- table_text = td + SkipTo(td_end).setParseAction(pyparsing_common.stripHTMLTags)("body") + td_end
- print(table_text.parseString(text).body)
-
- Prints::
-
- More info at the pyparsing wiki page
- """
- return pyparsing_common._html_stripper.transformString(tokens[0])
-
- _commasepitem = Combine(OneOrMore(~Literal(",")
- + ~LineEnd()
- + Word(printables, excludeChars=',')
- + Optional(White(" \t")))).streamline().setName("commaItem")
- comma_separated_list = delimitedList(Optional(quotedString.copy()
- | _commasepitem, default='')
- ).setName("comma separated list")
- """Predefined expression of 1 or more printable words or quoted strings, separated by commas."""
-
- upcaseTokens = staticmethod(tokenMap(lambda t: _ustr(t).upper()))
- """Parse action to convert tokens to upper case."""
-
- downcaseTokens = staticmethod(tokenMap(lambda t: _ustr(t).lower()))
- """Parse action to convert tokens to lower case."""
-
-
-class _lazyclassproperty(object):
- def __init__(self, fn):
- self.fn = fn
- self.__doc__ = fn.__doc__
- self.__name__ = fn.__name__
-
- def __get__(self, obj, cls):
- if cls is None:
- cls = type(obj)
- if not hasattr(cls, '_intern') or any(cls._intern is getattr(superclass, '_intern', [])
- for superclass in cls.__mro__[1:]):
- cls._intern = {}
- attrname = self.fn.__name__
- if attrname not in cls._intern:
- cls._intern[attrname] = self.fn(cls)
- return cls._intern[attrname]
-
-
-class unicode_set(object):
- """
- A set of Unicode characters, for language-specific strings for
- ``alphas``, ``nums``, ``alphanums``, and ``printables``.
- A unicode_set is defined by a list of ranges in the Unicode character
- set, in a class attribute ``_ranges``, such as::
-
- _ranges = [(0x0020, 0x007e), (0x00a0, 0x00ff),]
-
- A unicode set can also be defined using multiple inheritance of other unicode sets::
-
- class CJK(Chinese, Japanese, Korean):
- pass
- """
- _ranges = []
-
- @classmethod
- def _get_chars_for_ranges(cls):
- ret = []
- for cc in cls.__mro__:
- if cc is unicode_set:
- break
- for rr in cc._ranges:
- ret.extend(range(rr[0], rr[-1] + 1))
- return [unichr(c) for c in sorted(set(ret))]
-
- @_lazyclassproperty
- def printables(cls):
- "all non-whitespace characters in this range"
- return u''.join(filterfalse(unicode.isspace, cls._get_chars_for_ranges()))
-
- @_lazyclassproperty
- def alphas(cls):
- "all alphabetic characters in this range"
- return u''.join(filter(unicode.isalpha, cls._get_chars_for_ranges()))
-
- @_lazyclassproperty
- def nums(cls):
- "all numeric digit characters in this range"
- return u''.join(filter(unicode.isdigit, cls._get_chars_for_ranges()))
-
- @_lazyclassproperty
- def alphanums(cls):
- "all alphanumeric characters in this range"
- return cls.alphas + cls.nums
-
-
-class pyparsing_unicode(unicode_set):
- """
- A namespace class for defining common language unicode_sets.
- """
- _ranges = [(32, sys.maxunicode)]
-
- class Latin1(unicode_set):
- "Unicode set for Latin-1 Unicode Character Range"
- _ranges = [(0x0020, 0x007e), (0x00a0, 0x00ff),]
-
- class LatinA(unicode_set):
- "Unicode set for Latin-A Unicode Character Range"
- _ranges = [(0x0100, 0x017f),]
-
- class LatinB(unicode_set):
- "Unicode set for Latin-B Unicode Character Range"
- _ranges = [(0x0180, 0x024f),]
-
- class Greek(unicode_set):
- "Unicode set for Greek Unicode Character Ranges"
- _ranges = [
- (0x0370, 0x03ff), (0x1f00, 0x1f15), (0x1f18, 0x1f1d), (0x1f20, 0x1f45), (0x1f48, 0x1f4d),
- (0x1f50, 0x1f57), (0x1f59,), (0x1f5b,), (0x1f5d,), (0x1f5f, 0x1f7d), (0x1f80, 0x1fb4), (0x1fb6, 0x1fc4),
- (0x1fc6, 0x1fd3), (0x1fd6, 0x1fdb), (0x1fdd, 0x1fef), (0x1ff2, 0x1ff4), (0x1ff6, 0x1ffe),
- ]
-
- class Cyrillic(unicode_set):
- "Unicode set for Cyrillic Unicode Character Range"
- _ranges = [(0x0400, 0x04ff)]
-
- class Chinese(unicode_set):
- "Unicode set for Chinese Unicode Character Range"
- _ranges = [(0x4e00, 0x9fff), (0x3000, 0x303f),]
-
- class Japanese(unicode_set):
- "Unicode set for Japanese Unicode Character Range, combining Kanji, Hiragana, and Katakana ranges"
- _ranges = []
-
- class Kanji(unicode_set):
- "Unicode set for Kanji Unicode Character Range"
- _ranges = [(0x4E00, 0x9Fbf), (0x3000, 0x303f),]
-
- class Hiragana(unicode_set):
- "Unicode set for Hiragana Unicode Character Range"
- _ranges = [(0x3040, 0x309f),]
-
- class Katakana(unicode_set):
- "Unicode set for Katakana Unicode Character Range"
- _ranges = [(0x30a0, 0x30ff),]
-
- class Korean(unicode_set):
- "Unicode set for Korean Unicode Character Range"
- _ranges = [(0xac00, 0xd7af), (0x1100, 0x11ff), (0x3130, 0x318f), (0xa960, 0xa97f), (0xd7b0, 0xd7ff), (0x3000, 0x303f),]
-
- class CJK(Chinese, Japanese, Korean):
- "Unicode set for combined Chinese, Japanese, and Korean (CJK) Unicode Character Range"
- pass
-
- class Thai(unicode_set):
- "Unicode set for Thai Unicode Character Range"
- _ranges = [(0x0e01, 0x0e3a), (0x0e3f, 0x0e5b),]
-
- class Arabic(unicode_set):
- "Unicode set for Arabic Unicode Character Range"
- _ranges = [(0x0600, 0x061b), (0x061e, 0x06ff), (0x0700, 0x077f),]
-
- class Hebrew(unicode_set):
- "Unicode set for Hebrew Unicode Character Range"
- _ranges = [(0x0590, 0x05ff),]
-
- class Devanagari(unicode_set):
- "Unicode set for Devanagari Unicode Character Range"
- _ranges = [(0x0900, 0x097f), (0xa8e0, 0xa8ff)]
-
-pyparsing_unicode.Japanese._ranges = (pyparsing_unicode.Japanese.Kanji._ranges
- + pyparsing_unicode.Japanese.Hiragana._ranges
- + pyparsing_unicode.Japanese.Katakana._ranges)
-
-# define ranges in language character sets
-if PY_3:
- setattr(pyparsing_unicode, u"العربية", pyparsing_unicode.Arabic)
- setattr(pyparsing_unicode, u"中文", pyparsing_unicode.Chinese)
- setattr(pyparsing_unicode, u"кириллица", pyparsing_unicode.Cyrillic)
- setattr(pyparsing_unicode, u"Ελληνικά", pyparsing_unicode.Greek)
- setattr(pyparsing_unicode, u"עִברִית", pyparsing_unicode.Hebrew)
- setattr(pyparsing_unicode, u"日本語", pyparsing_unicode.Japanese)
- setattr(pyparsing_unicode.Japanese, u"漢字", pyparsing_unicode.Japanese.Kanji)
- setattr(pyparsing_unicode.Japanese, u"カタカナ", pyparsing_unicode.Japanese.Katakana)
- setattr(pyparsing_unicode.Japanese, u"ã²ã‚‰ãŒãª", pyparsing_unicode.Japanese.Hiragana)
- setattr(pyparsing_unicode, u"한국어", pyparsing_unicode.Korean)
- setattr(pyparsing_unicode, u"ไทย", pyparsing_unicode.Thai)
- setattr(pyparsing_unicode, u"देवनागरी", pyparsing_unicode.Devanagari)
-
-
-class pyparsing_test:
- """
- namespace class for classes useful in writing unit tests
- """
-
- class reset_pyparsing_context:
- """
- Context manager to be used when writing unit tests that modify pyparsing config values:
- - packrat parsing
- - default whitespace characters.
- - default keyword characters
- - literal string auto-conversion class
- - __diag__ settings
-
- Example:
- with reset_pyparsing_context():
- # test that literals used to construct a grammar are automatically suppressed
- ParserElement.inlineLiteralsUsing(Suppress)
-
- term = Word(alphas) | Word(nums)
- group = Group('(' + term[...] + ')')
-
- # assert that the '()' characters are not included in the parsed tokens
- self.assertParseAndCheckLisst(group, "(abc 123 def)", ['abc', '123', 'def'])
-
- # after exiting context manager, literals are converted to Literal expressions again
- """
-
- def __init__(self):
- self._save_context = {}
-
- def save(self):
- self._save_context["default_whitespace"] = ParserElement.DEFAULT_WHITE_CHARS
- self._save_context["default_keyword_chars"] = Keyword.DEFAULT_KEYWORD_CHARS
- self._save_context[
- "literal_string_class"
- ] = ParserElement._literalStringClass
- self._save_context["packrat_enabled"] = ParserElement._packratEnabled
- self._save_context["packrat_parse"] = ParserElement._parse
- self._save_context["__diag__"] = {
- name: getattr(__diag__, name) for name in __diag__._all_names
- }
- self._save_context["__compat__"] = {
- "collect_all_And_tokens": __compat__.collect_all_And_tokens
- }
- return self
-
- def restore(self):
- # reset pyparsing global state
- if (
- ParserElement.DEFAULT_WHITE_CHARS
- != self._save_context["default_whitespace"]
- ):
- ParserElement.setDefaultWhitespaceChars(
- self._save_context["default_whitespace"]
- )
- Keyword.DEFAULT_KEYWORD_CHARS = self._save_context["default_keyword_chars"]
- ParserElement.inlineLiteralsUsing(
- self._save_context["literal_string_class"]
- )
- for name, value in self._save_context["__diag__"].items():
- setattr(__diag__, name, value)
- ParserElement._packratEnabled = self._save_context["packrat_enabled"]
- ParserElement._parse = self._save_context["packrat_parse"]
- __compat__.collect_all_And_tokens = self._save_context["__compat__"]
-
- def __enter__(self):
- return self.save()
-
- def __exit__(self, *args):
- return self.restore()
-
- class TestParseResultsAsserts:
- """
- A mixin class to add parse results assertion methods to normal unittest.TestCase classes.
- """
- def assertParseResultsEquals(
- self, result, expected_list=None, expected_dict=None, msg=None
- ):
- """
- Unit test assertion to compare a ParseResults object with an optional expected_list,
- and compare any defined results names with an optional expected_dict.
- """
- if expected_list is not None:
- self.assertEqual(expected_list, result.asList(), msg=msg)
- if expected_dict is not None:
- self.assertEqual(expected_dict, result.asDict(), msg=msg)
-
- def assertParseAndCheckList(
- self, expr, test_string, expected_list, msg=None, verbose=True
- ):
- """
- Convenience wrapper assert to test a parser element and input string, and assert that
- the resulting ParseResults.asList() is equal to the expected_list.
- """
- result = expr.parseString(test_string, parseAll=True)
- if verbose:
- print(result.dump())
- self.assertParseResultsEquals(result, expected_list=expected_list, msg=msg)
-
- def assertParseAndCheckDict(
- self, expr, test_string, expected_dict, msg=None, verbose=True
- ):
- """
- Convenience wrapper assert to test a parser element and input string, and assert that
- the resulting ParseResults.asDict() is equal to the expected_dict.
- """
- result = expr.parseString(test_string, parseAll=True)
- if verbose:
- print(result.dump())
- self.assertParseResultsEquals(result, expected_dict=expected_dict, msg=msg)
-
- def assertRunTestResults(
- self, run_tests_report, expected_parse_results=None, msg=None
- ):
- """
- Unit test assertion to evaluate output of ParserElement.runTests(). If a list of
- list-dict tuples is given as the expected_parse_results argument, then these are zipped
- with the report tuples returned by runTests and evaluated using assertParseResultsEquals.
- Finally, asserts that the overall runTests() success value is True.
-
- :param run_tests_report: tuple(bool, [tuple(str, ParseResults or Exception)]) returned from runTests
- :param expected_parse_results (optional): [tuple(str, list, dict, Exception)]
- """
- run_test_success, run_test_results = run_tests_report
-
- if expected_parse_results is not None:
- merged = [
- (rpt[0], rpt[1], expected)
- for rpt, expected in zip(run_test_results, expected_parse_results)
- ]
- for test_string, result, expected in merged:
- # expected should be a tuple containing a list and/or a dict or an exception,
- # and optional failure message string
- # an empty tuple will skip any result validation
- fail_msg = next(
- (exp for exp in expected if isinstance(exp, str)), None
- )
- expected_exception = next(
- (
- exp
- for exp in expected
- if isinstance(exp, type) and issubclass(exp, Exception)
- ),
- None,
- )
- if expected_exception is not None:
- with self.assertRaises(
- expected_exception=expected_exception, msg=fail_msg or msg
- ):
- if isinstance(result, Exception):
- raise result
- else:
- expected_list = next(
- (exp for exp in expected if isinstance(exp, list)), None
- )
- expected_dict = next(
- (exp for exp in expected if isinstance(exp, dict)), None
- )
- if (expected_list, expected_dict) != (None, None):
- self.assertParseResultsEquals(
- result,
- expected_list=expected_list,
- expected_dict=expected_dict,
- msg=fail_msg or msg,
- )
- else:
- # warning here maybe?
- print("no validation for {!r}".format(test_string))
-
- # do this last, in case some specific test results can be reported instead
- self.assertTrue(
- run_test_success, msg=msg if msg is not None else "failed runTests"
- )
-
- @contextmanager
- def assertRaisesParseException(self, exc_type=ParseException, msg=None):
- with self.assertRaises(exc_type, msg=msg):
- yield
-
-
-if __name__ == "__main__":
-
- selectToken = CaselessLiteral("select")
- fromToken = CaselessLiteral("from")
-
- ident = Word(alphas, alphanums + "_$")
-
- columnName = delimitedList(ident, ".", combine=True).setParseAction(upcaseTokens)
- columnNameList = Group(delimitedList(columnName)).setName("columns")
- columnSpec = ('*' | columnNameList)
-
- tableName = delimitedList(ident, ".", combine=True).setParseAction(upcaseTokens)
- tableNameList = Group(delimitedList(tableName)).setName("tables")
-
- simpleSQL = selectToken("command") + columnSpec("columns") + fromToken + tableNameList("tables")
-
- # demo runTests method, including embedded comments in test string
- simpleSQL.runTests("""
- # '*' as column list and dotted table name
- select * from SYS.XYZZY
-
- # caseless match on "SELECT", and casts back to "select"
- SELECT * from XYZZY, ABC
-
- # list of column names, and mixed case SELECT keyword
- Select AA,BB,CC from Sys.dual
-
- # multiple tables
- Select A, B, C from Sys.dual, Table2
-
- # invalid SELECT keyword - should fail
- Xelect A, B, C from Sys.dual
-
- # incomplete command - should fail
- Select
-
- # invalid column name - should fail
- Select ^^^ frox Sys.dual
-
- """)
-
- pyparsing_common.number.runTests("""
- 100
- -100
- +100
- 3.14159
- 6.02e23
- 1e-12
- """)
-
- # any int or real number, returned as float
- pyparsing_common.fnumber.runTests("""
- 100
- -100
- +100
- 3.14159
- 6.02e23
- 1e-12
- """)
-
- pyparsing_common.hex_integer.runTests("""
- 100
- FF
- """)
-
- import uuid
- pyparsing_common.uuid.setParseAction(tokenMap(uuid.UUID))
- pyparsing_common.uuid.runTests("""
- 12345678-1234-5678-1234-567812345678
- """)
diff --git a/src/pip/_vendor/pyparsing.pyi b/src/pip/_vendor/pyparsing.pyi
deleted file mode 100644
index 8e9de6b02..000000000
--- a/src/pip/_vendor/pyparsing.pyi
+++ /dev/null
@@ -1 +0,0 @@
-from pyparsing import * \ No newline at end of file
diff --git a/src/pip/_vendor/pyparsing.LICENSE b/src/pip/_vendor/pyparsing/LICENSE
index 1bf98523e..1bf98523e 100644
--- a/src/pip/_vendor/pyparsing.LICENSE
+++ b/src/pip/_vendor/pyparsing/LICENSE
diff --git a/src/pip/_vendor/pyparsing/__init__.py b/src/pip/_vendor/pyparsing/__init__.py
new file mode 100644
index 000000000..75372500e
--- /dev/null
+++ b/src/pip/_vendor/pyparsing/__init__.py
@@ -0,0 +1,331 @@
+# module pyparsing.py
+#
+# Copyright (c) 2003-2022 Paul T. McGuire
+#
+# 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.
+#
+
+__doc__ = """
+pyparsing module - Classes and methods to define and execute parsing grammars
+=============================================================================
+
+The pyparsing module is an alternative approach to creating and
+executing simple grammars, vs. the traditional lex/yacc approach, or the
+use of regular expressions. With pyparsing, you don't need to learn
+a new syntax for defining grammars or matching expressions - the parsing
+module provides a library of classes that you use to construct the
+grammar directly in Python.
+
+Here is a program to parse "Hello, World!" (or any greeting of the form
+``"<salutation>, <addressee>!"``), built up using :class:`Word`,
+:class:`Literal`, and :class:`And` elements
+(the :meth:`'+'<ParserElement.__add__>` operators create :class:`And` expressions,
+and the strings are auto-converted to :class:`Literal` expressions)::
+
+ from pip._vendor.pyparsing import Word, alphas
+
+ # define grammar of a greeting
+ greet = Word(alphas) + "," + Word(alphas) + "!"
+
+ hello = "Hello, World!"
+ print(hello, "->", greet.parse_string(hello))
+
+The program outputs the following::
+
+ Hello, World! -> ['Hello', ',', 'World', '!']
+
+The Python representation of the grammar is quite readable, owing to the
+self-explanatory class names, and the use of :class:`'+'<And>`,
+:class:`'|'<MatchFirst>`, :class:`'^'<Or>` and :class:`'&'<Each>` operators.
+
+The :class:`ParseResults` object returned from
+:class:`ParserElement.parseString` can be
+accessed as a nested list, a dictionary, or an object with named
+attributes.
+
+The pyparsing module handles some of the problems that are typically
+vexing when writing text parsers:
+
+ - extra or missing whitespace (the above program will also handle
+ "Hello,World!", "Hello , World !", etc.)
+ - quoted strings
+ - embedded comments
+
+
+Getting Started -
+-----------------
+Visit the classes :class:`ParserElement` and :class:`ParseResults` to
+see the base classes that most other pyparsing
+classes inherit from. Use the docstrings for examples of how to:
+
+ - construct literal match expressions from :class:`Literal` and
+ :class:`CaselessLiteral` classes
+ - construct character word-group expressions using the :class:`Word`
+ class
+ - see how to create repetitive expressions using :class:`ZeroOrMore`
+ and :class:`OneOrMore` classes
+ - use :class:`'+'<And>`, :class:`'|'<MatchFirst>`, :class:`'^'<Or>`,
+ and :class:`'&'<Each>` operators to combine simple expressions into
+ more complex ones
+ - associate names with your parsed results using
+ :class:`ParserElement.setResultsName`
+ - access the parsed data, which is returned as a :class:`ParseResults`
+ object
+ - find some helpful expression short-cuts like :class:`delimitedList`
+ and :class:`oneOf`
+ - find more useful common expressions in the :class:`pyparsing_common`
+ namespace class
+"""
+from typing import NamedTuple
+
+
+class version_info(NamedTuple):
+ major: int
+ minor: int
+ micro: int
+ releaselevel: str
+ serial: int
+
+ @property
+ def __version__(self):
+ return (
+ "{}.{}.{}".format(self.major, self.minor, self.micro)
+ + (
+ "{}{}{}".format(
+ "r" if self.releaselevel[0] == "c" else "",
+ self.releaselevel[0],
+ self.serial,
+ ),
+ "",
+ )[self.releaselevel == "final"]
+ )
+
+ def __str__(self):
+ return "{} {} / {}".format(__name__, self.__version__, __version_time__)
+
+ def __repr__(self):
+ return "{}.{}({})".format(
+ __name__,
+ type(self).__name__,
+ ", ".join("{}={!r}".format(*nv) for nv in zip(self._fields, self)),
+ )
+
+
+__version_info__ = version_info(3, 0, 9, "final", 0)
+__version_time__ = "05 May 2022 07:02 UTC"
+__version__ = __version_info__.__version__
+__versionTime__ = __version_time__
+__author__ = "Paul McGuire <ptmcg.gm+pyparsing@gmail.com>"
+
+from .util import *
+from .exceptions import *
+from .actions import *
+from .core import __diag__, __compat__
+from .results import *
+from .core import *
+from .core import _builtin_exprs as core_builtin_exprs
+from .helpers import *
+from .helpers import _builtin_exprs as helper_builtin_exprs
+
+from .unicode import unicode_set, UnicodeRangeList, pyparsing_unicode as unicode
+from .testing import pyparsing_test as testing
+from .common import (
+ pyparsing_common as common,
+ _builtin_exprs as common_builtin_exprs,
+)
+
+# define backward compat synonyms
+if "pyparsing_unicode" not in globals():
+ pyparsing_unicode = unicode
+if "pyparsing_common" not in globals():
+ pyparsing_common = common
+if "pyparsing_test" not in globals():
+ pyparsing_test = testing
+
+core_builtin_exprs += common_builtin_exprs + helper_builtin_exprs
+
+
+__all__ = [
+ "__version__",
+ "__version_time__",
+ "__author__",
+ "__compat__",
+ "__diag__",
+ "And",
+ "AtLineStart",
+ "AtStringStart",
+ "CaselessKeyword",
+ "CaselessLiteral",
+ "CharsNotIn",
+ "Combine",
+ "Dict",
+ "Each",
+ "Empty",
+ "FollowedBy",
+ "Forward",
+ "GoToColumn",
+ "Group",
+ "IndentedBlock",
+ "Keyword",
+ "LineEnd",
+ "LineStart",
+ "Literal",
+ "Located",
+ "PrecededBy",
+ "MatchFirst",
+ "NoMatch",
+ "NotAny",
+ "OneOrMore",
+ "OnlyOnce",
+ "OpAssoc",
+ "Opt",
+ "Optional",
+ "Or",
+ "ParseBaseException",
+ "ParseElementEnhance",
+ "ParseException",
+ "ParseExpression",
+ "ParseFatalException",
+ "ParseResults",
+ "ParseSyntaxException",
+ "ParserElement",
+ "PositionToken",
+ "QuotedString",
+ "RecursiveGrammarException",
+ "Regex",
+ "SkipTo",
+ "StringEnd",
+ "StringStart",
+ "Suppress",
+ "Token",
+ "TokenConverter",
+ "White",
+ "Word",
+ "WordEnd",
+ "WordStart",
+ "ZeroOrMore",
+ "Char",
+ "alphanums",
+ "alphas",
+ "alphas8bit",
+ "any_close_tag",
+ "any_open_tag",
+ "c_style_comment",
+ "col",
+ "common_html_entity",
+ "counted_array",
+ "cpp_style_comment",
+ "dbl_quoted_string",
+ "dbl_slash_comment",
+ "delimited_list",
+ "dict_of",
+ "empty",
+ "hexnums",
+ "html_comment",
+ "identchars",
+ "identbodychars",
+ "java_style_comment",
+ "line",
+ "line_end",
+ "line_start",
+ "lineno",
+ "make_html_tags",
+ "make_xml_tags",
+ "match_only_at_col",
+ "match_previous_expr",
+ "match_previous_literal",
+ "nested_expr",
+ "null_debug_action",
+ "nums",
+ "one_of",
+ "printables",
+ "punc8bit",
+ "python_style_comment",
+ "quoted_string",
+ "remove_quotes",
+ "replace_with",
+ "replace_html_entity",
+ "rest_of_line",
+ "sgl_quoted_string",
+ "srange",
+ "string_end",
+ "string_start",
+ "trace_parse_action",
+ "unicode_string",
+ "with_attribute",
+ "indentedBlock",
+ "original_text_for",
+ "ungroup",
+ "infix_notation",
+ "locatedExpr",
+ "with_class",
+ "CloseMatch",
+ "token_map",
+ "pyparsing_common",
+ "pyparsing_unicode",
+ "unicode_set",
+ "condition_as_parse_action",
+ "pyparsing_test",
+ # pre-PEP8 compatibility names
+ "__versionTime__",
+ "anyCloseTag",
+ "anyOpenTag",
+ "cStyleComment",
+ "commonHTMLEntity",
+ "countedArray",
+ "cppStyleComment",
+ "dblQuotedString",
+ "dblSlashComment",
+ "delimitedList",
+ "dictOf",
+ "htmlComment",
+ "javaStyleComment",
+ "lineEnd",
+ "lineStart",
+ "makeHTMLTags",
+ "makeXMLTags",
+ "matchOnlyAtCol",
+ "matchPreviousExpr",
+ "matchPreviousLiteral",
+ "nestedExpr",
+ "nullDebugAction",
+ "oneOf",
+ "opAssoc",
+ "pythonStyleComment",
+ "quotedString",
+ "removeQuotes",
+ "replaceHTMLEntity",
+ "replaceWith",
+ "restOfLine",
+ "sglQuotedString",
+ "stringEnd",
+ "stringStart",
+ "traceParseAction",
+ "unicodeString",
+ "withAttribute",
+ "indentedBlock",
+ "originalTextFor",
+ "infixNotation",
+ "locatedExpr",
+ "withClass",
+ "tokenMap",
+ "conditionAsParseAction",
+ "autoname_elements",
+]
diff --git a/src/pip/_vendor/pyparsing/actions.py b/src/pip/_vendor/pyparsing/actions.py
new file mode 100644
index 000000000..f72c66e74
--- /dev/null
+++ b/src/pip/_vendor/pyparsing/actions.py
@@ -0,0 +1,207 @@
+# actions.py
+
+from .exceptions import ParseException
+from .util import col
+
+
+class OnlyOnce:
+ """
+ Wrapper for parse actions, to ensure they are only called once.
+ """
+
+ def __init__(self, method_call):
+ from .core import _trim_arity
+
+ self.callable = _trim_arity(method_call)
+ self.called = False
+
+ def __call__(self, s, l, t):
+ if not self.called:
+ results = self.callable(s, l, t)
+ self.called = True
+ return results
+ raise ParseException(s, l, "OnlyOnce obj called multiple times w/out reset")
+
+ def reset(self):
+ """
+ Allow the associated parse action to be called once more.
+ """
+
+ self.called = False
+
+
+def match_only_at_col(n):
+ """
+ Helper method for defining parse actions that require matching at
+ a specific column in the input text.
+ """
+
+ def verify_col(strg, locn, toks):
+ if col(locn, strg) != n:
+ raise ParseException(strg, locn, "matched token not at column {}".format(n))
+
+ return verify_col
+
+
+def replace_with(repl_str):
+ """
+ Helper method for common parse actions that simply return
+ a literal value. Especially useful when used with
+ :class:`transform_string<ParserElement.transform_string>` ().
+
+ Example::
+
+ num = Word(nums).set_parse_action(lambda toks: int(toks[0]))
+ na = one_of("N/A NA").set_parse_action(replace_with(math.nan))
+ term = na | num
+
+ term[1, ...].parse_string("324 234 N/A 234") # -> [324, 234, nan, 234]
+ """
+ return lambda s, l, t: [repl_str]
+
+
+def remove_quotes(s, l, t):
+ """
+ Helper parse action for removing quotation marks from parsed
+ quoted strings.
+
+ Example::
+
+ # by default, quotation marks are included in parsed results
+ quoted_string.parse_string("'Now is the Winter of our Discontent'") # -> ["'Now is the Winter of our Discontent'"]
+
+ # use remove_quotes to strip quotation marks from parsed results
+ quoted_string.set_parse_action(remove_quotes)
+ quoted_string.parse_string("'Now is the Winter of our Discontent'") # -> ["Now is the Winter of our Discontent"]
+ """
+ return t[0][1:-1]
+
+
+def with_attribute(*args, **attr_dict):
+ """
+ Helper to create a validating parse action to be used with start
+ tags created with :class:`make_xml_tags` or
+ :class:`make_html_tags`. Use ``with_attribute`` to qualify
+ a starting tag with a required attribute value, to avoid false
+ matches on common tags such as ``<TD>`` or ``<DIV>``.
+
+ Call ``with_attribute`` with a series of attribute names and
+ values. Specify the list of filter attributes names and values as:
+
+ - keyword arguments, as in ``(align="right")``, or
+ - as an explicit dict with ``**`` operator, when an attribute
+ name is also a Python reserved word, as in ``**{"class":"Customer", "align":"right"}``
+ - a list of name-value tuples, as in ``(("ns1:class", "Customer"), ("ns2:align", "right"))``
+
+ For attribute names with a namespace prefix, you must use the second
+ form. Attribute names are matched insensitive to upper/lower case.
+
+ If just testing for ``class`` (with or without a namespace), use
+ :class:`with_class`.
+
+ To verify that the attribute exists, but without specifying a value,
+ pass ``with_attribute.ANY_VALUE`` as the value.
+
+ Example::
+
+ html = '''
+ <div>
+ Some text
+ <div type="grid">1 4 0 1 0</div>
+ <div type="graph">1,3 2,3 1,1</div>
+ <div>this has no type</div>
+ </div>
+
+ '''
+ div,div_end = make_html_tags("div")
+
+ # only match div tag having a type attribute with value "grid"
+ div_grid = div().set_parse_action(with_attribute(type="grid"))
+ grid_expr = div_grid + SkipTo(div | div_end)("body")
+ for grid_header in grid_expr.search_string(html):
+ print(grid_header.body)
+
+ # construct a match with any div tag having a type attribute, regardless of the value
+ div_any_type = div().set_parse_action(with_attribute(type=with_attribute.ANY_VALUE))
+ div_expr = div_any_type + SkipTo(div | div_end)("body")
+ for div_header in div_expr.search_string(html):
+ print(div_header.body)
+
+ prints::
+
+ 1 4 0 1 0
+
+ 1 4 0 1 0
+ 1,3 2,3 1,1
+ """
+ if args:
+ attrs = args[:]
+ else:
+ attrs = attr_dict.items()
+ attrs = [(k, v) for k, v in attrs]
+
+ def pa(s, l, tokens):
+ for attrName, attrValue in attrs:
+ if attrName not in tokens:
+ raise ParseException(s, l, "no matching attribute " + attrName)
+ if attrValue != with_attribute.ANY_VALUE and tokens[attrName] != attrValue:
+ raise ParseException(
+ s,
+ l,
+ "attribute {!r} has value {!r}, must be {!r}".format(
+ attrName, tokens[attrName], attrValue
+ ),
+ )
+
+ return pa
+
+
+with_attribute.ANY_VALUE = object()
+
+
+def with_class(classname, namespace=""):
+ """
+ Simplified version of :class:`with_attribute` when
+ matching on a div class - made difficult because ``class`` is
+ a reserved word in Python.
+
+ Example::
+
+ html = '''
+ <div>
+ Some text
+ <div class="grid">1 4 0 1 0</div>
+ <div class="graph">1,3 2,3 1,1</div>
+ <div>this &lt;div&gt; has no class</div>
+ </div>
+
+ '''
+ div,div_end = make_html_tags("div")
+ div_grid = div().set_parse_action(with_class("grid"))
+
+ grid_expr = div_grid + SkipTo(div | div_end)("body")
+ for grid_header in grid_expr.search_string(html):
+ print(grid_header.body)
+
+ div_any_type = div().set_parse_action(with_class(withAttribute.ANY_VALUE))
+ div_expr = div_any_type + SkipTo(div | div_end)("body")
+ for div_header in div_expr.search_string(html):
+ print(div_header.body)
+
+ prints::
+
+ 1 4 0 1 0
+
+ 1 4 0 1 0
+ 1,3 2,3 1,1
+ """
+ classattr = "{}:class".format(namespace) if namespace else "class"
+ return with_attribute(**{classattr: classname})
+
+
+# pre-PEP8 compatibility symbols
+replaceWith = replace_with
+removeQuotes = remove_quotes
+withAttribute = with_attribute
+withClass = with_class
+matchOnlyAtCol = match_only_at_col
diff --git a/src/pip/_vendor/pyparsing/common.py b/src/pip/_vendor/pyparsing/common.py
new file mode 100644
index 000000000..1859fb79c
--- /dev/null
+++ b/src/pip/_vendor/pyparsing/common.py
@@ -0,0 +1,424 @@
+# common.py
+from .core import *
+from .helpers import delimited_list, any_open_tag, any_close_tag
+from datetime import datetime
+
+
+# some other useful expressions - using lower-case class name since we are really using this as a namespace
+class pyparsing_common:
+ """Here are some common low-level expressions that may be useful in
+ jump-starting parser development:
+
+ - numeric forms (:class:`integers<integer>`, :class:`reals<real>`,
+ :class:`scientific notation<sci_real>`)
+ - common :class:`programming identifiers<identifier>`
+ - network addresses (:class:`MAC<mac_address>`,
+ :class:`IPv4<ipv4_address>`, :class:`IPv6<ipv6_address>`)
+ - ISO8601 :class:`dates<iso8601_date>` and
+ :class:`datetime<iso8601_datetime>`
+ - :class:`UUID<uuid>`
+ - :class:`comma-separated list<comma_separated_list>`
+ - :class:`url`
+
+ Parse actions:
+
+ - :class:`convertToInteger`
+ - :class:`convertToFloat`
+ - :class:`convertToDate`
+ - :class:`convertToDatetime`
+ - :class:`stripHTMLTags`
+ - :class:`upcaseTokens`
+ - :class:`downcaseTokens`
+
+ Example::
+
+ pyparsing_common.number.runTests('''
+ # any int or real number, returned as the appropriate type
+ 100
+ -100
+ +100
+ 3.14159
+ 6.02e23
+ 1e-12
+ ''')
+
+ pyparsing_common.fnumber.runTests('''
+ # any int or real number, returned as float
+ 100
+ -100
+ +100
+ 3.14159
+ 6.02e23
+ 1e-12
+ ''')
+
+ pyparsing_common.hex_integer.runTests('''
+ # hex numbers
+ 100
+ FF
+ ''')
+
+ pyparsing_common.fraction.runTests('''
+ # fractions
+ 1/2
+ -3/4
+ ''')
+
+ pyparsing_common.mixed_integer.runTests('''
+ # mixed fractions
+ 1
+ 1/2
+ -3/4
+ 1-3/4
+ ''')
+
+ import uuid
+ pyparsing_common.uuid.setParseAction(tokenMap(uuid.UUID))
+ pyparsing_common.uuid.runTests('''
+ # uuid
+ 12345678-1234-5678-1234-567812345678
+ ''')
+
+ prints::
+
+ # any int or real number, returned as the appropriate type
+ 100
+ [100]
+
+ -100
+ [-100]
+
+ +100
+ [100]
+
+ 3.14159
+ [3.14159]
+
+ 6.02e23
+ [6.02e+23]
+
+ 1e-12
+ [1e-12]
+
+ # any int or real number, returned as float
+ 100
+ [100.0]
+
+ -100
+ [-100.0]
+
+ +100
+ [100.0]
+
+ 3.14159
+ [3.14159]
+
+ 6.02e23
+ [6.02e+23]
+
+ 1e-12
+ [1e-12]
+
+ # hex numbers
+ 100
+ [256]
+
+ FF
+ [255]
+
+ # fractions
+ 1/2
+ [0.5]
+
+ -3/4
+ [-0.75]
+
+ # mixed fractions
+ 1
+ [1]
+
+ 1/2
+ [0.5]
+
+ -3/4
+ [-0.75]
+
+ 1-3/4
+ [1.75]
+
+ # uuid
+ 12345678-1234-5678-1234-567812345678
+ [UUID('12345678-1234-5678-1234-567812345678')]
+ """
+
+ convert_to_integer = token_map(int)
+ """
+ Parse action for converting parsed integers to Python int
+ """
+
+ convert_to_float = token_map(float)
+ """
+ Parse action for converting parsed numbers to Python float
+ """
+
+ integer = Word(nums).set_name("integer").set_parse_action(convert_to_integer)
+ """expression that parses an unsigned integer, returns an int"""
+
+ hex_integer = (
+ Word(hexnums).set_name("hex integer").set_parse_action(token_map(int, 16))
+ )
+ """expression that parses a hexadecimal integer, returns an int"""
+
+ signed_integer = (
+ Regex(r"[+-]?\d+")
+ .set_name("signed integer")
+ .set_parse_action(convert_to_integer)
+ )
+ """expression that parses an integer with optional leading sign, returns an int"""
+
+ fraction = (
+ signed_integer().set_parse_action(convert_to_float)
+ + "/"
+ + signed_integer().set_parse_action(convert_to_float)
+ ).set_name("fraction")
+ """fractional expression of an integer divided by an integer, returns a float"""
+ fraction.add_parse_action(lambda tt: tt[0] / tt[-1])
+
+ mixed_integer = (
+ fraction | signed_integer + Opt(Opt("-").suppress() + fraction)
+ ).set_name("fraction or mixed integer-fraction")
+ """mixed integer of the form 'integer - fraction', with optional leading integer, returns float"""
+ mixed_integer.add_parse_action(sum)
+
+ real = (
+ Regex(r"[+-]?(?:\d+\.\d*|\.\d+)")
+ .set_name("real number")
+ .set_parse_action(convert_to_float)
+ )
+ """expression that parses a floating point number and returns a float"""
+
+ sci_real = (
+ Regex(r"[+-]?(?:\d+(?:[eE][+-]?\d+)|(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+)?)")
+ .set_name("real number with scientific notation")
+ .set_parse_action(convert_to_float)
+ )
+ """expression that parses a floating point number with optional
+ scientific notation and returns a float"""
+
+ # streamlining this expression makes the docs nicer-looking
+ number = (sci_real | real | signed_integer).setName("number").streamline()
+ """any numeric expression, returns the corresponding Python type"""
+
+ fnumber = (
+ Regex(r"[+-]?\d+\.?\d*([eE][+-]?\d+)?")
+ .set_name("fnumber")
+ .set_parse_action(convert_to_float)
+ )
+ """any int or real number, returned as float"""
+
+ identifier = Word(identchars, identbodychars).set_name("identifier")
+ """typical code identifier (leading alpha or '_', followed by 0 or more alphas, nums, or '_')"""
+
+ ipv4_address = Regex(
+ r"(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})){3}"
+ ).set_name("IPv4 address")
+ "IPv4 address (``0.0.0.0 - 255.255.255.255``)"
+
+ _ipv6_part = Regex(r"[0-9a-fA-F]{1,4}").set_name("hex_integer")
+ _full_ipv6_address = (_ipv6_part + (":" + _ipv6_part) * 7).set_name(
+ "full IPv6 address"
+ )
+ _short_ipv6_address = (
+ Opt(_ipv6_part + (":" + _ipv6_part) * (0, 6))
+ + "::"
+ + Opt(_ipv6_part + (":" + _ipv6_part) * (0, 6))
+ ).set_name("short IPv6 address")
+ _short_ipv6_address.add_condition(
+ lambda t: sum(1 for tt in t if pyparsing_common._ipv6_part.matches(tt)) < 8
+ )
+ _mixed_ipv6_address = ("::ffff:" + ipv4_address).set_name("mixed IPv6 address")
+ ipv6_address = Combine(
+ (_full_ipv6_address | _mixed_ipv6_address | _short_ipv6_address).set_name(
+ "IPv6 address"
+ )
+ ).set_name("IPv6 address")
+ "IPv6 address (long, short, or mixed form)"
+
+ mac_address = Regex(
+ r"[0-9a-fA-F]{2}([:.-])[0-9a-fA-F]{2}(?:\1[0-9a-fA-F]{2}){4}"
+ ).set_name("MAC address")
+ "MAC address xx:xx:xx:xx:xx (may also have '-' or '.' delimiters)"
+
+ @staticmethod
+ def convert_to_date(fmt: str = "%Y-%m-%d"):
+ """
+ Helper to create a parse action for converting parsed date string to Python datetime.date
+
+ Params -
+ - fmt - format to be passed to datetime.strptime (default= ``"%Y-%m-%d"``)
+
+ Example::
+
+ date_expr = pyparsing_common.iso8601_date.copy()
+ date_expr.setParseAction(pyparsing_common.convertToDate())
+ print(date_expr.parseString("1999-12-31"))
+
+ prints::
+
+ [datetime.date(1999, 12, 31)]
+ """
+
+ def cvt_fn(ss, ll, tt):
+ try:
+ return datetime.strptime(tt[0], fmt).date()
+ except ValueError as ve:
+ raise ParseException(ss, ll, str(ve))
+
+ return cvt_fn
+
+ @staticmethod
+ def convert_to_datetime(fmt: str = "%Y-%m-%dT%H:%M:%S.%f"):
+ """Helper to create a parse action for converting parsed
+ datetime string to Python datetime.datetime
+
+ Params -
+ - fmt - format to be passed to datetime.strptime (default= ``"%Y-%m-%dT%H:%M:%S.%f"``)
+
+ Example::
+
+ dt_expr = pyparsing_common.iso8601_datetime.copy()
+ dt_expr.setParseAction(pyparsing_common.convertToDatetime())
+ print(dt_expr.parseString("1999-12-31T23:59:59.999"))
+
+ prints::
+
+ [datetime.datetime(1999, 12, 31, 23, 59, 59, 999000)]
+ """
+
+ def cvt_fn(s, l, t):
+ try:
+ return datetime.strptime(t[0], fmt)
+ except ValueError as ve:
+ raise ParseException(s, l, str(ve))
+
+ return cvt_fn
+
+ iso8601_date = Regex(
+ r"(?P<year>\d{4})(?:-(?P<month>\d\d)(?:-(?P<day>\d\d))?)?"
+ ).set_name("ISO8601 date")
+ "ISO8601 date (``yyyy-mm-dd``)"
+
+ iso8601_datetime = Regex(
+ r"(?P<year>\d{4})-(?P<month>\d\d)-(?P<day>\d\d)[T ](?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d(\.\d*)?)?)?(?P<tz>Z|[+-]\d\d:?\d\d)?"
+ ).set_name("ISO8601 datetime")
+ "ISO8601 datetime (``yyyy-mm-ddThh:mm:ss.s(Z|+-00:00)``) - trailing seconds, milliseconds, and timezone optional; accepts separating ``'T'`` or ``' '``"
+
+ uuid = Regex(r"[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}").set_name("UUID")
+ "UUID (``xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx``)"
+
+ _html_stripper = any_open_tag.suppress() | any_close_tag.suppress()
+
+ @staticmethod
+ def strip_html_tags(s: str, l: int, tokens: ParseResults):
+ """Parse action to remove HTML tags from web page HTML source
+
+ Example::
+
+ # strip HTML links from normal text
+ text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>'
+ td, td_end = makeHTMLTags("TD")
+ table_text = td + SkipTo(td_end).setParseAction(pyparsing_common.stripHTMLTags)("body") + td_end
+ print(table_text.parseString(text).body)
+
+ Prints::
+
+ More info at the pyparsing wiki page
+ """
+ return pyparsing_common._html_stripper.transform_string(tokens[0])
+
+ _commasepitem = (
+ Combine(
+ OneOrMore(
+ ~Literal(",")
+ + ~LineEnd()
+ + Word(printables, exclude_chars=",")
+ + Opt(White(" \t") + ~FollowedBy(LineEnd() | ","))
+ )
+ )
+ .streamline()
+ .set_name("commaItem")
+ )
+ comma_separated_list = delimited_list(
+ Opt(quoted_string.copy() | _commasepitem, default="")
+ ).set_name("comma separated list")
+ """Predefined expression of 1 or more printable words or quoted strings, separated by commas."""
+
+ upcase_tokens = staticmethod(token_map(lambda t: t.upper()))
+ """Parse action to convert tokens to upper case."""
+
+ downcase_tokens = staticmethod(token_map(lambda t: t.lower()))
+ """Parse action to convert tokens to lower case."""
+
+ # fmt: off
+ url = Regex(
+ # https://mathiasbynens.be/demo/url-regex
+ # https://gist.github.com/dperini/729294
+ r"^" +
+ # protocol identifier (optional)
+ # short syntax // still required
+ r"(?:(?:(?P<scheme>https?|ftp):)?\/\/)" +
+ # user:pass BasicAuth (optional)
+ r"(?:(?P<auth>\S+(?::\S*)?)@)?" +
+ r"(?P<host>" +
+ # IP address exclusion
+ # private & local networks
+ r"(?!(?:10|127)(?:\.\d{1,3}){3})" +
+ r"(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})" +
+ r"(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})" +
+ # IP address dotted notation octets
+ # excludes loopback network 0.0.0.0
+ # excludes reserved space >= 224.0.0.0
+ # excludes network & broadcast addresses
+ # (first & last IP address of each class)
+ r"(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])" +
+ r"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}" +
+ r"(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))" +
+ r"|" +
+ # host & domain names, may end with dot
+ # can be replaced by a shortest alternative
+ # (?![-_])(?:[-\w\u00a1-\uffff]{0,63}[^-_]\.)+
+ r"(?:" +
+ r"(?:" +
+ r"[a-z0-9\u00a1-\uffff]" +
+ r"[a-z0-9\u00a1-\uffff_-]{0,62}" +
+ r")?" +
+ r"[a-z0-9\u00a1-\uffff]\." +
+ r")+" +
+ # TLD identifier name, may end with dot
+ r"(?:[a-z\u00a1-\uffff]{2,}\.?)" +
+ r")" +
+ # port number (optional)
+ r"(:(?P<port>\d{2,5}))?" +
+ # resource path (optional)
+ r"(?P<path>\/[^?# ]*)?" +
+ # query string (optional)
+ r"(\?(?P<query>[^#]*))?" +
+ # fragment (optional)
+ r"(#(?P<fragment>\S*))?" +
+ r"$"
+ ).set_name("url")
+ # fmt: on
+
+ # pre-PEP8 compatibility names
+ convertToInteger = convert_to_integer
+ convertToFloat = convert_to_float
+ convertToDate = convert_to_date
+ convertToDatetime = convert_to_datetime
+ stripHTMLTags = strip_html_tags
+ upcaseTokens = upcase_tokens
+ downcaseTokens = downcase_tokens
+
+
+_builtin_exprs = [
+ v for v in vars(pyparsing_common).values() if isinstance(v, ParserElement)
+]
diff --git a/src/pip/_vendor/pyparsing/core.py b/src/pip/_vendor/pyparsing/core.py
new file mode 100644
index 000000000..6ff3c766f
--- /dev/null
+++ b/src/pip/_vendor/pyparsing/core.py
@@ -0,0 +1,5814 @@
+#
+# core.py
+#
+import os
+import typing
+from typing import (
+ NamedTuple,
+ Union,
+ Callable,
+ Any,
+ Generator,
+ Tuple,
+ List,
+ TextIO,
+ Set,
+ Sequence,
+)
+from abc import ABC, abstractmethod
+from enum import Enum
+import string
+import copy
+import warnings
+import re
+import sys
+from collections.abc import Iterable
+import traceback
+import types
+from operator import itemgetter
+from functools import wraps
+from threading import RLock
+from pathlib import Path
+
+from .util import (
+ _FifoCache,
+ _UnboundedCache,
+ __config_flags,
+ _collapse_string_to_ranges,
+ _escape_regex_range_chars,
+ _bslash,
+ _flatten,
+ LRUMemo as _LRUMemo,
+ UnboundedMemo as _UnboundedMemo,
+)
+from .exceptions import *
+from .actions import *
+from .results import ParseResults, _ParseResultsWithOffset
+from .unicode import pyparsing_unicode
+
+_MAX_INT = sys.maxsize
+str_type: Tuple[type, ...] = (str, bytes)
+
+#
+# Copyright (c) 2003-2022 Paul T. McGuire
+#
+# 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.
+#
+
+
+if sys.version_info >= (3, 8):
+ from functools import cached_property
+else:
+
+ class cached_property:
+ def __init__(self, func):
+ self._func = func
+
+ def __get__(self, instance, owner=None):
+ ret = instance.__dict__[self._func.__name__] = self._func(instance)
+ return ret
+
+
+class __compat__(__config_flags):
+ """
+ A cross-version compatibility configuration for pyparsing features that will be
+ released in a future version. By setting values in this configuration to True,
+ those features can be enabled in prior versions for compatibility development
+ and testing.
+
+ - ``collect_all_And_tokens`` - flag to enable fix for Issue #63 that fixes erroneous grouping
+ of results names when an :class:`And` expression is nested within an :class:`Or` or :class:`MatchFirst`;
+ maintained for compatibility, but setting to ``False`` no longer restores pre-2.3.1
+ behavior
+ """
+
+ _type_desc = "compatibility"
+
+ collect_all_And_tokens = True
+
+ _all_names = [__ for __ in locals() if not __.startswith("_")]
+ _fixed_names = """
+ collect_all_And_tokens
+ """.split()
+
+
+class __diag__(__config_flags):
+ _type_desc = "diagnostic"
+
+ warn_multiple_tokens_in_named_alternation = False
+ warn_ungrouped_named_tokens_in_collection = False
+ warn_name_set_on_empty_Forward = False
+ warn_on_parse_using_empty_Forward = False
+ warn_on_assignment_to_Forward = False
+ warn_on_multiple_string_args_to_oneof = False
+ warn_on_match_first_with_lshift_operator = False
+ enable_debug_on_named_expressions = False
+
+ _all_names = [__ for __ in locals() if not __.startswith("_")]
+ _warning_names = [name for name in _all_names if name.startswith("warn")]
+ _debug_names = [name for name in _all_names if name.startswith("enable_debug")]
+
+ @classmethod
+ def enable_all_warnings(cls) -> None:
+ for name in cls._warning_names:
+ cls.enable(name)
+
+
+class Diagnostics(Enum):
+ """
+ Diagnostic configuration (all default to disabled)
+ - ``warn_multiple_tokens_in_named_alternation`` - flag to enable warnings when a results
+ name is defined on a :class:`MatchFirst` or :class:`Or` expression with one or more :class:`And` subexpressions
+ - ``warn_ungrouped_named_tokens_in_collection`` - flag to enable warnings when a results
+ name is defined on a containing expression with ungrouped subexpressions that also
+ have results names
+ - ``warn_name_set_on_empty_Forward`` - flag to enable warnings when a :class:`Forward` is defined
+ with a results name, but has no contents defined
+ - ``warn_on_parse_using_empty_Forward`` - flag to enable warnings when a :class:`Forward` is
+ defined in a grammar but has never had an expression attached to it
+ - ``warn_on_assignment_to_Forward`` - flag to enable warnings when a :class:`Forward` is defined
+ but is overwritten by assigning using ``'='`` instead of ``'<<='`` or ``'<<'``
+ - ``warn_on_multiple_string_args_to_oneof`` - flag to enable warnings when :class:`one_of` is
+ incorrectly called with multiple str arguments
+ - ``enable_debug_on_named_expressions`` - flag to auto-enable debug on all subsequent
+ calls to :class:`ParserElement.set_name`
+
+ Diagnostics are enabled/disabled by calling :class:`enable_diag` and :class:`disable_diag`.
+ All warnings can be enabled by calling :class:`enable_all_warnings`.
+ """
+
+ warn_multiple_tokens_in_named_alternation = 0
+ warn_ungrouped_named_tokens_in_collection = 1
+ warn_name_set_on_empty_Forward = 2
+ warn_on_parse_using_empty_Forward = 3
+ warn_on_assignment_to_Forward = 4
+ warn_on_multiple_string_args_to_oneof = 5
+ warn_on_match_first_with_lshift_operator = 6
+ enable_debug_on_named_expressions = 7
+
+
+def enable_diag(diag_enum: Diagnostics) -> None:
+ """
+ Enable a global pyparsing diagnostic flag (see :class:`Diagnostics`).
+ """
+ __diag__.enable(diag_enum.name)
+
+
+def disable_diag(diag_enum: Diagnostics) -> None:
+ """
+ Disable a global pyparsing diagnostic flag (see :class:`Diagnostics`).
+ """
+ __diag__.disable(diag_enum.name)
+
+
+def enable_all_warnings() -> None:
+ """
+ Enable all global pyparsing diagnostic warnings (see :class:`Diagnostics`).
+ """
+ __diag__.enable_all_warnings()
+
+
+# hide abstract class
+del __config_flags
+
+
+def _should_enable_warnings(
+ cmd_line_warn_options: typing.Iterable[str], warn_env_var: typing.Optional[str]
+) -> bool:
+ enable = bool(warn_env_var)
+ for warn_opt in cmd_line_warn_options:
+ w_action, w_message, w_category, w_module, w_line = (warn_opt + "::::").split(
+ ":"
+ )[:5]
+ if not w_action.lower().startswith("i") and (
+ not (w_message or w_category or w_module) or w_module == "pyparsing"
+ ):
+ enable = True
+ elif w_action.lower().startswith("i") and w_module in ("pyparsing", ""):
+ enable = False
+ return enable
+
+
+if _should_enable_warnings(
+ sys.warnoptions, os.environ.get("PYPARSINGENABLEALLWARNINGS")
+):
+ enable_all_warnings()
+
+
+# build list of single arg builtins, that can be used as parse actions
+_single_arg_builtins = {
+ sum,
+ len,
+ sorted,
+ reversed,
+ list,
+ tuple,
+ set,
+ any,
+ all,
+ min,
+ max,
+}
+
+_generatorType = types.GeneratorType
+ParseAction = Union[
+ Callable[[], Any],
+ Callable[[ParseResults], Any],
+ Callable[[int, ParseResults], Any],
+ Callable[[str, int, ParseResults], Any],
+]
+ParseCondition = Union[
+ Callable[[], bool],
+ Callable[[ParseResults], bool],
+ Callable[[int, ParseResults], bool],
+ Callable[[str, int, ParseResults], bool],
+]
+ParseFailAction = Callable[[str, int, "ParserElement", Exception], None]
+DebugStartAction = Callable[[str, int, "ParserElement", bool], None]
+DebugSuccessAction = Callable[
+ [str, int, int, "ParserElement", ParseResults, bool], None
+]
+DebugExceptionAction = Callable[[str, int, "ParserElement", Exception, bool], None]
+
+
+alphas = string.ascii_uppercase + string.ascii_lowercase
+identchars = pyparsing_unicode.Latin1.identchars
+identbodychars = pyparsing_unicode.Latin1.identbodychars
+nums = "0123456789"
+hexnums = nums + "ABCDEFabcdef"
+alphanums = alphas + nums
+printables = "".join([c for c in string.printable if c not in string.whitespace])
+
+_trim_arity_call_line: traceback.StackSummary = None
+
+
+def _trim_arity(func, max_limit=3):
+ """decorator to trim function calls to match the arity of the target"""
+ global _trim_arity_call_line
+
+ if func in _single_arg_builtins:
+ return lambda s, l, t: func(t)
+
+ limit = 0
+ found_arity = False
+
+ def extract_tb(tb, limit=0):
+ frames = traceback.extract_tb(tb, limit=limit)
+ frame_summary = frames[-1]
+ return [frame_summary[:2]]
+
+ # synthesize what would be returned by traceback.extract_stack at the call to
+ # user's parse action 'func', so that we don't incur call penalty at parse time
+
+ # fmt: off
+ LINE_DIFF = 7
+ # IF ANY CODE CHANGES, EVEN JUST COMMENTS OR BLANK LINES, BETWEEN THE NEXT LINE AND
+ # THE CALL TO FUNC INSIDE WRAPPER, LINE_DIFF MUST BE MODIFIED!!!!
+ _trim_arity_call_line = (_trim_arity_call_line or traceback.extract_stack(limit=2)[-1])
+ pa_call_line_synth = (_trim_arity_call_line[0], _trim_arity_call_line[1] + LINE_DIFF)
+
+ def wrapper(*args):
+ nonlocal found_arity, limit
+ while 1:
+ try:
+ ret = func(*args[limit:])
+ found_arity = True
+ return ret
+ except TypeError as te:
+ # re-raise TypeErrors if they did not come from our arity testing
+ if found_arity:
+ raise
+ else:
+ tb = te.__traceback__
+ trim_arity_type_error = (
+ extract_tb(tb, limit=2)[-1][:2] == pa_call_line_synth
+ )
+ del tb
+
+ if trim_arity_type_error:
+ if limit < max_limit:
+ limit += 1
+ continue
+
+ raise
+ # fmt: on
+
+ # copy func name to wrapper for sensible debug output
+ # (can't use functools.wraps, since that messes with function signature)
+ func_name = getattr(func, "__name__", getattr(func, "__class__").__name__)
+ wrapper.__name__ = func_name
+ wrapper.__doc__ = func.__doc__
+
+ return wrapper
+
+
+def condition_as_parse_action(
+ fn: ParseCondition, message: str = None, fatal: bool = False
+) -> ParseAction:
+ """
+ Function to convert a simple predicate function that returns ``True`` or ``False``
+ into a parse action. Can be used in places when a parse action is required
+ and :class:`ParserElement.add_condition` cannot be used (such as when adding a condition
+ to an operator level in :class:`infix_notation`).
+
+ Optional keyword arguments:
+
+ - ``message`` - define a custom message to be used in the raised exception
+ - ``fatal`` - if True, will raise :class:`ParseFatalException` to stop parsing immediately;
+ otherwise will raise :class:`ParseException`
+
+ """
+ msg = message if message is not None else "failed user-defined condition"
+ exc_type = ParseFatalException if fatal else ParseException
+ fn = _trim_arity(fn)
+
+ @wraps(fn)
+ def pa(s, l, t):
+ if not bool(fn(s, l, t)):
+ raise exc_type(s, l, msg)
+
+ return pa
+
+
+def _default_start_debug_action(
+ instring: str, loc: int, expr: "ParserElement", cache_hit: bool = False
+):
+ cache_hit_str = "*" if cache_hit else ""
+ print(
+ (
+ "{}Match {} at loc {}({},{})\n {}\n {}^".format(
+ cache_hit_str,
+ expr,
+ loc,
+ lineno(loc, instring),
+ col(loc, instring),
+ line(loc, instring),
+ " " * (col(loc, instring) - 1),
+ )
+ )
+ )
+
+
+def _default_success_debug_action(
+ instring: str,
+ startloc: int,
+ endloc: int,
+ expr: "ParserElement",
+ toks: ParseResults,
+ cache_hit: bool = False,
+):
+ cache_hit_str = "*" if cache_hit else ""
+ print("{}Matched {} -> {}".format(cache_hit_str, expr, toks.as_list()))
+
+
+def _default_exception_debug_action(
+ instring: str,
+ loc: int,
+ expr: "ParserElement",
+ exc: Exception,
+ cache_hit: bool = False,
+):
+ cache_hit_str = "*" if cache_hit else ""
+ print(
+ "{}Match {} failed, {} raised: {}".format(
+ cache_hit_str, expr, type(exc).__name__, exc
+ )
+ )
+
+
+def null_debug_action(*args):
+ """'Do-nothing' debug action, to suppress debugging output during parsing."""
+
+
+class ParserElement(ABC):
+ """Abstract base level parser element class."""
+
+ DEFAULT_WHITE_CHARS: str = " \n\t\r"
+ verbose_stacktrace: bool = False
+ _literalStringClass: typing.Optional[type] = None
+
+ @staticmethod
+ def set_default_whitespace_chars(chars: str) -> None:
+ r"""
+ Overrides the default whitespace chars
+
+ Example::
+
+ # default whitespace chars are space, <TAB> and newline
+ Word(alphas)[1, ...].parse_string("abc def\nghi jkl") # -> ['abc', 'def', 'ghi', 'jkl']
+
+ # change to just treat newline as significant
+ ParserElement.set_default_whitespace_chars(" \t")
+ Word(alphas)[1, ...].parse_string("abc def\nghi jkl") # -> ['abc', 'def']
+ """
+ ParserElement.DEFAULT_WHITE_CHARS = chars
+
+ # update whitespace all parse expressions defined in this module
+ for expr in _builtin_exprs:
+ if expr.copyDefaultWhiteChars:
+ expr.whiteChars = set(chars)
+
+ @staticmethod
+ def inline_literals_using(cls: type) -> None:
+ """
+ Set class to be used for inclusion of string literals into a parser.
+
+ Example::
+
+ # default literal class used is Literal
+ integer = Word(nums)
+ date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
+
+ date_str.parse_string("1999/12/31") # -> ['1999', '/', '12', '/', '31']
+
+
+ # change to Suppress
+ ParserElement.inline_literals_using(Suppress)
+ date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
+
+ date_str.parse_string("1999/12/31") # -> ['1999', '12', '31']
+ """
+ ParserElement._literalStringClass = cls
+
+ class DebugActions(NamedTuple):
+ debug_try: typing.Optional[DebugStartAction]
+ debug_match: typing.Optional[DebugSuccessAction]
+ debug_fail: typing.Optional[DebugExceptionAction]
+
+ def __init__(self, savelist: bool = False):
+ self.parseAction: List[ParseAction] = list()
+ self.failAction: typing.Optional[ParseFailAction] = None
+ self.customName = None
+ self._defaultName = None
+ self.resultsName = None
+ self.saveAsList = savelist
+ self.skipWhitespace = True
+ self.whiteChars = set(ParserElement.DEFAULT_WHITE_CHARS)
+ self.copyDefaultWhiteChars = True
+ # used when checking for left-recursion
+ self.mayReturnEmpty = False
+ self.keepTabs = False
+ self.ignoreExprs: List["ParserElement"] = list()
+ self.debug = False
+ self.streamlined = False
+ # optimize exception handling for subclasses that don't advance parse index
+ self.mayIndexError = True
+ self.errmsg = ""
+ # mark results names as modal (report only last) or cumulative (list all)
+ self.modalResults = True
+ # custom debug actions
+ self.debugActions = self.DebugActions(None, None, None)
+ # avoid redundant calls to preParse
+ self.callPreparse = True
+ self.callDuringTry = False
+ self.suppress_warnings_: List[Diagnostics] = []
+
+ def suppress_warning(self, warning_type: Diagnostics) -> "ParserElement":
+ """
+ Suppress warnings emitted for a particular diagnostic on this expression.
+
+ Example::
+
+ base = pp.Forward()
+ base.suppress_warning(Diagnostics.warn_on_parse_using_empty_Forward)
+
+ # statement would normally raise a warning, but is now suppressed
+ print(base.parseString("x"))
+
+ """
+ self.suppress_warnings_.append(warning_type)
+ return self
+
+ def copy(self) -> "ParserElement":
+ """
+ Make a copy of this :class:`ParserElement`. Useful for defining
+ different parse actions for the same parsing pattern, using copies of
+ the original parse element.
+
+ Example::
+
+ integer = Word(nums).set_parse_action(lambda toks: int(toks[0]))
+ integerK = integer.copy().add_parse_action(lambda toks: toks[0] * 1024) + Suppress("K")
+ integerM = integer.copy().add_parse_action(lambda toks: toks[0] * 1024 * 1024) + Suppress("M")
+
+ print((integerK | integerM | integer)[1, ...].parse_string("5K 100 640K 256M"))
+
+ prints::
+
+ [5120, 100, 655360, 268435456]
+
+ Equivalent form of ``expr.copy()`` is just ``expr()``::
+
+ integerM = integer().add_parse_action(lambda toks: toks[0] * 1024 * 1024) + Suppress("M")
+ """
+ cpy = copy.copy(self)
+ cpy.parseAction = self.parseAction[:]
+ cpy.ignoreExprs = self.ignoreExprs[:]
+ if self.copyDefaultWhiteChars:
+ cpy.whiteChars = set(ParserElement.DEFAULT_WHITE_CHARS)
+ return cpy
+
+ def set_results_name(
+ self, name: str, list_all_matches: bool = False, *, listAllMatches: bool = False
+ ) -> "ParserElement":
+ """
+ Define name for referencing matching tokens as a nested attribute
+ of the returned parse results.
+
+ Normally, results names are assigned as you would assign keys in a dict:
+ any existing value is overwritten by later values. If it is necessary to
+ keep all values captured for a particular results name, call ``set_results_name``
+ with ``list_all_matches`` = True.
+
+ NOTE: ``set_results_name`` returns a *copy* of the original :class:`ParserElement` object;
+ this is so that the client can define a basic element, such as an
+ integer, and reference it in multiple places with different names.
+
+ You can also set results names using the abbreviated syntax,
+ ``expr("name")`` in place of ``expr.set_results_name("name")``
+ - see :class:`__call__`. If ``list_all_matches`` is required, use
+ ``expr("name*")``.
+
+ Example::
+
+ date_str = (integer.set_results_name("year") + '/'
+ + integer.set_results_name("month") + '/'
+ + integer.set_results_name("day"))
+
+ # equivalent form:
+ date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
+ """
+ listAllMatches = listAllMatches or list_all_matches
+ return self._setResultsName(name, listAllMatches)
+
+ def _setResultsName(self, name, listAllMatches=False):
+ if name is None:
+ return self
+ newself = self.copy()
+ if name.endswith("*"):
+ name = name[:-1]
+ listAllMatches = True
+ newself.resultsName = name
+ newself.modalResults = not listAllMatches
+ return newself
+
+ def set_break(self, break_flag: bool = True) -> "ParserElement":
+ """
+ Method to invoke the Python pdb debugger when this element is
+ about to be parsed. Set ``break_flag`` to ``True`` to enable, ``False`` to
+ disable.
+ """
+ if break_flag:
+ _parseMethod = self._parse
+
+ def breaker(instring, loc, doActions=True, callPreParse=True):
+ import pdb
+
+ # this call to pdb.set_trace() is intentional, not a checkin error
+ pdb.set_trace()
+ return _parseMethod(instring, loc, doActions, callPreParse)
+
+ breaker._originalParseMethod = _parseMethod
+ self._parse = breaker
+ else:
+ if hasattr(self._parse, "_originalParseMethod"):
+ self._parse = self._parse._originalParseMethod
+ return self
+
+ def set_parse_action(self, *fns: ParseAction, **kwargs) -> "ParserElement":
+ """
+ Define one or more actions to perform when successfully matching parse element definition.
+
+ Parse actions can be called to perform data conversions, do extra validation,
+ update external data structures, or enhance or replace the parsed tokens.
+ Each parse action ``fn`` is a callable method with 0-3 arguments, called as
+ ``fn(s, loc, toks)`` , ``fn(loc, toks)`` , ``fn(toks)`` , or just ``fn()`` , where:
+
+ - s = the original string being parsed (see note below)
+ - loc = the location of the matching substring
+ - toks = a list of the matched tokens, packaged as a :class:`ParseResults` object
+
+ The parsed tokens are passed to the parse action as ParseResults. They can be
+ modified in place using list-style append, extend, and pop operations to update
+ the parsed list elements; and with dictionary-style item set and del operations
+ to add, update, or remove any named results. If the tokens are modified in place,
+ it is not necessary to return them with a return statement.
+
+ Parse actions can also completely replace the given tokens, with another ``ParseResults``
+ object, or with some entirely different object (common for parse actions that perform data
+ conversions). A convenient way to build a new parse result is to define the values
+ using a dict, and then create the return value using :class:`ParseResults.from_dict`.
+
+ If None is passed as the ``fn`` parse action, all previously added parse actions for this
+ expression are cleared.
+
+ Optional keyword arguments:
+
+ - call_during_try = (default= ``False``) indicate if parse action should be run during
+ lookaheads and alternate testing. For parse actions that have side effects, it is
+ important to only call the parse action once it is determined that it is being
+ called as part of a successful parse. For parse actions that perform additional
+ validation, then call_during_try should be passed as True, so that the validation
+ code is included in the preliminary "try" parses.
+
+ Note: the default parsing behavior is to expand tabs in the input string
+ before starting the parsing process. See :class:`parse_string` for more
+ information on parsing strings containing ``<TAB>`` s, and suggested
+ methods to maintain a consistent view of the parsed string, the parse
+ location, and line and column positions within the parsed string.
+
+ Example::
+
+ # parse dates in the form YYYY/MM/DD
+
+ # use parse action to convert toks from str to int at parse time
+ def convert_to_int(toks):
+ return int(toks[0])
+
+ # use a parse action to verify that the date is a valid date
+ def is_valid_date(instring, loc, toks):
+ from datetime import date
+ year, month, day = toks[::2]
+ try:
+ date(year, month, day)
+ except ValueError:
+ raise ParseException(instring, loc, "invalid date given")
+
+ integer = Word(nums)
+ date_str = integer + '/' + integer + '/' + integer
+
+ # add parse actions
+ integer.set_parse_action(convert_to_int)
+ date_str.set_parse_action(is_valid_date)
+
+ # note that integer fields are now ints, not strings
+ date_str.run_tests('''
+ # successful parse - note that integer fields were converted to ints
+ 1999/12/31
+
+ # fail - invalid date
+ 1999/13/31
+ ''')
+ """
+ if list(fns) == [None]:
+ self.parseAction = []
+ else:
+ if not all(callable(fn) for fn in fns):
+ raise TypeError("parse actions must be callable")
+ self.parseAction = [_trim_arity(fn) for fn in fns]
+ self.callDuringTry = kwargs.get(
+ "call_during_try", kwargs.get("callDuringTry", False)
+ )
+ return self
+
+ def add_parse_action(self, *fns: ParseAction, **kwargs) -> "ParserElement":
+ """
+ Add one or more parse actions to expression's list of parse actions. See :class:`set_parse_action`.
+
+ See examples in :class:`copy`.
+ """
+ self.parseAction += [_trim_arity(fn) for fn in fns]
+ self.callDuringTry = self.callDuringTry or kwargs.get(
+ "call_during_try", kwargs.get("callDuringTry", False)
+ )
+ return self
+
+ def add_condition(self, *fns: ParseCondition, **kwargs) -> "ParserElement":
+ """Add a boolean predicate function to expression's list of parse actions. See
+ :class:`set_parse_action` for function call signatures. Unlike ``set_parse_action``,
+ functions passed to ``add_condition`` need to return boolean success/fail of the condition.
+
+ Optional keyword arguments:
+
+ - message = define a custom message to be used in the raised exception
+ - fatal = if True, will raise ParseFatalException to stop parsing immediately; otherwise will raise
+ ParseException
+ - call_during_try = boolean to indicate if this method should be called during internal tryParse calls,
+ default=False
+
+ Example::
+
+ integer = Word(nums).set_parse_action(lambda toks: int(toks[0]))
+ year_int = integer.copy()
+ year_int.add_condition(lambda toks: toks[0] >= 2000, message="Only support years 2000 and later")
+ date_str = year_int + '/' + integer + '/' + integer
+
+ result = date_str.parse_string("1999/12/31") # -> Exception: Only support years 2000 and later (at char 0),
+ (line:1, col:1)
+ """
+ for fn in fns:
+ self.parseAction.append(
+ condition_as_parse_action(
+ fn, message=kwargs.get("message"), fatal=kwargs.get("fatal", False)
+ )
+ )
+
+ self.callDuringTry = self.callDuringTry or kwargs.get(
+ "call_during_try", kwargs.get("callDuringTry", False)
+ )
+ return self
+
+ def set_fail_action(self, fn: ParseFailAction) -> "ParserElement":
+ """
+ Define action to perform if parsing fails at this expression.
+ Fail acton fn is a callable function that takes the arguments
+ ``fn(s, loc, expr, err)`` where:
+
+ - s = string being parsed
+ - loc = location where expression match was attempted and failed
+ - expr = the parse expression that failed
+ - err = the exception thrown
+
+ The function returns no value. It may throw :class:`ParseFatalException`
+ if it is desired to stop parsing immediately."""
+ self.failAction = fn
+ return self
+
+ def _skipIgnorables(self, instring, loc):
+ exprsFound = True
+ while exprsFound:
+ exprsFound = False
+ for e in self.ignoreExprs:
+ try:
+ while 1:
+ loc, dummy = e._parse(instring, loc)
+ exprsFound = True
+ except ParseException:
+ pass
+ return loc
+
+ def preParse(self, instring, loc):
+ if self.ignoreExprs:
+ loc = self._skipIgnorables(instring, loc)
+
+ if self.skipWhitespace:
+ instrlen = len(instring)
+ white_chars = self.whiteChars
+ while loc < instrlen and instring[loc] in white_chars:
+ loc += 1
+
+ return loc
+
+ def parseImpl(self, instring, loc, doActions=True):
+ return loc, []
+
+ def postParse(self, instring, loc, tokenlist):
+ return tokenlist
+
+ # @profile
+ def _parseNoCache(
+ self, instring, loc, doActions=True, callPreParse=True
+ ) -> Tuple[int, ParseResults]:
+ TRY, MATCH, FAIL = 0, 1, 2
+ debugging = self.debug # and doActions)
+ len_instring = len(instring)
+
+ if debugging or self.failAction:
+ # print("Match {} at loc {}({}, {})".format(self, loc, lineno(loc, instring), col(loc, instring)))
+ try:
+ if callPreParse and self.callPreparse:
+ pre_loc = self.preParse(instring, loc)
+ else:
+ pre_loc = loc
+ tokens_start = pre_loc
+ if self.debugActions.debug_try:
+ self.debugActions.debug_try(instring, tokens_start, self, False)
+ if self.mayIndexError or pre_loc >= len_instring:
+ try:
+ loc, tokens = self.parseImpl(instring, pre_loc, doActions)
+ except IndexError:
+ raise ParseException(instring, len_instring, self.errmsg, self)
+ else:
+ loc, tokens = self.parseImpl(instring, pre_loc, doActions)
+ except Exception as err:
+ # print("Exception raised:", err)
+ if self.debugActions.debug_fail:
+ self.debugActions.debug_fail(
+ instring, tokens_start, self, err, False
+ )
+ if self.failAction:
+ self.failAction(instring, tokens_start, self, err)
+ raise
+ else:
+ if callPreParse and self.callPreparse:
+ pre_loc = self.preParse(instring, loc)
+ else:
+ pre_loc = loc
+ tokens_start = pre_loc
+ if self.mayIndexError or pre_loc >= len_instring:
+ try:
+ loc, tokens = self.parseImpl(instring, pre_loc, doActions)
+ except IndexError:
+ raise ParseException(instring, len_instring, self.errmsg, self)
+ else:
+ loc, tokens = self.parseImpl(instring, pre_loc, doActions)
+
+ tokens = self.postParse(instring, loc, tokens)
+
+ ret_tokens = ParseResults(
+ tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults
+ )
+ if self.parseAction and (doActions or self.callDuringTry):
+ if debugging:
+ try:
+ for fn in self.parseAction:
+ try:
+ tokens = fn(instring, tokens_start, ret_tokens)
+ except IndexError as parse_action_exc:
+ exc = ParseException("exception raised in parse action")
+ raise exc from parse_action_exc
+
+ if tokens is not None and tokens is not ret_tokens:
+ ret_tokens = ParseResults(
+ tokens,
+ self.resultsName,
+ asList=self.saveAsList
+ and isinstance(tokens, (ParseResults, list)),
+ modal=self.modalResults,
+ )
+ except Exception as err:
+ # print "Exception raised in user parse action:", err
+ if self.debugActions.debug_fail:
+ self.debugActions.debug_fail(
+ instring, tokens_start, self, err, False
+ )
+ raise
+ else:
+ for fn in self.parseAction:
+ try:
+ tokens = fn(instring, tokens_start, ret_tokens)
+ except IndexError as parse_action_exc:
+ exc = ParseException("exception raised in parse action")
+ raise exc from parse_action_exc
+
+ if tokens is not None and tokens is not ret_tokens:
+ ret_tokens = ParseResults(
+ tokens,
+ self.resultsName,
+ asList=self.saveAsList
+ and isinstance(tokens, (ParseResults, list)),
+ modal=self.modalResults,
+ )
+ if debugging:
+ # print("Matched", self, "->", ret_tokens.as_list())
+ if self.debugActions.debug_match:
+ self.debugActions.debug_match(
+ instring, tokens_start, loc, self, ret_tokens, False
+ )
+
+ return loc, ret_tokens
+
+ def try_parse(self, instring: str, loc: int, raise_fatal: bool = False) -> int:
+ try:
+ return self._parse(instring, loc, doActions=False)[0]
+ except ParseFatalException:
+ if raise_fatal:
+ raise
+ raise ParseException(instring, loc, self.errmsg, self)
+
+ def can_parse_next(self, instring: str, loc: int) -> bool:
+ try:
+ self.try_parse(instring, loc)
+ except (ParseException, IndexError):
+ return False
+ else:
+ return True
+
+ # cache for left-recursion in Forward references
+ recursion_lock = RLock()
+ recursion_memos: typing.Dict[
+ Tuple[int, "Forward", bool], Tuple[int, Union[ParseResults, Exception]]
+ ] = {}
+
+ # argument cache for optimizing repeated calls when backtracking through recursive expressions
+ packrat_cache = (
+ {}
+ ) # this is set later by enabled_packrat(); this is here so that reset_cache() doesn't fail
+ packrat_cache_lock = RLock()
+ packrat_cache_stats = [0, 0]
+
+ # this method gets repeatedly called during backtracking with the same arguments -
+ # we can cache these arguments and save ourselves the trouble of re-parsing the contained expression
+ def _parseCache(
+ self, instring, loc, doActions=True, callPreParse=True
+ ) -> Tuple[int, ParseResults]:
+ HIT, MISS = 0, 1
+ TRY, MATCH, FAIL = 0, 1, 2
+ lookup = (self, instring, loc, callPreParse, doActions)
+ with ParserElement.packrat_cache_lock:
+ cache = ParserElement.packrat_cache
+ value = cache.get(lookup)
+ if value is cache.not_in_cache:
+ ParserElement.packrat_cache_stats[MISS] += 1
+ try:
+ value = self._parseNoCache(instring, loc, doActions, callPreParse)
+ except ParseBaseException as pe:
+ # cache a copy of the exception, without the traceback
+ cache.set(lookup, pe.__class__(*pe.args))
+ raise
+ else:
+ cache.set(lookup, (value[0], value[1].copy(), loc))
+ return value
+ else:
+ ParserElement.packrat_cache_stats[HIT] += 1
+ if self.debug and self.debugActions.debug_try:
+ try:
+ self.debugActions.debug_try(instring, loc, self, cache_hit=True)
+ except TypeError:
+ pass
+ if isinstance(value, Exception):
+ if self.debug and self.debugActions.debug_fail:
+ try:
+ self.debugActions.debug_fail(
+ instring, loc, self, value, cache_hit=True
+ )
+ except TypeError:
+ pass
+ raise value
+
+ loc_, result, endloc = value[0], value[1].copy(), value[2]
+ if self.debug and self.debugActions.debug_match:
+ try:
+ self.debugActions.debug_match(
+ instring, loc_, endloc, self, result, cache_hit=True
+ )
+ except TypeError:
+ pass
+
+ return loc_, result
+
+ _parse = _parseNoCache
+
+ @staticmethod
+ def reset_cache() -> None:
+ ParserElement.packrat_cache.clear()
+ ParserElement.packrat_cache_stats[:] = [0] * len(
+ ParserElement.packrat_cache_stats
+ )
+ ParserElement.recursion_memos.clear()
+
+ _packratEnabled = False
+ _left_recursion_enabled = False
+
+ @staticmethod
+ def disable_memoization() -> None:
+ """
+ Disables active Packrat or Left Recursion parsing and their memoization
+
+ This method also works if neither Packrat nor Left Recursion are enabled.
+ This makes it safe to call before activating Packrat nor Left Recursion
+ to clear any previous settings.
+ """
+ ParserElement.reset_cache()
+ ParserElement._left_recursion_enabled = False
+ ParserElement._packratEnabled = False
+ ParserElement._parse = ParserElement._parseNoCache
+
+ @staticmethod
+ def enable_left_recursion(
+ cache_size_limit: typing.Optional[int] = None, *, force=False
+ ) -> None:
+ """
+ Enables "bounded recursion" parsing, which allows for both direct and indirect
+ left-recursion. During parsing, left-recursive :class:`Forward` elements are
+ repeatedly matched with a fixed recursion depth that is gradually increased
+ until finding the longest match.
+
+ Example::
+
+ from pip._vendor import pyparsing as pp
+ pp.ParserElement.enable_left_recursion()
+
+ E = pp.Forward("E")
+ num = pp.Word(pp.nums)
+ # match `num`, or `num '+' num`, or `num '+' num '+' num`, ...
+ E <<= E + '+' - num | num
+
+ print(E.parse_string("1+2+3"))
+
+ Recursion search naturally memoizes matches of ``Forward`` elements and may
+ thus skip reevaluation of parse actions during backtracking. This may break
+ programs with parse actions which rely on strict ordering of side-effects.
+
+ Parameters:
+
+ - cache_size_limit - (default=``None``) - memoize at most this many
+ ``Forward`` elements during matching; if ``None`` (the default),
+ memoize all ``Forward`` elements.
+
+ Bounded Recursion parsing works similar but not identical to Packrat parsing,
+ thus the two cannot be used together. Use ``force=True`` to disable any
+ previous, conflicting settings.
+ """
+ if force:
+ ParserElement.disable_memoization()
+ elif ParserElement._packratEnabled:
+ raise RuntimeError("Packrat and Bounded Recursion are not compatible")
+ if cache_size_limit is None:
+ ParserElement.recursion_memos = _UnboundedMemo()
+ elif cache_size_limit > 0:
+ ParserElement.recursion_memos = _LRUMemo(capacity=cache_size_limit)
+ else:
+ raise NotImplementedError("Memo size of %s" % cache_size_limit)
+ ParserElement._left_recursion_enabled = True
+
+ @staticmethod
+ def enable_packrat(cache_size_limit: int = 128, *, force: bool = False) -> None:
+ """
+ Enables "packrat" parsing, which adds memoizing to the parsing logic.
+ Repeated parse attempts at the same string location (which happens
+ often in many complex grammars) can immediately return a cached value,
+ instead of re-executing parsing/validating code. Memoizing is done of
+ both valid results and parsing exceptions.
+
+ Parameters:
+
+ - cache_size_limit - (default= ``128``) - if an integer value is provided
+ will limit the size of the packrat cache; if None is passed, then
+ the cache size will be unbounded; if 0 is passed, the cache will
+ be effectively disabled.
+
+ This speedup may break existing programs that use parse actions that
+ have side-effects. For this reason, packrat parsing is disabled when
+ you first import pyparsing. To activate the packrat feature, your
+ program must call the class method :class:`ParserElement.enable_packrat`.
+ For best results, call ``enable_packrat()`` immediately after
+ importing pyparsing.
+
+ Example::
+
+ from pip._vendor import pyparsing
+ pyparsing.ParserElement.enable_packrat()
+
+ Packrat parsing works similar but not identical to Bounded Recursion parsing,
+ thus the two cannot be used together. Use ``force=True`` to disable any
+ previous, conflicting settings.
+ """
+ if force:
+ ParserElement.disable_memoization()
+ elif ParserElement._left_recursion_enabled:
+ raise RuntimeError("Packrat and Bounded Recursion are not compatible")
+ if not ParserElement._packratEnabled:
+ ParserElement._packratEnabled = True
+ if cache_size_limit is None:
+ ParserElement.packrat_cache = _UnboundedCache()
+ else:
+ ParserElement.packrat_cache = _FifoCache(cache_size_limit)
+ ParserElement._parse = ParserElement._parseCache
+
+ def parse_string(
+ self, instring: str, parse_all: bool = False, *, parseAll: bool = False
+ ) -> ParseResults:
+ """
+ Parse a string with respect to the parser definition. This function is intended as the primary interface to the
+ client code.
+
+ :param instring: The input string to be parsed.
+ :param parse_all: If set, the entire input string must match the grammar.
+ :param parseAll: retained for pre-PEP8 compatibility, will be removed in a future release.
+ :raises ParseException: Raised if ``parse_all`` is set and the input string does not match the whole grammar.
+ :returns: the parsed data as a :class:`ParseResults` object, which may be accessed as a `list`, a `dict`, or
+ an object with attributes if the given parser includes results names.
+
+ If the input string is required to match the entire grammar, ``parse_all`` flag must be set to ``True``. This
+ is also equivalent to ending the grammar with :class:`StringEnd`().
+
+ To report proper column numbers, ``parse_string`` operates on a copy of the input string where all tabs are
+ converted to spaces (8 spaces per tab, as per the default in ``string.expandtabs``). If the input string
+ contains tabs and the grammar uses parse actions that use the ``loc`` argument to index into the string
+ being parsed, one can ensure a consistent view of the input string by doing one of the following:
+
+ - calling ``parse_with_tabs`` on your grammar before calling ``parse_string`` (see :class:`parse_with_tabs`),
+ - define your parse action using the full ``(s,loc,toks)`` signature, and reference the input string using the
+ parse action's ``s`` argument, or
+ - explicitly expand the tabs in your input string before calling ``parse_string``.
+
+ Examples:
+
+ By default, partial matches are OK.
+
+ >>> res = Word('a').parse_string('aaaaabaaa')
+ >>> print(res)
+ ['aaaaa']
+
+ The parsing behavior varies by the inheriting class of this abstract class. Please refer to the children
+ directly to see more examples.
+
+ It raises an exception if parse_all flag is set and instring does not match the whole grammar.
+
+ >>> res = Word('a').parse_string('aaaaabaaa', parse_all=True)
+ Traceback (most recent call last):
+ ...
+ pyparsing.ParseException: Expected end of text, found 'b' (at char 5), (line:1, col:6)
+ """
+ parseAll = parse_all or parseAll
+
+ ParserElement.reset_cache()
+ if not self.streamlined:
+ self.streamline()
+ for e in self.ignoreExprs:
+ e.streamline()
+ if not self.keepTabs:
+ instring = instring.expandtabs()
+ try:
+ loc, tokens = self._parse(instring, 0)
+ if parseAll:
+ loc = self.preParse(instring, loc)
+ se = Empty() + StringEnd()
+ se._parse(instring, loc)
+ except ParseBaseException as exc:
+ if ParserElement.verbose_stacktrace:
+ raise
+ else:
+ # catch and re-raise exception from here, clearing out pyparsing internal stack trace
+ raise exc.with_traceback(None)
+ else:
+ return tokens
+
+ def scan_string(
+ self,
+ instring: str,
+ max_matches: int = _MAX_INT,
+ overlap: bool = False,
+ *,
+ debug: bool = False,
+ maxMatches: int = _MAX_INT,
+ ) -> Generator[Tuple[ParseResults, int, int], None, None]:
+ """
+ Scan the input string for expression matches. Each match will return the
+ matching tokens, start location, and end location. May be called with optional
+ ``max_matches`` argument, to clip scanning after 'n' matches are found. If
+ ``overlap`` is specified, then overlapping matches will be reported.
+
+ Note that the start and end locations are reported relative to the string
+ being parsed. See :class:`parse_string` for more information on parsing
+ strings with embedded tabs.
+
+ Example::
+
+ source = "sldjf123lsdjjkf345sldkjf879lkjsfd987"
+ print(source)
+ for tokens, start, end in Word(alphas).scan_string(source):
+ print(' '*start + '^'*(end-start))
+ print(' '*start + tokens[0])
+
+ prints::
+
+ sldjf123lsdjjkf345sldkjf879lkjsfd987
+ ^^^^^
+ sldjf
+ ^^^^^^^
+ lsdjjkf
+ ^^^^^^
+ sldkjf
+ ^^^^^^
+ lkjsfd
+ """
+ maxMatches = min(maxMatches, max_matches)
+ if not self.streamlined:
+ self.streamline()
+ for e in self.ignoreExprs:
+ e.streamline()
+
+ if not self.keepTabs:
+ instring = str(instring).expandtabs()
+ instrlen = len(instring)
+ loc = 0
+ preparseFn = self.preParse
+ parseFn = self._parse
+ ParserElement.resetCache()
+ matches = 0
+ try:
+ while loc <= instrlen and matches < maxMatches:
+ try:
+ preloc = preparseFn(instring, loc)
+ nextLoc, tokens = parseFn(instring, preloc, callPreParse=False)
+ except ParseException:
+ loc = preloc + 1
+ else:
+ if nextLoc > loc:
+ matches += 1
+ if debug:
+ print(
+ {
+ "tokens": tokens.asList(),
+ "start": preloc,
+ "end": nextLoc,
+ }
+ )
+ yield tokens, preloc, nextLoc
+ if overlap:
+ nextloc = preparseFn(instring, loc)
+ if nextloc > loc:
+ loc = nextLoc
+ else:
+ loc += 1
+ else:
+ loc = nextLoc
+ else:
+ loc = preloc + 1
+ except ParseBaseException as exc:
+ if ParserElement.verbose_stacktrace:
+ raise
+ else:
+ # catch and re-raise exception from here, clears out pyparsing internal stack trace
+ raise exc.with_traceback(None)
+
+ def transform_string(self, instring: str, *, debug: bool = False) -> str:
+ """
+ Extension to :class:`scan_string`, to modify matching text with modified tokens that may
+ be returned from a parse action. To use ``transform_string``, define a grammar and
+ attach a parse action to it that modifies the returned token list.
+ Invoking ``transform_string()`` on a target string will then scan for matches,
+ and replace the matched text patterns according to the logic in the parse
+ action. ``transform_string()`` returns the resulting transformed string.
+
+ Example::
+
+ wd = Word(alphas)
+ wd.set_parse_action(lambda toks: toks[0].title())
+
+ print(wd.transform_string("now is the winter of our discontent made glorious summer by this sun of york."))
+
+ prints::
+
+ Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York.
+ """
+ out: List[str] = []
+ lastE = 0
+ # force preservation of <TAB>s, to minimize unwanted transformation of string, and to
+ # keep string locs straight between transform_string and scan_string
+ self.keepTabs = True
+ try:
+ for t, s, e in self.scan_string(instring, debug=debug):
+ out.append(instring[lastE:s])
+ if t:
+ if isinstance(t, ParseResults):
+ out += t.as_list()
+ elif isinstance(t, Iterable) and not isinstance(t, str_type):
+ out.extend(t)
+ else:
+ out.append(t)
+ lastE = e
+ out.append(instring[lastE:])
+ out = [o for o in out if o]
+ return "".join([str(s) for s in _flatten(out)])
+ except ParseBaseException as exc:
+ if ParserElement.verbose_stacktrace:
+ raise
+ else:
+ # catch and re-raise exception from here, clears out pyparsing internal stack trace
+ raise exc.with_traceback(None)
+
+ def search_string(
+ self,
+ instring: str,
+ max_matches: int = _MAX_INT,
+ *,
+ debug: bool = False,
+ maxMatches: int = _MAX_INT,
+ ) -> ParseResults:
+ """
+ Another extension to :class:`scan_string`, simplifying the access to the tokens found
+ to match the given parse expression. May be called with optional
+ ``max_matches`` argument, to clip searching after 'n' matches are found.
+
+ Example::
+
+ # a capitalized word starts with an uppercase letter, followed by zero or more lowercase letters
+ cap_word = Word(alphas.upper(), alphas.lower())
+
+ print(cap_word.search_string("More than Iron, more than Lead, more than Gold I need Electricity"))
+
+ # the sum() builtin can be used to merge results into a single ParseResults object
+ print(sum(cap_word.search_string("More than Iron, more than Lead, more than Gold I need Electricity")))
+
+ prints::
+
+ [['More'], ['Iron'], ['Lead'], ['Gold'], ['I'], ['Electricity']]
+ ['More', 'Iron', 'Lead', 'Gold', 'I', 'Electricity']
+ """
+ maxMatches = min(maxMatches, max_matches)
+ try:
+ return ParseResults(
+ [t for t, s, e in self.scan_string(instring, maxMatches, debug=debug)]
+ )
+ except ParseBaseException as exc:
+ if ParserElement.verbose_stacktrace:
+ raise
+ else:
+ # catch and re-raise exception from here, clears out pyparsing internal stack trace
+ raise exc.with_traceback(None)
+
+ def split(
+ self,
+ instring: str,
+ maxsplit: int = _MAX_INT,
+ include_separators: bool = False,
+ *,
+ includeSeparators=False,
+ ) -> Generator[str, None, None]:
+ """
+ Generator method to split a string using the given expression as a separator.
+ May be called with optional ``maxsplit`` argument, to limit the number of splits;
+ and the optional ``include_separators`` argument (default= ``False``), if the separating
+ matching text should be included in the split results.
+
+ Example::
+
+ punc = one_of(list(".,;:/-!?"))
+ print(list(punc.split("This, this?, this sentence, is badly punctuated!")))
+
+ prints::
+
+ ['This', ' this', '', ' this sentence', ' is badly punctuated', '']
+ """
+ includeSeparators = includeSeparators or include_separators
+ last = 0
+ for t, s, e in self.scan_string(instring, max_matches=maxsplit):
+ yield instring[last:s]
+ if includeSeparators:
+ yield t[0]
+ last = e
+ yield instring[last:]
+
+ def __add__(self, other) -> "ParserElement":
+ """
+ Implementation of ``+`` operator - returns :class:`And`. Adding strings to a :class:`ParserElement`
+ converts them to :class:`Literal`s by default.
+
+ Example::
+
+ greet = Word(alphas) + "," + Word(alphas) + "!"
+ hello = "Hello, World!"
+ print(hello, "->", greet.parse_string(hello))
+
+ prints::
+
+ Hello, World! -> ['Hello', ',', 'World', '!']
+
+ ``...`` may be used as a parse expression as a short form of :class:`SkipTo`.
+
+ Literal('start') + ... + Literal('end')
+
+ is equivalent to:
+
+ Literal('start') + SkipTo('end')("_skipped*") + Literal('end')
+
+ Note that the skipped text is returned with '_skipped' as a results name,
+ and to support having multiple skips in the same parser, the value returned is
+ a list of all skipped text.
+ """
+ if other is Ellipsis:
+ return _PendingSkip(self)
+
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ if not isinstance(other, ParserElement):
+ raise TypeError(
+ "Cannot combine element of type {} with ParserElement".format(
+ type(other).__name__
+ )
+ )
+ return And([self, other])
+
+ def __radd__(self, other) -> "ParserElement":
+ """
+ Implementation of ``+`` operator when left operand is not a :class:`ParserElement`
+ """
+ if other is Ellipsis:
+ return SkipTo(self)("_skipped*") + self
+
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ if not isinstance(other, ParserElement):
+ raise TypeError(
+ "Cannot combine element of type {} with ParserElement".format(
+ type(other).__name__
+ )
+ )
+ return other + self
+
+ def __sub__(self, other) -> "ParserElement":
+ """
+ Implementation of ``-`` operator, returns :class:`And` with error stop
+ """
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ if not isinstance(other, ParserElement):
+ raise TypeError(
+ "Cannot combine element of type {} with ParserElement".format(
+ type(other).__name__
+ )
+ )
+ return self + And._ErrorStop() + other
+
+ def __rsub__(self, other) -> "ParserElement":
+ """
+ Implementation of ``-`` operator when left operand is not a :class:`ParserElement`
+ """
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ if not isinstance(other, ParserElement):
+ raise TypeError(
+ "Cannot combine element of type {} with ParserElement".format(
+ type(other).__name__
+ )
+ )
+ return other - self
+
+ def __mul__(self, other) -> "ParserElement":
+ """
+ Implementation of ``*`` operator, allows use of ``expr * 3`` in place of
+ ``expr + expr + expr``. Expressions may also be multiplied by a 2-integer
+ tuple, similar to ``{min, max}`` multipliers in regular expressions. Tuples
+ may also include ``None`` as in:
+ - ``expr*(n, None)`` or ``expr*(n, )`` is equivalent
+ to ``expr*n + ZeroOrMore(expr)``
+ (read as "at least n instances of ``expr``")
+ - ``expr*(None, n)`` is equivalent to ``expr*(0, n)``
+ (read as "0 to n instances of ``expr``")
+ - ``expr*(None, None)`` is equivalent to ``ZeroOrMore(expr)``
+ - ``expr*(1, None)`` is equivalent to ``OneOrMore(expr)``
+
+ Note that ``expr*(None, n)`` does not raise an exception if
+ more than n exprs exist in the input stream; that is,
+ ``expr*(None, n)`` does not enforce a maximum number of expr
+ occurrences. If this behavior is desired, then write
+ ``expr*(None, n) + ~expr``
+ """
+ if other is Ellipsis:
+ other = (0, None)
+ elif isinstance(other, tuple) and other[:1] == (Ellipsis,):
+ other = ((0,) + other[1:] + (None,))[:2]
+
+ if isinstance(other, int):
+ minElements, optElements = other, 0
+ elif isinstance(other, tuple):
+ other = tuple(o if o is not Ellipsis else None for o in other)
+ other = (other + (None, None))[:2]
+ if other[0] is None:
+ other = (0, other[1])
+ if isinstance(other[0], int) and other[1] is None:
+ if other[0] == 0:
+ return ZeroOrMore(self)
+ if other[0] == 1:
+ return OneOrMore(self)
+ else:
+ return self * other[0] + ZeroOrMore(self)
+ elif isinstance(other[0], int) and isinstance(other[1], int):
+ minElements, optElements = other
+ optElements -= minElements
+ else:
+ raise TypeError(
+ "cannot multiply ParserElement and ({}) objects".format(
+ ",".join(type(item).__name__ for item in other)
+ )
+ )
+ else:
+ raise TypeError(
+ "cannot multiply ParserElement and {} objects".format(
+ type(other).__name__
+ )
+ )
+
+ if minElements < 0:
+ raise ValueError("cannot multiply ParserElement by negative value")
+ if optElements < 0:
+ raise ValueError(
+ "second tuple value must be greater or equal to first tuple value"
+ )
+ if minElements == optElements == 0:
+ return And([])
+
+ if optElements:
+
+ def makeOptionalList(n):
+ if n > 1:
+ return Opt(self + makeOptionalList(n - 1))
+ else:
+ return Opt(self)
+
+ if minElements:
+ if minElements == 1:
+ ret = self + makeOptionalList(optElements)
+ else:
+ ret = And([self] * minElements) + makeOptionalList(optElements)
+ else:
+ ret = makeOptionalList(optElements)
+ else:
+ if minElements == 1:
+ ret = self
+ else:
+ ret = And([self] * minElements)
+ return ret
+
+ def __rmul__(self, other) -> "ParserElement":
+ return self.__mul__(other)
+
+ def __or__(self, other) -> "ParserElement":
+ """
+ Implementation of ``|`` operator - returns :class:`MatchFirst`
+ """
+ if other is Ellipsis:
+ return _PendingSkip(self, must_skip=True)
+
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ if not isinstance(other, ParserElement):
+ raise TypeError(
+ "Cannot combine element of type {} with ParserElement".format(
+ type(other).__name__
+ )
+ )
+ return MatchFirst([self, other])
+
+ def __ror__(self, other) -> "ParserElement":
+ """
+ Implementation of ``|`` operator when left operand is not a :class:`ParserElement`
+ """
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ if not isinstance(other, ParserElement):
+ raise TypeError(
+ "Cannot combine element of type {} with ParserElement".format(
+ type(other).__name__
+ )
+ )
+ return other | self
+
+ def __xor__(self, other) -> "ParserElement":
+ """
+ Implementation of ``^`` operator - returns :class:`Or`
+ """
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ if not isinstance(other, ParserElement):
+ raise TypeError(
+ "Cannot combine element of type {} with ParserElement".format(
+ type(other).__name__
+ )
+ )
+ return Or([self, other])
+
+ def __rxor__(self, other) -> "ParserElement":
+ """
+ Implementation of ``^`` operator when left operand is not a :class:`ParserElement`
+ """
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ if not isinstance(other, ParserElement):
+ raise TypeError(
+ "Cannot combine element of type {} with ParserElement".format(
+ type(other).__name__
+ )
+ )
+ return other ^ self
+
+ def __and__(self, other) -> "ParserElement":
+ """
+ Implementation of ``&`` operator - returns :class:`Each`
+ """
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ if not isinstance(other, ParserElement):
+ raise TypeError(
+ "Cannot combine element of type {} with ParserElement".format(
+ type(other).__name__
+ )
+ )
+ return Each([self, other])
+
+ def __rand__(self, other) -> "ParserElement":
+ """
+ Implementation of ``&`` operator when left operand is not a :class:`ParserElement`
+ """
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ if not isinstance(other, ParserElement):
+ raise TypeError(
+ "Cannot combine element of type {} with ParserElement".format(
+ type(other).__name__
+ )
+ )
+ return other & self
+
+ def __invert__(self) -> "ParserElement":
+ """
+ Implementation of ``~`` operator - returns :class:`NotAny`
+ """
+ return NotAny(self)
+
+ # disable __iter__ to override legacy use of sequential access to __getitem__ to
+ # iterate over a sequence
+ __iter__ = None
+
+ def __getitem__(self, key):
+ """
+ use ``[]`` indexing notation as a short form for expression repetition:
+
+ - ``expr[n]`` is equivalent to ``expr*n``
+ - ``expr[m, n]`` is equivalent to ``expr*(m, n)``
+ - ``expr[n, ...]`` or ``expr[n,]`` is equivalent
+ to ``expr*n + ZeroOrMore(expr)``
+ (read as "at least n instances of ``expr``")
+ - ``expr[..., n]`` is equivalent to ``expr*(0, n)``
+ (read as "0 to n instances of ``expr``")
+ - ``expr[...]`` and ``expr[0, ...]`` are equivalent to ``ZeroOrMore(expr)``
+ - ``expr[1, ...]`` is equivalent to ``OneOrMore(expr)``
+
+ ``None`` may be used in place of ``...``.
+
+ Note that ``expr[..., n]`` and ``expr[m, n]``do not raise an exception
+ if more than ``n`` ``expr``s exist in the input stream. If this behavior is
+ desired, then write ``expr[..., n] + ~expr``.
+ """
+
+ # convert single arg keys to tuples
+ try:
+ if isinstance(key, str_type):
+ key = (key,)
+ iter(key)
+ except TypeError:
+ key = (key, key)
+
+ if len(key) > 2:
+ raise TypeError(
+ "only 1 or 2 index arguments supported ({}{})".format(
+ key[:5], "... [{}]".format(len(key)) if len(key) > 5 else ""
+ )
+ )
+
+ # clip to 2 elements
+ ret = self * tuple(key[:2])
+ return ret
+
+ def __call__(self, name: str = None) -> "ParserElement":
+ """
+ Shortcut for :class:`set_results_name`, with ``list_all_matches=False``.
+
+ If ``name`` is given with a trailing ``'*'`` character, then ``list_all_matches`` will be
+ passed as ``True``.
+
+ If ``name` is omitted, same as calling :class:`copy`.
+
+ Example::
+
+ # these are equivalent
+ userdata = Word(alphas).set_results_name("name") + Word(nums + "-").set_results_name("socsecno")
+ userdata = Word(alphas)("name") + Word(nums + "-")("socsecno")
+ """
+ if name is not None:
+ return self._setResultsName(name)
+ else:
+ return self.copy()
+
+ def suppress(self) -> "ParserElement":
+ """
+ Suppresses the output of this :class:`ParserElement`; useful to keep punctuation from
+ cluttering up returned output.
+ """
+ return Suppress(self)
+
+ def ignore_whitespace(self, recursive: bool = True) -> "ParserElement":
+ """
+ Enables the skipping of whitespace before matching the characters in the
+ :class:`ParserElement`'s defined pattern.
+
+ :param recursive: If ``True`` (the default), also enable whitespace skipping in child elements (if any)
+ """
+ self.skipWhitespace = True
+ return self
+
+ def leave_whitespace(self, recursive: bool = True) -> "ParserElement":
+ """
+ Disables the skipping of whitespace before matching the characters in the
+ :class:`ParserElement`'s defined pattern. This is normally only used internally by
+ the pyparsing module, but may be needed in some whitespace-sensitive grammars.
+
+ :param recursive: If true (the default), also disable whitespace skipping in child elements (if any)
+ """
+ self.skipWhitespace = False
+ return self
+
+ def set_whitespace_chars(
+ self, chars: Union[Set[str], str], copy_defaults: bool = False
+ ) -> "ParserElement":
+ """
+ Overrides the default whitespace chars
+ """
+ self.skipWhitespace = True
+ self.whiteChars = set(chars)
+ self.copyDefaultWhiteChars = copy_defaults
+ return self
+
+ def parse_with_tabs(self) -> "ParserElement":
+ """
+ Overrides default behavior to expand ``<TAB>`` s to spaces before parsing the input string.
+ Must be called before ``parse_string`` when the input grammar contains elements that
+ match ``<TAB>`` characters.
+ """
+ self.keepTabs = True
+ return self
+
+ def ignore(self, other: "ParserElement") -> "ParserElement":
+ """
+ Define expression to be ignored (e.g., comments) while doing pattern
+ matching; may be called repeatedly, to define multiple comment or other
+ ignorable patterns.
+
+ Example::
+
+ patt = Word(alphas)[1, ...]
+ patt.parse_string('ablaj /* comment */ lskjd')
+ # -> ['ablaj']
+
+ patt.ignore(c_style_comment)
+ patt.parse_string('ablaj /* comment */ lskjd')
+ # -> ['ablaj', 'lskjd']
+ """
+ import typing
+
+ if isinstance(other, str_type):
+ other = Suppress(other)
+
+ if isinstance(other, Suppress):
+ if other not in self.ignoreExprs:
+ self.ignoreExprs.append(other)
+ else:
+ self.ignoreExprs.append(Suppress(other.copy()))
+ return self
+
+ def set_debug_actions(
+ self,
+ start_action: DebugStartAction,
+ success_action: DebugSuccessAction,
+ exception_action: DebugExceptionAction,
+ ) -> "ParserElement":
+ """
+ Customize display of debugging messages while doing pattern matching:
+
+ - ``start_action`` - method to be called when an expression is about to be parsed;
+ should have the signature ``fn(input_string: str, location: int, expression: ParserElement, cache_hit: bool)``
+
+ - ``success_action`` - method to be called when an expression has successfully parsed;
+ should have the signature ``fn(input_string: str, start_location: int, end_location: int, expression: ParserELement, parsed_tokens: ParseResults, cache_hit: bool)``
+
+ - ``exception_action`` - method to be called when expression fails to parse;
+ should have the signature ``fn(input_string: str, location: int, expression: ParserElement, exception: Exception, cache_hit: bool)``
+ """
+ self.debugActions = self.DebugActions(
+ start_action or _default_start_debug_action,
+ success_action or _default_success_debug_action,
+ exception_action or _default_exception_debug_action,
+ )
+ self.debug = True
+ return self
+
+ def set_debug(self, flag: bool = True) -> "ParserElement":
+ """
+ Enable display of debugging messages while doing pattern matching.
+ Set ``flag`` to ``True`` to enable, ``False`` to disable.
+
+ Example::
+
+ wd = Word(alphas).set_name("alphaword")
+ integer = Word(nums).set_name("numword")
+ term = wd | integer
+
+ # turn on debugging for wd
+ wd.set_debug()
+
+ term[1, ...].parse_string("abc 123 xyz 890")
+
+ prints::
+
+ Match alphaword at loc 0(1,1)
+ Matched alphaword -> ['abc']
+ Match alphaword at loc 3(1,4)
+ Exception raised:Expected alphaword (at char 4), (line:1, col:5)
+ Match alphaword at loc 7(1,8)
+ Matched alphaword -> ['xyz']
+ Match alphaword at loc 11(1,12)
+ Exception raised:Expected alphaword (at char 12), (line:1, col:13)
+ Match alphaword at loc 15(1,16)
+ Exception raised:Expected alphaword (at char 15), (line:1, col:16)
+
+ The output shown is that produced by the default debug actions - custom debug actions can be
+ specified using :class:`set_debug_actions`. Prior to attempting
+ to match the ``wd`` expression, the debugging message ``"Match <exprname> at loc <n>(<line>,<col>)"``
+ is shown. Then if the parse succeeds, a ``"Matched"`` message is shown, or an ``"Exception raised"``
+ message is shown. Also note the use of :class:`set_name` to assign a human-readable name to the expression,
+ which makes debugging and exception messages easier to understand - for instance, the default
+ name created for the :class:`Word` expression without calling ``set_name`` is ``"W:(A-Za-z)"``.
+ """
+ if flag:
+ self.set_debug_actions(
+ _default_start_debug_action,
+ _default_success_debug_action,
+ _default_exception_debug_action,
+ )
+ else:
+ self.debug = False
+ return self
+
+ @property
+ def default_name(self) -> str:
+ if self._defaultName is None:
+ self._defaultName = self._generateDefaultName()
+ return self._defaultName
+
+ @abstractmethod
+ def _generateDefaultName(self):
+ """
+ Child classes must define this method, which defines how the ``default_name`` is set.
+ """
+
+ def set_name(self, name: str) -> "ParserElement":
+ """
+ Define name for this expression, makes debugging and exception messages clearer.
+ Example::
+ Word(nums).parse_string("ABC") # -> Exception: Expected W:(0-9) (at char 0), (line:1, col:1)
+ Word(nums).set_name("integer").parse_string("ABC") # -> Exception: Expected integer (at char 0), (line:1, col:1)
+ """
+ self.customName = name
+ self.errmsg = "Expected " + self.name
+ if __diag__.enable_debug_on_named_expressions:
+ self.set_debug()
+ return self
+
+ @property
+ def name(self) -> str:
+ # This will use a user-defined name if available, but otherwise defaults back to the auto-generated name
+ return self.customName if self.customName is not None else self.default_name
+
+ def __str__(self) -> str:
+ return self.name
+
+ def __repr__(self) -> str:
+ return str(self)
+
+ def streamline(self) -> "ParserElement":
+ self.streamlined = True
+ self._defaultName = None
+ return self
+
+ def recurse(self) -> Sequence["ParserElement"]:
+ return []
+
+ def _checkRecursion(self, parseElementList):
+ subRecCheckList = parseElementList[:] + [self]
+ for e in self.recurse():
+ e._checkRecursion(subRecCheckList)
+
+ def validate(self, validateTrace=None) -> None:
+ """
+ Check defined expressions for valid structure, check for infinite recursive definitions.
+ """
+ self._checkRecursion([])
+
+ def parse_file(
+ self,
+ file_or_filename: Union[str, Path, TextIO],
+ encoding: str = "utf-8",
+ parse_all: bool = False,
+ *,
+ parseAll: bool = False,
+ ) -> ParseResults:
+ """
+ Execute the parse expression on the given file or filename.
+ If a filename is specified (instead of a file object),
+ the entire file is opened, read, and closed before parsing.
+ """
+ parseAll = parseAll or parse_all
+ try:
+ file_contents = file_or_filename.read()
+ except AttributeError:
+ with open(file_or_filename, "r", encoding=encoding) as f:
+ file_contents = f.read()
+ try:
+ return self.parse_string(file_contents, parseAll)
+ except ParseBaseException as exc:
+ if ParserElement.verbose_stacktrace:
+ raise
+ else:
+ # catch and re-raise exception from here, clears out pyparsing internal stack trace
+ raise exc.with_traceback(None)
+
+ def __eq__(self, other):
+ if self is other:
+ return True
+ elif isinstance(other, str_type):
+ return self.matches(other, parse_all=True)
+ elif isinstance(other, ParserElement):
+ return vars(self) == vars(other)
+ return False
+
+ def __hash__(self):
+ return id(self)
+
+ def matches(
+ self, test_string: str, parse_all: bool = True, *, parseAll: bool = True
+ ) -> bool:
+ """
+ Method for quick testing of a parser against a test string. Good for simple
+ inline microtests of sub expressions while building up larger parser.
+
+ Parameters:
+ - ``test_string`` - to test against this expression for a match
+ - ``parse_all`` - (default= ``True``) - flag to pass to :class:`parse_string` when running tests
+
+ Example::
+
+ expr = Word(nums)
+ assert expr.matches("100")
+ """
+ parseAll = parseAll and parse_all
+ try:
+ self.parse_string(str(test_string), parse_all=parseAll)
+ return True
+ except ParseBaseException:
+ return False
+
+ def run_tests(
+ self,
+ tests: Union[str, List[str]],
+ parse_all: bool = True,
+ comment: typing.Optional[Union["ParserElement", str]] = "#",
+ full_dump: bool = True,
+ print_results: bool = True,
+ failure_tests: bool = False,
+ post_parse: Callable[[str, ParseResults], str] = None,
+ file: typing.Optional[TextIO] = None,
+ with_line_numbers: bool = False,
+ *,
+ parseAll: bool = True,
+ fullDump: bool = True,
+ printResults: bool = True,
+ failureTests: bool = False,
+ postParse: Callable[[str, ParseResults], str] = None,
+ ) -> Tuple[bool, List[Tuple[str, Union[ParseResults, Exception]]]]:
+ """
+ Execute the parse expression on a series of test strings, showing each
+ test, the parsed results or where the parse failed. Quick and easy way to
+ run a parse expression against a list of sample strings.
+
+ Parameters:
+ - ``tests`` - a list of separate test strings, or a multiline string of test strings
+ - ``parse_all`` - (default= ``True``) - flag to pass to :class:`parse_string` when running tests
+ - ``comment`` - (default= ``'#'``) - expression for indicating embedded comments in the test
+ string; pass None to disable comment filtering
+ - ``full_dump`` - (default= ``True``) - dump results as list followed by results names in nested outline;
+ if False, only dump nested list
+ - ``print_results`` - (default= ``True``) prints test output to stdout
+ - ``failure_tests`` - (default= ``False``) indicates if these tests are expected to fail parsing
+ - ``post_parse`` - (default= ``None``) optional callback for successful parse results; called as
+ `fn(test_string, parse_results)` and returns a string to be added to the test output
+ - ``file`` - (default= ``None``) optional file-like object to which test output will be written;
+ if None, will default to ``sys.stdout``
+ - ``with_line_numbers`` - default= ``False``) show test strings with line and column numbers
+
+ Returns: a (success, results) tuple, where success indicates that all tests succeeded
+ (or failed if ``failure_tests`` is True), and the results contain a list of lines of each
+ test's output
+
+ Example::
+
+ number_expr = pyparsing_common.number.copy()
+
+ result = number_expr.run_tests('''
+ # unsigned integer
+ 100
+ # negative integer
+ -100
+ # float with scientific notation
+ 6.02e23
+ # integer with scientific notation
+ 1e-12
+ ''')
+ print("Success" if result[0] else "Failed!")
+
+ result = number_expr.run_tests('''
+ # stray character
+ 100Z
+ # missing leading digit before '.'
+ -.100
+ # too many '.'
+ 3.14.159
+ ''', failure_tests=True)
+ print("Success" if result[0] else "Failed!")
+
+ prints::
+
+ # unsigned integer
+ 100
+ [100]
+
+ # negative integer
+ -100
+ [-100]
+
+ # float with scientific notation
+ 6.02e23
+ [6.02e+23]
+
+ # integer with scientific notation
+ 1e-12
+ [1e-12]
+
+ Success
+
+ # stray character
+ 100Z
+ ^
+ FAIL: Expected end of text (at char 3), (line:1, col:4)
+
+ # missing leading digit before '.'
+ -.100
+ ^
+ FAIL: Expected {real number with scientific notation | real number | signed integer} (at char 0), (line:1, col:1)
+
+ # too many '.'
+ 3.14.159
+ ^
+ FAIL: Expected end of text (at char 4), (line:1, col:5)
+
+ Success
+
+ Each test string must be on a single line. If you want to test a string that spans multiple
+ lines, create a test like this::
+
+ expr.run_tests(r"this is a test\\n of strings that spans \\n 3 lines")
+
+ (Note that this is a raw string literal, you must include the leading ``'r'``.)
+ """
+ from .testing import pyparsing_test
+
+ parseAll = parseAll and parse_all
+ fullDump = fullDump and full_dump
+ printResults = printResults and print_results
+ failureTests = failureTests or failure_tests
+ postParse = postParse or post_parse
+ if isinstance(tests, str_type):
+ line_strip = type(tests).strip
+ tests = [line_strip(test_line) for test_line in tests.rstrip().splitlines()]
+ if isinstance(comment, str_type):
+ comment = Literal(comment)
+ if file is None:
+ file = sys.stdout
+ print_ = file.write
+
+ result: Union[ParseResults, Exception]
+ allResults = []
+ comments = []
+ success = True
+ NL = Literal(r"\n").add_parse_action(replace_with("\n")).ignore(quoted_string)
+ BOM = "\ufeff"
+ for t in tests:
+ if comment is not None and comment.matches(t, False) or comments and not t:
+ comments.append(
+ pyparsing_test.with_line_numbers(t) if with_line_numbers else t
+ )
+ continue
+ if not t:
+ continue
+ out = [
+ "\n" + "\n".join(comments) if comments else "",
+ pyparsing_test.with_line_numbers(t) if with_line_numbers else t,
+ ]
+ comments = []
+ try:
+ # convert newline marks to actual newlines, and strip leading BOM if present
+ t = NL.transform_string(t.lstrip(BOM))
+ result = self.parse_string(t, parse_all=parseAll)
+ except ParseBaseException as pe:
+ fatal = "(FATAL)" if isinstance(pe, ParseFatalException) else ""
+ out.append(pe.explain())
+ out.append("FAIL: " + str(pe))
+ if ParserElement.verbose_stacktrace:
+ out.extend(traceback.format_tb(pe.__traceback__))
+ success = success and failureTests
+ result = pe
+ except Exception as exc:
+ out.append("FAIL-EXCEPTION: {}: {}".format(type(exc).__name__, exc))
+ if ParserElement.verbose_stacktrace:
+ out.extend(traceback.format_tb(exc.__traceback__))
+ success = success and failureTests
+ result = exc
+ else:
+ success = success and not failureTests
+ if postParse is not None:
+ try:
+ pp_value = postParse(t, result)
+ if pp_value is not None:
+ if isinstance(pp_value, ParseResults):
+ out.append(pp_value.dump())
+ else:
+ out.append(str(pp_value))
+ else:
+ out.append(result.dump())
+ except Exception as e:
+ out.append(result.dump(full=fullDump))
+ out.append(
+ "{} failed: {}: {}".format(
+ postParse.__name__, type(e).__name__, e
+ )
+ )
+ else:
+ out.append(result.dump(full=fullDump))
+ out.append("")
+
+ if printResults:
+ print_("\n".join(out))
+
+ allResults.append((t, result))
+
+ return success, allResults
+
+ def create_diagram(
+ self,
+ output_html: Union[TextIO, Path, str],
+ vertical: int = 3,
+ show_results_names: bool = False,
+ show_groups: bool = False,
+ **kwargs,
+ ) -> None:
+ """
+ Create a railroad diagram for the parser.
+
+ Parameters:
+ - output_html (str or file-like object) - output target for generated
+ diagram HTML
+ - vertical (int) - threshold for formatting multiple alternatives vertically
+ instead of horizontally (default=3)
+ - show_results_names - bool flag whether diagram should show annotations for
+ defined results names
+ - show_groups - bool flag whether groups should be highlighted with an unlabeled surrounding box
+ Additional diagram-formatting keyword arguments can also be included;
+ see railroad.Diagram class.
+ """
+
+ try:
+ from .diagram import to_railroad, railroad_to_html
+ except ImportError as ie:
+ raise Exception(
+ "must ``pip install pyparsing[diagrams]`` to generate parser railroad diagrams"
+ ) from ie
+
+ self.streamline()
+
+ railroad = to_railroad(
+ self,
+ vertical=vertical,
+ show_results_names=show_results_names,
+ show_groups=show_groups,
+ diagram_kwargs=kwargs,
+ )
+ if isinstance(output_html, (str, Path)):
+ with open(output_html, "w", encoding="utf-8") as diag_file:
+ diag_file.write(railroad_to_html(railroad))
+ else:
+ # we were passed a file-like object, just write to it
+ output_html.write(railroad_to_html(railroad))
+
+ setDefaultWhitespaceChars = set_default_whitespace_chars
+ inlineLiteralsUsing = inline_literals_using
+ setResultsName = set_results_name
+ setBreak = set_break
+ setParseAction = set_parse_action
+ addParseAction = add_parse_action
+ addCondition = add_condition
+ setFailAction = set_fail_action
+ tryParse = try_parse
+ canParseNext = can_parse_next
+ resetCache = reset_cache
+ enableLeftRecursion = enable_left_recursion
+ enablePackrat = enable_packrat
+ parseString = parse_string
+ scanString = scan_string
+ searchString = search_string
+ transformString = transform_string
+ setWhitespaceChars = set_whitespace_chars
+ parseWithTabs = parse_with_tabs
+ setDebugActions = set_debug_actions
+ setDebug = set_debug
+ defaultName = default_name
+ setName = set_name
+ parseFile = parse_file
+ runTests = run_tests
+ ignoreWhitespace = ignore_whitespace
+ leaveWhitespace = leave_whitespace
+
+
+class _PendingSkip(ParserElement):
+ # internal placeholder class to hold a place were '...' is added to a parser element,
+ # once another ParserElement is added, this placeholder will be replaced with a SkipTo
+ def __init__(self, expr: ParserElement, must_skip: bool = False):
+ super().__init__()
+ self.anchor = expr
+ self.must_skip = must_skip
+
+ def _generateDefaultName(self):
+ return str(self.anchor + Empty()).replace("Empty", "...")
+
+ def __add__(self, other) -> "ParserElement":
+ skipper = SkipTo(other).set_name("...")("_skipped*")
+ if self.must_skip:
+
+ def must_skip(t):
+ if not t._skipped or t._skipped.as_list() == [""]:
+ del t[0]
+ t.pop("_skipped", None)
+
+ def show_skip(t):
+ if t._skipped.as_list()[-1:] == [""]:
+ t.pop("_skipped")
+ t["_skipped"] = "missing <" + repr(self.anchor) + ">"
+
+ return (
+ self.anchor + skipper().add_parse_action(must_skip)
+ | skipper().add_parse_action(show_skip)
+ ) + other
+
+ return self.anchor + skipper + other
+
+ def __repr__(self):
+ return self.defaultName
+
+ def parseImpl(self, *args):
+ raise Exception(
+ "use of `...` expression without following SkipTo target expression"
+ )
+
+
+class Token(ParserElement):
+ """Abstract :class:`ParserElement` subclass, for defining atomic
+ matching patterns.
+ """
+
+ def __init__(self):
+ super().__init__(savelist=False)
+
+ def _generateDefaultName(self):
+ return type(self).__name__
+
+
+class Empty(Token):
+ """
+ An empty token, will always match.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.mayReturnEmpty = True
+ self.mayIndexError = False
+
+
+class NoMatch(Token):
+ """
+ A token that will never match.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.mayReturnEmpty = True
+ self.mayIndexError = False
+ self.errmsg = "Unmatchable token"
+
+ def parseImpl(self, instring, loc, doActions=True):
+ raise ParseException(instring, loc, self.errmsg, self)
+
+
+class Literal(Token):
+ """
+ Token to exactly match a specified string.
+
+ Example::
+
+ Literal('blah').parse_string('blah') # -> ['blah']
+ Literal('blah').parse_string('blahfooblah') # -> ['blah']
+ Literal('blah').parse_string('bla') # -> Exception: Expected "blah"
+
+ For case-insensitive matching, use :class:`CaselessLiteral`.
+
+ For keyword matching (force word break before and after the matched string),
+ use :class:`Keyword` or :class:`CaselessKeyword`.
+ """
+
+ def __init__(self, match_string: str = "", *, matchString: str = ""):
+ super().__init__()
+ match_string = matchString or match_string
+ self.match = match_string
+ self.matchLen = len(match_string)
+ try:
+ self.firstMatchChar = match_string[0]
+ except IndexError:
+ raise ValueError("null string passed to Literal; use Empty() instead")
+ self.errmsg = "Expected " + self.name
+ self.mayReturnEmpty = False
+ self.mayIndexError = False
+
+ # Performance tuning: modify __class__ to select
+ # a parseImpl optimized for single-character check
+ if self.matchLen == 1 and type(self) is Literal:
+ self.__class__ = _SingleCharLiteral
+
+ def _generateDefaultName(self):
+ return repr(self.match)
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if instring[loc] == self.firstMatchChar and instring.startswith(
+ self.match, loc
+ ):
+ return loc + self.matchLen, self.match
+ raise ParseException(instring, loc, self.errmsg, self)
+
+
+class _SingleCharLiteral(Literal):
+ def parseImpl(self, instring, loc, doActions=True):
+ if instring[loc] == self.firstMatchChar:
+ return loc + 1, self.match
+ raise ParseException(instring, loc, self.errmsg, self)
+
+
+ParserElement._literalStringClass = Literal
+
+
+class Keyword(Token):
+ """
+ Token to exactly match a specified string as a keyword, that is,
+ it must be immediately followed by a non-keyword character. Compare
+ with :class:`Literal`:
+
+ - ``Literal("if")`` will match the leading ``'if'`` in
+ ``'ifAndOnlyIf'``.
+ - ``Keyword("if")`` will not; it will only match the leading
+ ``'if'`` in ``'if x=1'``, or ``'if(y==2)'``
+
+ Accepts two optional constructor arguments in addition to the
+ keyword string:
+
+ - ``identChars`` is a string of characters that would be valid
+ identifier characters, defaulting to all alphanumerics + "_" and
+ "$"
+ - ``caseless`` allows case-insensitive matching, default is ``False``.
+
+ Example::
+
+ Keyword("start").parse_string("start") # -> ['start']
+ Keyword("start").parse_string("starting") # -> Exception
+
+ For case-insensitive matching, use :class:`CaselessKeyword`.
+ """
+
+ DEFAULT_KEYWORD_CHARS = alphanums + "_$"
+
+ def __init__(
+ self,
+ match_string: str = "",
+ ident_chars: typing.Optional[str] = None,
+ caseless: bool = False,
+ *,
+ matchString: str = "",
+ identChars: typing.Optional[str] = None,
+ ):
+ super().__init__()
+ identChars = identChars or ident_chars
+ if identChars is None:
+ identChars = Keyword.DEFAULT_KEYWORD_CHARS
+ match_string = matchString or match_string
+ self.match = match_string
+ self.matchLen = len(match_string)
+ try:
+ self.firstMatchChar = match_string[0]
+ except IndexError:
+ raise ValueError("null string passed to Keyword; use Empty() instead")
+ self.errmsg = "Expected {} {}".format(type(self).__name__, self.name)
+ self.mayReturnEmpty = False
+ self.mayIndexError = False
+ self.caseless = caseless
+ if caseless:
+ self.caselessmatch = match_string.upper()
+ identChars = identChars.upper()
+ self.identChars = set(identChars)
+
+ def _generateDefaultName(self):
+ return repr(self.match)
+
+ def parseImpl(self, instring, loc, doActions=True):
+ errmsg = self.errmsg
+ errloc = loc
+ if self.caseless:
+ if instring[loc : loc + self.matchLen].upper() == self.caselessmatch:
+ if loc == 0 or instring[loc - 1].upper() not in self.identChars:
+ if (
+ loc >= len(instring) - self.matchLen
+ or instring[loc + self.matchLen].upper() not in self.identChars
+ ):
+ return loc + self.matchLen, self.match
+ else:
+ # followed by keyword char
+ errmsg += ", was immediately followed by keyword character"
+ errloc = loc + self.matchLen
+ else:
+ # preceded by keyword char
+ errmsg += ", keyword was immediately preceded by keyword character"
+ errloc = loc - 1
+ # else no match just raise plain exception
+
+ else:
+ if (
+ instring[loc] == self.firstMatchChar
+ and self.matchLen == 1
+ or instring.startswith(self.match, loc)
+ ):
+ if loc == 0 or instring[loc - 1] not in self.identChars:
+ if (
+ loc >= len(instring) - self.matchLen
+ or instring[loc + self.matchLen] not in self.identChars
+ ):
+ return loc + self.matchLen, self.match
+ else:
+ # followed by keyword char
+ errmsg += (
+ ", keyword was immediately followed by keyword character"
+ )
+ errloc = loc + self.matchLen
+ else:
+ # preceded by keyword char
+ errmsg += ", keyword was immediately preceded by keyword character"
+ errloc = loc - 1
+ # else no match just raise plain exception
+
+ raise ParseException(instring, errloc, errmsg, self)
+
+ @staticmethod
+ def set_default_keyword_chars(chars) -> None:
+ """
+ Overrides the default characters used by :class:`Keyword` expressions.
+ """
+ Keyword.DEFAULT_KEYWORD_CHARS = chars
+
+ setDefaultKeywordChars = set_default_keyword_chars
+
+
+class CaselessLiteral(Literal):
+ """
+ Token to match a specified string, ignoring case of letters.
+ Note: the matched results will always be in the case of the given
+ match string, NOT the case of the input text.
+
+ Example::
+
+ CaselessLiteral("CMD")[1, ...].parse_string("cmd CMD Cmd10")
+ # -> ['CMD', 'CMD', 'CMD']
+
+ (Contrast with example for :class:`CaselessKeyword`.)
+ """
+
+ def __init__(self, match_string: str = "", *, matchString: str = ""):
+ match_string = matchString or match_string
+ super().__init__(match_string.upper())
+ # Preserve the defining literal.
+ self.returnString = match_string
+ self.errmsg = "Expected " + self.name
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if instring[loc : loc + self.matchLen].upper() == self.match:
+ return loc + self.matchLen, self.returnString
+ raise ParseException(instring, loc, self.errmsg, self)
+
+
+class CaselessKeyword(Keyword):
+ """
+ Caseless version of :class:`Keyword`.
+
+ Example::
+
+ CaselessKeyword("CMD")[1, ...].parse_string("cmd CMD Cmd10")
+ # -> ['CMD', 'CMD']
+
+ (Contrast with example for :class:`CaselessLiteral`.)
+ """
+
+ def __init__(
+ self,
+ match_string: str = "",
+ ident_chars: typing.Optional[str] = None,
+ *,
+ matchString: str = "",
+ identChars: typing.Optional[str] = None,
+ ):
+ identChars = identChars or ident_chars
+ match_string = matchString or match_string
+ super().__init__(match_string, identChars, caseless=True)
+
+
+class CloseMatch(Token):
+ """A variation on :class:`Literal` which matches "close" matches,
+ that is, strings with at most 'n' mismatching characters.
+ :class:`CloseMatch` takes parameters:
+
+ - ``match_string`` - string to be matched
+ - ``caseless`` - a boolean indicating whether to ignore casing when comparing characters
+ - ``max_mismatches`` - (``default=1``) maximum number of
+ mismatches allowed to count as a match
+
+ The results from a successful parse will contain the matched text
+ from the input string and the following named results:
+
+ - ``mismatches`` - a list of the positions within the
+ match_string where mismatches were found
+ - ``original`` - the original match_string used to compare
+ against the input string
+
+ If ``mismatches`` is an empty list, then the match was an exact
+ match.
+
+ Example::
+
+ patt = CloseMatch("ATCATCGAATGGA")
+ patt.parse_string("ATCATCGAAXGGA") # -> (['ATCATCGAAXGGA'], {'mismatches': [[9]], 'original': ['ATCATCGAATGGA']})
+ patt.parse_string("ATCAXCGAAXGGA") # -> Exception: Expected 'ATCATCGAATGGA' (with up to 1 mismatches) (at char 0), (line:1, col:1)
+
+ # exact match
+ patt.parse_string("ATCATCGAATGGA") # -> (['ATCATCGAATGGA'], {'mismatches': [[]], 'original': ['ATCATCGAATGGA']})
+
+ # close match allowing up to 2 mismatches
+ patt = CloseMatch("ATCATCGAATGGA", max_mismatches=2)
+ patt.parse_string("ATCAXCGAAXGGA") # -> (['ATCAXCGAAXGGA'], {'mismatches': [[4, 9]], 'original': ['ATCATCGAATGGA']})
+ """
+
+ def __init__(
+ self,
+ match_string: str,
+ max_mismatches: int = None,
+ *,
+ maxMismatches: int = 1,
+ caseless=False,
+ ):
+ maxMismatches = max_mismatches if max_mismatches is not None else maxMismatches
+ super().__init__()
+ self.match_string = match_string
+ self.maxMismatches = maxMismatches
+ self.errmsg = "Expected {!r} (with up to {} mismatches)".format(
+ self.match_string, self.maxMismatches
+ )
+ self.caseless = caseless
+ self.mayIndexError = False
+ self.mayReturnEmpty = False
+
+ def _generateDefaultName(self):
+ return "{}:{!r}".format(type(self).__name__, self.match_string)
+
+ def parseImpl(self, instring, loc, doActions=True):
+ start = loc
+ instrlen = len(instring)
+ maxloc = start + len(self.match_string)
+
+ if maxloc <= instrlen:
+ match_string = self.match_string
+ match_stringloc = 0
+ mismatches = []
+ maxMismatches = self.maxMismatches
+
+ for match_stringloc, s_m in enumerate(
+ zip(instring[loc:maxloc], match_string)
+ ):
+ src, mat = s_m
+ if self.caseless:
+ src, mat = src.lower(), mat.lower()
+
+ if src != mat:
+ mismatches.append(match_stringloc)
+ if len(mismatches) > maxMismatches:
+ break
+ else:
+ loc = start + match_stringloc + 1
+ results = ParseResults([instring[start:loc]])
+ results["original"] = match_string
+ results["mismatches"] = mismatches
+ return loc, results
+
+ raise ParseException(instring, loc, self.errmsg, self)
+
+
+class Word(Token):
+ """Token for matching words composed of allowed character sets.
+ Parameters:
+ - ``init_chars`` - string of all characters that should be used to
+ match as a word; "ABC" will match "AAA", "ABAB", "CBAC", etc.;
+ if ``body_chars`` is also specified, then this is the string of
+ initial characters
+ - ``body_chars`` - string of characters that
+ can be used for matching after a matched initial character as
+ given in ``init_chars``; if omitted, same as the initial characters
+ (default=``None``)
+ - ``min`` - minimum number of characters to match (default=1)
+ - ``max`` - maximum number of characters to match (default=0)
+ - ``exact`` - exact number of characters to match (default=0)
+ - ``as_keyword`` - match as a keyword (default=``False``)
+ - ``exclude_chars`` - characters that might be
+ found in the input ``body_chars`` string but which should not be
+ accepted for matching ;useful to define a word of all
+ printables except for one or two characters, for instance
+ (default=``None``)
+
+ :class:`srange` is useful for defining custom character set strings
+ for defining :class:`Word` expressions, using range notation from
+ regular expression character sets.
+
+ A common mistake is to use :class:`Word` to match a specific literal
+ string, as in ``Word("Address")``. Remember that :class:`Word`
+ uses the string argument to define *sets* of matchable characters.
+ This expression would match "Add", "AAA", "dAred", or any other word
+ made up of the characters 'A', 'd', 'r', 'e', and 's'. To match an
+ exact literal string, use :class:`Literal` or :class:`Keyword`.
+
+ pyparsing includes helper strings for building Words:
+
+ - :class:`alphas`
+ - :class:`nums`
+ - :class:`alphanums`
+ - :class:`hexnums`
+ - :class:`alphas8bit` (alphabetic characters in ASCII range 128-255
+ - accented, tilded, umlauted, etc.)
+ - :class:`punc8bit` (non-alphabetic characters in ASCII range
+ 128-255 - currency, symbols, superscripts, diacriticals, etc.)
+ - :class:`printables` (any non-whitespace character)
+
+ ``alphas``, ``nums``, and ``printables`` are also defined in several
+ Unicode sets - see :class:`pyparsing_unicode``.
+
+ Example::
+
+ # a word composed of digits
+ integer = Word(nums) # equivalent to Word("0123456789") or Word(srange("0-9"))
+
+ # a word with a leading capital, and zero or more lowercase
+ capital_word = Word(alphas.upper(), alphas.lower())
+
+ # hostnames are alphanumeric, with leading alpha, and '-'
+ hostname = Word(alphas, alphanums + '-')
+
+ # roman numeral (not a strict parser, accepts invalid mix of characters)
+ roman = Word("IVXLCDM")
+
+ # any string of non-whitespace characters, except for ','
+ csv_value = Word(printables, exclude_chars=",")
+ """
+
+ def __init__(
+ self,
+ init_chars: str = "",
+ body_chars: typing.Optional[str] = None,
+ min: int = 1,
+ max: int = 0,
+ exact: int = 0,
+ as_keyword: bool = False,
+ exclude_chars: typing.Optional[str] = None,
+ *,
+ initChars: typing.Optional[str] = None,
+ bodyChars: typing.Optional[str] = None,
+ asKeyword: bool = False,
+ excludeChars: typing.Optional[str] = None,
+ ):
+ initChars = initChars or init_chars
+ bodyChars = bodyChars or body_chars
+ asKeyword = asKeyword or as_keyword
+ excludeChars = excludeChars or exclude_chars
+ super().__init__()
+ if not initChars:
+ raise ValueError(
+ "invalid {}, initChars cannot be empty string".format(
+ type(self).__name__
+ )
+ )
+
+ initChars = set(initChars)
+ self.initChars = initChars
+ if excludeChars:
+ excludeChars = set(excludeChars)
+ initChars -= excludeChars
+ if bodyChars:
+ bodyChars = set(bodyChars) - excludeChars
+ self.initCharsOrig = "".join(sorted(initChars))
+
+ if bodyChars:
+ self.bodyCharsOrig = "".join(sorted(bodyChars))
+ self.bodyChars = set(bodyChars)
+ else:
+ self.bodyCharsOrig = "".join(sorted(initChars))
+ self.bodyChars = set(initChars)
+
+ self.maxSpecified = max > 0
+
+ if min < 1:
+ raise ValueError(
+ "cannot specify a minimum length < 1; use Opt(Word()) if zero-length word is permitted"
+ )
+
+ self.minLen = min
+
+ if max > 0:
+ self.maxLen = max
+ else:
+ self.maxLen = _MAX_INT
+
+ if exact > 0:
+ self.maxLen = exact
+ self.minLen = exact
+
+ self.errmsg = "Expected " + self.name
+ self.mayIndexError = False
+ self.asKeyword = asKeyword
+
+ # see if we can make a regex for this Word
+ if " " not in self.initChars | self.bodyChars and (min == 1 and exact == 0):
+ if self.bodyChars == self.initChars:
+ if max == 0:
+ repeat = "+"
+ elif max == 1:
+ repeat = ""
+ else:
+ repeat = "{{{},{}}}".format(
+ self.minLen, "" if self.maxLen == _MAX_INT else self.maxLen
+ )
+ self.reString = "[{}]{}".format(
+ _collapse_string_to_ranges(self.initChars),
+ repeat,
+ )
+ elif len(self.initChars) == 1:
+ if max == 0:
+ repeat = "*"
+ else:
+ repeat = "{{0,{}}}".format(max - 1)
+ self.reString = "{}[{}]{}".format(
+ re.escape(self.initCharsOrig),
+ _collapse_string_to_ranges(self.bodyChars),
+ repeat,
+ )
+ else:
+ if max == 0:
+ repeat = "*"
+ elif max == 2:
+ repeat = ""
+ else:
+ repeat = "{{0,{}}}".format(max - 1)
+ self.reString = "[{}][{}]{}".format(
+ _collapse_string_to_ranges(self.initChars),
+ _collapse_string_to_ranges(self.bodyChars),
+ repeat,
+ )
+ if self.asKeyword:
+ self.reString = r"\b" + self.reString + r"\b"
+
+ try:
+ self.re = re.compile(self.reString)
+ except re.error:
+ self.re = None
+ else:
+ self.re_match = self.re.match
+ self.__class__ = _WordRegex
+
+ def _generateDefaultName(self):
+ def charsAsStr(s):
+ max_repr_len = 16
+ s = _collapse_string_to_ranges(s, re_escape=False)
+ if len(s) > max_repr_len:
+ return s[: max_repr_len - 3] + "..."
+ else:
+ return s
+
+ if self.initChars != self.bodyChars:
+ base = "W:({}, {})".format(
+ charsAsStr(self.initChars), charsAsStr(self.bodyChars)
+ )
+ else:
+ base = "W:({})".format(charsAsStr(self.initChars))
+
+ # add length specification
+ if self.minLen > 1 or self.maxLen != _MAX_INT:
+ if self.minLen == self.maxLen:
+ if self.minLen == 1:
+ return base[2:]
+ else:
+ return base + "{{{}}}".format(self.minLen)
+ elif self.maxLen == _MAX_INT:
+ return base + "{{{},...}}".format(self.minLen)
+ else:
+ return base + "{{{},{}}}".format(self.minLen, self.maxLen)
+ return base
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if instring[loc] not in self.initChars:
+ raise ParseException(instring, loc, self.errmsg, self)
+
+ start = loc
+ loc += 1
+ instrlen = len(instring)
+ bodychars = self.bodyChars
+ maxloc = start + self.maxLen
+ maxloc = min(maxloc, instrlen)
+ while loc < maxloc and instring[loc] in bodychars:
+ loc += 1
+
+ throwException = False
+ if loc - start < self.minLen:
+ throwException = True
+ elif self.maxSpecified and loc < instrlen and instring[loc] in bodychars:
+ throwException = True
+ elif self.asKeyword:
+ if (
+ start > 0
+ and instring[start - 1] in bodychars
+ or loc < instrlen
+ and instring[loc] in bodychars
+ ):
+ throwException = True
+
+ if throwException:
+ raise ParseException(instring, loc, self.errmsg, self)
+
+ return loc, instring[start:loc]
+
+
+class _WordRegex(Word):
+ def parseImpl(self, instring, loc, doActions=True):
+ result = self.re_match(instring, loc)
+ if not result:
+ raise ParseException(instring, loc, self.errmsg, self)
+
+ loc = result.end()
+ return loc, result.group()
+
+
+class Char(_WordRegex):
+ """A short-cut class for defining :class:`Word` ``(characters, exact=1)``,
+ when defining a match of any single character in a string of
+ characters.
+ """
+
+ def __init__(
+ self,
+ charset: str,
+ as_keyword: bool = False,
+ exclude_chars: typing.Optional[str] = None,
+ *,
+ asKeyword: bool = False,
+ excludeChars: typing.Optional[str] = None,
+ ):
+ asKeyword = asKeyword or as_keyword
+ excludeChars = excludeChars or exclude_chars
+ super().__init__(
+ charset, exact=1, asKeyword=asKeyword, excludeChars=excludeChars
+ )
+ self.reString = "[{}]".format(_collapse_string_to_ranges(self.initChars))
+ if asKeyword:
+ self.reString = r"\b{}\b".format(self.reString)
+ self.re = re.compile(self.reString)
+ self.re_match = self.re.match
+
+
+class Regex(Token):
+ r"""Token for matching strings that match a given regular
+ expression. Defined with string specifying the regular expression in
+ a form recognized by the stdlib Python `re module <https://docs.python.org/3/library/re.html>`_.
+ If the given regex contains named groups (defined using ``(?P<name>...)``),
+ these will be preserved as named :class:`ParseResults`.
+
+ If instead of the Python stdlib ``re`` module you wish to use a different RE module
+ (such as the ``regex`` module), you can do so by building your ``Regex`` object with
+ a compiled RE that was compiled using ``regex``.
+
+ Example::
+
+ realnum = Regex(r"[+-]?\d+\.\d*")
+ # ref: https://stackoverflow.com/questions/267399/how-do-you-match-only-valid-roman-numerals-with-a-regular-expression
+ roman = Regex(r"M{0,4}(CM|CD|D?{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})")
+
+ # named fields in a regex will be returned as named results
+ date = Regex(r'(?P<year>\d{4})-(?P<month>\d\d?)-(?P<day>\d\d?)')
+
+ # the Regex class will accept re's compiled using the regex module
+ import regex
+ parser = pp.Regex(regex.compile(r'[0-9]'))
+ """
+
+ def __init__(
+ self,
+ pattern: Any,
+ flags: Union[re.RegexFlag, int] = 0,
+ as_group_list: bool = False,
+ as_match: bool = False,
+ *,
+ asGroupList: bool = False,
+ asMatch: bool = False,
+ ):
+ """The parameters ``pattern`` and ``flags`` are passed
+ to the ``re.compile()`` function as-is. See the Python
+ `re module <https://docs.python.org/3/library/re.html>`_ module for an
+ explanation of the acceptable patterns and flags.
+ """
+ super().__init__()
+ asGroupList = asGroupList or as_group_list
+ asMatch = asMatch or as_match
+
+ if isinstance(pattern, str_type):
+ if not pattern:
+ raise ValueError("null string passed to Regex; use Empty() instead")
+
+ self._re = None
+ self.reString = self.pattern = pattern
+ self.flags = flags
+
+ elif hasattr(pattern, "pattern") and hasattr(pattern, "match"):
+ self._re = pattern
+ self.pattern = self.reString = pattern.pattern
+ self.flags = flags
+
+ else:
+ raise TypeError(
+ "Regex may only be constructed with a string or a compiled RE object"
+ )
+
+ self.errmsg = "Expected " + self.name
+ self.mayIndexError = False
+ self.asGroupList = asGroupList
+ self.asMatch = asMatch
+ if self.asGroupList:
+ self.parseImpl = self.parseImplAsGroupList
+ if self.asMatch:
+ self.parseImpl = self.parseImplAsMatch
+
+ @cached_property
+ def re(self):
+ if self._re:
+ return self._re
+ else:
+ try:
+ return re.compile(self.pattern, self.flags)
+ except re.error:
+ raise ValueError(
+ "invalid pattern ({!r}) passed to Regex".format(self.pattern)
+ )
+
+ @cached_property
+ def re_match(self):
+ return self.re.match
+
+ @cached_property
+ def mayReturnEmpty(self):
+ return self.re_match("") is not None
+
+ def _generateDefaultName(self):
+ return "Re:({})".format(repr(self.pattern).replace("\\\\", "\\"))
+
+ def parseImpl(self, instring, loc, doActions=True):
+ result = self.re_match(instring, loc)
+ if not result:
+ raise ParseException(instring, loc, self.errmsg, self)
+
+ loc = result.end()
+ ret = ParseResults(result.group())
+ d = result.groupdict()
+ if d:
+ for k, v in d.items():
+ ret[k] = v
+ return loc, ret
+
+ def parseImplAsGroupList(self, instring, loc, doActions=True):
+ result = self.re_match(instring, loc)
+ if not result:
+ raise ParseException(instring, loc, self.errmsg, self)
+
+ loc = result.end()
+ ret = result.groups()
+ return loc, ret
+
+ def parseImplAsMatch(self, instring, loc, doActions=True):
+ result = self.re_match(instring, loc)
+ if not result:
+ raise ParseException(instring, loc, self.errmsg, self)
+
+ loc = result.end()
+ ret = result
+ return loc, ret
+
+ def sub(self, repl: str) -> ParserElement:
+ r"""
+ Return :class:`Regex` with an attached parse action to transform the parsed
+ result as if called using `re.sub(expr, repl, string) <https://docs.python.org/3/library/re.html#re.sub>`_.
+
+ Example::
+
+ make_html = Regex(r"(\w+):(.*?):").sub(r"<\1>\2</\1>")
+ print(make_html.transform_string("h1:main title:"))
+ # prints "<h1>main title</h1>"
+ """
+ if self.asGroupList:
+ raise TypeError("cannot use sub() with Regex(asGroupList=True)")
+
+ if self.asMatch and callable(repl):
+ raise TypeError("cannot use sub() with a callable with Regex(asMatch=True)")
+
+ if self.asMatch:
+
+ def pa(tokens):
+ return tokens[0].expand(repl)
+
+ else:
+
+ def pa(tokens):
+ return self.re.sub(repl, tokens[0])
+
+ return self.add_parse_action(pa)
+
+
+class QuotedString(Token):
+ r"""
+ Token for matching strings that are delimited by quoting characters.
+
+ Defined with the following parameters:
+
+ - ``quote_char`` - string of one or more characters defining the
+ quote delimiting string
+ - ``esc_char`` - character to re_escape quotes, typically backslash
+ (default= ``None``)
+ - ``esc_quote`` - special quote sequence to re_escape an embedded quote
+ string (such as SQL's ``""`` to re_escape an embedded ``"``)
+ (default= ``None``)
+ - ``multiline`` - boolean indicating whether quotes can span
+ multiple lines (default= ``False``)
+ - ``unquote_results`` - boolean indicating whether the matched text
+ should be unquoted (default= ``True``)
+ - ``end_quote_char`` - string of one or more characters defining the
+ end of the quote delimited string (default= ``None`` => same as
+ quote_char)
+ - ``convert_whitespace_escapes`` - convert escaped whitespace
+ (``'\t'``, ``'\n'``, etc.) to actual whitespace
+ (default= ``True``)
+
+ Example::
+
+ qs = QuotedString('"')
+ print(qs.search_string('lsjdf "This is the quote" sldjf'))
+ complex_qs = QuotedString('{{', end_quote_char='}}')
+ print(complex_qs.search_string('lsjdf {{This is the "quote"}} sldjf'))
+ sql_qs = QuotedString('"', esc_quote='""')
+ print(sql_qs.search_string('lsjdf "This is the quote with ""embedded"" quotes" sldjf'))
+
+ prints::
+
+ [['This is the quote']]
+ [['This is the "quote"']]
+ [['This is the quote with "embedded" quotes']]
+ """
+ ws_map = ((r"\t", "\t"), (r"\n", "\n"), (r"\f", "\f"), (r"\r", "\r"))
+
+ def __init__(
+ self,
+ quote_char: str = "",
+ esc_char: typing.Optional[str] = None,
+ esc_quote: typing.Optional[str] = None,
+ multiline: bool = False,
+ unquote_results: bool = True,
+ end_quote_char: typing.Optional[str] = None,
+ convert_whitespace_escapes: bool = True,
+ *,
+ quoteChar: str = "",
+ escChar: typing.Optional[str] = None,
+ escQuote: typing.Optional[str] = None,
+ unquoteResults: bool = True,
+ endQuoteChar: typing.Optional[str] = None,
+ convertWhitespaceEscapes: bool = True,
+ ):
+ super().__init__()
+ escChar = escChar or esc_char
+ escQuote = escQuote or esc_quote
+ unquoteResults = unquoteResults and unquote_results
+ endQuoteChar = endQuoteChar or end_quote_char
+ convertWhitespaceEscapes = (
+ convertWhitespaceEscapes and convert_whitespace_escapes
+ )
+ quote_char = quoteChar or quote_char
+
+ # remove white space from quote chars - wont work anyway
+ quote_char = quote_char.strip()
+ if not quote_char:
+ raise ValueError("quote_char cannot be the empty string")
+
+ if endQuoteChar is None:
+ endQuoteChar = quote_char
+ else:
+ endQuoteChar = endQuoteChar.strip()
+ if not endQuoteChar:
+ raise ValueError("endQuoteChar cannot be the empty string")
+
+ self.quoteChar = quote_char
+ self.quoteCharLen = len(quote_char)
+ self.firstQuoteChar = quote_char[0]
+ self.endQuoteChar = endQuoteChar
+ self.endQuoteCharLen = len(endQuoteChar)
+ self.escChar = escChar
+ self.escQuote = escQuote
+ self.unquoteResults = unquoteResults
+ self.convertWhitespaceEscapes = convertWhitespaceEscapes
+
+ sep = ""
+ inner_pattern = ""
+
+ if escQuote:
+ inner_pattern += r"{}(?:{})".format(sep, re.escape(escQuote))
+ sep = "|"
+
+ if escChar:
+ inner_pattern += r"{}(?:{}.)".format(sep, re.escape(escChar))
+ sep = "|"
+ self.escCharReplacePattern = re.escape(self.escChar) + "(.)"
+
+ if len(self.endQuoteChar) > 1:
+ inner_pattern += (
+ "{}(?:".format(sep)
+ + "|".join(
+ "(?:{}(?!{}))".format(
+ re.escape(self.endQuoteChar[:i]),
+ re.escape(self.endQuoteChar[i:]),
+ )
+ for i in range(len(self.endQuoteChar) - 1, 0, -1)
+ )
+ + ")"
+ )
+ sep = "|"
+
+ if multiline:
+ self.flags = re.MULTILINE | re.DOTALL
+ inner_pattern += r"{}(?:[^{}{}])".format(
+ sep,
+ _escape_regex_range_chars(self.endQuoteChar[0]),
+ (_escape_regex_range_chars(escChar) if escChar is not None else ""),
+ )
+ else:
+ self.flags = 0
+ inner_pattern += r"{}(?:[^{}\n\r{}])".format(
+ sep,
+ _escape_regex_range_chars(self.endQuoteChar[0]),
+ (_escape_regex_range_chars(escChar) if escChar is not None else ""),
+ )
+
+ self.pattern = "".join(
+ [
+ re.escape(self.quoteChar),
+ "(?:",
+ inner_pattern,
+ ")*",
+ re.escape(self.endQuoteChar),
+ ]
+ )
+
+ try:
+ self.re = re.compile(self.pattern, self.flags)
+ self.reString = self.pattern
+ self.re_match = self.re.match
+ except re.error:
+ raise ValueError(
+ "invalid pattern {!r} passed to Regex".format(self.pattern)
+ )
+
+ self.errmsg = "Expected " + self.name
+ self.mayIndexError = False
+ self.mayReturnEmpty = True
+
+ def _generateDefaultName(self):
+ if self.quoteChar == self.endQuoteChar and isinstance(self.quoteChar, str_type):
+ return "string enclosed in {!r}".format(self.quoteChar)
+
+ return "quoted string, starting with {} ending with {}".format(
+ self.quoteChar, self.endQuoteChar
+ )
+
+ def parseImpl(self, instring, loc, doActions=True):
+ result = (
+ instring[loc] == self.firstQuoteChar
+ and self.re_match(instring, loc)
+ or None
+ )
+ if not result:
+ raise ParseException(instring, loc, self.errmsg, self)
+
+ loc = result.end()
+ ret = result.group()
+
+ if self.unquoteResults:
+
+ # strip off quotes
+ ret = ret[self.quoteCharLen : -self.endQuoteCharLen]
+
+ if isinstance(ret, str_type):
+ # replace escaped whitespace
+ if "\\" in ret and self.convertWhitespaceEscapes:
+ for wslit, wschar in self.ws_map:
+ ret = ret.replace(wslit, wschar)
+
+ # replace escaped characters
+ if self.escChar:
+ ret = re.sub(self.escCharReplacePattern, r"\g<1>", ret)
+
+ # replace escaped quotes
+ if self.escQuote:
+ ret = ret.replace(self.escQuote, self.endQuoteChar)
+
+ return loc, ret
+
+
+class CharsNotIn(Token):
+ """Token for matching words composed of characters *not* in a given
+ set (will include whitespace in matched characters if not listed in
+ the provided exclusion set - see example). Defined with string
+ containing all disallowed characters, and an optional minimum,
+ maximum, and/or exact length. The default value for ``min`` is
+ 1 (a minimum value < 1 is not valid); the default values for
+ ``max`` and ``exact`` are 0, meaning no maximum or exact
+ length restriction.
+
+ Example::
+
+ # define a comma-separated-value as anything that is not a ','
+ csv_value = CharsNotIn(',')
+ print(delimited_list(csv_value).parse_string("dkls,lsdkjf,s12 34,@!#,213"))
+
+ prints::
+
+ ['dkls', 'lsdkjf', 's12 34', '@!#', '213']
+ """
+
+ def __init__(
+ self,
+ not_chars: str = "",
+ min: int = 1,
+ max: int = 0,
+ exact: int = 0,
+ *,
+ notChars: str = "",
+ ):
+ super().__init__()
+ self.skipWhitespace = False
+ self.notChars = not_chars or notChars
+ self.notCharsSet = set(self.notChars)
+
+ if min < 1:
+ raise ValueError(
+ "cannot specify a minimum length < 1; use "
+ "Opt(CharsNotIn()) if zero-length char group is permitted"
+ )
+
+ self.minLen = min
+
+ if max > 0:
+ self.maxLen = max
+ else:
+ self.maxLen = _MAX_INT
+
+ if exact > 0:
+ self.maxLen = exact
+ self.minLen = exact
+
+ self.errmsg = "Expected " + self.name
+ self.mayReturnEmpty = self.minLen == 0
+ self.mayIndexError = False
+
+ def _generateDefaultName(self):
+ not_chars_str = _collapse_string_to_ranges(self.notChars)
+ if len(not_chars_str) > 16:
+ return "!W:({}...)".format(self.notChars[: 16 - 3])
+ else:
+ return "!W:({})".format(self.notChars)
+
+ def parseImpl(self, instring, loc, doActions=True):
+ notchars = self.notCharsSet
+ if instring[loc] in notchars:
+ raise ParseException(instring, loc, self.errmsg, self)
+
+ start = loc
+ loc += 1
+ maxlen = min(start + self.maxLen, len(instring))
+ while loc < maxlen and instring[loc] not in notchars:
+ loc += 1
+
+ if loc - start < self.minLen:
+ raise ParseException(instring, loc, self.errmsg, self)
+
+ return loc, instring[start:loc]
+
+
+class White(Token):
+ """Special matching class for matching whitespace. Normally,
+ whitespace is ignored by pyparsing grammars. This class is included
+ when some whitespace structures are significant. Define with
+ a string containing the whitespace characters to be matched; default
+ is ``" \\t\\r\\n"``. Also takes optional ``min``,
+ ``max``, and ``exact`` arguments, as defined for the
+ :class:`Word` class.
+ """
+
+ whiteStrs = {
+ " ": "<SP>",
+ "\t": "<TAB>",
+ "\n": "<LF>",
+ "\r": "<CR>",
+ "\f": "<FF>",
+ "\u00A0": "<NBSP>",
+ "\u1680": "<OGHAM_SPACE_MARK>",
+ "\u180E": "<MONGOLIAN_VOWEL_SEPARATOR>",
+ "\u2000": "<EN_QUAD>",
+ "\u2001": "<EM_QUAD>",
+ "\u2002": "<EN_SPACE>",
+ "\u2003": "<EM_SPACE>",
+ "\u2004": "<THREE-PER-EM_SPACE>",
+ "\u2005": "<FOUR-PER-EM_SPACE>",
+ "\u2006": "<SIX-PER-EM_SPACE>",
+ "\u2007": "<FIGURE_SPACE>",
+ "\u2008": "<PUNCTUATION_SPACE>",
+ "\u2009": "<THIN_SPACE>",
+ "\u200A": "<HAIR_SPACE>",
+ "\u200B": "<ZERO_WIDTH_SPACE>",
+ "\u202F": "<NNBSP>",
+ "\u205F": "<MMSP>",
+ "\u3000": "<IDEOGRAPHIC_SPACE>",
+ }
+
+ def __init__(self, ws: str = " \t\r\n", min: int = 1, max: int = 0, exact: int = 0):
+ super().__init__()
+ self.matchWhite = ws
+ self.set_whitespace_chars(
+ "".join(c for c in self.whiteStrs if c not in self.matchWhite),
+ copy_defaults=True,
+ )
+ # self.leave_whitespace()
+ self.mayReturnEmpty = True
+ self.errmsg = "Expected " + self.name
+
+ self.minLen = min
+
+ if max > 0:
+ self.maxLen = max
+ else:
+ self.maxLen = _MAX_INT
+
+ if exact > 0:
+ self.maxLen = exact
+ self.minLen = exact
+
+ def _generateDefaultName(self):
+ return "".join(White.whiteStrs[c] for c in self.matchWhite)
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if instring[loc] not in self.matchWhite:
+ raise ParseException(instring, loc, self.errmsg, self)
+ start = loc
+ loc += 1
+ maxloc = start + self.maxLen
+ maxloc = min(maxloc, len(instring))
+ while loc < maxloc and instring[loc] in self.matchWhite:
+ loc += 1
+
+ if loc - start < self.minLen:
+ raise ParseException(instring, loc, self.errmsg, self)
+
+ return loc, instring[start:loc]
+
+
+class PositionToken(Token):
+ def __init__(self):
+ super().__init__()
+ self.mayReturnEmpty = True
+ self.mayIndexError = False
+
+
+class GoToColumn(PositionToken):
+ """Token to advance to a specific column of input text; useful for
+ tabular report scraping.
+ """
+
+ def __init__(self, colno: int):
+ super().__init__()
+ self.col = colno
+
+ def preParse(self, instring, loc):
+ if col(loc, instring) != self.col:
+ instrlen = len(instring)
+ if self.ignoreExprs:
+ loc = self._skipIgnorables(instring, loc)
+ while (
+ loc < instrlen
+ and instring[loc].isspace()
+ and col(loc, instring) != self.col
+ ):
+ loc += 1
+ return loc
+
+ def parseImpl(self, instring, loc, doActions=True):
+ thiscol = col(loc, instring)
+ if thiscol > self.col:
+ raise ParseException(instring, loc, "Text not in expected column", self)
+ newloc = loc + self.col - thiscol
+ ret = instring[loc:newloc]
+ return newloc, ret
+
+
+class LineStart(PositionToken):
+ r"""Matches if current position is at the beginning of a line within
+ the parse string
+
+ Example::
+
+ test = '''\
+ AAA this line
+ AAA and this line
+ AAA but not this one
+ B AAA and definitely not this one
+ '''
+
+ for t in (LineStart() + 'AAA' + restOfLine).search_string(test):
+ print(t)
+
+ prints::
+
+ ['AAA', ' this line']
+ ['AAA', ' and this line']
+
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.leave_whitespace()
+ self.orig_whiteChars = set() | self.whiteChars
+ self.whiteChars.discard("\n")
+ self.skipper = Empty().set_whitespace_chars(self.whiteChars)
+ self.errmsg = "Expected start of line"
+
+ def preParse(self, instring, loc):
+ if loc == 0:
+ return loc
+ else:
+ ret = self.skipper.preParse(instring, loc)
+ if "\n" in self.orig_whiteChars:
+ while instring[ret : ret + 1] == "\n":
+ ret = self.skipper.preParse(instring, ret + 1)
+ return ret
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if col(loc, instring) == 1:
+ return loc, []
+ raise ParseException(instring, loc, self.errmsg, self)
+
+
+class LineEnd(PositionToken):
+ """Matches if current position is at the end of a line within the
+ parse string
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.whiteChars.discard("\n")
+ self.set_whitespace_chars(self.whiteChars, copy_defaults=False)
+ self.errmsg = "Expected end of line"
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if loc < len(instring):
+ if instring[loc] == "\n":
+ return loc + 1, "\n"
+ else:
+ raise ParseException(instring, loc, self.errmsg, self)
+ elif loc == len(instring):
+ return loc + 1, []
+ else:
+ raise ParseException(instring, loc, self.errmsg, self)
+
+
+class StringStart(PositionToken):
+ """Matches if current position is at the beginning of the parse
+ string
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.errmsg = "Expected start of text"
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if loc != 0:
+ # see if entire string up to here is just whitespace and ignoreables
+ if loc != self.preParse(instring, 0):
+ raise ParseException(instring, loc, self.errmsg, self)
+ return loc, []
+
+
+class StringEnd(PositionToken):
+ """
+ Matches if current position is at the end of the parse string
+ """
+
+ def __init__(self):
+ super().__init__()
+ self.errmsg = "Expected end of text"
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if loc < len(instring):
+ raise ParseException(instring, loc, self.errmsg, self)
+ elif loc == len(instring):
+ return loc + 1, []
+ elif loc > len(instring):
+ return loc, []
+ else:
+ raise ParseException(instring, loc, self.errmsg, self)
+
+
+class WordStart(PositionToken):
+ """Matches if the current position is at the beginning of a
+ :class:`Word`, and is not preceded by any character in a given
+ set of ``word_chars`` (default= ``printables``). To emulate the
+ ``\b`` behavior of regular expressions, use
+ ``WordStart(alphanums)``. ``WordStart`` will also match at
+ the beginning of the string being parsed, or at the beginning of
+ a line.
+ """
+
+ def __init__(self, word_chars: str = printables, *, wordChars: str = printables):
+ wordChars = word_chars if wordChars == printables else wordChars
+ super().__init__()
+ self.wordChars = set(wordChars)
+ self.errmsg = "Not at the start of a word"
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if loc != 0:
+ if (
+ instring[loc - 1] in self.wordChars
+ or instring[loc] not in self.wordChars
+ ):
+ raise ParseException(instring, loc, self.errmsg, self)
+ return loc, []
+
+
+class WordEnd(PositionToken):
+ """Matches if the current position is at the end of a :class:`Word`,
+ and is not followed by any character in a given set of ``word_chars``
+ (default= ``printables``). To emulate the ``\b`` behavior of
+ regular expressions, use ``WordEnd(alphanums)``. ``WordEnd``
+ will also match at the end of the string being parsed, or at the end
+ of a line.
+ """
+
+ def __init__(self, word_chars: str = printables, *, wordChars: str = printables):
+ wordChars = word_chars if wordChars == printables else wordChars
+ super().__init__()
+ self.wordChars = set(wordChars)
+ self.skipWhitespace = False
+ self.errmsg = "Not at the end of a word"
+
+ def parseImpl(self, instring, loc, doActions=True):
+ instrlen = len(instring)
+ if instrlen > 0 and loc < instrlen:
+ if (
+ instring[loc] in self.wordChars
+ or instring[loc - 1] not in self.wordChars
+ ):
+ raise ParseException(instring, loc, self.errmsg, self)
+ return loc, []
+
+
+class ParseExpression(ParserElement):
+ """Abstract subclass of ParserElement, for combining and
+ post-processing parsed tokens.
+ """
+
+ def __init__(self, exprs: typing.Iterable[ParserElement], savelist: bool = False):
+ super().__init__(savelist)
+ self.exprs: List[ParserElement]
+ if isinstance(exprs, _generatorType):
+ exprs = list(exprs)
+
+ if isinstance(exprs, str_type):
+ self.exprs = [self._literalStringClass(exprs)]
+ elif isinstance(exprs, ParserElement):
+ self.exprs = [exprs]
+ elif isinstance(exprs, Iterable):
+ exprs = list(exprs)
+ # if sequence of strings provided, wrap with Literal
+ if any(isinstance(expr, str_type) for expr in exprs):
+ exprs = (
+ self._literalStringClass(e) if isinstance(e, str_type) else e
+ for e in exprs
+ )
+ self.exprs = list(exprs)
+ else:
+ try:
+ self.exprs = list(exprs)
+ except TypeError:
+ self.exprs = [exprs]
+ self.callPreparse = False
+
+ def recurse(self) -> Sequence[ParserElement]:
+ return self.exprs[:]
+
+ def append(self, other) -> ParserElement:
+ self.exprs.append(other)
+ self._defaultName = None
+ return self
+
+ def leave_whitespace(self, recursive: bool = True) -> ParserElement:
+ """
+ Extends ``leave_whitespace`` defined in base class, and also invokes ``leave_whitespace`` on
+ all contained expressions.
+ """
+ super().leave_whitespace(recursive)
+
+ if recursive:
+ self.exprs = [e.copy() for e in self.exprs]
+ for e in self.exprs:
+ e.leave_whitespace(recursive)
+ return self
+
+ def ignore_whitespace(self, recursive: bool = True) -> ParserElement:
+ """
+ Extends ``ignore_whitespace`` defined in base class, and also invokes ``leave_whitespace`` on
+ all contained expressions.
+ """
+ super().ignore_whitespace(recursive)
+ if recursive:
+ self.exprs = [e.copy() for e in self.exprs]
+ for e in self.exprs:
+ e.ignore_whitespace(recursive)
+ return self
+
+ def ignore(self, other) -> ParserElement:
+ if isinstance(other, Suppress):
+ if other not in self.ignoreExprs:
+ super().ignore(other)
+ for e in self.exprs:
+ e.ignore(self.ignoreExprs[-1])
+ else:
+ super().ignore(other)
+ for e in self.exprs:
+ e.ignore(self.ignoreExprs[-1])
+ return self
+
+ def _generateDefaultName(self):
+ return "{}:({})".format(self.__class__.__name__, str(self.exprs))
+
+ def streamline(self) -> ParserElement:
+ if self.streamlined:
+ return self
+
+ super().streamline()
+
+ for e in self.exprs:
+ e.streamline()
+
+ # collapse nested :class:`And`'s of the form ``And(And(And(a, b), c), d)`` to ``And(a, b, c, d)``
+ # but only if there are no parse actions or resultsNames on the nested And's
+ # (likewise for :class:`Or`'s and :class:`MatchFirst`'s)
+ if len(self.exprs) == 2:
+ other = self.exprs[0]
+ if (
+ isinstance(other, self.__class__)
+ and not other.parseAction
+ and other.resultsName is None
+ and not other.debug
+ ):
+ self.exprs = other.exprs[:] + [self.exprs[1]]
+ self._defaultName = None
+ self.mayReturnEmpty |= other.mayReturnEmpty
+ self.mayIndexError |= other.mayIndexError
+
+ other = self.exprs[-1]
+ if (
+ isinstance(other, self.__class__)
+ and not other.parseAction
+ and other.resultsName is None
+ and not other.debug
+ ):
+ self.exprs = self.exprs[:-1] + other.exprs[:]
+ self._defaultName = None
+ self.mayReturnEmpty |= other.mayReturnEmpty
+ self.mayIndexError |= other.mayIndexError
+
+ self.errmsg = "Expected " + str(self)
+
+ return self
+
+ def validate(self, validateTrace=None) -> None:
+ tmp = (validateTrace if validateTrace is not None else [])[:] + [self]
+ for e in self.exprs:
+ e.validate(tmp)
+ self._checkRecursion([])
+
+ def copy(self) -> ParserElement:
+ ret = super().copy()
+ ret.exprs = [e.copy() for e in self.exprs]
+ return ret
+
+ def _setResultsName(self, name, listAllMatches=False):
+ if (
+ __diag__.warn_ungrouped_named_tokens_in_collection
+ and Diagnostics.warn_ungrouped_named_tokens_in_collection
+ not in self.suppress_warnings_
+ ):
+ for e in self.exprs:
+ if (
+ isinstance(e, ParserElement)
+ and e.resultsName
+ and Diagnostics.warn_ungrouped_named_tokens_in_collection
+ not in e.suppress_warnings_
+ ):
+ warnings.warn(
+ "{}: setting results name {!r} on {} expression "
+ "collides with {!r} on contained expression".format(
+ "warn_ungrouped_named_tokens_in_collection",
+ name,
+ type(self).__name__,
+ e.resultsName,
+ ),
+ stacklevel=3,
+ )
+
+ return super()._setResultsName(name, listAllMatches)
+
+ ignoreWhitespace = ignore_whitespace
+ leaveWhitespace = leave_whitespace
+
+
+class And(ParseExpression):
+ """
+ Requires all given :class:`ParseExpression` s to be found in the given order.
+ Expressions may be separated by whitespace.
+ May be constructed using the ``'+'`` operator.
+ May also be constructed using the ``'-'`` operator, which will
+ suppress backtracking.
+
+ Example::
+
+ integer = Word(nums)
+ name_expr = Word(alphas)[1, ...]
+
+ expr = And([integer("id"), name_expr("name"), integer("age")])
+ # more easily written as:
+ expr = integer("id") + name_expr("name") + integer("age")
+ """
+
+ class _ErrorStop(Empty):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.leave_whitespace()
+
+ def _generateDefaultName(self):
+ return "-"
+
+ def __init__(
+ self, exprs_arg: typing.Iterable[ParserElement], savelist: bool = True
+ ):
+ exprs: List[ParserElement] = list(exprs_arg)
+ if exprs and Ellipsis in exprs:
+ tmp = []
+ for i, expr in enumerate(exprs):
+ if expr is Ellipsis:
+ if i < len(exprs) - 1:
+ skipto_arg: ParserElement = (Empty() + exprs[i + 1]).exprs[-1]
+ tmp.append(SkipTo(skipto_arg)("_skipped*"))
+ else:
+ raise Exception(
+ "cannot construct And with sequence ending in ..."
+ )
+ else:
+ tmp.append(expr)
+ exprs[:] = tmp
+ super().__init__(exprs, savelist)
+ if self.exprs:
+ self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
+ if not isinstance(self.exprs[0], White):
+ self.set_whitespace_chars(
+ self.exprs[0].whiteChars,
+ copy_defaults=self.exprs[0].copyDefaultWhiteChars,
+ )
+ self.skipWhitespace = self.exprs[0].skipWhitespace
+ else:
+ self.skipWhitespace = False
+ else:
+ self.mayReturnEmpty = True
+ self.callPreparse = True
+
+ def streamline(self) -> ParserElement:
+ # collapse any _PendingSkip's
+ if self.exprs:
+ if any(
+ isinstance(e, ParseExpression)
+ and e.exprs
+ and isinstance(e.exprs[-1], _PendingSkip)
+ for e in self.exprs[:-1]
+ ):
+ for i, e in enumerate(self.exprs[:-1]):
+ if e is None:
+ continue
+ if (
+ isinstance(e, ParseExpression)
+ and e.exprs
+ and isinstance(e.exprs[-1], _PendingSkip)
+ ):
+ e.exprs[-1] = e.exprs[-1] + self.exprs[i + 1]
+ self.exprs[i + 1] = None
+ self.exprs = [e for e in self.exprs if e is not None]
+
+ super().streamline()
+
+ # link any IndentedBlocks to the prior expression
+ for prev, cur in zip(self.exprs, self.exprs[1:]):
+ # traverse cur or any first embedded expr of cur looking for an IndentedBlock
+ # (but watch out for recursive grammar)
+ seen = set()
+ while cur:
+ if id(cur) in seen:
+ break
+ seen.add(id(cur))
+ if isinstance(cur, IndentedBlock):
+ prev.add_parse_action(
+ lambda s, l, t, cur_=cur: setattr(
+ cur_, "parent_anchor", col(l, s)
+ )
+ )
+ break
+ subs = cur.recurse()
+ cur = next(iter(subs), None)
+
+ self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
+ return self
+
+ def parseImpl(self, instring, loc, doActions=True):
+ # pass False as callPreParse arg to _parse for first element, since we already
+ # pre-parsed the string as part of our And pre-parsing
+ loc, resultlist = self.exprs[0]._parse(
+ instring, loc, doActions, callPreParse=False
+ )
+ errorStop = False
+ for e in self.exprs[1:]:
+ # if isinstance(e, And._ErrorStop):
+ if type(e) is And._ErrorStop:
+ errorStop = True
+ continue
+ if errorStop:
+ try:
+ loc, exprtokens = e._parse(instring, loc, doActions)
+ except ParseSyntaxException:
+ raise
+ except ParseBaseException as pe:
+ pe.__traceback__ = None
+ raise ParseSyntaxException._from_exception(pe)
+ except IndexError:
+ raise ParseSyntaxException(
+ instring, len(instring), self.errmsg, self
+ )
+ else:
+ loc, exprtokens = e._parse(instring, loc, doActions)
+ if exprtokens or exprtokens.haskeys():
+ resultlist += exprtokens
+ return loc, resultlist
+
+ def __iadd__(self, other):
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ return self.append(other) # And([self, other])
+
+ def _checkRecursion(self, parseElementList):
+ subRecCheckList = parseElementList[:] + [self]
+ for e in self.exprs:
+ e._checkRecursion(subRecCheckList)
+ if not e.mayReturnEmpty:
+ break
+
+ def _generateDefaultName(self):
+ inner = " ".join(str(e) for e in self.exprs)
+ # strip off redundant inner {}'s
+ while len(inner) > 1 and inner[0 :: len(inner) - 1] == "{}":
+ inner = inner[1:-1]
+ return "{" + inner + "}"
+
+
+class Or(ParseExpression):
+ """Requires that at least one :class:`ParseExpression` is found. If
+ two expressions match, the expression that matches the longest
+ string will be used. May be constructed using the ``'^'``
+ operator.
+
+ Example::
+
+ # construct Or using '^' operator
+
+ number = Word(nums) ^ Combine(Word(nums) + '.' + Word(nums))
+ print(number.search_string("123 3.1416 789"))
+
+ prints::
+
+ [['123'], ['3.1416'], ['789']]
+ """
+
+ def __init__(self, exprs: typing.Iterable[ParserElement], savelist: bool = False):
+ super().__init__(exprs, savelist)
+ if self.exprs:
+ self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)
+ self.skipWhitespace = all(e.skipWhitespace for e in self.exprs)
+ else:
+ self.mayReturnEmpty = True
+
+ def streamline(self) -> ParserElement:
+ super().streamline()
+ if self.exprs:
+ self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)
+ self.saveAsList = any(e.saveAsList for e in self.exprs)
+ self.skipWhitespace = all(
+ e.skipWhitespace and not isinstance(e, White) for e in self.exprs
+ )
+ else:
+ self.saveAsList = False
+ return self
+
+ def parseImpl(self, instring, loc, doActions=True):
+ maxExcLoc = -1
+ maxException = None
+ matches = []
+ fatals = []
+ if all(e.callPreparse for e in self.exprs):
+ loc = self.preParse(instring, loc)
+ for e in self.exprs:
+ try:
+ loc2 = e.try_parse(instring, loc, raise_fatal=True)
+ except ParseFatalException as pfe:
+ pfe.__traceback__ = None
+ pfe.parserElement = e
+ fatals.append(pfe)
+ maxException = None
+ maxExcLoc = -1
+ except ParseException as err:
+ if not fatals:
+ err.__traceback__ = None
+ if err.loc > maxExcLoc:
+ maxException = err
+ maxExcLoc = err.loc
+ except IndexError:
+ if len(instring) > maxExcLoc:
+ maxException = ParseException(
+ instring, len(instring), e.errmsg, self
+ )
+ maxExcLoc = len(instring)
+ else:
+ # save match among all matches, to retry longest to shortest
+ matches.append((loc2, e))
+
+ if matches:
+ # re-evaluate all matches in descending order of length of match, in case attached actions
+ # might change whether or how much they match of the input.
+ matches.sort(key=itemgetter(0), reverse=True)
+
+ if not doActions:
+ # no further conditions or parse actions to change the selection of
+ # alternative, so the first match will be the best match
+ best_expr = matches[0][1]
+ return best_expr._parse(instring, loc, doActions)
+
+ longest = -1, None
+ for loc1, expr1 in matches:
+ if loc1 <= longest[0]:
+ # already have a longer match than this one will deliver, we are done
+ return longest
+
+ try:
+ loc2, toks = expr1._parse(instring, loc, doActions)
+ except ParseException as err:
+ err.__traceback__ = None
+ if err.loc > maxExcLoc:
+ maxException = err
+ maxExcLoc = err.loc
+ else:
+ if loc2 >= loc1:
+ return loc2, toks
+ # didn't match as much as before
+ elif loc2 > longest[0]:
+ longest = loc2, toks
+
+ if longest != (-1, None):
+ return longest
+
+ if fatals:
+ if len(fatals) > 1:
+ fatals.sort(key=lambda e: -e.loc)
+ if fatals[0].loc == fatals[1].loc:
+ fatals.sort(key=lambda e: (-e.loc, -len(str(e.parserElement))))
+ max_fatal = fatals[0]
+ raise max_fatal
+
+ if maxException is not None:
+ maxException.msg = self.errmsg
+ raise maxException
+ else:
+ raise ParseException(
+ instring, loc, "no defined alternatives to match", self
+ )
+
+ def __ixor__(self, other):
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ return self.append(other) # Or([self, other])
+
+ def _generateDefaultName(self):
+ return "{" + " ^ ".join(str(e) for e in self.exprs) + "}"
+
+ def _setResultsName(self, name, listAllMatches=False):
+ if (
+ __diag__.warn_multiple_tokens_in_named_alternation
+ and Diagnostics.warn_multiple_tokens_in_named_alternation
+ not in self.suppress_warnings_
+ ):
+ if any(
+ isinstance(e, And)
+ and Diagnostics.warn_multiple_tokens_in_named_alternation
+ not in e.suppress_warnings_
+ for e in self.exprs
+ ):
+ warnings.warn(
+ "{}: setting results name {!r} on {} expression "
+ "will return a list of all parsed tokens in an And alternative, "
+ "in prior versions only the first token was returned; enclose "
+ "contained argument in Group".format(
+ "warn_multiple_tokens_in_named_alternation",
+ name,
+ type(self).__name__,
+ ),
+ stacklevel=3,
+ )
+
+ return super()._setResultsName(name, listAllMatches)
+
+
+class MatchFirst(ParseExpression):
+ """Requires that at least one :class:`ParseExpression` is found. If
+ more than one expression matches, the first one listed is the one that will
+ match. May be constructed using the ``'|'`` operator.
+
+ Example::
+
+ # construct MatchFirst using '|' operator
+
+ # watch the order of expressions to match
+ number = Word(nums) | Combine(Word(nums) + '.' + Word(nums))
+ print(number.search_string("123 3.1416 789")) # Fail! -> [['123'], ['3'], ['1416'], ['789']]
+
+ # put more selective expression first
+ number = Combine(Word(nums) + '.' + Word(nums)) | Word(nums)
+ print(number.search_string("123 3.1416 789")) # Better -> [['123'], ['3.1416'], ['789']]
+ """
+
+ def __init__(self, exprs: typing.Iterable[ParserElement], savelist: bool = False):
+ super().__init__(exprs, savelist)
+ if self.exprs:
+ self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)
+ self.skipWhitespace = all(e.skipWhitespace for e in self.exprs)
+ else:
+ self.mayReturnEmpty = True
+
+ def streamline(self) -> ParserElement:
+ if self.streamlined:
+ return self
+
+ super().streamline()
+ if self.exprs:
+ self.saveAsList = any(e.saveAsList for e in self.exprs)
+ self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)
+ self.skipWhitespace = all(
+ e.skipWhitespace and not isinstance(e, White) for e in self.exprs
+ )
+ else:
+ self.saveAsList = False
+ self.mayReturnEmpty = True
+ return self
+
+ def parseImpl(self, instring, loc, doActions=True):
+ maxExcLoc = -1
+ maxException = None
+
+ for e in self.exprs:
+ try:
+ return e._parse(
+ instring,
+ loc,
+ doActions,
+ )
+ except ParseFatalException as pfe:
+ pfe.__traceback__ = None
+ pfe.parserElement = e
+ raise
+ except ParseException as err:
+ if err.loc > maxExcLoc:
+ maxException = err
+ maxExcLoc = err.loc
+ except IndexError:
+ if len(instring) > maxExcLoc:
+ maxException = ParseException(
+ instring, len(instring), e.errmsg, self
+ )
+ maxExcLoc = len(instring)
+
+ if maxException is not None:
+ maxException.msg = self.errmsg
+ raise maxException
+ else:
+ raise ParseException(
+ instring, loc, "no defined alternatives to match", self
+ )
+
+ def __ior__(self, other):
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ return self.append(other) # MatchFirst([self, other])
+
+ def _generateDefaultName(self):
+ return "{" + " | ".join(str(e) for e in self.exprs) + "}"
+
+ def _setResultsName(self, name, listAllMatches=False):
+ if (
+ __diag__.warn_multiple_tokens_in_named_alternation
+ and Diagnostics.warn_multiple_tokens_in_named_alternation
+ not in self.suppress_warnings_
+ ):
+ if any(
+ isinstance(e, And)
+ and Diagnostics.warn_multiple_tokens_in_named_alternation
+ not in e.suppress_warnings_
+ for e in self.exprs
+ ):
+ warnings.warn(
+ "{}: setting results name {!r} on {} expression "
+ "will return a list of all parsed tokens in an And alternative, "
+ "in prior versions only the first token was returned; enclose "
+ "contained argument in Group".format(
+ "warn_multiple_tokens_in_named_alternation",
+ name,
+ type(self).__name__,
+ ),
+ stacklevel=3,
+ )
+
+ return super()._setResultsName(name, listAllMatches)
+
+
+class Each(ParseExpression):
+ """Requires all given :class:`ParseExpression` s to be found, but in
+ any order. Expressions may be separated by whitespace.
+
+ May be constructed using the ``'&'`` operator.
+
+ Example::
+
+ color = one_of("RED ORANGE YELLOW GREEN BLUE PURPLE BLACK WHITE BROWN")
+ shape_type = one_of("SQUARE CIRCLE TRIANGLE STAR HEXAGON OCTAGON")
+ integer = Word(nums)
+ shape_attr = "shape:" + shape_type("shape")
+ posn_attr = "posn:" + Group(integer("x") + ',' + integer("y"))("posn")
+ color_attr = "color:" + color("color")
+ size_attr = "size:" + integer("size")
+
+ # use Each (using operator '&') to accept attributes in any order
+ # (shape and posn are required, color and size are optional)
+ shape_spec = shape_attr & posn_attr & Opt(color_attr) & Opt(size_attr)
+
+ shape_spec.run_tests('''
+ shape: SQUARE color: BLACK posn: 100, 120
+ shape: CIRCLE size: 50 color: BLUE posn: 50,80
+ color:GREEN size:20 shape:TRIANGLE posn:20,40
+ '''
+ )
+
+ prints::
+
+ shape: SQUARE color: BLACK posn: 100, 120
+ ['shape:', 'SQUARE', 'color:', 'BLACK', 'posn:', ['100', ',', '120']]
+ - color: BLACK
+ - posn: ['100', ',', '120']
+ - x: 100
+ - y: 120
+ - shape: SQUARE
+
+
+ shape: CIRCLE size: 50 color: BLUE posn: 50,80
+ ['shape:', 'CIRCLE', 'size:', '50', 'color:', 'BLUE', 'posn:', ['50', ',', '80']]
+ - color: BLUE
+ - posn: ['50', ',', '80']
+ - x: 50
+ - y: 80
+ - shape: CIRCLE
+ - size: 50
+
+
+ color: GREEN size: 20 shape: TRIANGLE posn: 20,40
+ ['color:', 'GREEN', 'size:', '20', 'shape:', 'TRIANGLE', 'posn:', ['20', ',', '40']]
+ - color: GREEN
+ - posn: ['20', ',', '40']
+ - x: 20
+ - y: 40
+ - shape: TRIANGLE
+ - size: 20
+ """
+
+ def __init__(self, exprs: typing.Iterable[ParserElement], savelist: bool = True):
+ super().__init__(exprs, savelist)
+ if self.exprs:
+ self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
+ else:
+ self.mayReturnEmpty = True
+ self.skipWhitespace = True
+ self.initExprGroups = True
+ self.saveAsList = True
+
+ def streamline(self) -> ParserElement:
+ super().streamline()
+ if self.exprs:
+ self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
+ else:
+ self.mayReturnEmpty = True
+ return self
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if self.initExprGroups:
+ self.opt1map = dict(
+ (id(e.expr), e) for e in self.exprs if isinstance(e, Opt)
+ )
+ opt1 = [e.expr for e in self.exprs if isinstance(e, Opt)]
+ opt2 = [
+ e
+ for e in self.exprs
+ if e.mayReturnEmpty and not isinstance(e, (Opt, Regex, ZeroOrMore))
+ ]
+ self.optionals = opt1 + opt2
+ self.multioptionals = [
+ e.expr.set_results_name(e.resultsName, list_all_matches=True)
+ for e in self.exprs
+ if isinstance(e, _MultipleMatch)
+ ]
+ self.multirequired = [
+ e.expr.set_results_name(e.resultsName, list_all_matches=True)
+ for e in self.exprs
+ if isinstance(e, OneOrMore)
+ ]
+ self.required = [
+ e for e in self.exprs if not isinstance(e, (Opt, ZeroOrMore, OneOrMore))
+ ]
+ self.required += self.multirequired
+ self.initExprGroups = False
+
+ tmpLoc = loc
+ tmpReqd = self.required[:]
+ tmpOpt = self.optionals[:]
+ multis = self.multioptionals[:]
+ matchOrder = []
+
+ keepMatching = True
+ failed = []
+ fatals = []
+ while keepMatching:
+ tmpExprs = tmpReqd + tmpOpt + multis
+ failed.clear()
+ fatals.clear()
+ for e in tmpExprs:
+ try:
+ tmpLoc = e.try_parse(instring, tmpLoc, raise_fatal=True)
+ except ParseFatalException as pfe:
+ pfe.__traceback__ = None
+ pfe.parserElement = e
+ fatals.append(pfe)
+ failed.append(e)
+ except ParseException:
+ failed.append(e)
+ else:
+ matchOrder.append(self.opt1map.get(id(e), e))
+ if e in tmpReqd:
+ tmpReqd.remove(e)
+ elif e in tmpOpt:
+ tmpOpt.remove(e)
+ if len(failed) == len(tmpExprs):
+ keepMatching = False
+
+ # look for any ParseFatalExceptions
+ if fatals:
+ if len(fatals) > 1:
+ fatals.sort(key=lambda e: -e.loc)
+ if fatals[0].loc == fatals[1].loc:
+ fatals.sort(key=lambda e: (-e.loc, -len(str(e.parserElement))))
+ max_fatal = fatals[0]
+ raise max_fatal
+
+ if tmpReqd:
+ missing = ", ".join([str(e) for e in tmpReqd])
+ raise ParseException(
+ instring,
+ loc,
+ "Missing one or more required elements ({})".format(missing),
+ )
+
+ # add any unmatched Opts, in case they have default values defined
+ matchOrder += [e for e in self.exprs if isinstance(e, Opt) and e.expr in tmpOpt]
+
+ total_results = ParseResults([])
+ for e in matchOrder:
+ loc, results = e._parse(instring, loc, doActions)
+ total_results += results
+
+ return loc, total_results
+
+ def _generateDefaultName(self):
+ return "{" + " & ".join(str(e) for e in self.exprs) + "}"
+
+
+class ParseElementEnhance(ParserElement):
+ """Abstract subclass of :class:`ParserElement`, for combining and
+ post-processing parsed tokens.
+ """
+
+ def __init__(self, expr: Union[ParserElement, str], savelist: bool = False):
+ super().__init__(savelist)
+ if isinstance(expr, str_type):
+ if issubclass(self._literalStringClass, Token):
+ expr = self._literalStringClass(expr)
+ elif issubclass(type(self), self._literalStringClass):
+ expr = Literal(expr)
+ else:
+ expr = self._literalStringClass(Literal(expr))
+ self.expr = expr
+ if expr is not None:
+ self.mayIndexError = expr.mayIndexError
+ self.mayReturnEmpty = expr.mayReturnEmpty
+ self.set_whitespace_chars(
+ expr.whiteChars, copy_defaults=expr.copyDefaultWhiteChars
+ )
+ self.skipWhitespace = expr.skipWhitespace
+ self.saveAsList = expr.saveAsList
+ self.callPreparse = expr.callPreparse
+ self.ignoreExprs.extend(expr.ignoreExprs)
+
+ def recurse(self) -> Sequence[ParserElement]:
+ return [self.expr] if self.expr is not None else []
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if self.expr is not None:
+ return self.expr._parse(instring, loc, doActions, callPreParse=False)
+ else:
+ raise ParseException(instring, loc, "No expression defined", self)
+
+ def leave_whitespace(self, recursive: bool = True) -> ParserElement:
+ super().leave_whitespace(recursive)
+
+ if recursive:
+ self.expr = self.expr.copy()
+ if self.expr is not None:
+ self.expr.leave_whitespace(recursive)
+ return self
+
+ def ignore_whitespace(self, recursive: bool = True) -> ParserElement:
+ super().ignore_whitespace(recursive)
+
+ if recursive:
+ self.expr = self.expr.copy()
+ if self.expr is not None:
+ self.expr.ignore_whitespace(recursive)
+ return self
+
+ def ignore(self, other) -> ParserElement:
+ if isinstance(other, Suppress):
+ if other not in self.ignoreExprs:
+ super().ignore(other)
+ if self.expr is not None:
+ self.expr.ignore(self.ignoreExprs[-1])
+ else:
+ super().ignore(other)
+ if self.expr is not None:
+ self.expr.ignore(self.ignoreExprs[-1])
+ return self
+
+ def streamline(self) -> ParserElement:
+ super().streamline()
+ if self.expr is not None:
+ self.expr.streamline()
+ return self
+
+ def _checkRecursion(self, parseElementList):
+ if self in parseElementList:
+ raise RecursiveGrammarException(parseElementList + [self])
+ subRecCheckList = parseElementList[:] + [self]
+ if self.expr is not None:
+ self.expr._checkRecursion(subRecCheckList)
+
+ def validate(self, validateTrace=None) -> None:
+ if validateTrace is None:
+ validateTrace = []
+ tmp = validateTrace[:] + [self]
+ if self.expr is not None:
+ self.expr.validate(tmp)
+ self._checkRecursion([])
+
+ def _generateDefaultName(self):
+ return "{}:({})".format(self.__class__.__name__, str(self.expr))
+
+ ignoreWhitespace = ignore_whitespace
+ leaveWhitespace = leave_whitespace
+
+
+class IndentedBlock(ParseElementEnhance):
+ """
+ Expression to match one or more expressions at a given indentation level.
+ Useful for parsing text where structure is implied by indentation (like Python source code).
+ """
+
+ class _Indent(Empty):
+ def __init__(self, ref_col: int):
+ super().__init__()
+ self.errmsg = "expected indent at column {}".format(ref_col)
+ self.add_condition(lambda s, l, t: col(l, s) == ref_col)
+
+ class _IndentGreater(Empty):
+ def __init__(self, ref_col: int):
+ super().__init__()
+ self.errmsg = "expected indent at column greater than {}".format(ref_col)
+ self.add_condition(lambda s, l, t: col(l, s) > ref_col)
+
+ def __init__(
+ self, expr: ParserElement, *, recursive: bool = False, grouped: bool = True
+ ):
+ super().__init__(expr, savelist=True)
+ # if recursive:
+ # raise NotImplementedError("IndentedBlock with recursive is not implemented")
+ self._recursive = recursive
+ self._grouped = grouped
+ self.parent_anchor = 1
+
+ def parseImpl(self, instring, loc, doActions=True):
+ # advance parse position to non-whitespace by using an Empty()
+ # this should be the column to be used for all subsequent indented lines
+ anchor_loc = Empty().preParse(instring, loc)
+
+ # see if self.expr matches at the current location - if not it will raise an exception
+ # and no further work is necessary
+ self.expr.try_parse(instring, anchor_loc, doActions)
+
+ indent_col = col(anchor_loc, instring)
+ peer_detect_expr = self._Indent(indent_col)
+
+ inner_expr = Empty() + peer_detect_expr + self.expr
+ if self._recursive:
+ sub_indent = self._IndentGreater(indent_col)
+ nested_block = IndentedBlock(
+ self.expr, recursive=self._recursive, grouped=self._grouped
+ )
+ nested_block.set_debug(self.debug)
+ nested_block.parent_anchor = indent_col
+ inner_expr += Opt(sub_indent + nested_block)
+
+ inner_expr.set_name(f"inner {hex(id(inner_expr))[-4:].upper()}@{indent_col}")
+ block = OneOrMore(inner_expr)
+
+ trailing_undent = self._Indent(self.parent_anchor) | StringEnd()
+
+ if self._grouped:
+ wrapper = Group
+ else:
+ wrapper = lambda expr: expr
+ return (wrapper(block) + Optional(trailing_undent)).parseImpl(
+ instring, anchor_loc, doActions
+ )
+
+
+class AtStringStart(ParseElementEnhance):
+ """Matches if expression matches at the beginning of the parse
+ string::
+
+ AtStringStart(Word(nums)).parse_string("123")
+ # prints ["123"]
+
+ AtStringStart(Word(nums)).parse_string(" 123")
+ # raises ParseException
+ """
+
+ def __init__(self, expr: Union[ParserElement, str]):
+ super().__init__(expr)
+ self.callPreparse = False
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if loc != 0:
+ raise ParseException(instring, loc, "not found at string start")
+ return super().parseImpl(instring, loc, doActions)
+
+
+class AtLineStart(ParseElementEnhance):
+ r"""Matches if an expression matches at the beginning of a line within
+ the parse string
+
+ Example::
+
+ test = '''\
+ AAA this line
+ AAA and this line
+ AAA but not this one
+ B AAA and definitely not this one
+ '''
+
+ for t in (AtLineStart('AAA') + restOfLine).search_string(test):
+ print(t)
+
+ prints::
+
+ ['AAA', ' this line']
+ ['AAA', ' and this line']
+
+ """
+
+ def __init__(self, expr: Union[ParserElement, str]):
+ super().__init__(expr)
+ self.callPreparse = False
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if col(loc, instring) != 1:
+ raise ParseException(instring, loc, "not found at line start")
+ return super().parseImpl(instring, loc, doActions)
+
+
+class FollowedBy(ParseElementEnhance):
+ """Lookahead matching of the given parse expression.
+ ``FollowedBy`` does *not* advance the parsing position within
+ the input string, it only verifies that the specified parse
+ expression matches at the current position. ``FollowedBy``
+ always returns a null token list. If any results names are defined
+ in the lookahead expression, those *will* be returned for access by
+ name.
+
+ Example::
+
+ # use FollowedBy to match a label only if it is followed by a ':'
+ data_word = Word(alphas)
+ label = data_word + FollowedBy(':')
+ attr_expr = Group(label + Suppress(':') + OneOrMore(data_word, stop_on=label).set_parse_action(' '.join))
+
+ attr_expr[1, ...].parse_string("shape: SQUARE color: BLACK posn: upper left").pprint()
+
+ prints::
+
+ [['shape', 'SQUARE'], ['color', 'BLACK'], ['posn', 'upper left']]
+ """
+
+ def __init__(self, expr: Union[ParserElement, str]):
+ super().__init__(expr)
+ self.mayReturnEmpty = True
+
+ def parseImpl(self, instring, loc, doActions=True):
+ # by using self._expr.parse and deleting the contents of the returned ParseResults list
+ # we keep any named results that were defined in the FollowedBy expression
+ _, ret = self.expr._parse(instring, loc, doActions=doActions)
+ del ret[:]
+
+ return loc, ret
+
+
+class PrecededBy(ParseElementEnhance):
+ """Lookbehind matching of the given parse expression.
+ ``PrecededBy`` does not advance the parsing position within the
+ input string, it only verifies that the specified parse expression
+ matches prior to the current position. ``PrecededBy`` always
+ returns a null token list, but if a results name is defined on the
+ given expression, it is returned.
+
+ Parameters:
+
+ - expr - expression that must match prior to the current parse
+ location
+ - retreat - (default= ``None``) - (int) maximum number of characters
+ to lookbehind prior to the current parse location
+
+ If the lookbehind expression is a string, :class:`Literal`,
+ :class:`Keyword`, or a :class:`Word` or :class:`CharsNotIn`
+ with a specified exact or maximum length, then the retreat
+ parameter is not required. Otherwise, retreat must be specified to
+ give a maximum number of characters to look back from
+ the current parse position for a lookbehind match.
+
+ Example::
+
+ # VB-style variable names with type prefixes
+ int_var = PrecededBy("#") + pyparsing_common.identifier
+ str_var = PrecededBy("$") + pyparsing_common.identifier
+
+ """
+
+ def __init__(
+ self, expr: Union[ParserElement, str], retreat: typing.Optional[int] = None
+ ):
+ super().__init__(expr)
+ self.expr = self.expr().leave_whitespace()
+ self.mayReturnEmpty = True
+ self.mayIndexError = False
+ self.exact = False
+ if isinstance(expr, str_type):
+ retreat = len(expr)
+ self.exact = True
+ elif isinstance(expr, (Literal, Keyword)):
+ retreat = expr.matchLen
+ self.exact = True
+ elif isinstance(expr, (Word, CharsNotIn)) and expr.maxLen != _MAX_INT:
+ retreat = expr.maxLen
+ self.exact = True
+ elif isinstance(expr, PositionToken):
+ retreat = 0
+ self.exact = True
+ self.retreat = retreat
+ self.errmsg = "not preceded by " + str(expr)
+ self.skipWhitespace = False
+ self.parseAction.append(lambda s, l, t: t.__delitem__(slice(None, None)))
+
+ def parseImpl(self, instring, loc=0, doActions=True):
+ if self.exact:
+ if loc < self.retreat:
+ raise ParseException(instring, loc, self.errmsg)
+ start = loc - self.retreat
+ _, ret = self.expr._parse(instring, start)
+ else:
+ # retreat specified a maximum lookbehind window, iterate
+ test_expr = self.expr + StringEnd()
+ instring_slice = instring[max(0, loc - self.retreat) : loc]
+ last_expr = ParseException(instring, loc, self.errmsg)
+ for offset in range(1, min(loc, self.retreat + 1) + 1):
+ try:
+ # print('trying', offset, instring_slice, repr(instring_slice[loc - offset:]))
+ _, ret = test_expr._parse(
+ instring_slice, len(instring_slice) - offset
+ )
+ except ParseBaseException as pbe:
+ last_expr = pbe
+ else:
+ break
+ else:
+ raise last_expr
+ return loc, ret
+
+
+class Located(ParseElementEnhance):
+ """
+ Decorates a returned token with its starting and ending
+ locations in the input string.
+
+ This helper adds the following results names:
+
+ - ``locn_start`` - location where matched expression begins
+ - ``locn_end`` - location where matched expression ends
+ - ``value`` - the actual parsed results
+
+ Be careful if the input text contains ``<TAB>`` characters, you
+ may want to call :class:`ParserElement.parse_with_tabs`
+
+ Example::
+
+ wd = Word(alphas)
+ for match in Located(wd).search_string("ljsdf123lksdjjf123lkkjj1222"):
+ print(match)
+
+ prints::
+
+ [0, ['ljsdf'], 5]
+ [8, ['lksdjjf'], 15]
+ [18, ['lkkjj'], 23]
+
+ """
+
+ def parseImpl(self, instring, loc, doActions=True):
+ start = loc
+ loc, tokens = self.expr._parse(instring, start, doActions, callPreParse=False)
+ ret_tokens = ParseResults([start, tokens, loc])
+ ret_tokens["locn_start"] = start
+ ret_tokens["value"] = tokens
+ ret_tokens["locn_end"] = loc
+ if self.resultsName:
+ # must return as a list, so that the name will be attached to the complete group
+ return loc, [ret_tokens]
+ else:
+ return loc, ret_tokens
+
+
+class NotAny(ParseElementEnhance):
+ """
+ Lookahead to disallow matching with the given parse expression.
+ ``NotAny`` does *not* advance the parsing position within the
+ input string, it only verifies that the specified parse expression
+ does *not* match at the current position. Also, ``NotAny`` does
+ *not* skip over leading whitespace. ``NotAny`` always returns
+ a null token list. May be constructed using the ``'~'`` operator.
+
+ Example::
+
+ AND, OR, NOT = map(CaselessKeyword, "AND OR NOT".split())
+
+ # take care not to mistake keywords for identifiers
+ ident = ~(AND | OR | NOT) + Word(alphas)
+ boolean_term = Opt(NOT) + ident
+
+ # very crude boolean expression - to support parenthesis groups and
+ # operation hierarchy, use infix_notation
+ boolean_expr = boolean_term + ((AND | OR) + boolean_term)[...]
+
+ # integers that are followed by "." are actually floats
+ integer = Word(nums) + ~Char(".")
+ """
+
+ def __init__(self, expr: Union[ParserElement, str]):
+ super().__init__(expr)
+ # do NOT use self.leave_whitespace(), don't want to propagate to exprs
+ # self.leave_whitespace()
+ self.skipWhitespace = False
+
+ self.mayReturnEmpty = True
+ self.errmsg = "Found unwanted token, " + str(self.expr)
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if self.expr.can_parse_next(instring, loc):
+ raise ParseException(instring, loc, self.errmsg, self)
+ return loc, []
+
+ def _generateDefaultName(self):
+ return "~{" + str(self.expr) + "}"
+
+
+class _MultipleMatch(ParseElementEnhance):
+ def __init__(
+ self,
+ expr: ParserElement,
+ stop_on: typing.Optional[Union[ParserElement, str]] = None,
+ *,
+ stopOn: typing.Optional[Union[ParserElement, str]] = None,
+ ):
+ super().__init__(expr)
+ stopOn = stopOn or stop_on
+ self.saveAsList = True
+ ender = stopOn
+ if isinstance(ender, str_type):
+ ender = self._literalStringClass(ender)
+ self.stopOn(ender)
+
+ def stopOn(self, ender) -> ParserElement:
+ if isinstance(ender, str_type):
+ ender = self._literalStringClass(ender)
+ self.not_ender = ~ender if ender is not None else None
+ return self
+
+ def parseImpl(self, instring, loc, doActions=True):
+ self_expr_parse = self.expr._parse
+ self_skip_ignorables = self._skipIgnorables
+ check_ender = self.not_ender is not None
+ if check_ender:
+ try_not_ender = self.not_ender.tryParse
+
+ # must be at least one (but first see if we are the stopOn sentinel;
+ # if so, fail)
+ if check_ender:
+ try_not_ender(instring, loc)
+ loc, tokens = self_expr_parse(instring, loc, doActions)
+ try:
+ hasIgnoreExprs = not not self.ignoreExprs
+ while 1:
+ if check_ender:
+ try_not_ender(instring, loc)
+ if hasIgnoreExprs:
+ preloc = self_skip_ignorables(instring, loc)
+ else:
+ preloc = loc
+ loc, tmptokens = self_expr_parse(instring, preloc, doActions)
+ if tmptokens or tmptokens.haskeys():
+ tokens += tmptokens
+ except (ParseException, IndexError):
+ pass
+
+ return loc, tokens
+
+ def _setResultsName(self, name, listAllMatches=False):
+ if (
+ __diag__.warn_ungrouped_named_tokens_in_collection
+ and Diagnostics.warn_ungrouped_named_tokens_in_collection
+ not in self.suppress_warnings_
+ ):
+ for e in [self.expr] + self.expr.recurse():
+ if (
+ isinstance(e, ParserElement)
+ and e.resultsName
+ and Diagnostics.warn_ungrouped_named_tokens_in_collection
+ not in e.suppress_warnings_
+ ):
+ warnings.warn(
+ "{}: setting results name {!r} on {} expression "
+ "collides with {!r} on contained expression".format(
+ "warn_ungrouped_named_tokens_in_collection",
+ name,
+ type(self).__name__,
+ e.resultsName,
+ ),
+ stacklevel=3,
+ )
+
+ return super()._setResultsName(name, listAllMatches)
+
+
+class OneOrMore(_MultipleMatch):
+ """
+ Repetition of one or more of the given expression.
+
+ Parameters:
+ - expr - expression that must match one or more times
+ - stop_on - (default= ``None``) - expression for a terminating sentinel
+ (only required if the sentinel would ordinarily match the repetition
+ expression)
+
+ Example::
+
+ data_word = Word(alphas)
+ label = data_word + FollowedBy(':')
+ attr_expr = Group(label + Suppress(':') + OneOrMore(data_word).set_parse_action(' '.join))
+
+ text = "shape: SQUARE posn: upper left color: BLACK"
+ attr_expr[1, ...].parse_string(text).pprint() # Fail! read 'color' as data instead of next label -> [['shape', 'SQUARE color']]
+
+ # use stop_on attribute for OneOrMore to avoid reading label string as part of the data
+ attr_expr = Group(label + Suppress(':') + OneOrMore(data_word, stop_on=label).set_parse_action(' '.join))
+ OneOrMore(attr_expr).parse_string(text).pprint() # Better -> [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'BLACK']]
+
+ # could also be written as
+ (attr_expr * (1,)).parse_string(text).pprint()
+ """
+
+ def _generateDefaultName(self):
+ return "{" + str(self.expr) + "}..."
+
+
+class ZeroOrMore(_MultipleMatch):
+ """
+ Optional repetition of zero or more of the given expression.
+
+ Parameters:
+ - ``expr`` - expression that must match zero or more times
+ - ``stop_on`` - expression for a terminating sentinel
+ (only required if the sentinel would ordinarily match the repetition
+ expression) - (default= ``None``)
+
+ Example: similar to :class:`OneOrMore`
+ """
+
+ def __init__(
+ self,
+ expr: ParserElement,
+ stop_on: typing.Optional[Union[ParserElement, str]] = None,
+ *,
+ stopOn: typing.Optional[Union[ParserElement, str]] = None,
+ ):
+ super().__init__(expr, stopOn=stopOn or stop_on)
+ self.mayReturnEmpty = True
+
+ def parseImpl(self, instring, loc, doActions=True):
+ try:
+ return super().parseImpl(instring, loc, doActions)
+ except (ParseException, IndexError):
+ return loc, ParseResults([], name=self.resultsName)
+
+ def _generateDefaultName(self):
+ return "[" + str(self.expr) + "]..."
+
+
+class _NullToken:
+ def __bool__(self):
+ return False
+
+ def __str__(self):
+ return ""
+
+
+class Opt(ParseElementEnhance):
+ """
+ Optional matching of the given expression.
+
+ Parameters:
+ - ``expr`` - expression that must match zero or more times
+ - ``default`` (optional) - value to be returned if the optional expression is not found.
+
+ Example::
+
+ # US postal code can be a 5-digit zip, plus optional 4-digit qualifier
+ zip = Combine(Word(nums, exact=5) + Opt('-' + Word(nums, exact=4)))
+ zip.run_tests('''
+ # traditional ZIP code
+ 12345
+
+ # ZIP+4 form
+ 12101-0001
+
+ # invalid ZIP
+ 98765-
+ ''')
+
+ prints::
+
+ # traditional ZIP code
+ 12345
+ ['12345']
+
+ # ZIP+4 form
+ 12101-0001
+ ['12101-0001']
+
+ # invalid ZIP
+ 98765-
+ ^
+ FAIL: Expected end of text (at char 5), (line:1, col:6)
+ """
+
+ __optionalNotMatched = _NullToken()
+
+ def __init__(
+ self, expr: Union[ParserElement, str], default: Any = __optionalNotMatched
+ ):
+ super().__init__(expr, savelist=False)
+ self.saveAsList = self.expr.saveAsList
+ self.defaultValue = default
+ self.mayReturnEmpty = True
+
+ def parseImpl(self, instring, loc, doActions=True):
+ self_expr = self.expr
+ try:
+ loc, tokens = self_expr._parse(instring, loc, doActions, callPreParse=False)
+ except (ParseException, IndexError):
+ default_value = self.defaultValue
+ if default_value is not self.__optionalNotMatched:
+ if self_expr.resultsName:
+ tokens = ParseResults([default_value])
+ tokens[self_expr.resultsName] = default_value
+ else:
+ tokens = [default_value]
+ else:
+ tokens = []
+ return loc, tokens
+
+ def _generateDefaultName(self):
+ inner = str(self.expr)
+ # strip off redundant inner {}'s
+ while len(inner) > 1 and inner[0 :: len(inner) - 1] == "{}":
+ inner = inner[1:-1]
+ return "[" + inner + "]"
+
+
+Optional = Opt
+
+
+class SkipTo(ParseElementEnhance):
+ """
+ Token for skipping over all undefined text until the matched
+ expression is found.
+
+ Parameters:
+ - ``expr`` - target expression marking the end of the data to be skipped
+ - ``include`` - if ``True``, the target expression is also parsed
+ (the skipped text and target expression are returned as a 2-element
+ list) (default= ``False``).
+ - ``ignore`` - (default= ``None``) used to define grammars (typically quoted strings and
+ comments) that might contain false matches to the target expression
+ - ``fail_on`` - (default= ``None``) define expressions that are not allowed to be
+ included in the skipped test; if found before the target expression is found,
+ the :class:`SkipTo` is not a match
+
+ Example::
+
+ report = '''
+ Outstanding Issues Report - 1 Jan 2000
+
+ # | Severity | Description | Days Open
+ -----+----------+-------------------------------------------+-----------
+ 101 | Critical | Intermittent system crash | 6
+ 94 | Cosmetic | Spelling error on Login ('log|n') | 14
+ 79 | Minor | System slow when running too many reports | 47
+ '''
+ integer = Word(nums)
+ SEP = Suppress('|')
+ # use SkipTo to simply match everything up until the next SEP
+ # - ignore quoted strings, so that a '|' character inside a quoted string does not match
+ # - parse action will call token.strip() for each matched token, i.e., the description body
+ string_data = SkipTo(SEP, ignore=quoted_string)
+ string_data.set_parse_action(token_map(str.strip))
+ ticket_expr = (integer("issue_num") + SEP
+ + string_data("sev") + SEP
+ + string_data("desc") + SEP
+ + integer("days_open"))
+
+ for tkt in ticket_expr.search_string(report):
+ print tkt.dump()
+
+ prints::
+
+ ['101', 'Critical', 'Intermittent system crash', '6']
+ - days_open: '6'
+ - desc: 'Intermittent system crash'
+ - issue_num: '101'
+ - sev: 'Critical'
+ ['94', 'Cosmetic', "Spelling error on Login ('log|n')", '14']
+ - days_open: '14'
+ - desc: "Spelling error on Login ('log|n')"
+ - issue_num: '94'
+ - sev: 'Cosmetic'
+ ['79', 'Minor', 'System slow when running too many reports', '47']
+ - days_open: '47'
+ - desc: 'System slow when running too many reports'
+ - issue_num: '79'
+ - sev: 'Minor'
+ """
+
+ def __init__(
+ self,
+ other: Union[ParserElement, str],
+ include: bool = False,
+ ignore: bool = None,
+ fail_on: typing.Optional[Union[ParserElement, str]] = None,
+ *,
+ failOn: Union[ParserElement, str] = None,
+ ):
+ super().__init__(other)
+ failOn = failOn or fail_on
+ self.ignoreExpr = ignore
+ self.mayReturnEmpty = True
+ self.mayIndexError = False
+ self.includeMatch = include
+ self.saveAsList = False
+ if isinstance(failOn, str_type):
+ self.failOn = self._literalStringClass(failOn)
+ else:
+ self.failOn = failOn
+ self.errmsg = "No match found for " + str(self.expr)
+
+ def parseImpl(self, instring, loc, doActions=True):
+ startloc = loc
+ instrlen = len(instring)
+ self_expr_parse = self.expr._parse
+ self_failOn_canParseNext = (
+ self.failOn.canParseNext if self.failOn is not None else None
+ )
+ self_ignoreExpr_tryParse = (
+ self.ignoreExpr.tryParse if self.ignoreExpr is not None else None
+ )
+
+ tmploc = loc
+ while tmploc <= instrlen:
+ if self_failOn_canParseNext is not None:
+ # break if failOn expression matches
+ if self_failOn_canParseNext(instring, tmploc):
+ break
+
+ if self_ignoreExpr_tryParse is not None:
+ # advance past ignore expressions
+ while 1:
+ try:
+ tmploc = self_ignoreExpr_tryParse(instring, tmploc)
+ except ParseBaseException:
+ break
+
+ try:
+ self_expr_parse(instring, tmploc, doActions=False, callPreParse=False)
+ except (ParseException, IndexError):
+ # no match, advance loc in string
+ tmploc += 1
+ else:
+ # matched skipto expr, done
+ break
+
+ else:
+ # ran off the end of the input string without matching skipto expr, fail
+ raise ParseException(instring, loc, self.errmsg, self)
+
+ # build up return values
+ loc = tmploc
+ skiptext = instring[startloc:loc]
+ skipresult = ParseResults(skiptext)
+
+ if self.includeMatch:
+ loc, mat = self_expr_parse(instring, loc, doActions, callPreParse=False)
+ skipresult += mat
+
+ return loc, skipresult
+
+
+class Forward(ParseElementEnhance):
+ """
+ Forward declaration of an expression to be defined later -
+ used for recursive grammars, such as algebraic infix notation.
+ When the expression is known, it is assigned to the ``Forward``
+ variable using the ``'<<'`` operator.
+
+ Note: take care when assigning to ``Forward`` not to overlook
+ precedence of operators.
+
+ Specifically, ``'|'`` has a lower precedence than ``'<<'``, so that::
+
+ fwd_expr << a | b | c
+
+ will actually be evaluated as::
+
+ (fwd_expr << a) | b | c
+
+ thereby leaving b and c out as parseable alternatives. It is recommended that you
+ explicitly group the values inserted into the ``Forward``::
+
+ fwd_expr << (a | b | c)
+
+ Converting to use the ``'<<='`` operator instead will avoid this problem.
+
+ See :class:`ParseResults.pprint` for an example of a recursive
+ parser created using ``Forward``.
+ """
+
+ def __init__(self, other: typing.Optional[Union[ParserElement, str]] = None):
+ self.caller_frame = traceback.extract_stack(limit=2)[0]
+ super().__init__(other, savelist=False)
+ self.lshift_line = None
+
+ def __lshift__(self, other):
+ if hasattr(self, "caller_frame"):
+ del self.caller_frame
+ if isinstance(other, str_type):
+ other = self._literalStringClass(other)
+ self.expr = other
+ self.mayIndexError = self.expr.mayIndexError
+ self.mayReturnEmpty = self.expr.mayReturnEmpty
+ self.set_whitespace_chars(
+ self.expr.whiteChars, copy_defaults=self.expr.copyDefaultWhiteChars
+ )
+ self.skipWhitespace = self.expr.skipWhitespace
+ self.saveAsList = self.expr.saveAsList
+ self.ignoreExprs.extend(self.expr.ignoreExprs)
+ self.lshift_line = traceback.extract_stack(limit=2)[-2]
+ return self
+
+ def __ilshift__(self, other):
+ return self << other
+
+ def __or__(self, other):
+ caller_line = traceback.extract_stack(limit=2)[-2]
+ if (
+ __diag__.warn_on_match_first_with_lshift_operator
+ and caller_line == self.lshift_line
+ and Diagnostics.warn_on_match_first_with_lshift_operator
+ not in self.suppress_warnings_
+ ):
+ warnings.warn(
+ "using '<<' operator with '|' is probably an error, use '<<='",
+ stacklevel=2,
+ )
+ ret = super().__or__(other)
+ return ret
+
+ def __del__(self):
+ # see if we are getting dropped because of '=' reassignment of var instead of '<<=' or '<<'
+ if (
+ self.expr is None
+ and __diag__.warn_on_assignment_to_Forward
+ and Diagnostics.warn_on_assignment_to_Forward not in self.suppress_warnings_
+ ):
+ warnings.warn_explicit(
+ "Forward defined here but no expression attached later using '<<=' or '<<'",
+ UserWarning,
+ filename=self.caller_frame.filename,
+ lineno=self.caller_frame.lineno,
+ )
+
+ def parseImpl(self, instring, loc, doActions=True):
+ if (
+ self.expr is None
+ and __diag__.warn_on_parse_using_empty_Forward
+ and Diagnostics.warn_on_parse_using_empty_Forward
+ not in self.suppress_warnings_
+ ):
+ # walk stack until parse_string, scan_string, search_string, or transform_string is found
+ parse_fns = [
+ "parse_string",
+ "scan_string",
+ "search_string",
+ "transform_string",
+ ]
+ tb = traceback.extract_stack(limit=200)
+ for i, frm in enumerate(reversed(tb), start=1):
+ if frm.name in parse_fns:
+ stacklevel = i + 1
+ break
+ else:
+ stacklevel = 2
+ warnings.warn(
+ "Forward expression was never assigned a value, will not parse any input",
+ stacklevel=stacklevel,
+ )
+ if not ParserElement._left_recursion_enabled:
+ return super().parseImpl(instring, loc, doActions)
+ # ## Bounded Recursion algorithm ##
+ # Recursion only needs to be processed at ``Forward`` elements, since they are
+ # the only ones that can actually refer to themselves. The general idea is
+ # to handle recursion stepwise: We start at no recursion, then recurse once,
+ # recurse twice, ..., until more recursion offers no benefit (we hit the bound).
+ #
+ # The "trick" here is that each ``Forward`` gets evaluated in two contexts
+ # - to *match* a specific recursion level, and
+ # - to *search* the bounded recursion level
+ # and the two run concurrently. The *search* must *match* each recursion level
+ # to find the best possible match. This is handled by a memo table, which
+ # provides the previous match to the next level match attempt.
+ #
+ # See also "Left Recursion in Parsing Expression Grammars", Medeiros et al.
+ #
+ # There is a complication since we not only *parse* but also *transform* via
+ # actions: We do not want to run the actions too often while expanding. Thus,
+ # we expand using `doActions=False` and only run `doActions=True` if the next
+ # recursion level is acceptable.
+ with ParserElement.recursion_lock:
+ memo = ParserElement.recursion_memos
+ try:
+ # we are parsing at a specific recursion expansion - use it as-is
+ prev_loc, prev_result = memo[loc, self, doActions]
+ if isinstance(prev_result, Exception):
+ raise prev_result
+ return prev_loc, prev_result.copy()
+ except KeyError:
+ act_key = (loc, self, True)
+ peek_key = (loc, self, False)
+ # we are searching for the best recursion expansion - keep on improving
+ # both `doActions` cases must be tracked separately here!
+ prev_loc, prev_peek = memo[peek_key] = (
+ loc - 1,
+ ParseException(
+ instring, loc, "Forward recursion without base case", self
+ ),
+ )
+ if doActions:
+ memo[act_key] = memo[peek_key]
+ while True:
+ try:
+ new_loc, new_peek = super().parseImpl(instring, loc, False)
+ except ParseException:
+ # we failed before getting any match – do not hide the error
+ if isinstance(prev_peek, Exception):
+ raise
+ new_loc, new_peek = prev_loc, prev_peek
+ # the match did not get better: we are done
+ if new_loc <= prev_loc:
+ if doActions:
+ # replace the match for doActions=False as well,
+ # in case the action did backtrack
+ prev_loc, prev_result = memo[peek_key] = memo[act_key]
+ del memo[peek_key], memo[act_key]
+ return prev_loc, prev_result.copy()
+ del memo[peek_key]
+ return prev_loc, prev_peek.copy()
+ # the match did get better: see if we can improve further
+ else:
+ if doActions:
+ try:
+ memo[act_key] = super().parseImpl(instring, loc, True)
+ except ParseException as e:
+ memo[peek_key] = memo[act_key] = (new_loc, e)
+ raise
+ prev_loc, prev_peek = memo[peek_key] = new_loc, new_peek
+
+ def leave_whitespace(self, recursive: bool = True) -> ParserElement:
+ self.skipWhitespace = False
+ return self
+
+ def ignore_whitespace(self, recursive: bool = True) -> ParserElement:
+ self.skipWhitespace = True
+ return self
+
+ def streamline(self) -> ParserElement:
+ if not self.streamlined:
+ self.streamlined = True
+ if self.expr is not None:
+ self.expr.streamline()
+ return self
+
+ def validate(self, validateTrace=None) -> None:
+ if validateTrace is None:
+ validateTrace = []
+
+ if self not in validateTrace:
+ tmp = validateTrace[:] + [self]
+ if self.expr is not None:
+ self.expr.validate(tmp)
+ self._checkRecursion([])
+
+ def _generateDefaultName(self):
+ # Avoid infinite recursion by setting a temporary _defaultName
+ self._defaultName = ": ..."
+
+ # Use the string representation of main expression.
+ retString = "..."
+ try:
+ if self.expr is not None:
+ retString = str(self.expr)[:1000]
+ else:
+ retString = "None"
+ finally:
+ return self.__class__.__name__ + ": " + retString
+
+ def copy(self) -> ParserElement:
+ if self.expr is not None:
+ return super().copy()
+ else:
+ ret = Forward()
+ ret <<= self
+ return ret
+
+ def _setResultsName(self, name, list_all_matches=False):
+ if (
+ __diag__.warn_name_set_on_empty_Forward
+ and Diagnostics.warn_name_set_on_empty_Forward
+ not in self.suppress_warnings_
+ ):
+ if self.expr is None:
+ warnings.warn(
+ "{}: setting results name {!r} on {} expression "
+ "that has no contained expression".format(
+ "warn_name_set_on_empty_Forward", name, type(self).__name__
+ ),
+ stacklevel=3,
+ )
+
+ return super()._setResultsName(name, list_all_matches)
+
+ ignoreWhitespace = ignore_whitespace
+ leaveWhitespace = leave_whitespace
+
+
+class TokenConverter(ParseElementEnhance):
+ """
+ Abstract subclass of :class:`ParseExpression`, for converting parsed results.
+ """
+
+ def __init__(self, expr: Union[ParserElement, str], savelist=False):
+ super().__init__(expr) # , savelist)
+ self.saveAsList = False
+
+
+class Combine(TokenConverter):
+ """Converter to concatenate all matching tokens to a single string.
+ By default, the matching patterns must also be contiguous in the
+ input string; this can be disabled by specifying
+ ``'adjacent=False'`` in the constructor.
+
+ Example::
+
+ real = Word(nums) + '.' + Word(nums)
+ print(real.parse_string('3.1416')) # -> ['3', '.', '1416']
+ # will also erroneously match the following
+ print(real.parse_string('3. 1416')) # -> ['3', '.', '1416']
+
+ real = Combine(Word(nums) + '.' + Word(nums))
+ print(real.parse_string('3.1416')) # -> ['3.1416']
+ # no match when there are internal spaces
+ print(real.parse_string('3. 1416')) # -> Exception: Expected W:(0123...)
+ """
+
+ def __init__(
+ self,
+ expr: ParserElement,
+ join_string: str = "",
+ adjacent: bool = True,
+ *,
+ joinString: typing.Optional[str] = None,
+ ):
+ super().__init__(expr)
+ joinString = joinString if joinString is not None else join_string
+ # suppress whitespace-stripping in contained parse expressions, but re-enable it on the Combine itself
+ if adjacent:
+ self.leave_whitespace()
+ self.adjacent = adjacent
+ self.skipWhitespace = True
+ self.joinString = joinString
+ self.callPreparse = True
+
+ def ignore(self, other) -> ParserElement:
+ if self.adjacent:
+ ParserElement.ignore(self, other)
+ else:
+ super().ignore(other)
+ return self
+
+ def postParse(self, instring, loc, tokenlist):
+ retToks = tokenlist.copy()
+ del retToks[:]
+ retToks += ParseResults(
+ ["".join(tokenlist._asStringList(self.joinString))], modal=self.modalResults
+ )
+
+ if self.resultsName and retToks.haskeys():
+ return [retToks]
+ else:
+ return retToks
+
+
+class Group(TokenConverter):
+ """Converter to return the matched tokens as a list - useful for
+ returning tokens of :class:`ZeroOrMore` and :class:`OneOrMore` expressions.
+
+ The optional ``aslist`` argument when set to True will return the
+ parsed tokens as a Python list instead of a pyparsing ParseResults.
+
+ Example::
+
+ ident = Word(alphas)
+ num = Word(nums)
+ term = ident | num
+ func = ident + Opt(delimited_list(term))
+ print(func.parse_string("fn a, b, 100"))
+ # -> ['fn', 'a', 'b', '100']
+
+ func = ident + Group(Opt(delimited_list(term)))
+ print(func.parse_string("fn a, b, 100"))
+ # -> ['fn', ['a', 'b', '100']]
+ """
+
+ def __init__(self, expr: ParserElement, aslist: bool = False):
+ super().__init__(expr)
+ self.saveAsList = True
+ self._asPythonList = aslist
+
+ def postParse(self, instring, loc, tokenlist):
+ if self._asPythonList:
+ return ParseResults.List(
+ tokenlist.asList()
+ if isinstance(tokenlist, ParseResults)
+ else list(tokenlist)
+ )
+ else:
+ return [tokenlist]
+
+
+class Dict(TokenConverter):
+ """Converter to return a repetitive expression as a list, but also
+ as a dictionary. Each element can also be referenced using the first
+ token in the expression as its key. Useful for tabular report
+ scraping when the first column can be used as a item key.
+
+ The optional ``asdict`` argument when set to True will return the
+ parsed tokens as a Python dict instead of a pyparsing ParseResults.
+
+ Example::
+
+ data_word = Word(alphas)
+ label = data_word + FollowedBy(':')
+
+ text = "shape: SQUARE posn: upper left color: light blue texture: burlap"
+ attr_expr = (label + Suppress(':') + OneOrMore(data_word, stop_on=label).set_parse_action(' '.join))
+
+ # print attributes as plain groups
+ print(attr_expr[1, ...].parse_string(text).dump())
+
+ # instead of OneOrMore(expr), parse using Dict(Group(expr)[1, ...]) - Dict will auto-assign names
+ result = Dict(Group(attr_expr)[1, ...]).parse_string(text)
+ print(result.dump())
+
+ # access named fields as dict entries, or output as dict
+ print(result['shape'])
+ print(result.as_dict())
+
+ prints::
+
+ ['shape', 'SQUARE', 'posn', 'upper left', 'color', 'light blue', 'texture', 'burlap']
+ [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]
+ - color: 'light blue'
+ - posn: 'upper left'
+ - shape: 'SQUARE'
+ - texture: 'burlap'
+ SQUARE
+ {'color': 'light blue', 'posn': 'upper left', 'texture': 'burlap', 'shape': 'SQUARE'}
+
+ See more examples at :class:`ParseResults` of accessing fields by results name.
+ """
+
+ def __init__(self, expr: ParserElement, asdict: bool = False):
+ super().__init__(expr)
+ self.saveAsList = True
+ self._asPythonDict = asdict
+
+ def postParse(self, instring, loc, tokenlist):
+ for i, tok in enumerate(tokenlist):
+ if len(tok) == 0:
+ continue
+
+ ikey = tok[0]
+ if isinstance(ikey, int):
+ ikey = str(ikey).strip()
+
+ if len(tok) == 1:
+ tokenlist[ikey] = _ParseResultsWithOffset("", i)
+
+ elif len(tok) == 2 and not isinstance(tok[1], ParseResults):
+ tokenlist[ikey] = _ParseResultsWithOffset(tok[1], i)
+
+ else:
+ try:
+ dictvalue = tok.copy() # ParseResults(i)
+ except Exception:
+ exc = TypeError(
+ "could not extract dict values from parsed results"
+ " - Dict expression must contain Grouped expressions"
+ )
+ raise exc from None
+
+ del dictvalue[0]
+
+ if len(dictvalue) != 1 or (
+ isinstance(dictvalue, ParseResults) and dictvalue.haskeys()
+ ):
+ tokenlist[ikey] = _ParseResultsWithOffset(dictvalue, i)
+ else:
+ tokenlist[ikey] = _ParseResultsWithOffset(dictvalue[0], i)
+
+ if self._asPythonDict:
+ return [tokenlist.as_dict()] if self.resultsName else tokenlist.as_dict()
+ else:
+ return [tokenlist] if self.resultsName else tokenlist
+
+
+class Suppress(TokenConverter):
+ """Converter for ignoring the results of a parsed expression.
+
+ Example::
+
+ source = "a, b, c,d"
+ wd = Word(alphas)
+ wd_list1 = wd + (',' + wd)[...]
+ print(wd_list1.parse_string(source))
+
+ # often, delimiters that are useful during parsing are just in the
+ # way afterward - use Suppress to keep them out of the parsed output
+ wd_list2 = wd + (Suppress(',') + wd)[...]
+ print(wd_list2.parse_string(source))
+
+ # Skipped text (using '...') can be suppressed as well
+ source = "lead in START relevant text END trailing text"
+ start_marker = Keyword("START")
+ end_marker = Keyword("END")
+ find_body = Suppress(...) + start_marker + ... + end_marker
+ print(find_body.parse_string(source)
+
+ prints::
+
+ ['a', ',', 'b', ',', 'c', ',', 'd']
+ ['a', 'b', 'c', 'd']
+ ['START', 'relevant text ', 'END']
+
+ (See also :class:`delimited_list`.)
+ """
+
+ def __init__(self, expr: Union[ParserElement, str], savelist: bool = False):
+ if expr is ...:
+ expr = _PendingSkip(NoMatch())
+ super().__init__(expr)
+
+ def __add__(self, other) -> "ParserElement":
+ if isinstance(self.expr, _PendingSkip):
+ return Suppress(SkipTo(other)) + other
+ else:
+ return super().__add__(other)
+
+ def __sub__(self, other) -> "ParserElement":
+ if isinstance(self.expr, _PendingSkip):
+ return Suppress(SkipTo(other)) - other
+ else:
+ return super().__sub__(other)
+
+ def postParse(self, instring, loc, tokenlist):
+ return []
+
+ def suppress(self) -> ParserElement:
+ return self
+
+
+def trace_parse_action(f: ParseAction) -> ParseAction:
+ """Decorator for debugging parse actions.
+
+ When the parse action is called, this decorator will print
+ ``">> entering method-name(line:<current_source_line>, <parse_location>, <matched_tokens>)"``.
+ When the parse action completes, the decorator will print
+ ``"<<"`` followed by the returned value, or any exception that the parse action raised.
+
+ Example::
+
+ wd = Word(alphas)
+
+ @trace_parse_action
+ def remove_duplicate_chars(tokens):
+ return ''.join(sorted(set(''.join(tokens))))
+
+ wds = wd[1, ...].set_parse_action(remove_duplicate_chars)
+ print(wds.parse_string("slkdjs sld sldd sdlf sdljf"))
+
+ prints::
+
+ >>entering remove_duplicate_chars(line: 'slkdjs sld sldd sdlf sdljf', 0, (['slkdjs', 'sld', 'sldd', 'sdlf', 'sdljf'], {}))
+ <<leaving remove_duplicate_chars (ret: 'dfjkls')
+ ['dfjkls']
+ """
+ f = _trim_arity(f)
+
+ def z(*paArgs):
+ thisFunc = f.__name__
+ s, l, t = paArgs[-3:]
+ if len(paArgs) > 3:
+ thisFunc = paArgs[0].__class__.__name__ + "." + thisFunc
+ sys.stderr.write(
+ ">>entering {}(line: {!r}, {}, {!r})\n".format(thisFunc, line(l, s), l, t)
+ )
+ try:
+ ret = f(*paArgs)
+ except Exception as exc:
+ sys.stderr.write("<<leaving {} (exception: {})\n".format(thisFunc, exc))
+ raise
+ sys.stderr.write("<<leaving {} (ret: {!r})\n".format(thisFunc, ret))
+ return ret
+
+ z.__name__ = f.__name__
+ return z
+
+
+# convenience constants for positional expressions
+empty = Empty().set_name("empty")
+line_start = LineStart().set_name("line_start")
+line_end = LineEnd().set_name("line_end")
+string_start = StringStart().set_name("string_start")
+string_end = StringEnd().set_name("string_end")
+
+_escapedPunc = Word(_bslash, r"\[]-*.$+^?()~ ", exact=2).set_parse_action(
+ lambda s, l, t: t[0][1]
+)
+_escapedHexChar = Regex(r"\\0?[xX][0-9a-fA-F]+").set_parse_action(
+ lambda s, l, t: chr(int(t[0].lstrip(r"\0x"), 16))
+)
+_escapedOctChar = Regex(r"\\0[0-7]+").set_parse_action(
+ lambda s, l, t: chr(int(t[0][1:], 8))
+)
+_singleChar = (
+ _escapedPunc | _escapedHexChar | _escapedOctChar | CharsNotIn(r"\]", exact=1)
+)
+_charRange = Group(_singleChar + Suppress("-") + _singleChar)
+_reBracketExpr = (
+ Literal("[")
+ + Opt("^").set_results_name("negate")
+ + Group(OneOrMore(_charRange | _singleChar)).set_results_name("body")
+ + "]"
+)
+
+
+def srange(s: str) -> str:
+ r"""Helper to easily define string ranges for use in :class:`Word`
+ construction. Borrows syntax from regexp ``'[]'`` string range
+ definitions::
+
+ srange("[0-9]") -> "0123456789"
+ srange("[a-z]") -> "abcdefghijklmnopqrstuvwxyz"
+ srange("[a-z$_]") -> "abcdefghijklmnopqrstuvwxyz$_"
+
+ The input string must be enclosed in []'s, and the returned string
+ is the expanded character set joined into a single string. The
+ values enclosed in the []'s may be:
+
+ - a single character
+ - an escaped character with a leading backslash (such as ``\-``
+ or ``\]``)
+ - an escaped hex character with a leading ``'\x'``
+ (``\x21``, which is a ``'!'`` character) (``\0x##``
+ is also supported for backwards compatibility)
+ - an escaped octal character with a leading ``'\0'``
+ (``\041``, which is a ``'!'`` character)
+ - a range of any of the above, separated by a dash (``'a-z'``,
+ etc.)
+ - any combination of the above (``'aeiouy'``,
+ ``'a-zA-Z0-9_$'``, etc.)
+ """
+ _expanded = (
+ lambda p: p
+ if not isinstance(p, ParseResults)
+ else "".join(chr(c) for c in range(ord(p[0]), ord(p[1]) + 1))
+ )
+ try:
+ return "".join(_expanded(part) for part in _reBracketExpr.parse_string(s).body)
+ except Exception:
+ return ""
+
+
+def token_map(func, *args) -> ParseAction:
+ """Helper to define a parse action by mapping a function to all
+ elements of a :class:`ParseResults` list. If any additional args are passed,
+ they are forwarded to the given function as additional arguments
+ after the token, as in
+ ``hex_integer = Word(hexnums).set_parse_action(token_map(int, 16))``,
+ which will convert the parsed data to an integer using base 16.
+
+ Example (compare the last to example in :class:`ParserElement.transform_string`::
+
+ hex_ints = Word(hexnums)[1, ...].set_parse_action(token_map(int, 16))
+ hex_ints.run_tests('''
+ 00 11 22 aa FF 0a 0d 1a
+ ''')
+
+ upperword = Word(alphas).set_parse_action(token_map(str.upper))
+ upperword[1, ...].run_tests('''
+ my kingdom for a horse
+ ''')
+
+ wd = Word(alphas).set_parse_action(token_map(str.title))
+ wd[1, ...].set_parse_action(' '.join).run_tests('''
+ now is the winter of our discontent made glorious summer by this sun of york
+ ''')
+
+ prints::
+
+ 00 11 22 aa FF 0a 0d 1a
+ [0, 17, 34, 170, 255, 10, 13, 26]
+
+ my kingdom for a horse
+ ['MY', 'KINGDOM', 'FOR', 'A', 'HORSE']
+
+ now is the winter of our discontent made glorious summer by this sun of york
+ ['Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York']
+ """
+
+ def pa(s, l, t):
+ return [func(tokn, *args) for tokn in t]
+
+ func_name = getattr(func, "__name__", getattr(func, "__class__").__name__)
+ pa.__name__ = func_name
+
+ return pa
+
+
+def autoname_elements() -> None:
+ """
+ Utility to simplify mass-naming of parser elements, for
+ generating railroad diagram with named subdiagrams.
+ """
+ for name, var in sys._getframe().f_back.f_locals.items():
+ if isinstance(var, ParserElement) and not var.customName:
+ var.set_name(name)
+
+
+dbl_quoted_string = Combine(
+ Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*') + '"'
+).set_name("string enclosed in double quotes")
+
+sgl_quoted_string = Combine(
+ Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*") + "'"
+).set_name("string enclosed in single quotes")
+
+quoted_string = Combine(
+ Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*') + '"'
+ | Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*") + "'"
+).set_name("quotedString using single or double quotes")
+
+unicode_string = Combine("u" + quoted_string.copy()).set_name("unicode string literal")
+
+
+alphas8bit = srange(r"[\0xc0-\0xd6\0xd8-\0xf6\0xf8-\0xff]")
+punc8bit = srange(r"[\0xa1-\0xbf\0xd7\0xf7]")
+
+# build list of built-in expressions, for future reference if a global default value
+# gets updated
+_builtin_exprs: List[ParserElement] = [
+ v for v in vars().values() if isinstance(v, ParserElement)
+]
+
+# backward compatibility names
+tokenMap = token_map
+conditionAsParseAction = condition_as_parse_action
+nullDebugAction = null_debug_action
+sglQuotedString = sgl_quoted_string
+dblQuotedString = dbl_quoted_string
+quotedString = quoted_string
+unicodeString = unicode_string
+lineStart = line_start
+lineEnd = line_end
+stringStart = string_start
+stringEnd = string_end
+traceParseAction = trace_parse_action
diff --git a/src/pip/_vendor/pyparsing/diagram/__init__.py b/src/pip/_vendor/pyparsing/diagram/__init__.py
new file mode 100644
index 000000000..1506d66bf
--- /dev/null
+++ b/src/pip/_vendor/pyparsing/diagram/__init__.py
@@ -0,0 +1,642 @@
+import railroad
+from pip._vendor import pyparsing
+import typing
+from typing import (
+ List,
+ NamedTuple,
+ Generic,
+ TypeVar,
+ Dict,
+ Callable,
+ Set,
+ Iterable,
+)
+from jinja2 import Template
+from io import StringIO
+import inspect
+
+
+jinja2_template_source = """\
+<!DOCTYPE html>
+<html>
+<head>
+ {% if not head %}
+ <style type="text/css">
+ .railroad-heading {
+ font-family: monospace;
+ }
+ </style>
+ {% else %}
+ {{ head | safe }}
+ {% endif %}
+</head>
+<body>
+{{ body | safe }}
+{% for diagram in diagrams %}
+ <div class="railroad-group">
+ <h1 class="railroad-heading">{{ diagram.title }}</h1>
+ <div class="railroad-description">{{ diagram.text }}</div>
+ <div class="railroad-svg">
+ {{ diagram.svg }}
+ </div>
+ </div>
+{% endfor %}
+</body>
+</html>
+"""
+
+template = Template(jinja2_template_source)
+
+# Note: ideally this would be a dataclass, but we're supporting Python 3.5+ so we can't do this yet
+NamedDiagram = NamedTuple(
+ "NamedDiagram",
+ [("name", str), ("diagram", typing.Optional[railroad.DiagramItem]), ("index", int)],
+)
+"""
+A simple structure for associating a name with a railroad diagram
+"""
+
+T = TypeVar("T")
+
+
+class EachItem(railroad.Group):
+ """
+ Custom railroad item to compose a:
+ - Group containing a
+ - OneOrMore containing a
+ - Choice of the elements in the Each
+ with the group label indicating that all must be matched
+ """
+
+ all_label = "[ALL]"
+
+ def __init__(self, *items):
+ choice_item = railroad.Choice(len(items) - 1, *items)
+ one_or_more_item = railroad.OneOrMore(item=choice_item)
+ super().__init__(one_or_more_item, label=self.all_label)
+
+
+class AnnotatedItem(railroad.Group):
+ """
+ Simple subclass of Group that creates an annotation label
+ """
+
+ def __init__(self, label: str, item):
+ super().__init__(item=item, label="[{}]".format(label) if label else label)
+
+
+class EditablePartial(Generic[T]):
+ """
+ Acts like a functools.partial, but can be edited. In other words, it represents a type that hasn't yet been
+ constructed.
+ """
+
+ # We need this here because the railroad constructors actually transform the data, so can't be called until the
+ # entire tree is assembled
+
+ def __init__(self, func: Callable[..., T], args: list, kwargs: dict):
+ self.func = func
+ self.args = args
+ self.kwargs = kwargs
+
+ @classmethod
+ def from_call(cls, func: Callable[..., T], *args, **kwargs) -> "EditablePartial[T]":
+ """
+ If you call this function in the same way that you would call the constructor, it will store the arguments
+ as you expect. For example EditablePartial.from_call(Fraction, 1, 3)() == Fraction(1, 3)
+ """
+ return EditablePartial(func=func, args=list(args), kwargs=kwargs)
+
+ @property
+ def name(self):
+ return self.kwargs["name"]
+
+ def __call__(self) -> T:
+ """
+ Evaluate the partial and return the result
+ """
+ args = self.args.copy()
+ kwargs = self.kwargs.copy()
+
+ # This is a helpful hack to allow you to specify varargs parameters (e.g. *args) as keyword args (e.g.
+ # args=['list', 'of', 'things'])
+ arg_spec = inspect.getfullargspec(self.func)
+ if arg_spec.varargs in self.kwargs:
+ args += kwargs.pop(arg_spec.varargs)
+
+ return self.func(*args, **kwargs)
+
+
+def railroad_to_html(diagrams: List[NamedDiagram], **kwargs) -> str:
+ """
+ Given a list of NamedDiagram, produce a single HTML string that visualises those diagrams
+ :params kwargs: kwargs to be passed in to the template
+ """
+ data = []
+ for diagram in diagrams:
+ if diagram.diagram is None:
+ continue
+ io = StringIO()
+ diagram.diagram.writeSvg(io.write)
+ title = diagram.name
+ if diagram.index == 0:
+ title += " (root)"
+ data.append({"title": title, "text": "", "svg": io.getvalue()})
+
+ return template.render(diagrams=data, **kwargs)
+
+
+def resolve_partial(partial: "EditablePartial[T]") -> T:
+ """
+ Recursively resolves a collection of Partials into whatever type they are
+ """
+ if isinstance(partial, EditablePartial):
+ partial.args = resolve_partial(partial.args)
+ partial.kwargs = resolve_partial(partial.kwargs)
+ return partial()
+ elif isinstance(partial, list):
+ return [resolve_partial(x) for x in partial]
+ elif isinstance(partial, dict):
+ return {key: resolve_partial(x) for key, x in partial.items()}
+ else:
+ return partial
+
+
+def to_railroad(
+ element: pyparsing.ParserElement,
+ diagram_kwargs: typing.Optional[dict] = None,
+ vertical: int = 3,
+ show_results_names: bool = False,
+ show_groups: bool = False,
+) -> List[NamedDiagram]:
+ """
+ Convert a pyparsing element tree into a list of diagrams. This is the recommended entrypoint to diagram
+ creation if you want to access the Railroad tree before it is converted to HTML
+ :param element: base element of the parser being diagrammed
+ :param diagram_kwargs: kwargs to pass to the Diagram() constructor
+ :param vertical: (optional) - int - limit at which number of alternatives should be
+ shown vertically instead of horizontally
+ :param show_results_names - bool to indicate whether results name annotations should be
+ included in the diagram
+ :param show_groups - bool to indicate whether groups should be highlighted with an unlabeled
+ surrounding box
+ """
+ # Convert the whole tree underneath the root
+ lookup = ConverterState(diagram_kwargs=diagram_kwargs or {})
+ _to_diagram_element(
+ element,
+ lookup=lookup,
+ parent=None,
+ vertical=vertical,
+ show_results_names=show_results_names,
+ show_groups=show_groups,
+ )
+
+ root_id = id(element)
+ # Convert the root if it hasn't been already
+ if root_id in lookup:
+ if not element.customName:
+ lookup[root_id].name = ""
+ lookup[root_id].mark_for_extraction(root_id, lookup, force=True)
+
+ # Now that we're finished, we can convert from intermediate structures into Railroad elements
+ diags = list(lookup.diagrams.values())
+ if len(diags) > 1:
+ # collapse out duplicate diags with the same name
+ seen = set()
+ deduped_diags = []
+ for d in diags:
+ # don't extract SkipTo elements, they are uninformative as subdiagrams
+ if d.name == "...":
+ continue
+ if d.name is not None and d.name not in seen:
+ seen.add(d.name)
+ deduped_diags.append(d)
+ resolved = [resolve_partial(partial) for partial in deduped_diags]
+ else:
+ # special case - if just one diagram, always display it, even if
+ # it has no name
+ resolved = [resolve_partial(partial) for partial in diags]
+ return sorted(resolved, key=lambda diag: diag.index)
+
+
+def _should_vertical(
+ specification: int, exprs: Iterable[pyparsing.ParserElement]
+) -> bool:
+ """
+ Returns true if we should return a vertical list of elements
+ """
+ if specification is None:
+ return False
+ else:
+ return len(_visible_exprs(exprs)) >= specification
+
+
+class ElementState:
+ """
+ State recorded for an individual pyparsing Element
+ """
+
+ # Note: this should be a dataclass, but we have to support Python 3.5
+ def __init__(
+ self,
+ element: pyparsing.ParserElement,
+ converted: EditablePartial,
+ parent: EditablePartial,
+ number: int,
+ name: str = None,
+ parent_index: typing.Optional[int] = None,
+ ):
+ #: The pyparsing element that this represents
+ self.element: pyparsing.ParserElement = element
+ #: The name of the element
+ self.name: typing.Optional[str] = name
+ #: The output Railroad element in an unconverted state
+ self.converted: EditablePartial = converted
+ #: The parent Railroad element, which we store so that we can extract this if it's duplicated
+ self.parent: EditablePartial = parent
+ #: The order in which we found this element, used for sorting diagrams if this is extracted into a diagram
+ self.number: int = number
+ #: The index of this inside its parent
+ self.parent_index: typing.Optional[int] = parent_index
+ #: If true, we should extract this out into a subdiagram
+ self.extract: bool = False
+ #: If true, all of this element's children have been filled out
+ self.complete: bool = False
+
+ def mark_for_extraction(
+ self, el_id: int, state: "ConverterState", name: str = None, force: bool = False
+ ):
+ """
+ Called when this instance has been seen twice, and thus should eventually be extracted into a sub-diagram
+ :param el_id: id of the element
+ :param state: element/diagram state tracker
+ :param name: name to use for this element's text
+ :param force: If true, force extraction now, regardless of the state of this. Only useful for extracting the
+ root element when we know we're finished
+ """
+ self.extract = True
+
+ # Set the name
+ if not self.name:
+ if name:
+ # Allow forcing a custom name
+ self.name = name
+ elif self.element.customName:
+ self.name = self.element.customName
+ else:
+ self.name = ""
+
+ # Just because this is marked for extraction doesn't mean we can do it yet. We may have to wait for children
+ # to be added
+ # Also, if this is just a string literal etc, don't bother extracting it
+ if force or (self.complete and _worth_extracting(self.element)):
+ state.extract_into_diagram(el_id)
+
+
+class ConverterState:
+ """
+ Stores some state that persists between recursions into the element tree
+ """
+
+ def __init__(self, diagram_kwargs: typing.Optional[dict] = None):
+ #: A dictionary mapping ParserElements to state relating to them
+ self._element_diagram_states: Dict[int, ElementState] = {}
+ #: A dictionary mapping ParserElement IDs to subdiagrams generated from them
+ self.diagrams: Dict[int, EditablePartial[NamedDiagram]] = {}
+ #: The index of the next unnamed element
+ self.unnamed_index: int = 1
+ #: The index of the next element. This is used for sorting
+ self.index: int = 0
+ #: Shared kwargs that are used to customize the construction of diagrams
+ self.diagram_kwargs: dict = diagram_kwargs or {}
+ self.extracted_diagram_names: Set[str] = set()
+
+ def __setitem__(self, key: int, value: ElementState):
+ self._element_diagram_states[key] = value
+
+ def __getitem__(self, key: int) -> ElementState:
+ return self._element_diagram_states[key]
+
+ def __delitem__(self, key: int):
+ del self._element_diagram_states[key]
+
+ def __contains__(self, key: int):
+ return key in self._element_diagram_states
+
+ def generate_unnamed(self) -> int:
+ """
+ Generate a number used in the name of an otherwise unnamed diagram
+ """
+ self.unnamed_index += 1
+ return self.unnamed_index
+
+ def generate_index(self) -> int:
+ """
+ Generate a number used to index a diagram
+ """
+ self.index += 1
+ return self.index
+
+ def extract_into_diagram(self, el_id: int):
+ """
+ Used when we encounter the same token twice in the same tree. When this
+ happens, we replace all instances of that token with a terminal, and
+ create a new subdiagram for the token
+ """
+ position = self[el_id]
+
+ # Replace the original definition of this element with a regular block
+ if position.parent:
+ ret = EditablePartial.from_call(railroad.NonTerminal, text=position.name)
+ if "item" in position.parent.kwargs:
+ position.parent.kwargs["item"] = ret
+ elif "items" in position.parent.kwargs:
+ position.parent.kwargs["items"][position.parent_index] = ret
+
+ # If the element we're extracting is a group, skip to its content but keep the title
+ if position.converted.func == railroad.Group:
+ content = position.converted.kwargs["item"]
+ else:
+ content = position.converted
+
+ self.diagrams[el_id] = EditablePartial.from_call(
+ NamedDiagram,
+ name=position.name,
+ diagram=EditablePartial.from_call(
+ railroad.Diagram, content, **self.diagram_kwargs
+ ),
+ index=position.number,
+ )
+
+ del self[el_id]
+
+
+def _worth_extracting(element: pyparsing.ParserElement) -> bool:
+ """
+ Returns true if this element is worth having its own sub-diagram. Simply, if any of its children
+ themselves have children, then its complex enough to extract
+ """
+ children = element.recurse()
+ return any(child.recurse() for child in children)
+
+
+def _apply_diagram_item_enhancements(fn):
+ """
+ decorator to ensure enhancements to a diagram item (such as results name annotations)
+ get applied on return from _to_diagram_element (we do this since there are several
+ returns in _to_diagram_element)
+ """
+
+ def _inner(
+ element: pyparsing.ParserElement,
+ parent: typing.Optional[EditablePartial],
+ lookup: ConverterState = None,
+ vertical: int = None,
+ index: int = 0,
+ name_hint: str = None,
+ show_results_names: bool = False,
+ show_groups: bool = False,
+ ) -> typing.Optional[EditablePartial]:
+
+ ret = fn(
+ element,
+ parent,
+ lookup,
+ vertical,
+ index,
+ name_hint,
+ show_results_names,
+ show_groups,
+ )
+
+ # apply annotation for results name, if present
+ if show_results_names and ret is not None:
+ element_results_name = element.resultsName
+ if element_results_name:
+ # add "*" to indicate if this is a "list all results" name
+ element_results_name += "" if element.modalResults else "*"
+ ret = EditablePartial.from_call(
+ railroad.Group, item=ret, label=element_results_name
+ )
+
+ return ret
+
+ return _inner
+
+
+def _visible_exprs(exprs: Iterable[pyparsing.ParserElement]):
+ non_diagramming_exprs = (
+ pyparsing.ParseElementEnhance,
+ pyparsing.PositionToken,
+ pyparsing.And._ErrorStop,
+ )
+ return [
+ e
+ for e in exprs
+ if not (e.customName or e.resultsName or isinstance(e, non_diagramming_exprs))
+ ]
+
+
+@_apply_diagram_item_enhancements
+def _to_diagram_element(
+ element: pyparsing.ParserElement,
+ parent: typing.Optional[EditablePartial],
+ lookup: ConverterState = None,
+ vertical: int = None,
+ index: int = 0,
+ name_hint: str = None,
+ show_results_names: bool = False,
+ show_groups: bool = False,
+) -> typing.Optional[EditablePartial]:
+ """
+ Recursively converts a PyParsing Element to a railroad Element
+ :param lookup: The shared converter state that keeps track of useful things
+ :param index: The index of this element within the parent
+ :param parent: The parent of this element in the output tree
+ :param vertical: Controls at what point we make a list of elements vertical. If this is an integer (the default),
+ it sets the threshold of the number of items before we go vertical. If True, always go vertical, if False, never
+ do so
+ :param name_hint: If provided, this will override the generated name
+ :param show_results_names: bool flag indicating whether to add annotations for results names
+ :returns: The converted version of the input element, but as a Partial that hasn't yet been constructed
+ :param show_groups: bool flag indicating whether to show groups using bounding box
+ """
+ exprs = element.recurse()
+ name = name_hint or element.customName or element.__class__.__name__
+
+ # Python's id() is used to provide a unique identifier for elements
+ el_id = id(element)
+
+ element_results_name = element.resultsName
+
+ # Here we basically bypass processing certain wrapper elements if they contribute nothing to the diagram
+ if not element.customName:
+ if isinstance(
+ element,
+ (
+ # pyparsing.TokenConverter,
+ # pyparsing.Forward,
+ pyparsing.Located,
+ ),
+ ):
+ # However, if this element has a useful custom name, and its child does not, we can pass it on to the child
+ if exprs:
+ if not exprs[0].customName:
+ propagated_name = name
+ else:
+ propagated_name = None
+
+ return _to_diagram_element(
+ element.expr,
+ parent=parent,
+ lookup=lookup,
+ vertical=vertical,
+ index=index,
+ name_hint=propagated_name,
+ show_results_names=show_results_names,
+ show_groups=show_groups,
+ )
+
+ # If the element isn't worth extracting, we always treat it as the first time we say it
+ if _worth_extracting(element):
+ if el_id in lookup:
+ # If we've seen this element exactly once before, we are only just now finding out that it's a duplicate,
+ # so we have to extract it into a new diagram.
+ looked_up = lookup[el_id]
+ looked_up.mark_for_extraction(el_id, lookup, name=name_hint)
+ ret = EditablePartial.from_call(railroad.NonTerminal, text=looked_up.name)
+ return ret
+
+ elif el_id in lookup.diagrams:
+ # If we have seen the element at least twice before, and have already extracted it into a subdiagram, we
+ # just put in a marker element that refers to the sub-diagram
+ ret = EditablePartial.from_call(
+ railroad.NonTerminal, text=lookup.diagrams[el_id].kwargs["name"]
+ )
+ return ret
+
+ # Recursively convert child elements
+ # Here we find the most relevant Railroad element for matching pyparsing Element
+ # We use ``items=[]`` here to hold the place for where the child elements will go once created
+ if isinstance(element, pyparsing.And):
+ # detect And's created with ``expr*N`` notation - for these use a OneOrMore with a repeat
+ # (all will have the same name, and resultsName)
+ if not exprs:
+ return None
+ if len(set((e.name, e.resultsName) for e in exprs)) == 1:
+ ret = EditablePartial.from_call(
+ railroad.OneOrMore, item="", repeat=str(len(exprs))
+ )
+ elif _should_vertical(vertical, exprs):
+ ret = EditablePartial.from_call(railroad.Stack, items=[])
+ else:
+ ret = EditablePartial.from_call(railroad.Sequence, items=[])
+ elif isinstance(element, (pyparsing.Or, pyparsing.MatchFirst)):
+ if not exprs:
+ return None
+ if _should_vertical(vertical, exprs):
+ ret = EditablePartial.from_call(railroad.Choice, 0, items=[])
+ else:
+ ret = EditablePartial.from_call(railroad.HorizontalChoice, items=[])
+ elif isinstance(element, pyparsing.Each):
+ if not exprs:
+ return None
+ ret = EditablePartial.from_call(EachItem, items=[])
+ elif isinstance(element, pyparsing.NotAny):
+ ret = EditablePartial.from_call(AnnotatedItem, label="NOT", item="")
+ elif isinstance(element, pyparsing.FollowedBy):
+ ret = EditablePartial.from_call(AnnotatedItem, label="LOOKAHEAD", item="")
+ elif isinstance(element, pyparsing.PrecededBy):
+ ret = EditablePartial.from_call(AnnotatedItem, label="LOOKBEHIND", item="")
+ elif isinstance(element, pyparsing.Group):
+ if show_groups:
+ ret = EditablePartial.from_call(AnnotatedItem, label="", item="")
+ else:
+ ret = EditablePartial.from_call(railroad.Group, label="", item="")
+ elif isinstance(element, pyparsing.TokenConverter):
+ ret = EditablePartial.from_call(
+ AnnotatedItem, label=type(element).__name__.lower(), item=""
+ )
+ elif isinstance(element, pyparsing.Opt):
+ ret = EditablePartial.from_call(railroad.Optional, item="")
+ elif isinstance(element, pyparsing.OneOrMore):
+ ret = EditablePartial.from_call(railroad.OneOrMore, item="")
+ elif isinstance(element, pyparsing.ZeroOrMore):
+ ret = EditablePartial.from_call(railroad.ZeroOrMore, item="")
+ elif isinstance(element, pyparsing.Group):
+ ret = EditablePartial.from_call(
+ railroad.Group, item=None, label=element_results_name
+ )
+ elif isinstance(element, pyparsing.Empty) and not element.customName:
+ # Skip unnamed "Empty" elements
+ ret = None
+ elif len(exprs) > 1:
+ ret = EditablePartial.from_call(railroad.Sequence, items=[])
+ elif len(exprs) > 0 and not element_results_name:
+ ret = EditablePartial.from_call(railroad.Group, item="", label=name)
+ else:
+ terminal = EditablePartial.from_call(railroad.Terminal, element.defaultName)
+ ret = terminal
+
+ if ret is None:
+ return
+
+ # Indicate this element's position in the tree so we can extract it if necessary
+ lookup[el_id] = ElementState(
+ element=element,
+ converted=ret,
+ parent=parent,
+ parent_index=index,
+ number=lookup.generate_index(),
+ )
+ if element.customName:
+ lookup[el_id].mark_for_extraction(el_id, lookup, element.customName)
+
+ i = 0
+ for expr in exprs:
+ # Add a placeholder index in case we have to extract the child before we even add it to the parent
+ if "items" in ret.kwargs:
+ ret.kwargs["items"].insert(i, None)
+
+ item = _to_diagram_element(
+ expr,
+ parent=ret,
+ lookup=lookup,
+ vertical=vertical,
+ index=i,
+ show_results_names=show_results_names,
+ show_groups=show_groups,
+ )
+
+ # Some elements don't need to be shown in the diagram
+ if item is not None:
+ if "item" in ret.kwargs:
+ ret.kwargs["item"] = item
+ elif "items" in ret.kwargs:
+ # If we've already extracted the child, don't touch this index, since it's occupied by a nonterminal
+ ret.kwargs["items"][i] = item
+ i += 1
+ elif "items" in ret.kwargs:
+ # If we're supposed to skip this element, remove it from the parent
+ del ret.kwargs["items"][i]
+
+ # If all this items children are none, skip this item
+ if ret and (
+ ("items" in ret.kwargs and len(ret.kwargs["items"]) == 0)
+ or ("item" in ret.kwargs and ret.kwargs["item"] is None)
+ ):
+ ret = EditablePartial.from_call(railroad.Terminal, name)
+
+ # Mark this element as "complete", ie it has all of its children
+ if el_id in lookup:
+ lookup[el_id].complete = True
+
+ if el_id in lookup and lookup[el_id].extract and lookup[el_id].complete:
+ lookup.extract_into_diagram(el_id)
+ if ret is not None:
+ ret = EditablePartial.from_call(
+ railroad.NonTerminal, text=lookup.diagrams[el_id].kwargs["name"]
+ )
+
+ return ret
diff --git a/src/pip/_vendor/pyparsing/exceptions.py b/src/pip/_vendor/pyparsing/exceptions.py
new file mode 100644
index 000000000..a38447bb0
--- /dev/null
+++ b/src/pip/_vendor/pyparsing/exceptions.py
@@ -0,0 +1,267 @@
+# exceptions.py
+
+import re
+import sys
+import typing
+
+from .util import col, line, lineno, _collapse_string_to_ranges
+from .unicode import pyparsing_unicode as ppu
+
+
+class ExceptionWordUnicode(ppu.Latin1, ppu.LatinA, ppu.LatinB, ppu.Greek, ppu.Cyrillic):
+ pass
+
+
+_extract_alphanums = _collapse_string_to_ranges(ExceptionWordUnicode.alphanums)
+_exception_word_extractor = re.compile("([" + _extract_alphanums + "]{1,16})|.")
+
+
+class ParseBaseException(Exception):
+ """base exception class for all parsing runtime exceptions"""
+
+ # Performance tuning: we construct a *lot* of these, so keep this
+ # constructor as small and fast as possible
+ def __init__(
+ self,
+ pstr: str,
+ loc: int = 0,
+ msg: typing.Optional[str] = None,
+ elem=None,
+ ):
+ self.loc = loc
+ if msg is None:
+ self.msg = pstr
+ self.pstr = ""
+ else:
+ self.msg = msg
+ self.pstr = pstr
+ self.parser_element = self.parserElement = elem
+ self.args = (pstr, loc, msg)
+
+ @staticmethod
+ def explain_exception(exc, depth=16):
+ """
+ Method to take an exception and translate the Python internal traceback into a list
+ of the pyparsing expressions that caused the exception to be raised.
+
+ Parameters:
+
+ - exc - exception raised during parsing (need not be a ParseException, in support
+ of Python exceptions that might be raised in a parse action)
+ - depth (default=16) - number of levels back in the stack trace to list expression
+ and function names; if None, the full stack trace names will be listed; if 0, only
+ the failing input line, marker, and exception string will be shown
+
+ Returns a multi-line string listing the ParserElements and/or function names in the
+ exception's stack trace.
+ """
+ import inspect
+ from .core import ParserElement
+
+ if depth is None:
+ depth = sys.getrecursionlimit()
+ ret = []
+ if isinstance(exc, ParseBaseException):
+ ret.append(exc.line)
+ ret.append(" " * (exc.column - 1) + "^")
+ ret.append("{}: {}".format(type(exc).__name__, exc))
+
+ if depth > 0:
+ callers = inspect.getinnerframes(exc.__traceback__, context=depth)
+ seen = set()
+ for i, ff in enumerate(callers[-depth:]):
+ frm = ff[0]
+
+ f_self = frm.f_locals.get("self", None)
+ if isinstance(f_self, ParserElement):
+ if frm.f_code.co_name not in ("parseImpl", "_parseNoCache"):
+ continue
+ if id(f_self) in seen:
+ continue
+ seen.add(id(f_self))
+
+ self_type = type(f_self)
+ ret.append(
+ "{}.{} - {}".format(
+ self_type.__module__, self_type.__name__, f_self
+ )
+ )
+
+ elif f_self is not None:
+ self_type = type(f_self)
+ ret.append("{}.{}".format(self_type.__module__, self_type.__name__))
+
+ else:
+ code = frm.f_code
+ if code.co_name in ("wrapper", "<module>"):
+ continue
+
+ ret.append("{}".format(code.co_name))
+
+ depth -= 1
+ if not depth:
+ break
+
+ return "\n".join(ret)
+
+ @classmethod
+ def _from_exception(cls, pe):
+ """
+ internal factory method to simplify creating one type of ParseException
+ from another - avoids having __init__ signature conflicts among subclasses
+ """
+ return cls(pe.pstr, pe.loc, pe.msg, pe.parserElement)
+
+ @property
+ def line(self) -> str:
+ """
+ Return the line of text where the exception occurred.
+ """
+ return line(self.loc, self.pstr)
+
+ @property
+ def lineno(self) -> int:
+ """
+ Return the 1-based line number of text where the exception occurred.
+ """
+ return lineno(self.loc, self.pstr)
+
+ @property
+ def col(self) -> int:
+ """
+ Return the 1-based column on the line of text where the exception occurred.
+ """
+ return col(self.loc, self.pstr)
+
+ @property
+ def column(self) -> int:
+ """
+ Return the 1-based column on the line of text where the exception occurred.
+ """
+ return col(self.loc, self.pstr)
+
+ def __str__(self) -> str:
+ if self.pstr:
+ if self.loc >= len(self.pstr):
+ foundstr = ", found end of text"
+ else:
+ # pull out next word at error location
+ found_match = _exception_word_extractor.match(self.pstr, self.loc)
+ if found_match is not None:
+ found = found_match.group(0)
+ else:
+ found = self.pstr[self.loc : self.loc + 1]
+ foundstr = (", found %r" % found).replace(r"\\", "\\")
+ else:
+ foundstr = ""
+ return "{}{} (at char {}), (line:{}, col:{})".format(
+ self.msg, foundstr, self.loc, self.lineno, self.column
+ )
+
+ def __repr__(self):
+ return str(self)
+
+ def mark_input_line(self, marker_string: str = None, *, markerString=">!<") -> str:
+ """
+ Extracts the exception line from the input string, and marks
+ the location of the exception with a special symbol.
+ """
+ markerString = marker_string if marker_string is not None else markerString
+ line_str = self.line
+ line_column = self.column - 1
+ if markerString:
+ line_str = "".join(
+ (line_str[:line_column], markerString, line_str[line_column:])
+ )
+ return line_str.strip()
+
+ def explain(self, depth=16) -> str:
+ """
+ Method to translate the Python internal traceback into a list
+ of the pyparsing expressions that caused the exception to be raised.
+
+ Parameters:
+
+ - depth (default=16) - number of levels back in the stack trace to list expression
+ and function names; if None, the full stack trace names will be listed; if 0, only
+ the failing input line, marker, and exception string will be shown
+
+ Returns a multi-line string listing the ParserElements and/or function names in the
+ exception's stack trace.
+
+ Example::
+
+ expr = pp.Word(pp.nums) * 3
+ try:
+ expr.parse_string("123 456 A789")
+ except pp.ParseException as pe:
+ print(pe.explain(depth=0))
+
+ prints::
+
+ 123 456 A789
+ ^
+ ParseException: Expected W:(0-9), found 'A' (at char 8), (line:1, col:9)
+
+ Note: the diagnostic output will include string representations of the expressions
+ that failed to parse. These representations will be more helpful if you use `set_name` to
+ give identifiable names to your expressions. Otherwise they will use the default string
+ forms, which may be cryptic to read.
+
+ Note: pyparsing's default truncation of exception tracebacks may also truncate the
+ stack of expressions that are displayed in the ``explain`` output. To get the full listing
+ of parser expressions, you may have to set ``ParserElement.verbose_stacktrace = True``
+ """
+ return self.explain_exception(self, depth)
+
+ markInputline = mark_input_line
+
+
+class ParseException(ParseBaseException):
+ """
+ Exception thrown when a parse expression doesn't match the input string
+
+ Example::
+
+ try:
+ Word(nums).set_name("integer").parse_string("ABC")
+ except ParseException as pe:
+ print(pe)
+ print("column: {}".format(pe.column))
+
+ prints::
+
+ Expected integer (at char 0), (line:1, col:1)
+ column: 1
+
+ """
+
+
+class ParseFatalException(ParseBaseException):
+ """
+ User-throwable exception thrown when inconsistent parse content
+ is found; stops all parsing immediately
+ """
+
+
+class ParseSyntaxException(ParseFatalException):
+ """
+ Just like :class:`ParseFatalException`, but thrown internally
+ when an :class:`ErrorStop<And._ErrorStop>` ('-' operator) indicates
+ that parsing is to stop immediately because an unbacktrackable
+ syntax error has been found.
+ """
+
+
+class RecursiveGrammarException(Exception):
+ """
+ Exception thrown by :class:`ParserElement.validate` if the
+ grammar could be left-recursive; parser may need to enable
+ left recursion using :class:`ParserElement.enable_left_recursion<ParserElement.enable_left_recursion>`
+ """
+
+ def __init__(self, parseElementList):
+ self.parseElementTrace = parseElementList
+
+ def __str__(self) -> str:
+ return "RecursiveGrammarException: {}".format(self.parseElementTrace)
diff --git a/src/pip/_vendor/pyparsing/helpers.py b/src/pip/_vendor/pyparsing/helpers.py
new file mode 100644
index 000000000..9588b3b78
--- /dev/null
+++ b/src/pip/_vendor/pyparsing/helpers.py
@@ -0,0 +1,1088 @@
+# helpers.py
+import html.entities
+import re
+import typing
+
+from . import __diag__
+from .core import *
+from .util import _bslash, _flatten, _escape_regex_range_chars
+
+
+#
+# global helpers
+#
+def delimited_list(
+ expr: Union[str, ParserElement],
+ delim: Union[str, ParserElement] = ",",
+ combine: bool = False,
+ min: typing.Optional[int] = None,
+ max: typing.Optional[int] = None,
+ *,
+ allow_trailing_delim: bool = False,
+) -> ParserElement:
+ """Helper to define a delimited list of expressions - the delimiter
+ defaults to ','. By default, the list elements and delimiters can
+ have intervening whitespace, and comments, but this can be
+ overridden by passing ``combine=True`` in the constructor. If
+ ``combine`` is set to ``True``, the matching tokens are
+ returned as a single token string, with the delimiters included;
+ otherwise, the matching tokens are returned as a list of tokens,
+ with the delimiters suppressed.
+
+ If ``allow_trailing_delim`` is set to True, then the list may end with
+ a delimiter.
+
+ Example::
+
+ delimited_list(Word(alphas)).parse_string("aa,bb,cc") # -> ['aa', 'bb', 'cc']
+ delimited_list(Word(hexnums), delim=':', combine=True).parse_string("AA:BB:CC:DD:EE") # -> ['AA:BB:CC:DD:EE']
+ """
+ if isinstance(expr, str_type):
+ expr = ParserElement._literalStringClass(expr)
+
+ dlName = "{expr} [{delim} {expr}]...{end}".format(
+ expr=str(expr.copy().streamline()),
+ delim=str(delim),
+ end=" [{}]".format(str(delim)) if allow_trailing_delim else "",
+ )
+
+ if not combine:
+ delim = Suppress(delim)
+
+ if min is not None:
+ if min < 1:
+ raise ValueError("min must be greater than 0")
+ min -= 1
+ if max is not None:
+ if min is not None and max <= min:
+ raise ValueError("max must be greater than, or equal to min")
+ max -= 1
+ delimited_list_expr = expr + (delim + expr)[min, max]
+
+ if allow_trailing_delim:
+ delimited_list_expr += Opt(delim)
+
+ if combine:
+ return Combine(delimited_list_expr).set_name(dlName)
+ else:
+ return delimited_list_expr.set_name(dlName)
+
+
+def counted_array(
+ expr: ParserElement,
+ int_expr: typing.Optional[ParserElement] = None,
+ *,
+ intExpr: typing.Optional[ParserElement] = None,
+) -> ParserElement:
+ """Helper to define a counted list of expressions.
+
+ This helper defines a pattern of the form::
+
+ integer expr expr expr...
+
+ where the leading integer tells how many expr expressions follow.
+ The matched tokens returns the array of expr tokens as a list - the
+ leading count token is suppressed.
+
+ If ``int_expr`` is specified, it should be a pyparsing expression
+ that produces an integer value.
+
+ Example::
+
+ counted_array(Word(alphas)).parse_string('2 ab cd ef') # -> ['ab', 'cd']
+
+ # in this parser, the leading integer value is given in binary,
+ # '10' indicating that 2 values are in the array
+ binary_constant = Word('01').set_parse_action(lambda t: int(t[0], 2))
+ counted_array(Word(alphas), int_expr=binary_constant).parse_string('10 ab cd ef') # -> ['ab', 'cd']
+
+ # if other fields must be parsed after the count but before the
+ # list items, give the fields results names and they will
+ # be preserved in the returned ParseResults:
+ count_with_metadata = integer + Word(alphas)("type")
+ typed_array = counted_array(Word(alphanums), int_expr=count_with_metadata)("items")
+ result = typed_array.parse_string("3 bool True True False")
+ print(result.dump())
+
+ # prints
+ # ['True', 'True', 'False']
+ # - items: ['True', 'True', 'False']
+ # - type: 'bool'
+ """
+ intExpr = intExpr or int_expr
+ array_expr = Forward()
+
+ def count_field_parse_action(s, l, t):
+ nonlocal array_expr
+ n = t[0]
+ array_expr <<= (expr * n) if n else Empty()
+ # clear list contents, but keep any named results
+ del t[:]
+
+ if intExpr is None:
+ intExpr = Word(nums).set_parse_action(lambda t: int(t[0]))
+ else:
+ intExpr = intExpr.copy()
+ intExpr.set_name("arrayLen")
+ intExpr.add_parse_action(count_field_parse_action, call_during_try=True)
+ return (intExpr + array_expr).set_name("(len) " + str(expr) + "...")
+
+
+def match_previous_literal(expr: ParserElement) -> ParserElement:
+ """Helper to define an expression that is indirectly defined from
+ the tokens matched in a previous expression, that is, it looks for
+ a 'repeat' of a previous expression. For example::
+
+ first = Word(nums)
+ second = match_previous_literal(first)
+ match_expr = first + ":" + second
+
+ will match ``"1:1"``, but not ``"1:2"``. Because this
+ matches a previous literal, will also match the leading
+ ``"1:1"`` in ``"1:10"``. If this is not desired, use
+ :class:`match_previous_expr`. Do *not* use with packrat parsing
+ enabled.
+ """
+ rep = Forward()
+
+ def copy_token_to_repeater(s, l, t):
+ if t:
+ if len(t) == 1:
+ rep << t[0]
+ else:
+ # flatten t tokens
+ tflat = _flatten(t.as_list())
+ rep << And(Literal(tt) for tt in tflat)
+ else:
+ rep << Empty()
+
+ expr.add_parse_action(copy_token_to_repeater, callDuringTry=True)
+ rep.set_name("(prev) " + str(expr))
+ return rep
+
+
+def match_previous_expr(expr: ParserElement) -> ParserElement:
+ """Helper to define an expression that is indirectly defined from
+ the tokens matched in a previous expression, that is, it looks for
+ a 'repeat' of a previous expression. For example::
+
+ first = Word(nums)
+ second = match_previous_expr(first)
+ match_expr = first + ":" + second
+
+ will match ``"1:1"``, but not ``"1:2"``. Because this
+ matches by expressions, will *not* match the leading ``"1:1"``
+ in ``"1:10"``; the expressions are evaluated first, and then
+ compared, so ``"1"`` is compared with ``"10"``. Do *not* use
+ with packrat parsing enabled.
+ """
+ rep = Forward()
+ e2 = expr.copy()
+ rep <<= e2
+
+ def copy_token_to_repeater(s, l, t):
+ matchTokens = _flatten(t.as_list())
+
+ def must_match_these_tokens(s, l, t):
+ theseTokens = _flatten(t.as_list())
+ if theseTokens != matchTokens:
+ raise ParseException(
+ s, l, "Expected {}, found{}".format(matchTokens, theseTokens)
+ )
+
+ rep.set_parse_action(must_match_these_tokens, callDuringTry=True)
+
+ expr.add_parse_action(copy_token_to_repeater, callDuringTry=True)
+ rep.set_name("(prev) " + str(expr))
+ return rep
+
+
+def one_of(
+ strs: Union[typing.Iterable[str], str],
+ caseless: bool = False,
+ use_regex: bool = True,
+ as_keyword: bool = False,
+ *,
+ useRegex: bool = True,
+ asKeyword: bool = False,
+) -> ParserElement:
+ """Helper to quickly define a set of alternative :class:`Literal` s,
+ and makes sure to do longest-first testing when there is a conflict,
+ regardless of the input order, but returns
+ a :class:`MatchFirst` for best performance.
+
+ Parameters:
+
+ - ``strs`` - a string of space-delimited literals, or a collection of
+ string literals
+ - ``caseless`` - treat all literals as caseless - (default= ``False``)
+ - ``use_regex`` - as an optimization, will
+ generate a :class:`Regex` object; otherwise, will generate
+ a :class:`MatchFirst` object (if ``caseless=True`` or ``asKeyword=True``, or if
+ creating a :class:`Regex` raises an exception) - (default= ``True``)
+ - ``as_keyword`` - enforce :class:`Keyword`-style matching on the
+ generated expressions - (default= ``False``)
+ - ``asKeyword`` and ``useRegex`` are retained for pre-PEP8 compatibility,
+ but will be removed in a future release
+
+ Example::
+
+ comp_oper = one_of("< = > <= >= !=")
+ var = Word(alphas)
+ number = Word(nums)
+ term = var | number
+ comparison_expr = term + comp_oper + term
+ print(comparison_expr.search_string("B = 12 AA=23 B<=AA AA>12"))
+
+ prints::
+
+ [['B', '=', '12'], ['AA', '=', '23'], ['B', '<=', 'AA'], ['AA', '>', '12']]
+ """
+ asKeyword = asKeyword or as_keyword
+ useRegex = useRegex and use_regex
+
+ if (
+ isinstance(caseless, str_type)
+ and __diag__.warn_on_multiple_string_args_to_oneof
+ ):
+ warnings.warn(
+ "More than one string argument passed to one_of, pass"
+ " choices as a list or space-delimited string",
+ stacklevel=2,
+ )
+
+ if caseless:
+ isequal = lambda a, b: a.upper() == b.upper()
+ masks = lambda a, b: b.upper().startswith(a.upper())
+ parseElementClass = CaselessKeyword if asKeyword else CaselessLiteral
+ else:
+ isequal = lambda a, b: a == b
+ masks = lambda a, b: b.startswith(a)
+ parseElementClass = Keyword if asKeyword else Literal
+
+ symbols: List[str] = []
+ if isinstance(strs, str_type):
+ symbols = strs.split()
+ elif isinstance(strs, Iterable):
+ symbols = list(strs)
+ else:
+ raise TypeError("Invalid argument to one_of, expected string or iterable")
+ if not symbols:
+ return NoMatch()
+
+ # reorder given symbols to take care to avoid masking longer choices with shorter ones
+ # (but only if the given symbols are not just single characters)
+ if any(len(sym) > 1 for sym in symbols):
+ i = 0
+ while i < len(symbols) - 1:
+ cur = symbols[i]
+ for j, other in enumerate(symbols[i + 1 :]):
+ if isequal(other, cur):
+ del symbols[i + j + 1]
+ break
+ elif masks(cur, other):
+ del symbols[i + j + 1]
+ symbols.insert(i, other)
+ break
+ else:
+ i += 1
+
+ if useRegex:
+ re_flags: int = re.IGNORECASE if caseless else 0
+
+ try:
+ if all(len(sym) == 1 for sym in symbols):
+ # symbols are just single characters, create range regex pattern
+ patt = "[{}]".format(
+ "".join(_escape_regex_range_chars(sym) for sym in symbols)
+ )
+ else:
+ patt = "|".join(re.escape(sym) for sym in symbols)
+
+ # wrap with \b word break markers if defining as keywords
+ if asKeyword:
+ patt = r"\b(?:{})\b".format(patt)
+
+ ret = Regex(patt, flags=re_flags).set_name(" | ".join(symbols))
+
+ if caseless:
+ # add parse action to return symbols as specified, not in random
+ # casing as found in input string
+ symbol_map = {sym.lower(): sym for sym in symbols}
+ ret.add_parse_action(lambda s, l, t: symbol_map[t[0].lower()])
+
+ return ret
+
+ except re.error:
+ warnings.warn(
+ "Exception creating Regex for one_of, building MatchFirst", stacklevel=2
+ )
+
+ # last resort, just use MatchFirst
+ return MatchFirst(parseElementClass(sym) for sym in symbols).set_name(
+ " | ".join(symbols)
+ )
+
+
+def dict_of(key: ParserElement, value: ParserElement) -> ParserElement:
+ """Helper to easily and clearly define a dictionary by specifying
+ the respective patterns for the key and value. Takes care of
+ defining the :class:`Dict`, :class:`ZeroOrMore`, and
+ :class:`Group` tokens in the proper order. The key pattern
+ can include delimiting markers or punctuation, as long as they are
+ suppressed, thereby leaving the significant key text. The value
+ pattern can include named results, so that the :class:`Dict` results
+ can include named token fields.
+
+ Example::
+
+ text = "shape: SQUARE posn: upper left color: light blue texture: burlap"
+ attr_expr = (label + Suppress(':') + OneOrMore(data_word, stop_on=label).set_parse_action(' '.join))
+ print(attr_expr[1, ...].parse_string(text).dump())
+
+ attr_label = label
+ attr_value = Suppress(':') + OneOrMore(data_word, stop_on=label).set_parse_action(' '.join)
+
+ # similar to Dict, but simpler call format
+ result = dict_of(attr_label, attr_value).parse_string(text)
+ print(result.dump())
+ print(result['shape'])
+ print(result.shape) # object attribute access works too
+ print(result.as_dict())
+
+ prints::
+
+ [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]
+ - color: 'light blue'
+ - posn: 'upper left'
+ - shape: 'SQUARE'
+ - texture: 'burlap'
+ SQUARE
+ SQUARE
+ {'color': 'light blue', 'shape': 'SQUARE', 'posn': 'upper left', 'texture': 'burlap'}
+ """
+ return Dict(OneOrMore(Group(key + value)))
+
+
+def original_text_for(
+ expr: ParserElement, as_string: bool = True, *, asString: bool = True
+) -> ParserElement:
+ """Helper to return the original, untokenized text for a given
+ expression. Useful to restore the parsed fields of an HTML start
+ tag into the raw tag text itself, or to revert separate tokens with
+ intervening whitespace back to the original matching input text. By
+ default, returns astring containing the original parsed text.
+
+ If the optional ``as_string`` argument is passed as
+ ``False``, then the return value is
+ a :class:`ParseResults` containing any results names that
+ were originally matched, and a single token containing the original
+ matched text from the input string. So if the expression passed to
+ :class:`original_text_for` contains expressions with defined
+ results names, you must set ``as_string`` to ``False`` if you
+ want to preserve those results name values.
+
+ The ``asString`` pre-PEP8 argument is retained for compatibility,
+ but will be removed in a future release.
+
+ Example::
+
+ src = "this is test <b> bold <i>text</i> </b> normal text "
+ for tag in ("b", "i"):
+ opener, closer = make_html_tags(tag)
+ patt = original_text_for(opener + SkipTo(closer) + closer)
+ print(patt.search_string(src)[0])
+
+ prints::
+
+ ['<b> bold <i>text</i> </b>']
+ ['<i>text</i>']
+ """
+ asString = asString and as_string
+
+ locMarker = Empty().set_parse_action(lambda s, loc, t: loc)
+ endlocMarker = locMarker.copy()
+ endlocMarker.callPreparse = False
+ matchExpr = locMarker("_original_start") + expr + endlocMarker("_original_end")
+ if asString:
+ extractText = lambda s, l, t: s[t._original_start : t._original_end]
+ else:
+
+ def extractText(s, l, t):
+ t[:] = [s[t.pop("_original_start") : t.pop("_original_end")]]
+
+ matchExpr.set_parse_action(extractText)
+ matchExpr.ignoreExprs = expr.ignoreExprs
+ matchExpr.suppress_warning(Diagnostics.warn_ungrouped_named_tokens_in_collection)
+ return matchExpr
+
+
+def ungroup(expr: ParserElement) -> ParserElement:
+ """Helper to undo pyparsing's default grouping of And expressions,
+ even if all but one are non-empty.
+ """
+ return TokenConverter(expr).add_parse_action(lambda t: t[0])
+
+
+def locatedExpr(expr: ParserElement) -> ParserElement:
+ """
+ (DEPRECATED - future code should use the Located class)
+ Helper to decorate a returned token with its starting and ending
+ locations in the input string.
+
+ This helper adds the following results names:
+
+ - ``locn_start`` - location where matched expression begins
+ - ``locn_end`` - location where matched expression ends
+ - ``value`` - the actual parsed results
+
+ Be careful if the input text contains ``<TAB>`` characters, you
+ may want to call :class:`ParserElement.parseWithTabs`
+
+ Example::
+
+ wd = Word(alphas)
+ for match in locatedExpr(wd).searchString("ljsdf123lksdjjf123lkkjj1222"):
+ print(match)
+
+ prints::
+
+ [[0, 'ljsdf', 5]]
+ [[8, 'lksdjjf', 15]]
+ [[18, 'lkkjj', 23]]
+ """
+ locator = Empty().set_parse_action(lambda ss, ll, tt: ll)
+ return Group(
+ locator("locn_start")
+ + expr("value")
+ + locator.copy().leaveWhitespace()("locn_end")
+ )
+
+
+def nested_expr(
+ opener: Union[str, ParserElement] = "(",
+ closer: Union[str, ParserElement] = ")",
+ content: typing.Optional[ParserElement] = None,
+ ignore_expr: ParserElement = quoted_string(),
+ *,
+ ignoreExpr: ParserElement = quoted_string(),
+) -> ParserElement:
+ """Helper method for defining nested lists enclosed in opening and
+ closing delimiters (``"("`` and ``")"`` are the default).
+
+ Parameters:
+ - ``opener`` - opening character for a nested list
+ (default= ``"("``); can also be a pyparsing expression
+ - ``closer`` - closing character for a nested list
+ (default= ``")"``); can also be a pyparsing expression
+ - ``content`` - expression for items within the nested lists
+ (default= ``None``)
+ - ``ignore_expr`` - expression for ignoring opening and closing delimiters
+ (default= :class:`quoted_string`)
+ - ``ignoreExpr`` - this pre-PEP8 argument is retained for compatibility
+ but will be removed in a future release
+
+ If an expression is not provided for the content argument, the
+ nested expression will capture all whitespace-delimited content
+ between delimiters as a list of separate values.
+
+ Use the ``ignore_expr`` argument to define expressions that may
+ contain opening or closing characters that should not be treated as
+ opening or closing characters for nesting, such as quoted_string or
+ a comment expression. Specify multiple expressions using an
+ :class:`Or` or :class:`MatchFirst`. The default is
+ :class:`quoted_string`, but if no expressions are to be ignored, then
+ pass ``None`` for this argument.
+
+ Example::
+
+ data_type = one_of("void int short long char float double")
+ decl_data_type = Combine(data_type + Opt(Word('*')))
+ ident = Word(alphas+'_', alphanums+'_')
+ number = pyparsing_common.number
+ arg = Group(decl_data_type + ident)
+ LPAR, RPAR = map(Suppress, "()")
+
+ code_body = nested_expr('{', '}', ignore_expr=(quoted_string | c_style_comment))
+
+ c_function = (decl_data_type("type")
+ + ident("name")
+ + LPAR + Opt(delimited_list(arg), [])("args") + RPAR
+ + code_body("body"))
+ c_function.ignore(c_style_comment)
+
+ source_code = '''
+ int is_odd(int x) {
+ return (x%2);
+ }
+
+ int dec_to_hex(char hchar) {
+ if (hchar >= '0' && hchar <= '9') {
+ return (ord(hchar)-ord('0'));
+ } else {
+ return (10+ord(hchar)-ord('A'));
+ }
+ }
+ '''
+ for func in c_function.search_string(source_code):
+ print("%(name)s (%(type)s) args: %(args)s" % func)
+
+
+ prints::
+
+ is_odd (int) args: [['int', 'x']]
+ dec_to_hex (int) args: [['char', 'hchar']]
+ """
+ if ignoreExpr != ignore_expr:
+ ignoreExpr = ignore_expr if ignoreExpr == quoted_string() else ignoreExpr
+ if opener == closer:
+ raise ValueError("opening and closing strings cannot be the same")
+ if content is None:
+ if isinstance(opener, str_type) and isinstance(closer, str_type):
+ if len(opener) == 1 and len(closer) == 1:
+ if ignoreExpr is not None:
+ content = Combine(
+ OneOrMore(
+ ~ignoreExpr
+ + CharsNotIn(
+ opener + closer + ParserElement.DEFAULT_WHITE_CHARS,
+ exact=1,
+ )
+ )
+ ).set_parse_action(lambda t: t[0].strip())
+ else:
+ content = empty.copy() + CharsNotIn(
+ opener + closer + ParserElement.DEFAULT_WHITE_CHARS
+ ).set_parse_action(lambda t: t[0].strip())
+ else:
+ if ignoreExpr is not None:
+ content = Combine(
+ OneOrMore(
+ ~ignoreExpr
+ + ~Literal(opener)
+ + ~Literal(closer)
+ + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1)
+ )
+ ).set_parse_action(lambda t: t[0].strip())
+ else:
+ content = Combine(
+ OneOrMore(
+ ~Literal(opener)
+ + ~Literal(closer)
+ + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1)
+ )
+ ).set_parse_action(lambda t: t[0].strip())
+ else:
+ raise ValueError(
+ "opening and closing arguments must be strings if no content expression is given"
+ )
+ ret = Forward()
+ if ignoreExpr is not None:
+ ret <<= Group(
+ Suppress(opener) + ZeroOrMore(ignoreExpr | ret | content) + Suppress(closer)
+ )
+ else:
+ ret <<= Group(Suppress(opener) + ZeroOrMore(ret | content) + Suppress(closer))
+ ret.set_name("nested %s%s expression" % (opener, closer))
+ return ret
+
+
+def _makeTags(tagStr, xml, suppress_LT=Suppress("<"), suppress_GT=Suppress(">")):
+ """Internal helper to construct opening and closing tag expressions, given a tag name"""
+ if isinstance(tagStr, str_type):
+ resname = tagStr
+ tagStr = Keyword(tagStr, caseless=not xml)
+ else:
+ resname = tagStr.name
+
+ tagAttrName = Word(alphas, alphanums + "_-:")
+ if xml:
+ tagAttrValue = dbl_quoted_string.copy().set_parse_action(remove_quotes)
+ openTag = (
+ suppress_LT
+ + tagStr("tag")
+ + Dict(ZeroOrMore(Group(tagAttrName + Suppress("=") + tagAttrValue)))
+ + Opt("/", default=[False])("empty").set_parse_action(
+ lambda s, l, t: t[0] == "/"
+ )
+ + suppress_GT
+ )
+ else:
+ tagAttrValue = quoted_string.copy().set_parse_action(remove_quotes) | Word(
+ printables, exclude_chars=">"
+ )
+ openTag = (
+ suppress_LT
+ + tagStr("tag")
+ + Dict(
+ ZeroOrMore(
+ Group(
+ tagAttrName.set_parse_action(lambda t: t[0].lower())
+ + Opt(Suppress("=") + tagAttrValue)
+ )
+ )
+ )
+ + Opt("/", default=[False])("empty").set_parse_action(
+ lambda s, l, t: t[0] == "/"
+ )
+ + suppress_GT
+ )
+ closeTag = Combine(Literal("</") + tagStr + ">", adjacent=False)
+
+ openTag.set_name("<%s>" % resname)
+ # add start<tagname> results name in parse action now that ungrouped names are not reported at two levels
+ openTag.add_parse_action(
+ lambda t: t.__setitem__(
+ "start" + "".join(resname.replace(":", " ").title().split()), t.copy()
+ )
+ )
+ closeTag = closeTag(
+ "end" + "".join(resname.replace(":", " ").title().split())
+ ).set_name("</%s>" % resname)
+ openTag.tag = resname
+ closeTag.tag = resname
+ openTag.tag_body = SkipTo(closeTag())
+ return openTag, closeTag
+
+
+def make_html_tags(
+ tag_str: Union[str, ParserElement]
+) -> Tuple[ParserElement, ParserElement]:
+ """Helper to construct opening and closing tag expressions for HTML,
+ given a tag name. Matches tags in either upper or lower case,
+ attributes with namespaces and with quoted or unquoted values.
+
+ Example::
+
+ text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>'
+ # make_html_tags returns pyparsing expressions for the opening and
+ # closing tags as a 2-tuple
+ a, a_end = make_html_tags("A")
+ link_expr = a + SkipTo(a_end)("link_text") + a_end
+
+ for link in link_expr.search_string(text):
+ # attributes in the <A> tag (like "href" shown here) are
+ # also accessible as named results
+ print(link.link_text, '->', link.href)
+
+ prints::
+
+ pyparsing -> https://github.com/pyparsing/pyparsing/wiki
+ """
+ return _makeTags(tag_str, False)
+
+
+def make_xml_tags(
+ tag_str: Union[str, ParserElement]
+) -> Tuple[ParserElement, ParserElement]:
+ """Helper to construct opening and closing tag expressions for XML,
+ given a tag name. Matches tags only in the given upper/lower case.
+
+ Example: similar to :class:`make_html_tags`
+ """
+ return _makeTags(tag_str, True)
+
+
+any_open_tag: ParserElement
+any_close_tag: ParserElement
+any_open_tag, any_close_tag = make_html_tags(
+ Word(alphas, alphanums + "_:").set_name("any tag")
+)
+
+_htmlEntityMap = {k.rstrip(";"): v for k, v in html.entities.html5.items()}
+common_html_entity = Regex("&(?P<entity>" + "|".join(_htmlEntityMap) + ");").set_name(
+ "common HTML entity"
+)
+
+
+def replace_html_entity(t):
+ """Helper parser action to replace common HTML entities with their special characters"""
+ return _htmlEntityMap.get(t.entity)
+
+
+class OpAssoc(Enum):
+ LEFT = 1
+ RIGHT = 2
+
+
+InfixNotationOperatorArgType = Union[
+ ParserElement, str, Tuple[Union[ParserElement, str], Union[ParserElement, str]]
+]
+InfixNotationOperatorSpec = Union[
+ Tuple[
+ InfixNotationOperatorArgType,
+ int,
+ OpAssoc,
+ typing.Optional[ParseAction],
+ ],
+ Tuple[
+ InfixNotationOperatorArgType,
+ int,
+ OpAssoc,
+ ],
+]
+
+
+def infix_notation(
+ base_expr: ParserElement,
+ op_list: List[InfixNotationOperatorSpec],
+ lpar: Union[str, ParserElement] = Suppress("("),
+ rpar: Union[str, ParserElement] = Suppress(")"),
+) -> ParserElement:
+ """Helper method for constructing grammars of expressions made up of
+ operators working in a precedence hierarchy. Operators may be unary
+ or binary, left- or right-associative. Parse actions can also be
+ attached to operator expressions. The generated parser will also
+ recognize the use of parentheses to override operator precedences
+ (see example below).
+
+ Note: if you define a deep operator list, you may see performance
+ issues when using infix_notation. See
+ :class:`ParserElement.enable_packrat` for a mechanism to potentially
+ improve your parser performance.
+
+ Parameters:
+ - ``base_expr`` - expression representing the most basic operand to
+ be used in the expression
+ - ``op_list`` - list of tuples, one for each operator precedence level
+ in the expression grammar; each tuple is of the form ``(op_expr,
+ num_operands, right_left_assoc, (optional)parse_action)``, where:
+
+ - ``op_expr`` is the pyparsing expression for the operator; may also
+ be a string, which will be converted to a Literal; if ``num_operands``
+ is 3, ``op_expr`` is a tuple of two expressions, for the two
+ operators separating the 3 terms
+ - ``num_operands`` is the number of terms for this operator (must be 1,
+ 2, or 3)
+ - ``right_left_assoc`` is the indicator whether the operator is right
+ or left associative, using the pyparsing-defined constants
+ ``OpAssoc.RIGHT`` and ``OpAssoc.LEFT``.
+ - ``parse_action`` is the parse action to be associated with
+ expressions matching this operator expression (the parse action
+ tuple member may be omitted); if the parse action is passed
+ a tuple or list of functions, this is equivalent to calling
+ ``set_parse_action(*fn)``
+ (:class:`ParserElement.set_parse_action`)
+ - ``lpar`` - expression for matching left-parentheses; if passed as a
+ str, then will be parsed as Suppress(lpar). If lpar is passed as
+ an expression (such as ``Literal('(')``), then it will be kept in
+ the parsed results, and grouped with them. (default= ``Suppress('(')``)
+ - ``rpar`` - expression for matching right-parentheses; if passed as a
+ str, then will be parsed as Suppress(rpar). If rpar is passed as
+ an expression (such as ``Literal(')')``), then it will be kept in
+ the parsed results, and grouped with them. (default= ``Suppress(')')``)
+
+ Example::
+
+ # simple example of four-function arithmetic with ints and
+ # variable names
+ integer = pyparsing_common.signed_integer
+ varname = pyparsing_common.identifier
+
+ arith_expr = infix_notation(integer | varname,
+ [
+ ('-', 1, OpAssoc.RIGHT),
+ (one_of('* /'), 2, OpAssoc.LEFT),
+ (one_of('+ -'), 2, OpAssoc.LEFT),
+ ])
+
+ arith_expr.run_tests('''
+ 5+3*6
+ (5+3)*6
+ -2--11
+ ''', full_dump=False)
+
+ prints::
+
+ 5+3*6
+ [[5, '+', [3, '*', 6]]]
+
+ (5+3)*6
+ [[[5, '+', 3], '*', 6]]
+
+ -2--11
+ [[['-', 2], '-', ['-', 11]]]
+ """
+ # captive version of FollowedBy that does not do parse actions or capture results names
+ class _FB(FollowedBy):
+ def parseImpl(self, instring, loc, doActions=True):
+ self.expr.try_parse(instring, loc)
+ return loc, []
+
+ _FB.__name__ = "FollowedBy>"
+
+ ret = Forward()
+ if isinstance(lpar, str):
+ lpar = Suppress(lpar)
+ if isinstance(rpar, str):
+ rpar = Suppress(rpar)
+
+ # if lpar and rpar are not suppressed, wrap in group
+ if not (isinstance(rpar, Suppress) and isinstance(rpar, Suppress)):
+ lastExpr = base_expr | Group(lpar + ret + rpar)
+ else:
+ lastExpr = base_expr | (lpar + ret + rpar)
+
+ for i, operDef in enumerate(op_list):
+ opExpr, arity, rightLeftAssoc, pa = (operDef + (None,))[:4]
+ if isinstance(opExpr, str_type):
+ opExpr = ParserElement._literalStringClass(opExpr)
+ if arity == 3:
+ if not isinstance(opExpr, (tuple, list)) or len(opExpr) != 2:
+ raise ValueError(
+ "if numterms=3, opExpr must be a tuple or list of two expressions"
+ )
+ opExpr1, opExpr2 = opExpr
+ term_name = "{}{} term".format(opExpr1, opExpr2)
+ else:
+ term_name = "{} term".format(opExpr)
+
+ if not 1 <= arity <= 3:
+ raise ValueError("operator must be unary (1), binary (2), or ternary (3)")
+
+ if rightLeftAssoc not in (OpAssoc.LEFT, OpAssoc.RIGHT):
+ raise ValueError("operator must indicate right or left associativity")
+
+ thisExpr: Forward = Forward().set_name(term_name)
+ if rightLeftAssoc is OpAssoc.LEFT:
+ if arity == 1:
+ matchExpr = _FB(lastExpr + opExpr) + Group(lastExpr + opExpr[1, ...])
+ elif arity == 2:
+ if opExpr is not None:
+ matchExpr = _FB(lastExpr + opExpr + lastExpr) + Group(
+ lastExpr + (opExpr + lastExpr)[1, ...]
+ )
+ else:
+ matchExpr = _FB(lastExpr + lastExpr) + Group(lastExpr[2, ...])
+ elif arity == 3:
+ matchExpr = _FB(
+ lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr
+ ) + Group(lastExpr + OneOrMore(opExpr1 + lastExpr + opExpr2 + lastExpr))
+ elif rightLeftAssoc is OpAssoc.RIGHT:
+ if arity == 1:
+ # try to avoid LR with this extra test
+ if not isinstance(opExpr, Opt):
+ opExpr = Opt(opExpr)
+ matchExpr = _FB(opExpr.expr + thisExpr) + Group(opExpr + thisExpr)
+ elif arity == 2:
+ if opExpr is not None:
+ matchExpr = _FB(lastExpr + opExpr + thisExpr) + Group(
+ lastExpr + (opExpr + thisExpr)[1, ...]
+ )
+ else:
+ matchExpr = _FB(lastExpr + thisExpr) + Group(
+ lastExpr + thisExpr[1, ...]
+ )
+ elif arity == 3:
+ matchExpr = _FB(
+ lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr
+ ) + Group(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr)
+ if pa:
+ if isinstance(pa, (tuple, list)):
+ matchExpr.set_parse_action(*pa)
+ else:
+ matchExpr.set_parse_action(pa)
+ thisExpr <<= (matchExpr | lastExpr).setName(term_name)
+ lastExpr = thisExpr
+ ret <<= lastExpr
+ return ret
+
+
+def indentedBlock(blockStatementExpr, indentStack, indent=True, backup_stacks=[]):
+ """
+ (DEPRECATED - use IndentedBlock class instead)
+ Helper method for defining space-delimited indentation blocks,
+ such as those used to define block statements in Python source code.
+
+ Parameters:
+
+ - ``blockStatementExpr`` - expression defining syntax of statement that
+ is repeated within the indented block
+ - ``indentStack`` - list created by caller to manage indentation stack
+ (multiple ``statementWithIndentedBlock`` expressions within a single
+ grammar should share a common ``indentStack``)
+ - ``indent`` - boolean indicating whether block must be indented beyond
+ the current level; set to ``False`` for block of left-most statements
+ (default= ``True``)
+
+ A valid block must contain at least one ``blockStatement``.
+
+ (Note that indentedBlock uses internal parse actions which make it
+ incompatible with packrat parsing.)
+
+ Example::
+
+ data = '''
+ def A(z):
+ A1
+ B = 100
+ G = A2
+ A2
+ A3
+ B
+ def BB(a,b,c):
+ BB1
+ def BBA():
+ bba1
+ bba2
+ bba3
+ C
+ D
+ def spam(x,y):
+ def eggs(z):
+ pass
+ '''
+
+
+ indentStack = [1]
+ stmt = Forward()
+
+ identifier = Word(alphas, alphanums)
+ funcDecl = ("def" + identifier + Group("(" + Opt(delimitedList(identifier)) + ")") + ":")
+ func_body = indentedBlock(stmt, indentStack)
+ funcDef = Group(funcDecl + func_body)
+
+ rvalue = Forward()
+ funcCall = Group(identifier + "(" + Opt(delimitedList(rvalue)) + ")")
+ rvalue << (funcCall | identifier | Word(nums))
+ assignment = Group(identifier + "=" + rvalue)
+ stmt << (funcDef | assignment | identifier)
+
+ module_body = stmt[1, ...]
+
+ parseTree = module_body.parseString(data)
+ parseTree.pprint()
+
+ prints::
+
+ [['def',
+ 'A',
+ ['(', 'z', ')'],
+ ':',
+ [['A1'], [['B', '=', '100']], [['G', '=', 'A2']], ['A2'], ['A3']]],
+ 'B',
+ ['def',
+ 'BB',
+ ['(', 'a', 'b', 'c', ')'],
+ ':',
+ [['BB1'], [['def', 'BBA', ['(', ')'], ':', [['bba1'], ['bba2'], ['bba3']]]]]],
+ 'C',
+ 'D',
+ ['def',
+ 'spam',
+ ['(', 'x', 'y', ')'],
+ ':',
+ [[['def', 'eggs', ['(', 'z', ')'], ':', [['pass']]]]]]]
+ """
+ backup_stacks.append(indentStack[:])
+
+ def reset_stack():
+ indentStack[:] = backup_stacks[-1]
+
+ def checkPeerIndent(s, l, t):
+ if l >= len(s):
+ return
+ curCol = col(l, s)
+ if curCol != indentStack[-1]:
+ if curCol > indentStack[-1]:
+ raise ParseException(s, l, "illegal nesting")
+ raise ParseException(s, l, "not a peer entry")
+
+ def checkSubIndent(s, l, t):
+ curCol = col(l, s)
+ if curCol > indentStack[-1]:
+ indentStack.append(curCol)
+ else:
+ raise ParseException(s, l, "not a subentry")
+
+ def checkUnindent(s, l, t):
+ if l >= len(s):
+ return
+ curCol = col(l, s)
+ if not (indentStack and curCol in indentStack):
+ raise ParseException(s, l, "not an unindent")
+ if curCol < indentStack[-1]:
+ indentStack.pop()
+
+ NL = OneOrMore(LineEnd().set_whitespace_chars("\t ").suppress())
+ INDENT = (Empty() + Empty().set_parse_action(checkSubIndent)).set_name("INDENT")
+ PEER = Empty().set_parse_action(checkPeerIndent).set_name("")
+ UNDENT = Empty().set_parse_action(checkUnindent).set_name("UNINDENT")
+ if indent:
+ smExpr = Group(
+ Opt(NL)
+ + INDENT
+ + OneOrMore(PEER + Group(blockStatementExpr) + Opt(NL))
+ + UNDENT
+ )
+ else:
+ smExpr = Group(
+ Opt(NL)
+ + OneOrMore(PEER + Group(blockStatementExpr) + Opt(NL))
+ + Opt(UNDENT)
+ )
+
+ # add a parse action to remove backup_stack from list of backups
+ smExpr.add_parse_action(
+ lambda: backup_stacks.pop(-1) and None if backup_stacks else None
+ )
+ smExpr.set_fail_action(lambda a, b, c, d: reset_stack())
+ blockStatementExpr.ignore(_bslash + LineEnd())
+ return smExpr.set_name("indented block")
+
+
+# it's easy to get these comment structures wrong - they're very common, so may as well make them available
+c_style_comment = Combine(Regex(r"/\*(?:[^*]|\*(?!/))*") + "*/").set_name(
+ "C style comment"
+)
+"Comment of the form ``/* ... */``"
+
+html_comment = Regex(r"<!--[\s\S]*?-->").set_name("HTML comment")
+"Comment of the form ``<!-- ... -->``"
+
+rest_of_line = Regex(r".*").leave_whitespace().set_name("rest of line")
+dbl_slash_comment = Regex(r"//(?:\\\n|[^\n])*").set_name("// comment")
+"Comment of the form ``// ... (to end of line)``"
+
+cpp_style_comment = Combine(
+ Regex(r"/\*(?:[^*]|\*(?!/))*") + "*/" | dbl_slash_comment
+).set_name("C++ style comment")
+"Comment of either form :class:`c_style_comment` or :class:`dbl_slash_comment`"
+
+java_style_comment = cpp_style_comment
+"Same as :class:`cpp_style_comment`"
+
+python_style_comment = Regex(r"#.*").set_name("Python style comment")
+"Comment of the form ``# ... (to end of line)``"
+
+
+# build list of built-in expressions, for future reference if a global default value
+# gets updated
+_builtin_exprs: List[ParserElement] = [
+ v for v in vars().values() if isinstance(v, ParserElement)
+]
+
+
+# pre-PEP8 compatible names
+delimitedList = delimited_list
+countedArray = counted_array
+matchPreviousLiteral = match_previous_literal
+matchPreviousExpr = match_previous_expr
+oneOf = one_of
+dictOf = dict_of
+originalTextFor = original_text_for
+nestedExpr = nested_expr
+makeHTMLTags = make_html_tags
+makeXMLTags = make_xml_tags
+anyOpenTag, anyCloseTag = any_open_tag, any_close_tag
+commonHTMLEntity = common_html_entity
+replaceHTMLEntity = replace_html_entity
+opAssoc = OpAssoc
+infixNotation = infix_notation
+cStyleComment = c_style_comment
+htmlComment = html_comment
+restOfLine = rest_of_line
+dblSlashComment = dbl_slash_comment
+cppStyleComment = cpp_style_comment
+javaStyleComment = java_style_comment
+pythonStyleComment = python_style_comment
diff --git a/src/pip/_vendor/pyparsing/py.typed b/src/pip/_vendor/pyparsing/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pip/_vendor/pyparsing/py.typed
diff --git a/src/pip/_vendor/pyparsing/results.py b/src/pip/_vendor/pyparsing/results.py
new file mode 100644
index 000000000..00c9421d3
--- /dev/null
+++ b/src/pip/_vendor/pyparsing/results.py
@@ -0,0 +1,760 @@
+# results.py
+from collections.abc import MutableMapping, Mapping, MutableSequence, Iterator
+import pprint
+from weakref import ref as wkref
+from typing import Tuple, Any
+
+str_type: Tuple[type, ...] = (str, bytes)
+_generator_type = type((_ for _ in ()))
+
+
+class _ParseResultsWithOffset:
+ __slots__ = ["tup"]
+
+ def __init__(self, p1, p2):
+ self.tup = (p1, p2)
+
+ def __getitem__(self, i):
+ return self.tup[i]
+
+ def __getstate__(self):
+ return self.tup
+
+ def __setstate__(self, *args):
+ self.tup = args[0]
+
+
+class ParseResults:
+ """Structured parse results, to provide multiple means of access to
+ the parsed data:
+
+ - as a list (``len(results)``)
+ - by list index (``results[0], results[1]``, etc.)
+ - by attribute (``results.<results_name>`` - see :class:`ParserElement.set_results_name`)
+
+ Example::
+
+ integer = Word(nums)
+ date_str = (integer.set_results_name("year") + '/'
+ + integer.set_results_name("month") + '/'
+ + integer.set_results_name("day"))
+ # equivalent form:
+ # date_str = (integer("year") + '/'
+ # + integer("month") + '/'
+ # + integer("day"))
+
+ # parse_string returns a ParseResults object
+ result = date_str.parse_string("1999/12/31")
+
+ def test(s, fn=repr):
+ print("{} -> {}".format(s, fn(eval(s))))
+ test("list(result)")
+ test("result[0]")
+ test("result['month']")
+ test("result.day")
+ test("'month' in result")
+ test("'minutes' in result")
+ test("result.dump()", str)
+
+ prints::
+
+ list(result) -> ['1999', '/', '12', '/', '31']
+ result[0] -> '1999'
+ result['month'] -> '12'
+ result.day -> '31'
+ 'month' in result -> True
+ 'minutes' in result -> False
+ result.dump() -> ['1999', '/', '12', '/', '31']
+ - day: '31'
+ - month: '12'
+ - year: '1999'
+ """
+
+ _null_values: Tuple[Any, ...] = (None, [], "", ())
+
+ __slots__ = [
+ "_name",
+ "_parent",
+ "_all_names",
+ "_modal",
+ "_toklist",
+ "_tokdict",
+ "__weakref__",
+ ]
+
+ class List(list):
+ """
+ Simple wrapper class to distinguish parsed list results that should be preserved
+ as actual Python lists, instead of being converted to :class:`ParseResults`:
+
+ LBRACK, RBRACK = map(pp.Suppress, "[]")
+ element = pp.Forward()
+ item = ppc.integer
+ element_list = LBRACK + pp.delimited_list(element) + RBRACK
+
+ # add parse actions to convert from ParseResults to actual Python collection types
+ def as_python_list(t):
+ return pp.ParseResults.List(t.as_list())
+ element_list.add_parse_action(as_python_list)
+
+ element <<= item | element_list
+
+ element.run_tests('''
+ 100
+ [2,3,4]
+ [[2, 1],3,4]
+ [(2, 1),3,4]
+ (2,3,4)
+ ''', post_parse=lambda s, r: (r[0], type(r[0])))
+
+ prints:
+
+ 100
+ (100, <class 'int'>)
+
+ [2,3,4]
+ ([2, 3, 4], <class 'list'>)
+
+ [[2, 1],3,4]
+ ([[2, 1], 3, 4], <class 'list'>)
+
+ (Used internally by :class:`Group` when `aslist=True`.)
+ """
+
+ def __new__(cls, contained=None):
+ if contained is None:
+ contained = []
+
+ if not isinstance(contained, list):
+ raise TypeError(
+ "{} may only be constructed with a list,"
+ " not {}".format(cls.__name__, type(contained).__name__)
+ )
+
+ return list.__new__(cls)
+
+ def __new__(cls, toklist=None, name=None, **kwargs):
+ if isinstance(toklist, ParseResults):
+ return toklist
+ self = object.__new__(cls)
+ self._name = None
+ self._parent = None
+ self._all_names = set()
+
+ if toklist is None:
+ self._toklist = []
+ elif isinstance(toklist, (list, _generator_type)):
+ self._toklist = (
+ [toklist[:]]
+ if isinstance(toklist, ParseResults.List)
+ else list(toklist)
+ )
+ else:
+ self._toklist = [toklist]
+ self._tokdict = dict()
+ return self
+
+ # Performance tuning: we construct a *lot* of these, so keep this
+ # constructor as small and fast as possible
+ def __init__(
+ self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance
+ ):
+ self._modal = modal
+ if name is not None and name != "":
+ if isinstance(name, int):
+ name = str(name)
+ if not modal:
+ self._all_names = {name}
+ self._name = name
+ if toklist not in self._null_values:
+ if isinstance(toklist, (str_type, type)):
+ toklist = [toklist]
+ if asList:
+ if isinstance(toklist, ParseResults):
+ self[name] = _ParseResultsWithOffset(
+ ParseResults(toklist._toklist), 0
+ )
+ else:
+ self[name] = _ParseResultsWithOffset(
+ ParseResults(toklist[0]), 0
+ )
+ self[name]._name = name
+ else:
+ try:
+ self[name] = toklist[0]
+ except (KeyError, TypeError, IndexError):
+ if toklist is not self:
+ self[name] = toklist
+ else:
+ self._name = name
+
+ def __getitem__(self, i):
+ if isinstance(i, (int, slice)):
+ return self._toklist[i]
+ else:
+ if i not in self._all_names:
+ return self._tokdict[i][-1][0]
+ else:
+ return ParseResults([v[0] for v in self._tokdict[i]])
+
+ def __setitem__(self, k, v, isinstance=isinstance):
+ if isinstance(v, _ParseResultsWithOffset):
+ self._tokdict[k] = self._tokdict.get(k, list()) + [v]
+ sub = v[0]
+ elif isinstance(k, (int, slice)):
+ self._toklist[k] = v
+ sub = v
+ else:
+ self._tokdict[k] = self._tokdict.get(k, list()) + [
+ _ParseResultsWithOffset(v, 0)
+ ]
+ sub = v
+ if isinstance(sub, ParseResults):
+ sub._parent = wkref(self)
+
+ def __delitem__(self, i):
+ if isinstance(i, (int, slice)):
+ mylen = len(self._toklist)
+ del self._toklist[i]
+
+ # convert int to slice
+ if isinstance(i, int):
+ if i < 0:
+ i += mylen
+ i = slice(i, i + 1)
+ # get removed indices
+ removed = list(range(*i.indices(mylen)))
+ removed.reverse()
+ # fixup indices in token dictionary
+ for name, occurrences in self._tokdict.items():
+ for j in removed:
+ for k, (value, position) in enumerate(occurrences):
+ occurrences[k] = _ParseResultsWithOffset(
+ value, position - (position > j)
+ )
+ else:
+ del self._tokdict[i]
+
+ def __contains__(self, k) -> bool:
+ return k in self._tokdict
+
+ def __len__(self) -> int:
+ return len(self._toklist)
+
+ def __bool__(self) -> bool:
+ return not not (self._toklist or self._tokdict)
+
+ def __iter__(self) -> Iterator:
+ return iter(self._toklist)
+
+ def __reversed__(self) -> Iterator:
+ return iter(self._toklist[::-1])
+
+ def keys(self):
+ return iter(self._tokdict)
+
+ def values(self):
+ return (self[k] for k in self.keys())
+
+ def items(self):
+ return ((k, self[k]) for k in self.keys())
+
+ def haskeys(self) -> bool:
+ """
+ Since ``keys()`` returns an iterator, this method is helpful in bypassing
+ code that looks for the existence of any defined results names."""
+ return bool(self._tokdict)
+
+ def pop(self, *args, **kwargs):
+ """
+ Removes and returns item at specified index (default= ``last``).
+ Supports both ``list`` and ``dict`` semantics for ``pop()``. If
+ passed no argument or an integer argument, it will use ``list``
+ semantics and pop tokens from the list of parsed tokens. If passed
+ a non-integer argument (most likely a string), it will use ``dict``
+ semantics and pop the corresponding value from any defined results
+ names. A second default return value argument is supported, just as in
+ ``dict.pop()``.
+
+ Example::
+
+ numlist = Word(nums)[...]
+ print(numlist.parse_string("0 123 321")) # -> ['0', '123', '321']
+
+ def remove_first(tokens):
+ tokens.pop(0)
+ numlist.add_parse_action(remove_first)
+ print(numlist.parse_string("0 123 321")) # -> ['123', '321']
+
+ label = Word(alphas)
+ patt = label("LABEL") + Word(nums)[1, ...]
+ print(patt.parse_string("AAB 123 321").dump())
+
+ # Use pop() in a parse action to remove named result (note that corresponding value is not
+ # removed from list form of results)
+ def remove_LABEL(tokens):
+ tokens.pop("LABEL")
+ return tokens
+ patt.add_parse_action(remove_LABEL)
+ print(patt.parse_string("AAB 123 321").dump())
+
+ prints::
+
+ ['AAB', '123', '321']
+ - LABEL: 'AAB'
+
+ ['AAB', '123', '321']
+ """
+ if not args:
+ args = [-1]
+ for k, v in kwargs.items():
+ if k == "default":
+ args = (args[0], v)
+ else:
+ raise TypeError(
+ "pop() got an unexpected keyword argument {!r}".format(k)
+ )
+ if isinstance(args[0], int) or len(args) == 1 or args[0] in self:
+ index = args[0]
+ ret = self[index]
+ del self[index]
+ return ret
+ else:
+ defaultvalue = args[1]
+ return defaultvalue
+
+ def get(self, key, default_value=None):
+ """
+ Returns named result matching the given key, or if there is no
+ such name, then returns the given ``default_value`` or ``None`` if no
+ ``default_value`` is specified.
+
+ Similar to ``dict.get()``.
+
+ Example::
+
+ integer = Word(nums)
+ date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
+
+ result = date_str.parse_string("1999/12/31")
+ print(result.get("year")) # -> '1999'
+ print(result.get("hour", "not specified")) # -> 'not specified'
+ print(result.get("hour")) # -> None
+ """
+ if key in self:
+ return self[key]
+ else:
+ return default_value
+
+ def insert(self, index, ins_string):
+ """
+ Inserts new element at location index in the list of parsed tokens.
+
+ Similar to ``list.insert()``.
+
+ Example::
+
+ numlist = Word(nums)[...]
+ print(numlist.parse_string("0 123 321")) # -> ['0', '123', '321']
+
+ # use a parse action to insert the parse location in the front of the parsed results
+ def insert_locn(locn, tokens):
+ tokens.insert(0, locn)
+ numlist.add_parse_action(insert_locn)
+ print(numlist.parse_string("0 123 321")) # -> [0, '0', '123', '321']
+ """
+ self._toklist.insert(index, ins_string)
+ # fixup indices in token dictionary
+ for name, occurrences in self._tokdict.items():
+ for k, (value, position) in enumerate(occurrences):
+ occurrences[k] = _ParseResultsWithOffset(
+ value, position + (position > index)
+ )
+
+ def append(self, item):
+ """
+ Add single element to end of ``ParseResults`` list of elements.
+
+ Example::
+
+ numlist = Word(nums)[...]
+ print(numlist.parse_string("0 123 321")) # -> ['0', '123', '321']
+
+ # use a parse action to compute the sum of the parsed integers, and add it to the end
+ def append_sum(tokens):
+ tokens.append(sum(map(int, tokens)))
+ numlist.add_parse_action(append_sum)
+ print(numlist.parse_string("0 123 321")) # -> ['0', '123', '321', 444]
+ """
+ self._toklist.append(item)
+
+ def extend(self, itemseq):
+ """
+ Add sequence of elements to end of ``ParseResults`` list of elements.
+
+ Example::
+
+ patt = Word(alphas)[1, ...]
+
+ # use a parse action to append the reverse of the matched strings, to make a palindrome
+ def make_palindrome(tokens):
+ tokens.extend(reversed([t[::-1] for t in tokens]))
+ return ''.join(tokens)
+ patt.add_parse_action(make_palindrome)
+ print(patt.parse_string("lskdj sdlkjf lksd")) # -> 'lskdjsdlkjflksddsklfjkldsjdksl'
+ """
+ if isinstance(itemseq, ParseResults):
+ self.__iadd__(itemseq)
+ else:
+ self._toklist.extend(itemseq)
+
+ def clear(self):
+ """
+ Clear all elements and results names.
+ """
+ del self._toklist[:]
+ self._tokdict.clear()
+
+ def __getattr__(self, name):
+ try:
+ return self[name]
+ except KeyError:
+ if name.startswith("__"):
+ raise AttributeError(name)
+ return ""
+
+ def __add__(self, other) -> "ParseResults":
+ ret = self.copy()
+ ret += other
+ return ret
+
+ def __iadd__(self, other) -> "ParseResults":
+ if other._tokdict:
+ offset = len(self._toklist)
+ addoffset = lambda a: offset if a < 0 else a + offset
+ otheritems = other._tokdict.items()
+ otherdictitems = [
+ (k, _ParseResultsWithOffset(v[0], addoffset(v[1])))
+ for k, vlist in otheritems
+ for v in vlist
+ ]
+ for k, v in otherdictitems:
+ self[k] = v
+ if isinstance(v[0], ParseResults):
+ v[0]._parent = wkref(self)
+
+ self._toklist += other._toklist
+ self._all_names |= other._all_names
+ return self
+
+ def __radd__(self, other) -> "ParseResults":
+ if isinstance(other, int) and other == 0:
+ # useful for merging many ParseResults using sum() builtin
+ return self.copy()
+ else:
+ # this may raise a TypeError - so be it
+ return other + self
+
+ def __repr__(self) -> str:
+ return "{}({!r}, {})".format(type(self).__name__, self._toklist, self.as_dict())
+
+ def __str__(self) -> str:
+ return (
+ "["
+ + ", ".join(
+ [
+ str(i) if isinstance(i, ParseResults) else repr(i)
+ for i in self._toklist
+ ]
+ )
+ + "]"
+ )
+
+ def _asStringList(self, sep=""):
+ out = []
+ for item in self._toklist:
+ if out and sep:
+ out.append(sep)
+ if isinstance(item, ParseResults):
+ out += item._asStringList()
+ else:
+ out.append(str(item))
+ return out
+
+ def as_list(self) -> list:
+ """
+ Returns the parse results as a nested list of matching tokens, all converted to strings.
+
+ Example::
+
+ patt = Word(alphas)[1, ...]
+ result = patt.parse_string("sldkj lsdkj sldkj")
+ # even though the result prints in string-like form, it is actually a pyparsing ParseResults
+ print(type(result), result) # -> <class 'pyparsing.ParseResults'> ['sldkj', 'lsdkj', 'sldkj']
+
+ # Use as_list() to create an actual list
+ result_list = result.as_list()
+ print(type(result_list), result_list) # -> <class 'list'> ['sldkj', 'lsdkj', 'sldkj']
+ """
+ return [
+ res.as_list() if isinstance(res, ParseResults) else res
+ for res in self._toklist
+ ]
+
+ def as_dict(self) -> dict:
+ """
+ Returns the named parse results as a nested dictionary.
+
+ Example::
+
+ integer = Word(nums)
+ date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
+
+ result = date_str.parse_string('12/31/1999')
+ print(type(result), repr(result)) # -> <class 'pyparsing.ParseResults'> (['12', '/', '31', '/', '1999'], {'day': [('1999', 4)], 'year': [('12', 0)], 'month': [('31', 2)]})
+
+ result_dict = result.as_dict()
+ print(type(result_dict), repr(result_dict)) # -> <class 'dict'> {'day': '1999', 'year': '12', 'month': '31'}
+
+ # even though a ParseResults supports dict-like access, sometime you just need to have a dict
+ import json
+ print(json.dumps(result)) # -> Exception: TypeError: ... is not JSON serializable
+ print(json.dumps(result.as_dict())) # -> {"month": "31", "day": "1999", "year": "12"}
+ """
+
+ def to_item(obj):
+ if isinstance(obj, ParseResults):
+ return obj.as_dict() if obj.haskeys() else [to_item(v) for v in obj]
+ else:
+ return obj
+
+ return dict((k, to_item(v)) for k, v in self.items())
+
+ def copy(self) -> "ParseResults":
+ """
+ Returns a new copy of a :class:`ParseResults` object.
+ """
+ ret = ParseResults(self._toklist)
+ ret._tokdict = self._tokdict.copy()
+ ret._parent = self._parent
+ ret._all_names |= self._all_names
+ ret._name = self._name
+ return ret
+
+ def get_name(self):
+ r"""
+ Returns the results name for this token expression. Useful when several
+ different expressions might match at a particular location.
+
+ Example::
+
+ integer = Word(nums)
+ ssn_expr = Regex(r"\d\d\d-\d\d-\d\d\d\d")
+ house_number_expr = Suppress('#') + Word(nums, alphanums)
+ user_data = (Group(house_number_expr)("house_number")
+ | Group(ssn_expr)("ssn")
+ | Group(integer)("age"))
+ user_info = user_data[1, ...]
+
+ result = user_info.parse_string("22 111-22-3333 #221B")
+ for item in result:
+ print(item.get_name(), ':', item[0])
+
+ prints::
+
+ age : 22
+ ssn : 111-22-3333
+ house_number : 221B
+ """
+ if self._name:
+ return self._name
+ elif self._parent:
+ par = self._parent()
+
+ def find_in_parent(sub):
+ return next(
+ (
+ k
+ for k, vlist in par._tokdict.items()
+ for v, loc in vlist
+ if sub is v
+ ),
+ None,
+ )
+
+ return find_in_parent(self) if par else None
+ elif (
+ len(self) == 1
+ and len(self._tokdict) == 1
+ and next(iter(self._tokdict.values()))[0][1] in (0, -1)
+ ):
+ return next(iter(self._tokdict.keys()))
+ else:
+ return None
+
+ def dump(self, indent="", full=True, include_list=True, _depth=0) -> str:
+ """
+ Diagnostic method for listing out the contents of
+ a :class:`ParseResults`. Accepts an optional ``indent`` argument so
+ that this string can be embedded in a nested display of other data.
+
+ Example::
+
+ integer = Word(nums)
+ date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
+
+ result = date_str.parse_string('1999/12/31')
+ print(result.dump())
+
+ prints::
+
+ ['1999', '/', '12', '/', '31']
+ - day: '31'
+ - month: '12'
+ - year: '1999'
+ """
+ out = []
+ NL = "\n"
+ out.append(indent + str(self.as_list()) if include_list else "")
+
+ if full:
+ if self.haskeys():
+ items = sorted((str(k), v) for k, v in self.items())
+ for k, v in items:
+ if out:
+ out.append(NL)
+ out.append("{}{}- {}: ".format(indent, (" " * _depth), k))
+ if isinstance(v, ParseResults):
+ if v:
+ out.append(
+ v.dump(
+ indent=indent,
+ full=full,
+ include_list=include_list,
+ _depth=_depth + 1,
+ )
+ )
+ else:
+ out.append(str(v))
+ else:
+ out.append(repr(v))
+ if any(isinstance(vv, ParseResults) for vv in self):
+ v = self
+ for i, vv in enumerate(v):
+ if isinstance(vv, ParseResults):
+ out.append(
+ "\n{}{}[{}]:\n{}{}{}".format(
+ indent,
+ (" " * (_depth)),
+ i,
+ indent,
+ (" " * (_depth + 1)),
+ vv.dump(
+ indent=indent,
+ full=full,
+ include_list=include_list,
+ _depth=_depth + 1,
+ ),
+ )
+ )
+ else:
+ out.append(
+ "\n%s%s[%d]:\n%s%s%s"
+ % (
+ indent,
+ (" " * (_depth)),
+ i,
+ indent,
+ (" " * (_depth + 1)),
+ str(vv),
+ )
+ )
+
+ return "".join(out)
+
+ def pprint(self, *args, **kwargs):
+ """
+ Pretty-printer for parsed results as a list, using the
+ `pprint <https://docs.python.org/3/library/pprint.html>`_ module.
+ Accepts additional positional or keyword args as defined for
+ `pprint.pprint <https://docs.python.org/3/library/pprint.html#pprint.pprint>`_ .
+
+ Example::
+
+ ident = Word(alphas, alphanums)
+ num = Word(nums)
+ func = Forward()
+ term = ident | num | Group('(' + func + ')')
+ func <<= ident + Group(Optional(delimited_list(term)))
+ result = func.parse_string("fna a,b,(fnb c,d,200),100")
+ result.pprint(width=40)
+
+ prints::
+
+ ['fna',
+ ['a',
+ 'b',
+ ['(', 'fnb', ['c', 'd', '200'], ')'],
+ '100']]
+ """
+ pprint.pprint(self.as_list(), *args, **kwargs)
+
+ # add support for pickle protocol
+ def __getstate__(self):
+ return (
+ self._toklist,
+ (
+ self._tokdict.copy(),
+ self._parent is not None and self._parent() or None,
+ self._all_names,
+ self._name,
+ ),
+ )
+
+ def __setstate__(self, state):
+ self._toklist, (self._tokdict, par, inAccumNames, self._name) = state
+ self._all_names = set(inAccumNames)
+ if par is not None:
+ self._parent = wkref(par)
+ else:
+ self._parent = None
+
+ def __getnewargs__(self):
+ return self._toklist, self._name
+
+ def __dir__(self):
+ return dir(type(self)) + list(self.keys())
+
+ @classmethod
+ def from_dict(cls, other, name=None) -> "ParseResults":
+ """
+ Helper classmethod to construct a ``ParseResults`` from a ``dict``, preserving the
+ name-value relations as results names. If an optional ``name`` argument is
+ given, a nested ``ParseResults`` will be returned.
+ """
+
+ def is_iterable(obj):
+ try:
+ iter(obj)
+ except Exception:
+ return False
+ else:
+ return not isinstance(obj, str_type)
+
+ ret = cls([])
+ for k, v in other.items():
+ if isinstance(v, Mapping):
+ ret += cls.from_dict(v, name=k)
+ else:
+ ret += cls([v], name=k, asList=is_iterable(v))
+ if name is not None:
+ ret = cls([ret], name=name)
+ return ret
+
+ asList = as_list
+ asDict = as_dict
+ getName = get_name
+
+
+MutableMapping.register(ParseResults)
+MutableSequence.register(ParseResults)
diff --git a/src/pip/_vendor/pyparsing/testing.py b/src/pip/_vendor/pyparsing/testing.py
new file mode 100644
index 000000000..84a0ef170
--- /dev/null
+++ b/src/pip/_vendor/pyparsing/testing.py
@@ -0,0 +1,331 @@
+# testing.py
+
+from contextlib import contextmanager
+import typing
+
+from .core import (
+ ParserElement,
+ ParseException,
+ Keyword,
+ __diag__,
+ __compat__,
+)
+
+
+class pyparsing_test:
+ """
+ namespace class for classes useful in writing unit tests
+ """
+
+ class reset_pyparsing_context:
+ """
+ Context manager to be used when writing unit tests that modify pyparsing config values:
+ - packrat parsing
+ - bounded recursion parsing
+ - default whitespace characters.
+ - default keyword characters
+ - literal string auto-conversion class
+ - __diag__ settings
+
+ Example::
+
+ with reset_pyparsing_context():
+ # test that literals used to construct a grammar are automatically suppressed
+ ParserElement.inlineLiteralsUsing(Suppress)
+
+ term = Word(alphas) | Word(nums)
+ group = Group('(' + term[...] + ')')
+
+ # assert that the '()' characters are not included in the parsed tokens
+ self.assertParseAndCheckList(group, "(abc 123 def)", ['abc', '123', 'def'])
+
+ # after exiting context manager, literals are converted to Literal expressions again
+ """
+
+ def __init__(self):
+ self._save_context = {}
+
+ def save(self):
+ self._save_context["default_whitespace"] = ParserElement.DEFAULT_WHITE_CHARS
+ self._save_context["default_keyword_chars"] = Keyword.DEFAULT_KEYWORD_CHARS
+
+ self._save_context[
+ "literal_string_class"
+ ] = ParserElement._literalStringClass
+
+ self._save_context["verbose_stacktrace"] = ParserElement.verbose_stacktrace
+
+ self._save_context["packrat_enabled"] = ParserElement._packratEnabled
+ if ParserElement._packratEnabled:
+ self._save_context[
+ "packrat_cache_size"
+ ] = ParserElement.packrat_cache.size
+ else:
+ self._save_context["packrat_cache_size"] = None
+ self._save_context["packrat_parse"] = ParserElement._parse
+ self._save_context[
+ "recursion_enabled"
+ ] = ParserElement._left_recursion_enabled
+
+ self._save_context["__diag__"] = {
+ name: getattr(__diag__, name) for name in __diag__._all_names
+ }
+
+ self._save_context["__compat__"] = {
+ "collect_all_And_tokens": __compat__.collect_all_And_tokens
+ }
+
+ return self
+
+ def restore(self):
+ # reset pyparsing global state
+ if (
+ ParserElement.DEFAULT_WHITE_CHARS
+ != self._save_context["default_whitespace"]
+ ):
+ ParserElement.set_default_whitespace_chars(
+ self._save_context["default_whitespace"]
+ )
+
+ ParserElement.verbose_stacktrace = self._save_context["verbose_stacktrace"]
+
+ Keyword.DEFAULT_KEYWORD_CHARS = self._save_context["default_keyword_chars"]
+ ParserElement.inlineLiteralsUsing(
+ self._save_context["literal_string_class"]
+ )
+
+ for name, value in self._save_context["__diag__"].items():
+ (__diag__.enable if value else __diag__.disable)(name)
+
+ ParserElement._packratEnabled = False
+ if self._save_context["packrat_enabled"]:
+ ParserElement.enable_packrat(self._save_context["packrat_cache_size"])
+ else:
+ ParserElement._parse = self._save_context["packrat_parse"]
+ ParserElement._left_recursion_enabled = self._save_context[
+ "recursion_enabled"
+ ]
+
+ __compat__.collect_all_And_tokens = self._save_context["__compat__"]
+
+ return self
+
+ def copy(self):
+ ret = type(self)()
+ ret._save_context.update(self._save_context)
+ return ret
+
+ def __enter__(self):
+ return self.save()
+
+ def __exit__(self, *args):
+ self.restore()
+
+ class TestParseResultsAsserts:
+ """
+ A mixin class to add parse results assertion methods to normal unittest.TestCase classes.
+ """
+
+ def assertParseResultsEquals(
+ self, result, expected_list=None, expected_dict=None, msg=None
+ ):
+ """
+ Unit test assertion to compare a :class:`ParseResults` object with an optional ``expected_list``,
+ and compare any defined results names with an optional ``expected_dict``.
+ """
+ if expected_list is not None:
+ self.assertEqual(expected_list, result.as_list(), msg=msg)
+ if expected_dict is not None:
+ self.assertEqual(expected_dict, result.as_dict(), msg=msg)
+
+ def assertParseAndCheckList(
+ self, expr, test_string, expected_list, msg=None, verbose=True
+ ):
+ """
+ Convenience wrapper assert to test a parser element and input string, and assert that
+ the resulting ``ParseResults.asList()`` is equal to the ``expected_list``.
+ """
+ result = expr.parse_string(test_string, parse_all=True)
+ if verbose:
+ print(result.dump())
+ else:
+ print(result.as_list())
+ self.assertParseResultsEquals(result, expected_list=expected_list, msg=msg)
+
+ def assertParseAndCheckDict(
+ self, expr, test_string, expected_dict, msg=None, verbose=True
+ ):
+ """
+ Convenience wrapper assert to test a parser element and input string, and assert that
+ the resulting ``ParseResults.asDict()`` is equal to the ``expected_dict``.
+ """
+ result = expr.parse_string(test_string, parseAll=True)
+ if verbose:
+ print(result.dump())
+ else:
+ print(result.as_list())
+ self.assertParseResultsEquals(result, expected_dict=expected_dict, msg=msg)
+
+ def assertRunTestResults(
+ self, run_tests_report, expected_parse_results=None, msg=None
+ ):
+ """
+ Unit test assertion to evaluate output of ``ParserElement.runTests()``. If a list of
+ list-dict tuples is given as the ``expected_parse_results`` argument, then these are zipped
+ with the report tuples returned by ``runTests`` and evaluated using ``assertParseResultsEquals``.
+ Finally, asserts that the overall ``runTests()`` success value is ``True``.
+
+ :param run_tests_report: tuple(bool, [tuple(str, ParseResults or Exception)]) returned from runTests
+ :param expected_parse_results (optional): [tuple(str, list, dict, Exception)]
+ """
+ run_test_success, run_test_results = run_tests_report
+
+ if expected_parse_results is not None:
+ merged = [
+ (*rpt, expected)
+ for rpt, expected in zip(run_test_results, expected_parse_results)
+ ]
+ for test_string, result, expected in merged:
+ # expected should be a tuple containing a list and/or a dict or an exception,
+ # and optional failure message string
+ # an empty tuple will skip any result validation
+ fail_msg = next(
+ (exp for exp in expected if isinstance(exp, str)), None
+ )
+ expected_exception = next(
+ (
+ exp
+ for exp in expected
+ if isinstance(exp, type) and issubclass(exp, Exception)
+ ),
+ None,
+ )
+ if expected_exception is not None:
+ with self.assertRaises(
+ expected_exception=expected_exception, msg=fail_msg or msg
+ ):
+ if isinstance(result, Exception):
+ raise result
+ else:
+ expected_list = next(
+ (exp for exp in expected if isinstance(exp, list)), None
+ )
+ expected_dict = next(
+ (exp for exp in expected if isinstance(exp, dict)), None
+ )
+ if (expected_list, expected_dict) != (None, None):
+ self.assertParseResultsEquals(
+ result,
+ expected_list=expected_list,
+ expected_dict=expected_dict,
+ msg=fail_msg or msg,
+ )
+ else:
+ # warning here maybe?
+ print("no validation for {!r}".format(test_string))
+
+ # do this last, in case some specific test results can be reported instead
+ self.assertTrue(
+ run_test_success, msg=msg if msg is not None else "failed runTests"
+ )
+
+ @contextmanager
+ def assertRaisesParseException(self, exc_type=ParseException, msg=None):
+ with self.assertRaises(exc_type, msg=msg):
+ yield
+
+ @staticmethod
+ def with_line_numbers(
+ s: str,
+ start_line: typing.Optional[int] = None,
+ end_line: typing.Optional[int] = None,
+ expand_tabs: bool = True,
+ eol_mark: str = "|",
+ mark_spaces: typing.Optional[str] = None,
+ mark_control: typing.Optional[str] = None,
+ ) -> str:
+ """
+ Helpful method for debugging a parser - prints a string with line and column numbers.
+ (Line and column numbers are 1-based.)
+
+ :param s: tuple(bool, str - string to be printed with line and column numbers
+ :param start_line: int - (optional) starting line number in s to print (default=1)
+ :param end_line: int - (optional) ending line number in s to print (default=len(s))
+ :param expand_tabs: bool - (optional) expand tabs to spaces, to match the pyparsing default
+ :param eol_mark: str - (optional) string to mark the end of lines, helps visualize trailing spaces (default="|")
+ :param mark_spaces: str - (optional) special character to display in place of spaces
+ :param mark_control: str - (optional) convert non-printing control characters to a placeholding
+ character; valid values:
+ - "unicode" - replaces control chars with Unicode symbols, such as "â" and "âŠ"
+ - any single character string - replace control characters with given string
+ - None (default) - string is displayed as-is
+
+ :return: str - input string with leading line numbers and column number headers
+ """
+ if expand_tabs:
+ s = s.expandtabs()
+ if mark_control is not None:
+ if mark_control == "unicode":
+ tbl = str.maketrans(
+ {c: u for c, u in zip(range(0, 33), range(0x2400, 0x2433))}
+ | {127: 0x2421}
+ )
+ eol_mark = ""
+ else:
+ tbl = str.maketrans(
+ {c: mark_control for c in list(range(0, 32)) + [127]}
+ )
+ s = s.translate(tbl)
+ if mark_spaces is not None and mark_spaces != " ":
+ if mark_spaces == "unicode":
+ tbl = str.maketrans({9: 0x2409, 32: 0x2423})
+ s = s.translate(tbl)
+ else:
+ s = s.replace(" ", mark_spaces)
+ if start_line is None:
+ start_line = 1
+ if end_line is None:
+ end_line = len(s)
+ end_line = min(end_line, len(s))
+ start_line = min(max(1, start_line), end_line)
+
+ if mark_control != "unicode":
+ s_lines = s.splitlines()[start_line - 1 : end_line]
+ else:
+ s_lines = [line + "âŠ" for line in s.split("âŠ")[start_line - 1 : end_line]]
+ if not s_lines:
+ return ""
+
+ lineno_width = len(str(end_line))
+ max_line_len = max(len(line) for line in s_lines)
+ lead = " " * (lineno_width + 1)
+ if max_line_len >= 99:
+ header0 = (
+ lead
+ + "".join(
+ "{}{}".format(" " * 99, (i + 1) % 100)
+ for i in range(max(max_line_len // 100, 1))
+ )
+ + "\n"
+ )
+ else:
+ header0 = ""
+ header1 = (
+ header0
+ + lead
+ + "".join(
+ " {}".format((i + 1) % 10)
+ for i in range(-(-max_line_len // 10))
+ )
+ + "\n"
+ )
+ header2 = lead + "1234567890" * (-(-max_line_len // 10)) + "\n"
+ return (
+ header1
+ + header2
+ + "\n".join(
+ "{:{}d}:{}{}".format(i, lineno_width, line, eol_mark)
+ for i, line in enumerate(s_lines, start=start_line)
+ )
+ + "\n"
+ )
diff --git a/src/pip/_vendor/pyparsing/unicode.py b/src/pip/_vendor/pyparsing/unicode.py
new file mode 100644
index 000000000..065262039
--- /dev/null
+++ b/src/pip/_vendor/pyparsing/unicode.py
@@ -0,0 +1,352 @@
+# unicode.py
+
+import sys
+from itertools import filterfalse
+from typing import List, Tuple, Union
+
+
+class _lazyclassproperty:
+ def __init__(self, fn):
+ self.fn = fn
+ self.__doc__ = fn.__doc__
+ self.__name__ = fn.__name__
+
+ def __get__(self, obj, cls):
+ if cls is None:
+ cls = type(obj)
+ if not hasattr(cls, "_intern") or any(
+ cls._intern is getattr(superclass, "_intern", [])
+ for superclass in cls.__mro__[1:]
+ ):
+ cls._intern = {}
+ attrname = self.fn.__name__
+ if attrname not in cls._intern:
+ cls._intern[attrname] = self.fn(cls)
+ return cls._intern[attrname]
+
+
+UnicodeRangeList = List[Union[Tuple[int, int], Tuple[int]]]
+
+
+class unicode_set:
+ """
+ A set of Unicode characters, for language-specific strings for
+ ``alphas``, ``nums``, ``alphanums``, and ``printables``.
+ A unicode_set is defined by a list of ranges in the Unicode character
+ set, in a class attribute ``_ranges``. Ranges can be specified using
+ 2-tuples or a 1-tuple, such as::
+
+ _ranges = [
+ (0x0020, 0x007e),
+ (0x00a0, 0x00ff),
+ (0x0100,),
+ ]
+
+ Ranges are left- and right-inclusive. A 1-tuple of (x,) is treated as (x, x).
+
+ A unicode set can also be defined using multiple inheritance of other unicode sets::
+
+ class CJK(Chinese, Japanese, Korean):
+ pass
+ """
+
+ _ranges: UnicodeRangeList = []
+
+ @_lazyclassproperty
+ def _chars_for_ranges(cls):
+ ret = []
+ for cc in cls.__mro__:
+ if cc is unicode_set:
+ break
+ for rr in getattr(cc, "_ranges", ()):
+ ret.extend(range(rr[0], rr[-1] + 1))
+ return [chr(c) for c in sorted(set(ret))]
+
+ @_lazyclassproperty
+ def printables(cls):
+ "all non-whitespace characters in this range"
+ return "".join(filterfalse(str.isspace, cls._chars_for_ranges))
+
+ @_lazyclassproperty
+ def alphas(cls):
+ "all alphabetic characters in this range"
+ return "".join(filter(str.isalpha, cls._chars_for_ranges))
+
+ @_lazyclassproperty
+ def nums(cls):
+ "all numeric digit characters in this range"
+ return "".join(filter(str.isdigit, cls._chars_for_ranges))
+
+ @_lazyclassproperty
+ def alphanums(cls):
+ "all alphanumeric characters in this range"
+ return cls.alphas + cls.nums
+
+ @_lazyclassproperty
+ def identchars(cls):
+ "all characters in this range that are valid identifier characters, plus underscore '_'"
+ return "".join(
+ sorted(
+ set(
+ "".join(filter(str.isidentifier, cls._chars_for_ranges))
+ + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzªµº"
+ + "ÀÃÂÃÄÅÆÇÈÉÊËÌÃÃŽÃÃÑÒÓÔÕÖØÙÚÛÜÃÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ"
+ + "_"
+ )
+ )
+ )
+
+ @_lazyclassproperty
+ def identbodychars(cls):
+ """
+ all characters in this range that are valid identifier body characters,
+ plus the digits 0-9
+ """
+ return "".join(
+ sorted(
+ set(
+ cls.identchars
+ + "0123456789"
+ + "".join(
+ [c for c in cls._chars_for_ranges if ("_" + c).isidentifier()]
+ )
+ )
+ )
+ )
+
+
+class pyparsing_unicode(unicode_set):
+ """
+ A namespace class for defining common language unicode_sets.
+ """
+
+ # fmt: off
+
+ # define ranges in language character sets
+ _ranges: UnicodeRangeList = [
+ (0x0020, sys.maxunicode),
+ ]
+
+ class BasicMultilingualPlane(unicode_set):
+ "Unicode set for the Basic Multilingual Plane"
+ _ranges: UnicodeRangeList = [
+ (0x0020, 0xFFFF),
+ ]
+
+ class Latin1(unicode_set):
+ "Unicode set for Latin-1 Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x0020, 0x007E),
+ (0x00A0, 0x00FF),
+ ]
+
+ class LatinA(unicode_set):
+ "Unicode set for Latin-A Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x0100, 0x017F),
+ ]
+
+ class LatinB(unicode_set):
+ "Unicode set for Latin-B Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x0180, 0x024F),
+ ]
+
+ class Greek(unicode_set):
+ "Unicode set for Greek Unicode Character Ranges"
+ _ranges: UnicodeRangeList = [
+ (0x0342, 0x0345),
+ (0x0370, 0x0377),
+ (0x037A, 0x037F),
+ (0x0384, 0x038A),
+ (0x038C,),
+ (0x038E, 0x03A1),
+ (0x03A3, 0x03E1),
+ (0x03F0, 0x03FF),
+ (0x1D26, 0x1D2A),
+ (0x1D5E,),
+ (0x1D60,),
+ (0x1D66, 0x1D6A),
+ (0x1F00, 0x1F15),
+ (0x1F18, 0x1F1D),
+ (0x1F20, 0x1F45),
+ (0x1F48, 0x1F4D),
+ (0x1F50, 0x1F57),
+ (0x1F59,),
+ (0x1F5B,),
+ (0x1F5D,),
+ (0x1F5F, 0x1F7D),
+ (0x1F80, 0x1FB4),
+ (0x1FB6, 0x1FC4),
+ (0x1FC6, 0x1FD3),
+ (0x1FD6, 0x1FDB),
+ (0x1FDD, 0x1FEF),
+ (0x1FF2, 0x1FF4),
+ (0x1FF6, 0x1FFE),
+ (0x2129,),
+ (0x2719, 0x271A),
+ (0xAB65,),
+ (0x10140, 0x1018D),
+ (0x101A0,),
+ (0x1D200, 0x1D245),
+ (0x1F7A1, 0x1F7A7),
+ ]
+
+ class Cyrillic(unicode_set):
+ "Unicode set for Cyrillic Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x0400, 0x052F),
+ (0x1C80, 0x1C88),
+ (0x1D2B,),
+ (0x1D78,),
+ (0x2DE0, 0x2DFF),
+ (0xA640, 0xA672),
+ (0xA674, 0xA69F),
+ (0xFE2E, 0xFE2F),
+ ]
+
+ class Chinese(unicode_set):
+ "Unicode set for Chinese Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x2E80, 0x2E99),
+ (0x2E9B, 0x2EF3),
+ (0x31C0, 0x31E3),
+ (0x3400, 0x4DB5),
+ (0x4E00, 0x9FEF),
+ (0xA700, 0xA707),
+ (0xF900, 0xFA6D),
+ (0xFA70, 0xFAD9),
+ (0x16FE2, 0x16FE3),
+ (0x1F210, 0x1F212),
+ (0x1F214, 0x1F23B),
+ (0x1F240, 0x1F248),
+ (0x20000, 0x2A6D6),
+ (0x2A700, 0x2B734),
+ (0x2B740, 0x2B81D),
+ (0x2B820, 0x2CEA1),
+ (0x2CEB0, 0x2EBE0),
+ (0x2F800, 0x2FA1D),
+ ]
+
+ class Japanese(unicode_set):
+ "Unicode set for Japanese Unicode Character Range, combining Kanji, Hiragana, and Katakana ranges"
+ _ranges: UnicodeRangeList = []
+
+ class Kanji(unicode_set):
+ "Unicode set for Kanji Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x4E00, 0x9FBF),
+ (0x3000, 0x303F),
+ ]
+
+ class Hiragana(unicode_set):
+ "Unicode set for Hiragana Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x3041, 0x3096),
+ (0x3099, 0x30A0),
+ (0x30FC,),
+ (0xFF70,),
+ (0x1B001,),
+ (0x1B150, 0x1B152),
+ (0x1F200,),
+ ]
+
+ class Katakana(unicode_set):
+ "Unicode set for Katakana Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x3099, 0x309C),
+ (0x30A0, 0x30FF),
+ (0x31F0, 0x31FF),
+ (0x32D0, 0x32FE),
+ (0xFF65, 0xFF9F),
+ (0x1B000,),
+ (0x1B164, 0x1B167),
+ (0x1F201, 0x1F202),
+ (0x1F213,),
+ ]
+
+ class Hangul(unicode_set):
+ "Unicode set for Hangul (Korean) Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x1100, 0x11FF),
+ (0x302E, 0x302F),
+ (0x3131, 0x318E),
+ (0x3200, 0x321C),
+ (0x3260, 0x327B),
+ (0x327E,),
+ (0xA960, 0xA97C),
+ (0xAC00, 0xD7A3),
+ (0xD7B0, 0xD7C6),
+ (0xD7CB, 0xD7FB),
+ (0xFFA0, 0xFFBE),
+ (0xFFC2, 0xFFC7),
+ (0xFFCA, 0xFFCF),
+ (0xFFD2, 0xFFD7),
+ (0xFFDA, 0xFFDC),
+ ]
+
+ Korean = Hangul
+
+ class CJK(Chinese, Japanese, Hangul):
+ "Unicode set for combined Chinese, Japanese, and Korean (CJK) Unicode Character Range"
+
+ class Thai(unicode_set):
+ "Unicode set for Thai Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x0E01, 0x0E3A),
+ (0x0E3F, 0x0E5B)
+ ]
+
+ class Arabic(unicode_set):
+ "Unicode set for Arabic Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x0600, 0x061B),
+ (0x061E, 0x06FF),
+ (0x0700, 0x077F),
+ ]
+
+ class Hebrew(unicode_set):
+ "Unicode set for Hebrew Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x0591, 0x05C7),
+ (0x05D0, 0x05EA),
+ (0x05EF, 0x05F4),
+ (0xFB1D, 0xFB36),
+ (0xFB38, 0xFB3C),
+ (0xFB3E,),
+ (0xFB40, 0xFB41),
+ (0xFB43, 0xFB44),
+ (0xFB46, 0xFB4F),
+ ]
+
+ class Devanagari(unicode_set):
+ "Unicode set for Devanagari Unicode Character Range"
+ _ranges: UnicodeRangeList = [
+ (0x0900, 0x097F),
+ (0xA8E0, 0xA8FF)
+ ]
+
+ # fmt: on
+
+
+pyparsing_unicode.Japanese._ranges = (
+ pyparsing_unicode.Japanese.Kanji._ranges
+ + pyparsing_unicode.Japanese.Hiragana._ranges
+ + pyparsing_unicode.Japanese.Katakana._ranges
+)
+
+pyparsing_unicode.BMP = pyparsing_unicode.BasicMultilingualPlane
+
+# add language identifiers using language Unicode
+pyparsing_unicode.العربية = pyparsing_unicode.Arabic
+pyparsing_unicode.中文 = pyparsing_unicode.Chinese
+pyparsing_unicode.кириллица = pyparsing_unicode.Cyrillic
+pyparsing_unicode.Ελληνικά = pyparsing_unicode.Greek
+pyparsing_unicode.עִברִית = pyparsing_unicode.Hebrew
+pyparsing_unicode.日本語 = pyparsing_unicode.Japanese
+pyparsing_unicode.Japanese.漢字 = pyparsing_unicode.Japanese.Kanji
+pyparsing_unicode.Japanese.カタカナ = pyparsing_unicode.Japanese.Katakana
+pyparsing_unicode.Japanese.ã²ã‚‰ãŒãª = pyparsing_unicode.Japanese.Hiragana
+pyparsing_unicode.한국어 = pyparsing_unicode.Korean
+pyparsing_unicode.ไทย = pyparsing_unicode.Thai
+pyparsing_unicode.देवनागरी = pyparsing_unicode.Devanagari
diff --git a/src/pip/_vendor/pyparsing/util.py b/src/pip/_vendor/pyparsing/util.py
new file mode 100644
index 000000000..34ce092c6
--- /dev/null
+++ b/src/pip/_vendor/pyparsing/util.py
@@ -0,0 +1,235 @@
+# util.py
+import warnings
+import types
+import collections
+import itertools
+from functools import lru_cache
+from typing import List, Union, Iterable
+
+_bslash = chr(92)
+
+
+class __config_flags:
+ """Internal class for defining compatibility and debugging flags"""
+
+ _all_names: List[str] = []
+ _fixed_names: List[str] = []
+ _type_desc = "configuration"
+
+ @classmethod
+ def _set(cls, dname, value):
+ if dname in cls._fixed_names:
+ warnings.warn(
+ "{}.{} {} is {} and cannot be overridden".format(
+ cls.__name__,
+ dname,
+ cls._type_desc,
+ str(getattr(cls, dname)).upper(),
+ )
+ )
+ return
+ if dname in cls._all_names:
+ setattr(cls, dname, value)
+ else:
+ raise ValueError("no such {} {!r}".format(cls._type_desc, dname))
+
+ enable = classmethod(lambda cls, name: cls._set(name, True))
+ disable = classmethod(lambda cls, name: cls._set(name, False))
+
+
+@lru_cache(maxsize=128)
+def col(loc: int, strg: str) -> int:
+ """
+ Returns current column within a string, counting newlines as line separators.
+ The first column is number 1.
+
+ Note: the default parsing behavior is to expand tabs in the input string
+ before starting the parsing process. See
+ :class:`ParserElement.parseString` for more
+ information on parsing strings containing ``<TAB>`` s, and suggested
+ methods to maintain a consistent view of the parsed string, the parse
+ location, and line and column positions within the parsed string.
+ """
+ s = strg
+ return 1 if 0 < loc < len(s) and s[loc - 1] == "\n" else loc - s.rfind("\n", 0, loc)
+
+
+@lru_cache(maxsize=128)
+def lineno(loc: int, strg: str) -> int:
+ """Returns current line number within a string, counting newlines as line separators.
+ The first line is number 1.
+
+ Note - the default parsing behavior is to expand tabs in the input string
+ before starting the parsing process. See :class:`ParserElement.parseString`
+ for more information on parsing strings containing ``<TAB>`` s, and
+ suggested methods to maintain a consistent view of the parsed string, the
+ parse location, and line and column positions within the parsed string.
+ """
+ return strg.count("\n", 0, loc) + 1
+
+
+@lru_cache(maxsize=128)
+def line(loc: int, strg: str) -> str:
+ """
+ Returns the line of text containing loc within a string, counting newlines as line separators.
+ """
+ last_cr = strg.rfind("\n", 0, loc)
+ next_cr = strg.find("\n", loc)
+ return strg[last_cr + 1 : next_cr] if next_cr >= 0 else strg[last_cr + 1 :]
+
+
+class _UnboundedCache:
+ def __init__(self):
+ cache = {}
+ cache_get = cache.get
+ self.not_in_cache = not_in_cache = object()
+
+ def get(_, key):
+ return cache_get(key, not_in_cache)
+
+ def set_(_, key, value):
+ cache[key] = value
+
+ def clear(_):
+ cache.clear()
+
+ self.size = None
+ self.get = types.MethodType(get, self)
+ self.set = types.MethodType(set_, self)
+ self.clear = types.MethodType(clear, self)
+
+
+class _FifoCache:
+ def __init__(self, size):
+ self.not_in_cache = not_in_cache = object()
+ cache = collections.OrderedDict()
+ cache_get = cache.get
+
+ def get(_, key):
+ return cache_get(key, not_in_cache)
+
+ def set_(_, key, value):
+ cache[key] = value
+ while len(cache) > size:
+ cache.popitem(last=False)
+
+ def clear(_):
+ cache.clear()
+
+ self.size = size
+ self.get = types.MethodType(get, self)
+ self.set = types.MethodType(set_, self)
+ self.clear = types.MethodType(clear, self)
+
+
+class LRUMemo:
+ """
+ A memoizing mapping that retains `capacity` deleted items
+
+ The memo tracks retained items by their access order; once `capacity` items
+ are retained, the least recently used item is discarded.
+ """
+
+ def __init__(self, capacity):
+ self._capacity = capacity
+ self._active = {}
+ self._memory = collections.OrderedDict()
+
+ def __getitem__(self, key):
+ try:
+ return self._active[key]
+ except KeyError:
+ self._memory.move_to_end(key)
+ return self._memory[key]
+
+ def __setitem__(self, key, value):
+ self._memory.pop(key, None)
+ self._active[key] = value
+
+ def __delitem__(self, key):
+ try:
+ value = self._active.pop(key)
+ except KeyError:
+ pass
+ else:
+ while len(self._memory) >= self._capacity:
+ self._memory.popitem(last=False)
+ self._memory[key] = value
+
+ def clear(self):
+ self._active.clear()
+ self._memory.clear()
+
+
+class UnboundedMemo(dict):
+ """
+ A memoizing mapping that retains all deleted items
+ """
+
+ def __delitem__(self, key):
+ pass
+
+
+def _escape_regex_range_chars(s: str) -> str:
+ # escape these chars: ^-[]
+ for c in r"\^-[]":
+ s = s.replace(c, _bslash + c)
+ s = s.replace("\n", r"\n")
+ s = s.replace("\t", r"\t")
+ return str(s)
+
+
+def _collapse_string_to_ranges(
+ s: Union[str, Iterable[str]], re_escape: bool = True
+) -> str:
+ def is_consecutive(c):
+ c_int = ord(c)
+ is_consecutive.prev, prev = c_int, is_consecutive.prev
+ if c_int - prev > 1:
+ is_consecutive.value = next(is_consecutive.counter)
+ return is_consecutive.value
+
+ is_consecutive.prev = 0
+ is_consecutive.counter = itertools.count()
+ is_consecutive.value = -1
+
+ def escape_re_range_char(c):
+ return "\\" + c if c in r"\^-][" else c
+
+ def no_escape_re_range_char(c):
+ return c
+
+ if not re_escape:
+ escape_re_range_char = no_escape_re_range_char
+
+ ret = []
+ s = "".join(sorted(set(s)))
+ if len(s) > 3:
+ for _, chars in itertools.groupby(s, key=is_consecutive):
+ first = last = next(chars)
+ last = collections.deque(
+ itertools.chain(iter([last]), chars), maxlen=1
+ ).pop()
+ if first == last:
+ ret.append(escape_re_range_char(first))
+ else:
+ sep = "" if ord(last) == ord(first) + 1 else "-"
+ ret.append(
+ "{}{}{}".format(
+ escape_re_range_char(first), sep, escape_re_range_char(last)
+ )
+ )
+ else:
+ ret = [escape_re_range_char(c) for c in s]
+
+ return "".join(ret)
+
+
+def _flatten(ll: list) -> list:
+ ret = []
+ for i in ll:
+ if isinstance(i, list):
+ ret.extend(_flatten(i))
+ else:
+ ret.append(i)
+ return ret
diff --git a/src/pip/_vendor/requests/__init__.py b/src/pip/_vendor/requests/__init__.py
index 4f80e28fc..9e97059d1 100644
--- a/src/pip/_vendor/requests/__init__.py
+++ b/src/pip/_vendor/requests/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
# __
# /__) _ _ _ _ _/ _
# / ( (- (/ (/ (- _) / _)
@@ -40,8 +38,10 @@ is at <https://requests.readthedocs.io>.
:license: Apache 2.0, see LICENSE for more details.
"""
-from pip._vendor import urllib3
import warnings
+
+from pip._vendor import urllib3
+
from .exceptions import RequestsDependencyWarning
charset_normalizer_version = None
@@ -51,13 +51,14 @@ try:
except ImportError:
chardet_version = None
+
def check_compatibility(urllib3_version, chardet_version, charset_normalizer_version):
- urllib3_version = urllib3_version.split('.')
- assert urllib3_version != ['dev'] # Verify urllib3 isn't installed from git.
+ urllib3_version = urllib3_version.split(".")
+ assert urllib3_version != ["dev"] # Verify urllib3 isn't installed from git.
# Sometimes, urllib3 only reports its version as 16.1.
if len(urllib3_version) == 2:
- urllib3_version.append('0')
+ urllib3_version.append("0")
# Check urllib3 for compatibility.
major, minor, patch = urllib3_version # noqa: F811
@@ -69,36 +70,46 @@ def check_compatibility(urllib3_version, chardet_version, charset_normalizer_ver
# Check charset_normalizer for compatibility.
if chardet_version:
- major, minor, patch = chardet_version.split('.')[:3]
+ major, minor, patch = chardet_version.split(".")[:3]
major, minor, patch = int(major), int(minor), int(patch)
- # chardet_version >= 3.0.2, < 5.0.0
- assert (3, 0, 2) <= (major, minor, patch) < (5, 0, 0)
+ # chardet_version >= 3.0.2, < 6.0.0
+ assert (3, 0, 2) <= (major, minor, patch) < (6, 0, 0)
elif charset_normalizer_version:
- major, minor, patch = charset_normalizer_version.split('.')[:3]
+ major, minor, patch = charset_normalizer_version.split(".")[:3]
major, minor, patch = int(major), int(minor), int(patch)
# charset_normalizer >= 2.0.0 < 3.0.0
assert (2, 0, 0) <= (major, minor, patch) < (3, 0, 0)
else:
raise Exception("You need either charset_normalizer or chardet installed")
+
def _check_cryptography(cryptography_version):
# cryptography < 1.3.4
try:
- cryptography_version = list(map(int, cryptography_version.split('.')))
+ cryptography_version = list(map(int, cryptography_version.split(".")))
except ValueError:
return
if cryptography_version < [1, 3, 4]:
- warning = 'Old version of cryptography ({}) may cause slowdown.'.format(cryptography_version)
+ warning = "Old version of cryptography ({}) may cause slowdown.".format(
+ cryptography_version
+ )
warnings.warn(warning, RequestsDependencyWarning)
+
# Check imported dependencies for compatibility.
try:
- check_compatibility(urllib3.__version__, chardet_version, charset_normalizer_version)
+ check_compatibility(
+ urllib3.__version__, chardet_version, charset_normalizer_version
+ )
except (AssertionError, ValueError):
- warnings.warn("urllib3 ({}) or chardet ({})/charset_normalizer ({}) doesn't match a supported "
- "version!".format(urllib3.__version__, chardet_version, charset_normalizer_version),
- RequestsDependencyWarning)
+ warnings.warn(
+ "urllib3 ({}) or chardet ({})/charset_normalizer ({}) doesn't match a supported "
+ "version!".format(
+ urllib3.__version__, chardet_version, charset_normalizer_version
+ ),
+ RequestsDependencyWarning,
+ )
# Attempt to enable urllib3's fallback for SNI support
# if the standard library doesn't support SNI or the
@@ -116,39 +127,56 @@ try:
if not getattr(ssl, "HAS_SNI", False):
from pip._vendor.urllib3.contrib import pyopenssl
+
pyopenssl.inject_into_urllib3()
# Check cryptography version
from cryptography import __version__ as cryptography_version
+
_check_cryptography(cryptography_version)
except ImportError:
pass
# urllib3's DependencyWarnings should be silenced.
from pip._vendor.urllib3.exceptions import DependencyWarning
-warnings.simplefilter('ignore', DependencyWarning)
-from .__version__ import __title__, __description__, __url__, __version__
-from .__version__ import __build__, __author__, __author_email__, __license__
-from .__version__ import __copyright__, __cake__
-
-from . import utils
-from . import packages
-from .models import Request, Response, PreparedRequest
-from .api import request, get, head, post, patch, put, delete, options
-from .sessions import session, Session
-from .status_codes import codes
-from .exceptions import (
- RequestException, Timeout, URLRequired,
- TooManyRedirects, HTTPError, ConnectionError,
- FileModeWarning, ConnectTimeout, ReadTimeout
-)
+warnings.simplefilter("ignore", DependencyWarning)
# Set default logging handler to avoid "No handler found" warnings.
import logging
from logging import NullHandler
+from . import packages, utils
+from .__version__ import (
+ __author__,
+ __author_email__,
+ __build__,
+ __cake__,
+ __copyright__,
+ __description__,
+ __license__,
+ __title__,
+ __url__,
+ __version__,
+)
+from .api import delete, get, head, options, patch, post, put, request
+from .exceptions import (
+ ConnectionError,
+ ConnectTimeout,
+ FileModeWarning,
+ HTTPError,
+ JSONDecodeError,
+ ReadTimeout,
+ RequestException,
+ Timeout,
+ TooManyRedirects,
+ URLRequired,
+)
+from .models import PreparedRequest, Request, Response
+from .sessions import Session, session
+from .status_codes import codes
+
logging.getLogger(__name__).addHandler(NullHandler())
# FileModeWarnings go off per the default.
-warnings.simplefilter('default', FileModeWarning, append=True)
+warnings.simplefilter("default", FileModeWarning, append=True)
diff --git a/src/pip/_vendor/requests/__version__.py b/src/pip/_vendor/requests/__version__.py
index 0d7cde1df..e725ada65 100644
--- a/src/pip/_vendor/requests/__version__.py
+++ b/src/pip/_vendor/requests/__version__.py
@@ -2,13 +2,13 @@
# |( |- |.| | | |- `-. | `-.
# ' ' `-' `-`.`-' `-' `-' ' `-'
-__title__ = 'requests'
-__description__ = 'Python HTTP for Humans.'
-__url__ = 'https://requests.readthedocs.io'
-__version__ = '2.26.0'
-__build__ = 0x022600
-__author__ = 'Kenneth Reitz'
-__author_email__ = 'me@kennethreitz.org'
-__license__ = 'Apache 2.0'
-__copyright__ = 'Copyright 2020 Kenneth Reitz'
-__cake__ = u'\u2728 \U0001f370 \u2728'
+__title__ = "requests"
+__description__ = "Python HTTP for Humans."
+__url__ = "https://requests.readthedocs.io"
+__version__ = "2.28.1"
+__build__ = 0x022801
+__author__ = "Kenneth Reitz"
+__author_email__ = "me@kennethreitz.org"
+__license__ = "Apache 2.0"
+__copyright__ = "Copyright 2022 Kenneth Reitz"
+__cake__ = "\u2728 \U0001f370 \u2728"
diff --git a/src/pip/_vendor/requests/_internal_utils.py b/src/pip/_vendor/requests/_internal_utils.py
index 759d9a56b..7dc9bc533 100644
--- a/src/pip/_vendor/requests/_internal_utils.py
+++ b/src/pip/_vendor/requests/_internal_utils.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
requests._internal_utils
~~~~~~~~~~~~~~
@@ -7,11 +5,22 @@ requests._internal_utils
Provides utility functions that are consumed internally by Requests
which depend on extremely few external helpers (such as compat)
"""
+import re
+
+from .compat import builtin_str
+
+_VALID_HEADER_NAME_RE_BYTE = re.compile(rb"^[^:\s][^:\r\n]*$")
+_VALID_HEADER_NAME_RE_STR = re.compile(r"^[^:\s][^:\r\n]*$")
+_VALID_HEADER_VALUE_RE_BYTE = re.compile(rb"^\S[^\r\n]*$|^$")
+_VALID_HEADER_VALUE_RE_STR = re.compile(r"^\S[^\r\n]*$|^$")
-from .compat import is_py2, builtin_str, str
+HEADER_VALIDATORS = {
+ bytes: (_VALID_HEADER_NAME_RE_BYTE, _VALID_HEADER_VALUE_RE_BYTE),
+ str: (_VALID_HEADER_NAME_RE_STR, _VALID_HEADER_VALUE_RE_STR),
+}
-def to_native_string(string, encoding='ascii'):
+def to_native_string(string, encoding="ascii"):
"""Given a string object, regardless of type, returns a representation of
that string in the native string type, encoding and decoding where
necessary. This assumes ASCII unless told otherwise.
@@ -19,10 +28,7 @@ def to_native_string(string, encoding='ascii'):
if isinstance(string, builtin_str):
out = string
else:
- if is_py2:
- out = string.encode(encoding)
- else:
- out = string.decode(encoding)
+ out = string.decode(encoding)
return out
@@ -36,7 +42,7 @@ def unicode_is_ascii(u_string):
"""
assert isinstance(u_string, str)
try:
- u_string.encode('ascii')
+ u_string.encode("ascii")
return True
except UnicodeEncodeError:
return False
diff --git a/src/pip/_vendor/requests/adapters.py b/src/pip/_vendor/requests/adapters.py
index c30e7c92d..f68f7d467 100644
--- a/src/pip/_vendor/requests/adapters.py
+++ b/src/pip/_vendor/requests/adapters.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
requests.adapters
~~~~~~~~~~~~~~~~~
@@ -9,57 +7,76 @@ and maintain connections.
"""
import os.path
-import socket
+import socket # noqa: F401
+from pip._vendor.urllib3.exceptions import ClosedPoolError, ConnectTimeoutError
+from pip._vendor.urllib3.exceptions import HTTPError as _HTTPError
+from pip._vendor.urllib3.exceptions import InvalidHeader as _InvalidHeader
+from pip._vendor.urllib3.exceptions import (
+ LocationValueError,
+ MaxRetryError,
+ NewConnectionError,
+ ProtocolError,
+)
+from pip._vendor.urllib3.exceptions import ProxyError as _ProxyError
+from pip._vendor.urllib3.exceptions import ReadTimeoutError, ResponseError
+from pip._vendor.urllib3.exceptions import SSLError as _SSLError
from pip._vendor.urllib3.poolmanager import PoolManager, proxy_from_url
from pip._vendor.urllib3.response import HTTPResponse
-from pip._vendor.urllib3.util import parse_url
from pip._vendor.urllib3.util import Timeout as TimeoutSauce
+from pip._vendor.urllib3.util import parse_url
from pip._vendor.urllib3.util.retry import Retry
-from pip._vendor.urllib3.exceptions import ClosedPoolError
-from pip._vendor.urllib3.exceptions import ConnectTimeoutError
-from pip._vendor.urllib3.exceptions import HTTPError as _HTTPError
-from pip._vendor.urllib3.exceptions import MaxRetryError
-from pip._vendor.urllib3.exceptions import NewConnectionError
-from pip._vendor.urllib3.exceptions import ProxyError as _ProxyError
-from pip._vendor.urllib3.exceptions import ProtocolError
-from pip._vendor.urllib3.exceptions import ReadTimeoutError
-from pip._vendor.urllib3.exceptions import SSLError as _SSLError
-from pip._vendor.urllib3.exceptions import ResponseError
-from pip._vendor.urllib3.exceptions import LocationValueError
+from .auth import _basic_auth_str
+from .compat import basestring, urlparse
+from .cookies import extract_cookies_to_jar
+from .exceptions import (
+ ConnectionError,
+ ConnectTimeout,
+ InvalidHeader,
+ InvalidProxyURL,
+ InvalidSchema,
+ InvalidURL,
+ ProxyError,
+ ReadTimeout,
+ RetryError,
+ SSLError,
+)
from .models import Response
-from .compat import urlparse, basestring
-from .utils import (DEFAULT_CA_BUNDLE_PATH, extract_zipped_paths,
- get_encoding_from_headers, prepend_scheme_if_needed,
- get_auth_from_url, urldefragauth, select_proxy)
from .structures import CaseInsensitiveDict
-from .cookies import extract_cookies_to_jar
-from .exceptions import (ConnectionError, ConnectTimeout, ReadTimeout, SSLError,
- ProxyError, RetryError, InvalidSchema, InvalidProxyURL,
- InvalidURL)
-from .auth import _basic_auth_str
+from .utils import (
+ DEFAULT_CA_BUNDLE_PATH,
+ extract_zipped_paths,
+ get_auth_from_url,
+ get_encoding_from_headers,
+ prepend_scheme_if_needed,
+ select_proxy,
+ urldefragauth,
+)
try:
from pip._vendor.urllib3.contrib.socks import SOCKSProxyManager
except ImportError:
+
def SOCKSProxyManager(*args, **kwargs):
raise InvalidSchema("Missing dependencies for SOCKS support.")
+
DEFAULT_POOLBLOCK = False
DEFAULT_POOLSIZE = 10
DEFAULT_RETRIES = 0
DEFAULT_POOL_TIMEOUT = None
-class BaseAdapter(object):
+class BaseAdapter:
"""The Base Transport Adapter"""
def __init__(self):
- super(BaseAdapter, self).__init__()
+ super().__init__()
- def send(self, request, stream=False, timeout=None, verify=True,
- cert=None, proxies=None):
+ def send(
+ self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
+ ):
"""Sends PreparedRequest object. Returns Response object.
:param request: The :class:`PreparedRequest <PreparedRequest>` being sent.
@@ -107,12 +124,22 @@ class HTTPAdapter(BaseAdapter):
>>> a = requests.adapters.HTTPAdapter(max_retries=3)
>>> s.mount('http://', a)
"""
- __attrs__ = ['max_retries', 'config', '_pool_connections', '_pool_maxsize',
- '_pool_block']
- def __init__(self, pool_connections=DEFAULT_POOLSIZE,
- pool_maxsize=DEFAULT_POOLSIZE, max_retries=DEFAULT_RETRIES,
- pool_block=DEFAULT_POOLBLOCK):
+ __attrs__ = [
+ "max_retries",
+ "config",
+ "_pool_connections",
+ "_pool_maxsize",
+ "_pool_block",
+ ]
+
+ def __init__(
+ self,
+ pool_connections=DEFAULT_POOLSIZE,
+ pool_maxsize=DEFAULT_POOLSIZE,
+ max_retries=DEFAULT_RETRIES,
+ pool_block=DEFAULT_POOLBLOCK,
+ ):
if max_retries == DEFAULT_RETRIES:
self.max_retries = Retry(0, read=False)
else:
@@ -120,7 +147,7 @@ class HTTPAdapter(BaseAdapter):
self.config = {}
self.proxy_manager = {}
- super(HTTPAdapter, self).__init__()
+ super().__init__()
self._pool_connections = pool_connections
self._pool_maxsize = pool_maxsize
@@ -140,10 +167,13 @@ class HTTPAdapter(BaseAdapter):
for attr, value in state.items():
setattr(self, attr, value)
- self.init_poolmanager(self._pool_connections, self._pool_maxsize,
- block=self._pool_block)
+ self.init_poolmanager(
+ self._pool_connections, self._pool_maxsize, block=self._pool_block
+ )
- def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs):
+ def init_poolmanager(
+ self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs
+ ):
"""Initializes a urllib3 PoolManager.
This method should not be called from user code, and is only
@@ -160,8 +190,13 @@ class HTTPAdapter(BaseAdapter):
self._pool_maxsize = maxsize
self._pool_block = block
- self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize,
- block=block, strict=True, **pool_kwargs)
+ self.poolmanager = PoolManager(
+ num_pools=connections,
+ maxsize=maxsize,
+ block=block,
+ strict=True,
+ **pool_kwargs,
+ )
def proxy_manager_for(self, proxy, **proxy_kwargs):
"""Return urllib3 ProxyManager for the given proxy.
@@ -177,7 +212,7 @@ class HTTPAdapter(BaseAdapter):
"""
if proxy in self.proxy_manager:
manager = self.proxy_manager[proxy]
- elif proxy.lower().startswith('socks'):
+ elif proxy.lower().startswith("socks"):
username, password = get_auth_from_url(proxy)
manager = self.proxy_manager[proxy] = SOCKSProxyManager(
proxy,
@@ -186,7 +221,7 @@ class HTTPAdapter(BaseAdapter):
num_pools=self._pool_connections,
maxsize=self._pool_maxsize,
block=self._pool_block,
- **proxy_kwargs
+ **proxy_kwargs,
)
else:
proxy_headers = self.proxy_headers(proxy)
@@ -196,7 +231,8 @@ class HTTPAdapter(BaseAdapter):
num_pools=self._pool_connections,
maxsize=self._pool_maxsize,
block=self._pool_block,
- **proxy_kwargs)
+ **proxy_kwargs,
+ )
return manager
@@ -212,7 +248,7 @@ class HTTPAdapter(BaseAdapter):
to a CA bundle to use
:param cert: The SSL certificate to verify.
"""
- if url.lower().startswith('https') and verify:
+ if url.lower().startswith("https") and verify:
cert_loc = None
@@ -224,17 +260,19 @@ class HTTPAdapter(BaseAdapter):
cert_loc = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
if not cert_loc or not os.path.exists(cert_loc):
- raise IOError("Could not find a suitable TLS CA certificate bundle, "
- "invalid path: {}".format(cert_loc))
+ raise OSError(
+ f"Could not find a suitable TLS CA certificate bundle, "
+ f"invalid path: {cert_loc}"
+ )
- conn.cert_reqs = 'CERT_REQUIRED'
+ conn.cert_reqs = "CERT_REQUIRED"
if not os.path.isdir(cert_loc):
conn.ca_certs = cert_loc
else:
conn.ca_cert_dir = cert_loc
else:
- conn.cert_reqs = 'CERT_NONE'
+ conn.cert_reqs = "CERT_NONE"
conn.ca_certs = None
conn.ca_cert_dir = None
@@ -246,11 +284,14 @@ class HTTPAdapter(BaseAdapter):
conn.cert_file = cert
conn.key_file = None
if conn.cert_file and not os.path.exists(conn.cert_file):
- raise IOError("Could not find the TLS certificate file, "
- "invalid path: {}".format(conn.cert_file))
+ raise OSError(
+ f"Could not find the TLS certificate file, "
+ f"invalid path: {conn.cert_file}"
+ )
if conn.key_file and not os.path.exists(conn.key_file):
- raise IOError("Could not find the TLS key file, "
- "invalid path: {}".format(conn.key_file))
+ raise OSError(
+ f"Could not find the TLS key file, invalid path: {conn.key_file}"
+ )
def build_response(self, req, resp):
"""Builds a :class:`Response <requests.Response>` object from a urllib3
@@ -265,10 +306,10 @@ class HTTPAdapter(BaseAdapter):
response = Response()
# Fallback to None if there's no status_code, for whatever reason.
- response.status_code = getattr(resp, 'status', None)
+ response.status_code = getattr(resp, "status", None)
# Make headers case-insensitive.
- response.headers = CaseInsensitiveDict(getattr(resp, 'headers', {}))
+ response.headers = CaseInsensitiveDict(getattr(resp, "headers", {}))
# Set encoding.
response.encoding = get_encoding_from_headers(response.headers)
@@ -276,7 +317,7 @@ class HTTPAdapter(BaseAdapter):
response.reason = response.raw.reason
if isinstance(req.url, bytes):
- response.url = req.url.decode('utf-8')
+ response.url = req.url.decode("utf-8")
else:
response.url = req.url
@@ -301,11 +342,13 @@ class HTTPAdapter(BaseAdapter):
proxy = select_proxy(url, proxies)
if proxy:
- proxy = prepend_scheme_if_needed(proxy, 'http')
+ proxy = prepend_scheme_if_needed(proxy, "http")
proxy_url = parse_url(proxy)
if not proxy_url.host:
- raise InvalidProxyURL("Please check proxy URL. It is malformed"
- " and could be missing the host.")
+ raise InvalidProxyURL(
+ "Please check proxy URL. It is malformed "
+ "and could be missing the host."
+ )
proxy_manager = self.proxy_manager_for(proxy)
conn = proxy_manager.connection_from_url(url)
else:
@@ -343,11 +386,11 @@ class HTTPAdapter(BaseAdapter):
proxy = select_proxy(request.url, proxies)
scheme = urlparse(request.url).scheme
- is_proxied_http_request = (proxy and scheme != 'https')
+ is_proxied_http_request = proxy and scheme != "https"
using_socks_proxy = False
if proxy:
proxy_scheme = urlparse(proxy).scheme.lower()
- using_socks_proxy = proxy_scheme.startswith('socks')
+ using_socks_proxy = proxy_scheme.startswith("socks")
url = request.path_url
if is_proxied_http_request and not using_socks_proxy:
@@ -386,12 +429,13 @@ class HTTPAdapter(BaseAdapter):
username, password = get_auth_from_url(proxy)
if username:
- headers['Proxy-Authorization'] = _basic_auth_str(username,
- password)
+ headers["Proxy-Authorization"] = _basic_auth_str(username, password)
return headers
- def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
+ def send(
+ self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
+ ):
"""Sends PreparedRequest object. Returns Response object.
:param request: The :class:`PreparedRequest <PreparedRequest>` being sent.
@@ -415,20 +459,26 @@ class HTTPAdapter(BaseAdapter):
self.cert_verify(conn, request.url, verify, cert)
url = self.request_url(request, proxies)
- self.add_headers(request, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies)
+ self.add_headers(
+ request,
+ stream=stream,
+ timeout=timeout,
+ verify=verify,
+ cert=cert,
+ proxies=proxies,
+ )
- chunked = not (request.body is None or 'Content-Length' in request.headers)
+ chunked = not (request.body is None or "Content-Length" in request.headers)
if isinstance(timeout, tuple):
try:
connect, read = timeout
timeout = TimeoutSauce(connect=connect, read=read)
- except ValueError as e:
- # this may raise a string formatting error.
- err = ("Invalid timeout {}. Pass a (connect, read) "
- "timeout tuple, or a single float to set "
- "both timeouts to the same value".format(timeout))
- raise ValueError(err)
+ except ValueError:
+ raise ValueError(
+ f"Invalid timeout {timeout}. Pass a (connect, read) timeout tuple, "
+ f"or a single float to set both timeouts to the same value."
+ )
elif isinstance(timeout, TimeoutSauce):
pass
else:
@@ -446,20 +496,24 @@ class HTTPAdapter(BaseAdapter):
preload_content=False,
decode_content=False,
retries=self.max_retries,
- timeout=timeout
+ timeout=timeout,
)
# Send the request.
else:
- if hasattr(conn, 'proxy_pool'):
+ if hasattr(conn, "proxy_pool"):
conn = conn.proxy_pool
low_conn = conn._get_conn(timeout=DEFAULT_POOL_TIMEOUT)
try:
- low_conn.putrequest(request.method,
- url,
- skip_accept_encoding=True)
+ skip_host = "Host" in request.headers
+ low_conn.putrequest(
+ request.method,
+ url,
+ skip_accept_encoding=True,
+ skip_host=skip_host,
+ )
for header, value in request.headers.items():
low_conn.putheader(header, value)
@@ -467,34 +521,29 @@ class HTTPAdapter(BaseAdapter):
low_conn.endheaders()
for i in request.body:
- low_conn.send(hex(len(i))[2:].encode('utf-8'))
- low_conn.send(b'\r\n')
+ low_conn.send(hex(len(i))[2:].encode("utf-8"))
+ low_conn.send(b"\r\n")
low_conn.send(i)
- low_conn.send(b'\r\n')
- low_conn.send(b'0\r\n\r\n')
+ low_conn.send(b"\r\n")
+ low_conn.send(b"0\r\n\r\n")
# Receive the response from the server
- try:
- # For Python 2.7, use buffering of HTTP responses
- r = low_conn.getresponse(buffering=True)
- except TypeError:
- # For compatibility with Python 3.3+
- r = low_conn.getresponse()
+ r = low_conn.getresponse()
resp = HTTPResponse.from_httplib(
r,
pool=conn,
connection=low_conn,
preload_content=False,
- decode_content=False
+ decode_content=False,
)
- except:
+ except Exception:
# If we hit any problems here, clean up the connection.
- # Then, reraise so that we can handle the actual exception.
+ # Then, raise so that we can handle the actual exception.
low_conn.close()
raise
- except (ProtocolError, socket.error) as err:
+ except (ProtocolError, OSError) as err:
raise ConnectionError(err, request=request)
except MaxRetryError as e:
@@ -527,6 +576,8 @@ class HTTPAdapter(BaseAdapter):
raise SSLError(e, request=request)
elif isinstance(e, ReadTimeoutError):
raise ReadTimeout(e, request=request)
+ elif isinstance(e, _InvalidHeader):
+ raise InvalidHeader(e, request=request)
else:
raise
diff --git a/src/pip/_vendor/requests/api.py b/src/pip/_vendor/requests/api.py
index 4cba90eef..2f71aaed1 100644
--- a/src/pip/_vendor/requests/api.py
+++ b/src/pip/_vendor/requests/api.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
requests.api
~~~~~~~~~~~~
@@ -72,7 +70,7 @@ def get(url, params=None, **kwargs):
:rtype: requests.Response
"""
- return request('get', url, params=params, **kwargs)
+ return request("get", url, params=params, **kwargs)
def options(url, **kwargs):
@@ -84,7 +82,7 @@ def options(url, **kwargs):
:rtype: requests.Response
"""
- return request('options', url, **kwargs)
+ return request("options", url, **kwargs)
def head(url, **kwargs):
@@ -98,8 +96,8 @@ def head(url, **kwargs):
:rtype: requests.Response
"""
- kwargs.setdefault('allow_redirects', False)
- return request('head', url, **kwargs)
+ kwargs.setdefault("allow_redirects", False)
+ return request("head", url, **kwargs)
def post(url, data=None, json=None, **kwargs):
@@ -114,7 +112,7 @@ def post(url, data=None, json=None, **kwargs):
:rtype: requests.Response
"""
- return request('post', url, data=data, json=json, **kwargs)
+ return request("post", url, data=data, json=json, **kwargs)
def put(url, data=None, **kwargs):
@@ -129,7 +127,7 @@ def put(url, data=None, **kwargs):
:rtype: requests.Response
"""
- return request('put', url, data=data, **kwargs)
+ return request("put", url, data=data, **kwargs)
def patch(url, data=None, **kwargs):
@@ -144,7 +142,7 @@ def patch(url, data=None, **kwargs):
:rtype: requests.Response
"""
- return request('patch', url, data=data, **kwargs)
+ return request("patch", url, data=data, **kwargs)
def delete(url, **kwargs):
@@ -156,4 +154,4 @@ def delete(url, **kwargs):
:rtype: requests.Response
"""
- return request('delete', url, **kwargs)
+ return request("delete", url, **kwargs)
diff --git a/src/pip/_vendor/requests/auth.py b/src/pip/_vendor/requests/auth.py
index eeface39a..9733686dd 100644
--- a/src/pip/_vendor/requests/auth.py
+++ b/src/pip/_vendor/requests/auth.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
requests.auth
~~~~~~~~~~~~~
@@ -7,22 +5,21 @@ requests.auth
This module contains the authentication handlers for Requests.
"""
+import hashlib
import os
import re
-import time
-import hashlib
import threading
+import time
import warnings
-
from base64 import b64encode
-from .compat import urlparse, str, basestring
-from .cookies import extract_cookies_to_jar
from ._internal_utils import to_native_string
+from .compat import basestring, str, urlparse
+from .cookies import extract_cookies_to_jar
from .utils import parse_dict_header
-CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
-CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
+CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded"
+CONTENT_TYPE_MULTI_PART = "multipart/form-data"
def _basic_auth_str(username, password):
@@ -57,23 +54,23 @@ def _basic_auth_str(username, password):
# -- End Removal --
if isinstance(username, str):
- username = username.encode('latin1')
+ username = username.encode("latin1")
if isinstance(password, str):
- password = password.encode('latin1')
+ password = password.encode("latin1")
- authstr = 'Basic ' + to_native_string(
- b64encode(b':'.join((username, password))).strip()
+ authstr = "Basic " + to_native_string(
+ b64encode(b":".join((username, password))).strip()
)
return authstr
-class AuthBase(object):
+class AuthBase:
"""Base class that all auth implementations derive from"""
def __call__(self, r):
- raise NotImplementedError('Auth hooks must be callable.')
+ raise NotImplementedError("Auth hooks must be callable.")
class HTTPBasicAuth(AuthBase):
@@ -84,16 +81,18 @@ class HTTPBasicAuth(AuthBase):
self.password = password
def __eq__(self, other):
- return all([
- self.username == getattr(other, 'username', None),
- self.password == getattr(other, 'password', None)
- ])
+ return all(
+ [
+ self.username == getattr(other, "username", None),
+ self.password == getattr(other, "password", None),
+ ]
+ )
def __ne__(self, other):
return not self == other
def __call__(self, r):
- r.headers['Authorization'] = _basic_auth_str(self.username, self.password)
+ r.headers["Authorization"] = _basic_auth_str(self.username, self.password)
return r
@@ -101,7 +100,7 @@ class HTTPProxyAuth(HTTPBasicAuth):
"""Attaches HTTP Proxy Authentication to a given Request object."""
def __call__(self, r):
- r.headers['Proxy-Authorization'] = _basic_auth_str(self.username, self.password)
+ r.headers["Proxy-Authorization"] = _basic_auth_str(self.username, self.password)
return r
@@ -116,9 +115,9 @@ class HTTPDigestAuth(AuthBase):
def init_per_thread_state(self):
# Ensure state is initialized just once per-thread
- if not hasattr(self._thread_local, 'init'):
+ if not hasattr(self._thread_local, "init"):
self._thread_local.init = True
- self._thread_local.last_nonce = ''
+ self._thread_local.last_nonce = ""
self._thread_local.nonce_count = 0
self._thread_local.chal = {}
self._thread_local.pos = None
@@ -129,44 +128,52 @@ class HTTPDigestAuth(AuthBase):
:rtype: str
"""
- realm = self._thread_local.chal['realm']
- nonce = self._thread_local.chal['nonce']
- qop = self._thread_local.chal.get('qop')
- algorithm = self._thread_local.chal.get('algorithm')
- opaque = self._thread_local.chal.get('opaque')
+ realm = self._thread_local.chal["realm"]
+ nonce = self._thread_local.chal["nonce"]
+ qop = self._thread_local.chal.get("qop")
+ algorithm = self._thread_local.chal.get("algorithm")
+ opaque = self._thread_local.chal.get("opaque")
hash_utf8 = None
if algorithm is None:
- _algorithm = 'MD5'
+ _algorithm = "MD5"
else:
_algorithm = algorithm.upper()
# lambdas assume digest modules are imported at the top level
- if _algorithm == 'MD5' or _algorithm == 'MD5-SESS':
+ if _algorithm == "MD5" or _algorithm == "MD5-SESS":
+
def md5_utf8(x):
if isinstance(x, str):
- x = x.encode('utf-8')
+ x = x.encode("utf-8")
return hashlib.md5(x).hexdigest()
+
hash_utf8 = md5_utf8
- elif _algorithm == 'SHA':
+ elif _algorithm == "SHA":
+
def sha_utf8(x):
if isinstance(x, str):
- x = x.encode('utf-8')
+ x = x.encode("utf-8")
return hashlib.sha1(x).hexdigest()
+
hash_utf8 = sha_utf8
- elif _algorithm == 'SHA-256':
+ elif _algorithm == "SHA-256":
+
def sha256_utf8(x):
if isinstance(x, str):
- x = x.encode('utf-8')
+ x = x.encode("utf-8")
return hashlib.sha256(x).hexdigest()
+
hash_utf8 = sha256_utf8
- elif _algorithm == 'SHA-512':
+ elif _algorithm == "SHA-512":
+
def sha512_utf8(x):
if isinstance(x, str):
- x = x.encode('utf-8')
+ x = x.encode("utf-8")
return hashlib.sha512(x).hexdigest()
+
hash_utf8 = sha512_utf8
- KD = lambda s, d: hash_utf8("%s:%s" % (s, d))
+ KD = lambda s, d: hash_utf8(f"{s}:{d}") # noqa:E731
if hash_utf8 is None:
return None
@@ -177,10 +184,10 @@ class HTTPDigestAuth(AuthBase):
#: path is request-uri defined in RFC 2616 which should not be empty
path = p_parsed.path or "/"
if p_parsed.query:
- path += '?' + p_parsed.query
+ path += f"?{p_parsed.query}"
- A1 = '%s:%s:%s' % (self.username, realm, self.password)
- A2 = '%s:%s' % (method, path)
+ A1 = f"{self.username}:{realm}:{self.password}"
+ A2 = f"{method}:{path}"
HA1 = hash_utf8(A1)
HA2 = hash_utf8(A2)
@@ -189,22 +196,20 @@ class HTTPDigestAuth(AuthBase):
self._thread_local.nonce_count += 1
else:
self._thread_local.nonce_count = 1
- ncvalue = '%08x' % self._thread_local.nonce_count
- s = str(self._thread_local.nonce_count).encode('utf-8')
- s += nonce.encode('utf-8')
- s += time.ctime().encode('utf-8')
+ ncvalue = f"{self._thread_local.nonce_count:08x}"
+ s = str(self._thread_local.nonce_count).encode("utf-8")
+ s += nonce.encode("utf-8")
+ s += time.ctime().encode("utf-8")
s += os.urandom(8)
- cnonce = (hashlib.sha1(s).hexdigest()[:16])
- if _algorithm == 'MD5-SESS':
- HA1 = hash_utf8('%s:%s:%s' % (HA1, nonce, cnonce))
+ cnonce = hashlib.sha1(s).hexdigest()[:16]
+ if _algorithm == "MD5-SESS":
+ HA1 = hash_utf8(f"{HA1}:{nonce}:{cnonce}")
if not qop:
- respdig = KD(HA1, "%s:%s" % (nonce, HA2))
- elif qop == 'auth' or 'auth' in qop.split(','):
- noncebit = "%s:%s:%s:%s:%s" % (
- nonce, ncvalue, cnonce, 'auth', HA2
- )
+ respdig = KD(HA1, f"{nonce}:{HA2}")
+ elif qop == "auth" or "auth" in qop.split(","):
+ noncebit = f"{nonce}:{ncvalue}:{cnonce}:auth:{HA2}"
respdig = KD(HA1, noncebit)
else:
# XXX handle auth-int.
@@ -213,18 +218,20 @@ class HTTPDigestAuth(AuthBase):
self._thread_local.last_nonce = nonce
# XXX should the partial digests be encoded too?
- base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
- 'response="%s"' % (self.username, realm, nonce, path, respdig)
+ base = (
+ f'username="{self.username}", realm="{realm}", nonce="{nonce}", '
+ f'uri="{path}", response="{respdig}"'
+ )
if opaque:
- base += ', opaque="%s"' % opaque
+ base += f', opaque="{opaque}"'
if algorithm:
- base += ', algorithm="%s"' % algorithm
+ base += f', algorithm="{algorithm}"'
if entdig:
- base += ', digest="%s"' % entdig
+ base += f', digest="{entdig}"'
if qop:
- base += ', qop="auth", nc=%s, cnonce="%s"' % (ncvalue, cnonce)
+ base += f', qop="auth", nc={ncvalue}, cnonce="{cnonce}"'
- return 'Digest %s' % (base)
+ return f"Digest {base}"
def handle_redirect(self, r, **kwargs):
"""Reset num_401_calls counter on redirects."""
@@ -248,13 +255,13 @@ class HTTPDigestAuth(AuthBase):
# Rewind the file position indicator of the body to where
# it was to resend the request.
r.request.body.seek(self._thread_local.pos)
- s_auth = r.headers.get('www-authenticate', '')
+ s_auth = r.headers.get("www-authenticate", "")
- if 'digest' in s_auth.lower() and self._thread_local.num_401_calls < 2:
+ if "digest" in s_auth.lower() and self._thread_local.num_401_calls < 2:
self._thread_local.num_401_calls += 1
- pat = re.compile(r'digest ', flags=re.IGNORECASE)
- self._thread_local.chal = parse_dict_header(pat.sub('', s_auth, count=1))
+ pat = re.compile(r"digest ", flags=re.IGNORECASE)
+ self._thread_local.chal = parse_dict_header(pat.sub("", s_auth, count=1))
# Consume content and release the original connection
# to allow our new request to reuse the same one.
@@ -264,8 +271,9 @@ class HTTPDigestAuth(AuthBase):
extract_cookies_to_jar(prep._cookies, r.request, r.raw)
prep.prepare_cookies(prep._cookies)
- prep.headers['Authorization'] = self.build_digest_header(
- prep.method, prep.url)
+ prep.headers["Authorization"] = self.build_digest_header(
+ prep.method, prep.url
+ )
_r = r.connection.send(prep, **kwargs)
_r.history.append(r)
_r.request = prep
@@ -280,7 +288,7 @@ class HTTPDigestAuth(AuthBase):
self.init_per_thread_state()
# If we have a saved nonce, skip the 401
if self._thread_local.last_nonce:
- r.headers['Authorization'] = self.build_digest_header(r.method, r.url)
+ r.headers["Authorization"] = self.build_digest_header(r.method, r.url)
try:
self._thread_local.pos = r.body.tell()
except AttributeError:
@@ -289,17 +297,19 @@ class HTTPDigestAuth(AuthBase):
# file position of the previous body. Ensure it's set to
# None.
self._thread_local.pos = None
- r.register_hook('response', self.handle_401)
- r.register_hook('response', self.handle_redirect)
+ r.register_hook("response", self.handle_401)
+ r.register_hook("response", self.handle_redirect)
self._thread_local.num_401_calls = 1
return r
def __eq__(self, other):
- return all([
- self.username == getattr(other, 'username', None),
- self.password == getattr(other, 'password', None)
- ])
+ return all(
+ [
+ self.username == getattr(other, "username", None),
+ self.password == getattr(other, "password", None),
+ ]
+ )
def __ne__(self, other):
return not self == other
diff --git a/src/pip/_vendor/requests/certs.py b/src/pip/_vendor/requests/certs.py
index 06a594e58..2743144b9 100644
--- a/src/pip/_vendor/requests/certs.py
+++ b/src/pip/_vendor/requests/certs.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python
-# -*- coding: utf-8 -*-
"""
requests.certs
@@ -14,5 +13,5 @@ packaged CA bundle.
"""
from pip._vendor.certifi import where
-if __name__ == '__main__':
+if __name__ == "__main__":
print(where())
diff --git a/src/pip/_vendor/requests/compat.py b/src/pip/_vendor/requests/compat.py
index 9e2937167..9ab2bb486 100644
--- a/src/pip/_vendor/requests/compat.py
+++ b/src/pip/_vendor/requests/compat.py
@@ -1,11 +1,10 @@
-# -*- coding: utf-8 -*-
-
"""
requests.compat
~~~~~~~~~~~~~~~
-This module handles import compatibility issues between Python 2 and
-Python 3.
+This module previously handled import compatibility issues
+between Python 2 and Python 3. It remains for backwards
+compatibility until the next major version.
"""
from pip._vendor import chardet
@@ -20,57 +19,49 @@ import sys
_ver = sys.version_info
#: Python 2.x?
-is_py2 = (_ver[0] == 2)
+is_py2 = _ver[0] == 2
#: Python 3.x?
-is_py3 = (_ver[0] == 3)
+is_py3 = _ver[0] == 3
# Note: We've patched out simplejson support in pip because it prevents
# upgrading simplejson on Windows.
-# try:
-# import simplejson as json
-# except (ImportError, SyntaxError):
-# # simplejson does not support Python 3.2, it throws a SyntaxError
-# # because of u'...' Unicode literals.
import json
+from json import JSONDecodeError
-# ---------
-# Specifics
-# ---------
-
-if is_py2:
- from urllib import (
- quote, unquote, quote_plus, unquote_plus, urlencode, getproxies,
- proxy_bypass, proxy_bypass_environment, getproxies_environment)
- from urlparse import urlparse, urlunparse, urljoin, urlsplit, urldefrag
- from urllib2 import parse_http_list
- import cookielib
- from Cookie import Morsel
- from StringIO import StringIO
- # Keep OrderedDict for backwards compatibility.
- from collections import Callable, Mapping, MutableMapping, OrderedDict
-
-
- builtin_str = str
- bytes = str
- str = unicode
- basestring = basestring
- numeric_types = (int, long, float)
- integer_types = (int, long)
+# Keep OrderedDict for backwards compatibility.
+from collections import OrderedDict
+from collections.abc import Callable, Mapping, MutableMapping
+from http import cookiejar as cookielib
+from http.cookies import Morsel
+from io import StringIO
-elif is_py3:
- from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote, quote_plus, unquote_plus, urldefrag
- from urllib.request import parse_http_list, getproxies, proxy_bypass, proxy_bypass_environment, getproxies_environment
- from http import cookiejar as cookielib
- from http.cookies import Morsel
- from io import StringIO
- # Keep OrderedDict for backwards compatibility.
- from collections import OrderedDict
- from collections.abc import Callable, Mapping, MutableMapping
+# --------------
+# Legacy Imports
+# --------------
+from urllib.parse import (
+ quote,
+ quote_plus,
+ unquote,
+ unquote_plus,
+ urldefrag,
+ urlencode,
+ urljoin,
+ urlparse,
+ urlsplit,
+ urlunparse,
+)
+from urllib.request import (
+ getproxies,
+ getproxies_environment,
+ parse_http_list,
+ proxy_bypass,
+ proxy_bypass_environment,
+)
- builtin_str = str
- str = str
- bytes = bytes
- basestring = (str, bytes)
- numeric_types = (int, float)
- integer_types = (int,)
+builtin_str = str
+str = str
+bytes = bytes
+basestring = (str, bytes)
+numeric_types = (int, float)
+integer_types = (int,)
diff --git a/src/pip/_vendor/requests/cookies.py b/src/pip/_vendor/requests/cookies.py
index 56fccd9c2..bf54ab237 100644
--- a/src/pip/_vendor/requests/cookies.py
+++ b/src/pip/_vendor/requests/cookies.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
requests.cookies
~~~~~~~~~~~~~~~~
@@ -9,12 +7,12 @@ Compatibility code to be able to use `cookielib.CookieJar` with requests.
requests.utils imports from here, so be careful with imports.
"""
+import calendar
import copy
import time
-import calendar
from ._internal_utils import to_native_string
-from .compat import cookielib, urlparse, urlunparse, Morsel, MutableMapping
+from .compat import Morsel, MutableMapping, cookielib, urlparse, urlunparse
try:
import threading
@@ -22,7 +20,7 @@ except ImportError:
import dummy_threading as threading
-class MockRequest(object):
+class MockRequest:
"""Wraps a `requests.Request` to mimic a `urllib2.Request`.
The code in `cookielib.CookieJar` expects this interface in order to correctly
@@ -51,16 +49,22 @@ class MockRequest(object):
def get_full_url(self):
# Only return the response's URL if the user hadn't set the Host
# header
- if not self._r.headers.get('Host'):
+ if not self._r.headers.get("Host"):
return self._r.url
# If they did set it, retrieve it and reconstruct the expected domain
- host = to_native_string(self._r.headers['Host'], encoding='utf-8')
+ host = to_native_string(self._r.headers["Host"], encoding="utf-8")
parsed = urlparse(self._r.url)
# Reconstruct the URL as we expect it
- return urlunparse([
- parsed.scheme, host, parsed.path, parsed.params, parsed.query,
- parsed.fragment
- ])
+ return urlunparse(
+ [
+ parsed.scheme,
+ host,
+ parsed.path,
+ parsed.params,
+ parsed.query,
+ parsed.fragment,
+ ]
+ )
def is_unverifiable(self):
return True
@@ -73,7 +77,9 @@ class MockRequest(object):
def add_header(self, key, val):
"""cookielib has no legitimate use for this method; add it back if you find one."""
- raise NotImplementedError("Cookie headers should be added with add_unredirected_header()")
+ raise NotImplementedError(
+ "Cookie headers should be added with add_unredirected_header()"
+ )
def add_unredirected_header(self, name, value):
self._new_headers[name] = value
@@ -94,7 +100,7 @@ class MockRequest(object):
return self.get_host()
-class MockResponse(object):
+class MockResponse:
"""Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`.
...what? Basically, expose the parsed HTTP headers from the server response
@@ -122,8 +128,7 @@ def extract_cookies_to_jar(jar, request, response):
:param request: our own requests.Request object
:param response: urllib3.HTTPResponse object
"""
- if not (hasattr(response, '_original_response') and
- response._original_response):
+ if not (hasattr(response, "_original_response") and response._original_response):
return
# the _original_response field is the wrapped httplib.HTTPResponse object,
req = MockRequest(request)
@@ -140,7 +145,7 @@ def get_cookie_header(jar, request):
"""
r = MockRequest(request)
jar.add_cookie_header(r)
- return r.get_new_headers().get('Cookie')
+ return r.get_new_headers().get("Cookie")
def remove_cookie_by_name(cookiejar, name, domain=None, path=None):
@@ -205,7 +210,9 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
"""
# support client code that unsets cookies by assignment of a None value:
if value is None:
- remove_cookie_by_name(self, name, domain=kwargs.get('domain'), path=kwargs.get('path'))
+ remove_cookie_by_name(
+ self, name, domain=kwargs.get("domain"), path=kwargs.get("path")
+ )
return
if isinstance(value, Morsel):
@@ -305,16 +312,15 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
"""
dictionary = {}
for cookie in iter(self):
- if (
- (domain is None or cookie.domain == domain) and
- (path is None or cookie.path == path)
+ if (domain is None or cookie.domain == domain) and (
+ path is None or cookie.path == path
):
dictionary[cookie.name] = cookie.value
return dictionary
def __contains__(self, name):
try:
- return super(RequestsCookieJar, self).__contains__(name)
+ return super().__contains__(name)
except CookieConflictError:
return True
@@ -341,9 +347,13 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
remove_cookie_by_name(self, name)
def set_cookie(self, cookie, *args, **kwargs):
- if hasattr(cookie.value, 'startswith') and cookie.value.startswith('"') and cookie.value.endswith('"'):
- cookie.value = cookie.value.replace('\\"', '')
- return super(RequestsCookieJar, self).set_cookie(cookie, *args, **kwargs)
+ if (
+ hasattr(cookie.value, "startswith")
+ and cookie.value.startswith('"')
+ and cookie.value.endswith('"')
+ ):
+ cookie.value = cookie.value.replace('\\"', "")
+ return super().set_cookie(cookie, *args, **kwargs)
def update(self, other):
"""Updates this jar with cookies from another CookieJar or dict-like"""
@@ -351,7 +361,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
for cookie in other:
self.set_cookie(copy.copy(cookie))
else:
- super(RequestsCookieJar, self).update(other)
+ super().update(other)
def _find(self, name, domain=None, path=None):
"""Requests uses this method internally to get cookie values.
@@ -371,7 +381,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
if path is None or cookie.path == path:
return cookie.value
- raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))
+ raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
def _find_no_duplicates(self, name, domain=None, path=None):
"""Both ``__get_item__`` and ``get`` call this function: it's never
@@ -390,25 +400,29 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
if cookie.name == name:
if domain is None or cookie.domain == domain:
if path is None or cookie.path == path:
- if toReturn is not None: # if there are multiple cookies that meet passed in criteria
- raise CookieConflictError('There are multiple cookies with name, %r' % (name))
- toReturn = cookie.value # we will eventually return this as long as no cookie conflict
+ if toReturn is not None:
+ # if there are multiple cookies that meet passed in criteria
+ raise CookieConflictError(
+ f"There are multiple cookies with name, {name!r}"
+ )
+ # we will eventually return this as long as no cookie conflict
+ toReturn = cookie.value
if toReturn:
return toReturn
- raise KeyError('name=%r, domain=%r, path=%r' % (name, domain, path))
+ raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
def __getstate__(self):
"""Unlike a normal CookieJar, this class is pickleable."""
state = self.__dict__.copy()
# remove the unpickleable RLock object
- state.pop('_cookies_lock')
+ state.pop("_cookies_lock")
return state
def __setstate__(self, state):
"""Unlike a normal CookieJar, this class is pickleable."""
self.__dict__.update(state)
- if '_cookies_lock' not in self.__dict__:
+ if "_cookies_lock" not in self.__dict__:
self._cookies_lock = threading.RLock()
def copy(self):
@@ -427,7 +441,7 @@ def _copy_cookie_jar(jar):
if jar is None:
return None
- if hasattr(jar, 'copy'):
+ if hasattr(jar, "copy"):
# We're dealing with an instance of RequestsCookieJar
return jar.copy()
# We're dealing with a generic CookieJar instance
@@ -445,31 +459,32 @@ def create_cookie(name, value, **kwargs):
and sent on every request (this is sometimes called a "supercookie").
"""
result = {
- 'version': 0,
- 'name': name,
- 'value': value,
- 'port': None,
- 'domain': '',
- 'path': '/',
- 'secure': False,
- 'expires': None,
- 'discard': True,
- 'comment': None,
- 'comment_url': None,
- 'rest': {'HttpOnly': None},
- 'rfc2109': False,
+ "version": 0,
+ "name": name,
+ "value": value,
+ "port": None,
+ "domain": "",
+ "path": "/",
+ "secure": False,
+ "expires": None,
+ "discard": True,
+ "comment": None,
+ "comment_url": None,
+ "rest": {"HttpOnly": None},
+ "rfc2109": False,
}
badargs = set(kwargs) - set(result)
if badargs:
- err = 'create_cookie() got unexpected keyword arguments: %s'
- raise TypeError(err % list(badargs))
+ raise TypeError(
+ f"create_cookie() got unexpected keyword arguments: {list(badargs)}"
+ )
result.update(kwargs)
- result['port_specified'] = bool(result['port'])
- result['domain_specified'] = bool(result['domain'])
- result['domain_initial_dot'] = result['domain'].startswith('.')
- result['path_specified'] = bool(result['path'])
+ result["port_specified"] = bool(result["port"])
+ result["domain_specified"] = bool(result["domain"])
+ result["domain_initial_dot"] = result["domain"].startswith(".")
+ result["path_specified"] = bool(result["path"])
return cookielib.Cookie(**result)
@@ -478,30 +493,28 @@ def morsel_to_cookie(morsel):
"""Convert a Morsel object into a Cookie containing the one k/v pair."""
expires = None
- if morsel['max-age']:
+ if morsel["max-age"]:
try:
- expires = int(time.time() + int(morsel['max-age']))
+ expires = int(time.time() + int(morsel["max-age"]))
except ValueError:
- raise TypeError('max-age: %s must be integer' % morsel['max-age'])
- elif morsel['expires']:
- time_template = '%a, %d-%b-%Y %H:%M:%S GMT'
- expires = calendar.timegm(
- time.strptime(morsel['expires'], time_template)
- )
+ raise TypeError(f"max-age: {morsel['max-age']} must be integer")
+ elif morsel["expires"]:
+ time_template = "%a, %d-%b-%Y %H:%M:%S GMT"
+ expires = calendar.timegm(time.strptime(morsel["expires"], time_template))
return create_cookie(
- comment=morsel['comment'],
- comment_url=bool(morsel['comment']),
+ comment=morsel["comment"],
+ comment_url=bool(morsel["comment"]),
discard=False,
- domain=morsel['domain'],
+ domain=morsel["domain"],
expires=expires,
name=morsel.key,
- path=morsel['path'],
+ path=morsel["path"],
port=None,
- rest={'HttpOnly': morsel['httponly']},
+ rest={"HttpOnly": morsel["httponly"]},
rfc2109=False,
- secure=bool(morsel['secure']),
+ secure=bool(morsel["secure"]),
value=morsel.value,
- version=morsel['version'] or 0,
+ version=morsel["version"] or 0,
)
@@ -534,11 +547,10 @@ def merge_cookies(cookiejar, cookies):
:rtype: CookieJar
"""
if not isinstance(cookiejar, cookielib.CookieJar):
- raise ValueError('You can only merge into CookieJar')
+ raise ValueError("You can only merge into CookieJar")
if isinstance(cookies, dict):
- cookiejar = cookiejar_from_dict(
- cookies, cookiejar=cookiejar, overwrite=False)
+ cookiejar = cookiejar_from_dict(cookies, cookiejar=cookiejar, overwrite=False)
elif isinstance(cookies, cookielib.CookieJar):
try:
cookiejar.update(cookies)
diff --git a/src/pip/_vendor/requests/exceptions.py b/src/pip/_vendor/requests/exceptions.py
index 9f0ad778b..168d07390 100644
--- a/src/pip/_vendor/requests/exceptions.py
+++ b/src/pip/_vendor/requests/exceptions.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
requests.exceptions
~~~~~~~~~~~~~~~~~~~
@@ -8,6 +6,8 @@ This module contains the set of Requests' exceptions.
"""
from pip._vendor.urllib3.exceptions import HTTPError as BaseHTTPError
+from .compat import JSONDecodeError as CompatJSONDecodeError
+
class RequestException(IOError):
"""There was an ambiguous exception that occurred while handling your
@@ -16,19 +16,32 @@ class RequestException(IOError):
def __init__(self, *args, **kwargs):
"""Initialize RequestException with `request` and `response` objects."""
- response = kwargs.pop('response', None)
+ response = kwargs.pop("response", None)
self.response = response
- self.request = kwargs.pop('request', None)
- if (response is not None and not self.request and
- hasattr(response, 'request')):
+ self.request = kwargs.pop("request", None)
+ if response is not None and not self.request and hasattr(response, "request"):
self.request = self.response.request
- super(RequestException, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
class InvalidJSONError(RequestException):
"""A JSON error occurred."""
+class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError):
+ """Couldn't decode the text into json"""
+
+ def __init__(self, *args, **kwargs):
+ """
+ Construct the JSONDecodeError instance first with all
+ args. Then use it's args to construct the IOError so that
+ the json specific args aren't used as IOError specific args
+ and the error message from JSONDecodeError is preserved.
+ """
+ CompatJSONDecodeError.__init__(self, *args)
+ InvalidJSONError.__init__(self, *self.args, **kwargs)
+
+
class HTTPError(RequestException):
"""An HTTP error occurred."""
@@ -74,11 +87,11 @@ class TooManyRedirects(RequestException):
class MissingSchema(RequestException, ValueError):
- """The URL schema (e.g. http or https) is missing."""
+ """The URL scheme (e.g. http or https) is missing."""
class InvalidSchema(RequestException, ValueError):
- """See defaults.py for valid schemas."""
+ """The URL scheme provided is either invalid or unsupported."""
class InvalidURL(RequestException, ValueError):
@@ -112,6 +125,7 @@ class RetryError(RequestException):
class UnrewindableBodyError(RequestException):
"""Requests encountered an error when trying to rewind a body."""
+
# Warnings
diff --git a/src/pip/_vendor/requests/help.py b/src/pip/_vendor/requests/help.py
index 745f0d7b3..2d292c2f0 100644
--- a/src/pip/_vendor/requests/help.py
+++ b/src/pip/_vendor/requests/help.py
@@ -1,10 +1,9 @@
"""Module containing bug report helper(s)."""
-from __future__ import print_function
import json
import platform
-import sys
import ssl
+import sys
from pip._vendor import idna
from pip._vendor import urllib3
@@ -25,16 +24,16 @@ except ImportError:
OpenSSL = None
cryptography = None
else:
- import OpenSSL
import cryptography
+ import OpenSSL
def _implementation():
"""Return a dict with the Python implementation and version.
Provide both the name and the version of the Python implementation
- currently running. For example, on CPython 2.7.5 it will return
- {'name': 'CPython', 'version': '2.7.5'}.
+ currently running. For example, on CPython 3.10.3 it will return
+ {'name': 'CPython', 'version': '3.10.3'}.
This function works best on CPython and PyPy: in particular, it probably
doesn't work for Jython or IronPython. Future investigation should be done
@@ -42,83 +41,83 @@ def _implementation():
"""
implementation = platform.python_implementation()
- if implementation == 'CPython':
+ if implementation == "CPython":
implementation_version = platform.python_version()
- elif implementation == 'PyPy':
- implementation_version = '%s.%s.%s' % (sys.pypy_version_info.major,
- sys.pypy_version_info.minor,
- sys.pypy_version_info.micro)
- if sys.pypy_version_info.releaselevel != 'final':
- implementation_version = ''.join([
- implementation_version, sys.pypy_version_info.releaselevel
- ])
- elif implementation == 'Jython':
+ elif implementation == "PyPy":
+ implementation_version = "{}.{}.{}".format(
+ sys.pypy_version_info.major,
+ sys.pypy_version_info.minor,
+ sys.pypy_version_info.micro,
+ )
+ if sys.pypy_version_info.releaselevel != "final":
+ implementation_version = "".join(
+ [implementation_version, sys.pypy_version_info.releaselevel]
+ )
+ elif implementation == "Jython":
implementation_version = platform.python_version() # Complete Guess
- elif implementation == 'IronPython':
+ elif implementation == "IronPython":
implementation_version = platform.python_version() # Complete Guess
else:
- implementation_version = 'Unknown'
+ implementation_version = "Unknown"
- return {'name': implementation, 'version': implementation_version}
+ return {"name": implementation, "version": implementation_version}
def info():
"""Generate information for a bug report."""
try:
platform_info = {
- 'system': platform.system(),
- 'release': platform.release(),
+ "system": platform.system(),
+ "release": platform.release(),
}
- except IOError:
+ except OSError:
platform_info = {
- 'system': 'Unknown',
- 'release': 'Unknown',
+ "system": "Unknown",
+ "release": "Unknown",
}
implementation_info = _implementation()
- urllib3_info = {'version': urllib3.__version__}
- charset_normalizer_info = {'version': None}
- chardet_info = {'version': None}
+ urllib3_info = {"version": urllib3.__version__}
+ charset_normalizer_info = {"version": None}
+ chardet_info = {"version": None}
if charset_normalizer:
- charset_normalizer_info = {'version': charset_normalizer.__version__}
+ charset_normalizer_info = {"version": charset_normalizer.__version__}
if chardet:
- chardet_info = {'version': chardet.__version__}
+ chardet_info = {"version": chardet.__version__}
pyopenssl_info = {
- 'version': None,
- 'openssl_version': '',
+ "version": None,
+ "openssl_version": "",
}
if OpenSSL:
pyopenssl_info = {
- 'version': OpenSSL.__version__,
- 'openssl_version': '%x' % OpenSSL.SSL.OPENSSL_VERSION_NUMBER,
+ "version": OpenSSL.__version__,
+ "openssl_version": f"{OpenSSL.SSL.OPENSSL_VERSION_NUMBER:x}",
}
cryptography_info = {
- 'version': getattr(cryptography, '__version__', ''),
+ "version": getattr(cryptography, "__version__", ""),
}
idna_info = {
- 'version': getattr(idna, '__version__', ''),
+ "version": getattr(idna, "__version__", ""),
}
system_ssl = ssl.OPENSSL_VERSION_NUMBER
- system_ssl_info = {
- 'version': '%x' % system_ssl if system_ssl is not None else ''
- }
+ system_ssl_info = {"version": f"{system_ssl:x}" if system_ssl is not None else ""}
return {
- 'platform': platform_info,
- 'implementation': implementation_info,
- 'system_ssl': system_ssl_info,
- 'using_pyopenssl': pyopenssl is not None,
- 'using_charset_normalizer': chardet is None,
- 'pyOpenSSL': pyopenssl_info,
- 'urllib3': urllib3_info,
- 'chardet': chardet_info,
- 'charset_normalizer': charset_normalizer_info,
- 'cryptography': cryptography_info,
- 'idna': idna_info,
- 'requests': {
- 'version': requests_version,
+ "platform": platform_info,
+ "implementation": implementation_info,
+ "system_ssl": system_ssl_info,
+ "using_pyopenssl": pyopenssl is not None,
+ "using_charset_normalizer": chardet is None,
+ "pyOpenSSL": pyopenssl_info,
+ "urllib3": urllib3_info,
+ "chardet": chardet_info,
+ "charset_normalizer": charset_normalizer_info,
+ "cryptography": cryptography_info,
+ "idna": idna_info,
+ "requests": {
+ "version": requests_version,
},
}
@@ -128,5 +127,5 @@ def main():
print(json.dumps(info(), sort_keys=True, indent=2))
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/src/pip/_vendor/requests/hooks.py b/src/pip/_vendor/requests/hooks.py
index 7a51f212c..d181ba2ec 100644
--- a/src/pip/_vendor/requests/hooks.py
+++ b/src/pip/_vendor/requests/hooks.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
requests.hooks
~~~~~~~~~~~~~~
@@ -11,12 +9,13 @@ Available hooks:
``response``:
The response generated from a Request.
"""
-HOOKS = ['response']
+HOOKS = ["response"]
def default_hooks():
return {event: [] for event in HOOKS}
+
# TODO: response is the only one
@@ -25,7 +24,7 @@ def dispatch_hook(key, hooks, hook_data, **kwargs):
hooks = hooks or {}
hooks = hooks.get(key)
if hooks:
- if hasattr(hooks, '__call__'):
+ if hasattr(hooks, "__call__"):
hooks = [hooks]
for hook in hooks:
_hook_data = hook(hook_data, **kwargs)
diff --git a/src/pip/_vendor/requests/models.py b/src/pip/_vendor/requests/models.py
index c10c6011b..b45e81032 100644
--- a/src/pip/_vendor/requests/models.py
+++ b/src/pip/_vendor/requests/models.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
requests.models
~~~~~~~~~~~~~~~
@@ -8,46 +6,72 @@ This module contains the primary objects that power Requests.
"""
import datetime
-import sys
# Import encoding now, to avoid implicit import later.
# Implicit import within threads may cause LookupError when standard library is in a ZIP,
# such as in Embedded Python. See https://github.com/psf/requests/issues/3578.
-import encodings.idna
+import encodings.idna # noqa: F401
+from io import UnsupportedOperation
+from pip._vendor.urllib3.exceptions import (
+ DecodeError,
+ LocationParseError,
+ ProtocolError,
+ ReadTimeoutError,
+ SSLError,
+)
from pip._vendor.urllib3.fields import RequestField
from pip._vendor.urllib3.filepost import encode_multipart_formdata
from pip._vendor.urllib3.util import parse_url
-from pip._vendor.urllib3.exceptions import (
- DecodeError, ReadTimeoutError, ProtocolError, LocationParseError)
-
-from io import UnsupportedOperation
-from .hooks import default_hooks
-from .structures import CaseInsensitiveDict
-from .auth import HTTPBasicAuth
-from .cookies import cookiejar_from_dict, get_cookie_header, _copy_cookie_jar
-from .exceptions import (
- HTTPError, MissingSchema, InvalidURL, ChunkedEncodingError,
- ContentDecodingError, ConnectionError, StreamConsumedError, InvalidJSONError)
from ._internal_utils import to_native_string, unicode_is_ascii
-from .utils import (
- guess_filename, get_auth_from_url, requote_uri,
- stream_decode_response_unicode, to_key_val_list, parse_header_links,
- iter_slices, guess_json_utf, super_len, check_header_validity)
+from .auth import HTTPBasicAuth
from .compat import (
- Callable, Mapping,
- cookielib, urlunparse, urlsplit, urlencode, str, bytes,
- is_py2, chardet, builtin_str, basestring)
+ Callable,
+ JSONDecodeError,
+ Mapping,
+ basestring,
+ builtin_str,
+ chardet,
+ cookielib,
+)
from .compat import json as complexjson
+from .compat import urlencode, urlsplit, urlunparse
+from .cookies import _copy_cookie_jar, cookiejar_from_dict, get_cookie_header
+from .exceptions import (
+ ChunkedEncodingError,
+ ConnectionError,
+ ContentDecodingError,
+ HTTPError,
+ InvalidJSONError,
+ InvalidURL,
+)
+from .exceptions import JSONDecodeError as RequestsJSONDecodeError
+from .exceptions import MissingSchema
+from .exceptions import SSLError as RequestsSSLError
+from .exceptions import StreamConsumedError
+from .hooks import default_hooks
from .status_codes import codes
+from .structures import CaseInsensitiveDict
+from .utils import (
+ check_header_validity,
+ get_auth_from_url,
+ guess_filename,
+ guess_json_utf,
+ iter_slices,
+ parse_header_links,
+ requote_uri,
+ stream_decode_response_unicode,
+ super_len,
+ to_key_val_list,
+)
#: The set of HTTP status codes that indicate an automatically
#: processable redirect.
REDIRECT_STATI = (
- codes.moved, # 301
- codes.found, # 302
- codes.other, # 303
+ codes.moved, # 301
+ codes.found, # 302
+ codes.other, # 303
codes.temporary_redirect, # 307
codes.permanent_redirect, # 308
)
@@ -57,7 +81,7 @@ CONTENT_CHUNK_SIZE = 10 * 1024
ITER_CHUNK_SIZE = 512
-class RequestEncodingMixin(object):
+class RequestEncodingMixin:
@property
def path_url(self):
"""Build the path URL to use."""
@@ -68,16 +92,16 @@ class RequestEncodingMixin(object):
path = p.path
if not path:
- path = '/'
+ path = "/"
url.append(path)
query = p.query
if query:
- url.append('?')
+ url.append("?")
url.append(query)
- return ''.join(url)
+ return "".join(url)
@staticmethod
def _encode_params(data):
@@ -90,18 +114,21 @@ class RequestEncodingMixin(object):
if isinstance(data, (str, bytes)):
return data
- elif hasattr(data, 'read'):
+ elif hasattr(data, "read"):
return data
- elif hasattr(data, '__iter__'):
+ elif hasattr(data, "__iter__"):
result = []
for k, vs in to_key_val_list(data):
- if isinstance(vs, basestring) or not hasattr(vs, '__iter__'):
+ if isinstance(vs, basestring) or not hasattr(vs, "__iter__"):
vs = [vs]
for v in vs:
if v is not None:
result.append(
- (k.encode('utf-8') if isinstance(k, str) else k,
- v.encode('utf-8') if isinstance(v, str) else v))
+ (
+ k.encode("utf-8") if isinstance(k, str) else k,
+ v.encode("utf-8") if isinstance(v, str) else v,
+ )
+ )
return urlencode(result, doseq=True)
else:
return data
@@ -116,7 +143,7 @@ class RequestEncodingMixin(object):
The tuples may be 2-tuples (filename, fileobj), 3-tuples (filename, fileobj, contentype)
or 4-tuples (filename, fileobj, contentype, custom_headers).
"""
- if (not files):
+ if not files:
raise ValueError("Files must be provided.")
elif isinstance(data, basestring):
raise ValueError("Data must not be a string.")
@@ -126,7 +153,7 @@ class RequestEncodingMixin(object):
files = to_key_val_list(files or {})
for field, val in fields:
- if isinstance(val, basestring) or not hasattr(val, '__iter__'):
+ if isinstance(val, basestring) or not hasattr(val, "__iter__"):
val = [val]
for v in val:
if v is not None:
@@ -135,8 +162,13 @@ class RequestEncodingMixin(object):
v = str(v)
new_fields.append(
- (field.decode('utf-8') if isinstance(field, bytes) else field,
- v.encode('utf-8') if isinstance(v, str) else v))
+ (
+ field.decode("utf-8")
+ if isinstance(field, bytes)
+ else field,
+ v.encode("utf-8") if isinstance(v, str) else v,
+ )
+ )
for (k, v) in files:
# support for explicit filename
@@ -155,7 +187,7 @@ class RequestEncodingMixin(object):
if isinstance(fp, (str, bytes, bytearray)):
fdata = fp
- elif hasattr(fp, 'read'):
+ elif hasattr(fp, "read"):
fdata = fp.read()
elif fp is None:
continue
@@ -171,16 +203,16 @@ class RequestEncodingMixin(object):
return body, content_type
-class RequestHooksMixin(object):
+class RequestHooksMixin:
def register_hook(self, event, hook):
"""Properly register a hook."""
if event not in self.hooks:
- raise ValueError('Unsupported event specified, with event name "%s"' % (event))
+ raise ValueError(f'Unsupported event specified, with event name "{event}"')
if isinstance(hook, Callable):
self.hooks[event].append(hook)
- elif hasattr(hook, '__iter__'):
+ elif hasattr(hook, "__iter__"):
self.hooks[event].extend(h for h in hook if isinstance(h, Callable))
def deregister_hook(self, event, hook):
@@ -223,9 +255,19 @@ class Request(RequestHooksMixin):
<PreparedRequest [GET]>
"""
- def __init__(self,
- method=None, url=None, headers=None, files=None, data=None,
- params=None, auth=None, cookies=None, hooks=None, json=None):
+ def __init__(
+ self,
+ method=None,
+ url=None,
+ headers=None,
+ files=None,
+ data=None,
+ params=None,
+ auth=None,
+ cookies=None,
+ hooks=None,
+ json=None,
+ ):
# Default empty dicts for dict params.
data = [] if data is None else data
@@ -249,7 +291,7 @@ class Request(RequestHooksMixin):
self.cookies = cookies
def __repr__(self):
- return '<Request [%s]>' % (self.method)
+ return f"<Request [{self.method}]>"
def prepare(self):
"""Constructs a :class:`PreparedRequest <PreparedRequest>` for transmission and returns it."""
@@ -307,9 +349,19 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
#: integer denoting starting position of a readable file-like body.
self._body_position = None
- def prepare(self,
- method=None, url=None, headers=None, files=None, data=None,
- params=None, auth=None, cookies=None, hooks=None, json=None):
+ def prepare(
+ self,
+ method=None,
+ url=None,
+ headers=None,
+ files=None,
+ data=None,
+ params=None,
+ auth=None,
+ cookies=None,
+ hooks=None,
+ json=None,
+ ):
"""Prepares the entire request with the given parameters."""
self.prepare_method(method)
@@ -326,7 +378,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
self.prepare_hooks(hooks)
def __repr__(self):
- return '<PreparedRequest [%s]>' % (self.method)
+ return f"<PreparedRequest [{self.method}]>"
def copy(self):
p = PreparedRequest()
@@ -350,7 +402,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
from pip._vendor import idna
try:
- host = idna.encode(host, uts46=True).decode('utf-8')
+ host = idna.encode(host, uts46=True).decode("utf-8")
except idna.IDNAError:
raise UnicodeError
return host
@@ -363,9 +415,9 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
#: on python 3.x.
#: https://github.com/psf/requests/pull/2238
if isinstance(url, bytes):
- url = url.decode('utf8')
+ url = url.decode("utf8")
else:
- url = unicode(url) if is_py2 else str(url)
+ url = str(url)
# Remove leading whitespaces from url
url = url.lstrip()
@@ -373,7 +425,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
# Don't do any URL preparation for non-HTTP schemes like `mailto`,
# `data` etc to work around exceptions from `url_parse`, which
# handles RFC 3986 only.
- if ':' in url and not url.lower().startswith('http'):
+ if ":" in url and not url.lower().startswith("http"):
self.url = url
return
@@ -384,13 +436,13 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
raise InvalidURL(*e.args)
if not scheme:
- error = ("Invalid URL {0!r}: No schema supplied. Perhaps you meant http://{0}?")
- error = error.format(to_native_string(url, 'utf8'))
-
- raise MissingSchema(error)
+ raise MissingSchema(
+ f"Invalid URL {url!r}: No scheme supplied. "
+ f"Perhaps you meant http://{url}?"
+ )
if not host:
- raise InvalidURL("Invalid URL %r: No host supplied" % url)
+ raise InvalidURL(f"Invalid URL {url!r}: No host supplied")
# In general, we want to try IDNA encoding the hostname if the string contains
# non-ASCII characters. This allows users to automatically get the correct IDNA
@@ -400,33 +452,21 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
try:
host = self._get_idna_encoded_host(host)
except UnicodeError:
- raise InvalidURL('URL has an invalid label.')
- elif host.startswith(u'*'):
- raise InvalidURL('URL has an invalid label.')
+ raise InvalidURL("URL has an invalid label.")
+ elif host.startswith(("*", ".")):
+ raise InvalidURL("URL has an invalid label.")
# Carefully reconstruct the network location
- netloc = auth or ''
+ netloc = auth or ""
if netloc:
- netloc += '@'
+ netloc += "@"
netloc += host
if port:
- netloc += ':' + str(port)
+ netloc += f":{port}"
# Bare domains aren't valid URLs.
if not path:
- path = '/'
-
- if is_py2:
- if isinstance(scheme, str):
- scheme = scheme.encode('utf-8')
- if isinstance(netloc, str):
- netloc = netloc.encode('utf-8')
- if isinstance(path, str):
- path = path.encode('utf-8')
- if isinstance(query, str):
- query = query.encode('utf-8')
- if isinstance(fragment, str):
- fragment = fragment.encode('utf-8')
+ path = "/"
if isinstance(params, (str, bytes)):
params = to_native_string(params)
@@ -434,7 +474,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
enc_params = self._encode_params(params)
if enc_params:
if query:
- query = '%s&%s' % (query, enc_params)
+ query = f"{query}&{enc_params}"
else:
query = enc_params
@@ -465,20 +505,22 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
if not data and json is not None:
# urllib3 requires a bytes-like body. Python 2's json.dumps
# provides this natively, but Python 3 gives a Unicode string.
- content_type = 'application/json'
+ content_type = "application/json"
try:
- body = complexjson.dumps(json, allow_nan=False)
+ body = complexjson.dumps(json, allow_nan=False)
except ValueError as ve:
- raise InvalidJSONError(ve, request=self)
+ raise InvalidJSONError(ve, request=self)
if not isinstance(body, bytes):
- body = body.encode('utf-8')
+ body = body.encode("utf-8")
- is_stream = all([
- hasattr(data, '__iter__'),
- not isinstance(data, (basestring, list, tuple, Mapping))
- ])
+ is_stream = all(
+ [
+ hasattr(data, "__iter__"),
+ not isinstance(data, (basestring, list, tuple, Mapping)),
+ ]
+ )
if is_stream:
try:
@@ -488,24 +530,26 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
body = data
- if getattr(body, 'tell', None) is not None:
+ if getattr(body, "tell", None) is not None:
# Record the current file position before reading.
# This will allow us to rewind a file in the event
# of a redirect.
try:
self._body_position = body.tell()
- except (IOError, OSError):
+ except OSError:
# This differentiates from None, allowing us to catch
# a failed `tell()` later when trying to rewind the body
self._body_position = object()
if files:
- raise NotImplementedError('Streamed bodies and files are mutually exclusive.')
+ raise NotImplementedError(
+ "Streamed bodies and files are mutually exclusive."
+ )
if length:
- self.headers['Content-Length'] = builtin_str(length)
+ self.headers["Content-Length"] = builtin_str(length)
else:
- self.headers['Transfer-Encoding'] = 'chunked'
+ self.headers["Transfer-Encoding"] = "chunked"
else:
# Multi-part file uploads.
if files:
@@ -513,16 +557,16 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
else:
if data:
body = self._encode_params(data)
- if isinstance(data, basestring) or hasattr(data, 'read'):
+ if isinstance(data, basestring) or hasattr(data, "read"):
content_type = None
else:
- content_type = 'application/x-www-form-urlencoded'
+ content_type = "application/x-www-form-urlencoded"
self.prepare_content_length(body)
# Add content-type if it wasn't explicitly provided.
- if content_type and ('content-type' not in self.headers):
- self.headers['Content-Type'] = content_type
+ if content_type and ("content-type" not in self.headers):
+ self.headers["Content-Type"] = content_type
self.body = body
@@ -533,13 +577,16 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
if length:
# If length exists, set it. Otherwise, we fallback
# to Transfer-Encoding: chunked.
- self.headers['Content-Length'] = builtin_str(length)
- elif self.method not in ('GET', 'HEAD') and self.headers.get('Content-Length') is None:
+ self.headers["Content-Length"] = builtin_str(length)
+ elif (
+ self.method not in ("GET", "HEAD")
+ and self.headers.get("Content-Length") is None
+ ):
# Set Content-Length to 0 for methods that can have a body
# but don't provide one. (i.e. not GET or HEAD)
- self.headers['Content-Length'] = '0'
+ self.headers["Content-Length"] = "0"
- def prepare_auth(self, auth, url=''):
+ def prepare_auth(self, auth, url=""):
"""Prepares the given HTTP auth data."""
# If no Auth is explicitly provided, extract it from the URL first.
@@ -579,7 +626,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
cookie_header = get_cookie_header(self._cookies, self)
if cookie_header is not None:
- self.headers['Cookie'] = cookie_header
+ self.headers["Cookie"] = cookie_header
def prepare_hooks(self, hooks):
"""Prepares the given hooks."""
@@ -591,14 +638,22 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
self.register_hook(event, hooks[event])
-class Response(object):
+class Response:
"""The :class:`Response <Response>` object, which contains a
server's response to an HTTP request.
"""
__attrs__ = [
- '_content', 'status_code', 'headers', 'url', 'history',
- 'encoding', 'reason', 'cookies', 'elapsed', 'request'
+ "_content",
+ "status_code",
+ "headers",
+ "url",
+ "history",
+ "encoding",
+ "reason",
+ "cookies",
+ "elapsed",
+ "request",
]
def __init__(self):
@@ -667,11 +722,11 @@ class Response(object):
setattr(self, name, value)
# pickled objects do not have .raw
- setattr(self, '_content_consumed', True)
- setattr(self, 'raw', None)
+ setattr(self, "_content_consumed", True)
+ setattr(self, "raw", None)
def __repr__(self):
- return '<Response [%s]>' % (self.status_code)
+ return f"<Response [{self.status_code}]>"
def __bool__(self):
"""Returns True if :attr:`status_code` is less than 400.
@@ -717,12 +772,15 @@ class Response(object):
"""True if this Response is a well-formed HTTP redirect that could have
been processed automatically (by :meth:`Session.resolve_redirects`).
"""
- return ('location' in self.headers and self.status_code in REDIRECT_STATI)
+ return "location" in self.headers and self.status_code in REDIRECT_STATI
@property
def is_permanent_redirect(self):
"""True if this Response one of the permanent versions of redirect."""
- return ('location' in self.headers and self.status_code in (codes.moved_permanently, codes.permanent_redirect))
+ return "location" in self.headers and self.status_code in (
+ codes.moved_permanently,
+ codes.permanent_redirect,
+ )
@property
def next(self):
@@ -732,7 +790,7 @@ class Response(object):
@property
def apparent_encoding(self):
"""The apparent encoding, provided by the charset_normalizer or chardet libraries."""
- return chardet.detect(self.content)['encoding']
+ return chardet.detect(self.content)["encoding"]
def iter_content(self, chunk_size=1, decode_unicode=False):
"""Iterates over the response data. When stream=True is set on the
@@ -753,16 +811,17 @@ class Response(object):
def generate():
# Special case for urllib3.
- if hasattr(self.raw, 'stream'):
+ if hasattr(self.raw, "stream"):
try:
- for chunk in self.raw.stream(chunk_size, decode_content=True):
- yield chunk
+ yield from self.raw.stream(chunk_size, decode_content=True)
except ProtocolError as e:
raise ChunkedEncodingError(e)
except DecodeError as e:
raise ContentDecodingError(e)
except ReadTimeoutError as e:
raise ConnectionError(e)
+ except SSLError as e:
+ raise RequestsSSLError(e)
else:
# Standard file-like object.
while True:
@@ -776,7 +835,9 @@ class Response(object):
if self._content_consumed and isinstance(self._content, bool):
raise StreamConsumedError()
elif chunk_size is not None and not isinstance(chunk_size, int):
- raise TypeError("chunk_size must be an int, it is instead a %s." % type(chunk_size))
+ raise TypeError(
+ f"chunk_size must be an int, it is instead a {type(chunk_size)}."
+ )
# simulate reading small chunks of the content
reused_chunks = iter_slices(self._content, chunk_size)
@@ -789,7 +850,9 @@ class Response(object):
return chunks
- def iter_lines(self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=False, delimiter=None):
+ def iter_lines(
+ self, chunk_size=ITER_CHUNK_SIZE, decode_unicode=False, delimiter=None
+ ):
"""Iterates over the response data, one line at a time. When
stream=True is set on the request, this avoids reading the
content at once into memory for large responses.
@@ -799,7 +862,9 @@ class Response(object):
pending = None
- for chunk in self.iter_content(chunk_size=chunk_size, decode_unicode=decode_unicode):
+ for chunk in self.iter_content(
+ chunk_size=chunk_size, decode_unicode=decode_unicode
+ ):
if pending is not None:
chunk = pending + chunk
@@ -814,8 +879,7 @@ class Response(object):
else:
pending = None
- for line in lines:
- yield line
+ yield from lines
if pending is not None:
yield pending
@@ -827,13 +891,12 @@ class Response(object):
if self._content is False:
# Read the contents.
if self._content_consumed:
- raise RuntimeError(
- 'The content for this response was already consumed')
+ raise RuntimeError("The content for this response was already consumed")
if self.status_code == 0 or self.raw is None:
self._content = None
else:
- self._content = b''.join(self.iter_content(CONTENT_CHUNK_SIZE)) or b''
+ self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b""
self._content_consumed = True
# don't need to release the connection; that's been handled by urllib3
@@ -858,7 +921,7 @@ class Response(object):
encoding = self.encoding
if not self.content:
- return str('')
+ return ""
# Fallback to auto-detected encoding.
if self.encoding is None:
@@ -866,7 +929,7 @@ class Response(object):
# Decode unicode from given encoding.
try:
- content = str(self.content, encoding, errors='replace')
+ content = str(self.content, encoding, errors="replace")
except (LookupError, TypeError):
# A LookupError is raised if the encoding was not found which could
# indicate a misspelling or similar mistake.
@@ -874,7 +937,7 @@ class Response(object):
# A TypeError can be raised if encoding is None
#
# So we try blindly encoding.
- content = str(self.content, errors='replace')
+ content = str(self.content, errors="replace")
return content
@@ -882,12 +945,8 @@ class Response(object):
r"""Returns the json-encoded content of a response, if any.
:param \*\*kwargs: Optional arguments that ``json.loads`` takes.
- :raises simplejson.JSONDecodeError: If the response body does not
- contain valid json and simplejson is installed.
- :raises json.JSONDecodeError: If the response body does not contain
- valid json and simplejson is not installed on Python 3.
- :raises ValueError: If the response body does not contain valid
- json and simplejson is not installed on Python 2.
+ :raises requests.exceptions.JSONDecodeError: If the response body does not
+ contain valid json.
"""
if not self.encoding and self.content and len(self.content) > 3:
@@ -898,56 +957,65 @@ class Response(object):
encoding = guess_json_utf(self.content)
if encoding is not None:
try:
- return complexjson.loads(
- self.content.decode(encoding), **kwargs
- )
+ return complexjson.loads(self.content.decode(encoding), **kwargs)
except UnicodeDecodeError:
# Wrong UTF codec detected; usually because it's not UTF-8
# but some other 8-bit codec. This is an RFC violation,
# and the server didn't bother to tell us what codec *was*
# used.
pass
- return complexjson.loads(self.text, **kwargs)
+ except JSONDecodeError as e:
+ raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
+
+ try:
+ return complexjson.loads(self.text, **kwargs)
+ except JSONDecodeError as e:
+ # Catch JSON-related errors and raise as requests.JSONDecodeError
+ # This aliases json.JSONDecodeError and simplejson.JSONDecodeError
+ raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
@property
def links(self):
"""Returns the parsed header links of the response, if any."""
- header = self.headers.get('link')
+ header = self.headers.get("link")
- # l = MultiDict()
- l = {}
+ resolved_links = {}
if header:
links = parse_header_links(header)
for link in links:
- key = link.get('rel') or link.get('url')
- l[key] = link
+ key = link.get("rel") or link.get("url")
+ resolved_links[key] = link
- return l
+ return resolved_links
def raise_for_status(self):
"""Raises :class:`HTTPError`, if one occurred."""
- http_error_msg = ''
+ http_error_msg = ""
if isinstance(self.reason, bytes):
# We attempt to decode utf-8 first because some servers
# choose to localize their reason strings. If the string
# isn't utf-8, we fall back to iso-8859-1 for all other
# encodings. (See PR #3538)
try:
- reason = self.reason.decode('utf-8')
+ reason = self.reason.decode("utf-8")
except UnicodeDecodeError:
- reason = self.reason.decode('iso-8859-1')
+ reason = self.reason.decode("iso-8859-1")
else:
reason = self.reason
if 400 <= self.status_code < 500:
- http_error_msg = u'%s Client Error: %s for url: %s' % (self.status_code, reason, self.url)
+ http_error_msg = (
+ f"{self.status_code} Client Error: {reason} for url: {self.url}"
+ )
elif 500 <= self.status_code < 600:
- http_error_msg = u'%s Server Error: %s for url: %s' % (self.status_code, reason, self.url)
+ http_error_msg = (
+ f"{self.status_code} Server Error: {reason} for url: {self.url}"
+ )
if http_error_msg:
raise HTTPError(http_error_msg, response=self)
@@ -961,6 +1029,6 @@ class Response(object):
if not self._content_consumed:
self.raw.close()
- release_conn = getattr(self.raw, 'release_conn', None)
+ release_conn = getattr(self.raw, "release_conn", None)
if release_conn is not None:
release_conn()
diff --git a/src/pip/_vendor/requests/sessions.py b/src/pip/_vendor/requests/sessions.py
index ae4bcc8e7..6cb3b4dae 100644
--- a/src/pip/_vendor/requests/sessions.py
+++ b/src/pip/_vendor/requests/sessions.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
requests.sessions
~~~~~~~~~~~~~~~~~
@@ -10,39 +8,52 @@ requests (cookies, auth, proxies).
import os
import sys
import time
-from datetime import timedelta
from collections import OrderedDict
+from datetime import timedelta
+from ._internal_utils import to_native_string
+from .adapters import HTTPAdapter
from .auth import _basic_auth_str
-from .compat import cookielib, is_py3, urljoin, urlparse, Mapping
+from .compat import Mapping, cookielib, urljoin, urlparse
from .cookies import (
- cookiejar_from_dict, extract_cookies_to_jar, RequestsCookieJar, merge_cookies)
-from .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT
-from .hooks import default_hooks, dispatch_hook
-from ._internal_utils import to_native_string
-from .utils import to_key_val_list, default_headers, DEFAULT_PORTS
+ RequestsCookieJar,
+ cookiejar_from_dict,
+ extract_cookies_to_jar,
+ merge_cookies,
+)
from .exceptions import (
- TooManyRedirects, InvalidSchema, ChunkedEncodingError, ContentDecodingError)
-
-from .structures import CaseInsensitiveDict
-from .adapters import HTTPAdapter
-
-from .utils import (
- requote_uri, get_environ_proxies, get_netrc_auth, should_bypass_proxies,
- get_auth_from_url, rewind_body
+ ChunkedEncodingError,
+ ContentDecodingError,
+ InvalidSchema,
+ TooManyRedirects,
)
-
-from .status_codes import codes
+from .hooks import default_hooks, dispatch_hook
# formerly defined here, reexposed here for backward compatibility
-from .models import REDIRECT_STATI
+from .models import ( # noqa: F401
+ DEFAULT_REDIRECT_LIMIT,
+ REDIRECT_STATI,
+ PreparedRequest,
+ Request,
+)
+from .status_codes import codes
+from .structures import CaseInsensitiveDict
+from .utils import ( # noqa: F401
+ DEFAULT_PORTS,
+ default_headers,
+ get_auth_from_url,
+ get_environ_proxies,
+ get_netrc_auth,
+ requote_uri,
+ resolve_proxies,
+ rewind_body,
+ should_bypass_proxies,
+ to_key_val_list,
+)
# Preferred clock, based on which one is more accurate on a given system.
-if sys.platform == 'win32':
- try: # Python 3.4+
- preferred_clock = time.perf_counter
- except AttributeError: # Earlier than Python 3.
- preferred_clock = time.clock
+if sys.platform == "win32":
+ preferred_clock = time.perf_counter
else:
preferred_clock = time.time
@@ -61,8 +72,7 @@ def merge_setting(request_setting, session_setting, dict_class=OrderedDict):
# Bypass if not a dictionary (e.g. verify)
if not (
- isinstance(session_setting, Mapping) and
- isinstance(request_setting, Mapping)
+ isinstance(session_setting, Mapping) and isinstance(request_setting, Mapping)
):
return request_setting
@@ -84,17 +94,16 @@ def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict):
This is necessary because when request_hooks == {'response': []}, the
merge breaks Session hooks entirely.
"""
- if session_hooks is None or session_hooks.get('response') == []:
+ if session_hooks is None or session_hooks.get("response") == []:
return request_hooks
- if request_hooks is None or request_hooks.get('response') == []:
+ if request_hooks is None or request_hooks.get("response") == []:
return session_hooks
return merge_setting(request_hooks, session_hooks, dict_class)
-class SessionRedirectMixin(object):
-
+class SessionRedirectMixin:
def get_redirect_target(self, resp):
"""Receives a Response. Returns a redirect URI or ``None``"""
# Due to the nature of how requests processes redirects this method will
@@ -104,16 +113,15 @@ class SessionRedirectMixin(object):
# to cache the redirect location onto the response object as a private
# attribute.
if resp.is_redirect:
- location = resp.headers['location']
+ location = resp.headers["location"]
# Currently the underlying http module on py3 decode headers
# in latin1, but empirical evidence suggests that latin1 is very
# rarely used with non-ASCII characters in HTTP headers.
# It is more likely to get UTF8 header rather than latin1.
# This causes incorrect handling of UTF8 encoded location headers.
# To solve this, we re-encode the location in latin1.
- if is_py3:
- location = location.encode('latin1')
- return to_native_string(location, 'utf8')
+ location = location.encode("latin1")
+ return to_native_string(location, "utf8")
return None
def should_strip_auth(self, old_url, new_url):
@@ -126,23 +134,40 @@ class SessionRedirectMixin(object):
# ports. This isn't specified by RFC 7235, but is kept to avoid
# breaking backwards compatibility with older versions of requests
# that allowed any redirects on the same host.
- if (old_parsed.scheme == 'http' and old_parsed.port in (80, None)
- and new_parsed.scheme == 'https' and new_parsed.port in (443, None)):
+ if (
+ old_parsed.scheme == "http"
+ and old_parsed.port in (80, None)
+ and new_parsed.scheme == "https"
+ and new_parsed.port in (443, None)
+ ):
return False
# Handle default port usage corresponding to scheme.
changed_port = old_parsed.port != new_parsed.port
changed_scheme = old_parsed.scheme != new_parsed.scheme
default_port = (DEFAULT_PORTS.get(old_parsed.scheme, None), None)
- if (not changed_scheme and old_parsed.port in default_port
- and new_parsed.port in default_port):
+ if (
+ not changed_scheme
+ and old_parsed.port in default_port
+ and new_parsed.port in default_port
+ ):
return False
# Standard case: root URI must match
return changed_port or changed_scheme
- def resolve_redirects(self, resp, req, stream=False, timeout=None,
- verify=True, cert=None, proxies=None, yield_requests=False, **adapter_kwargs):
+ def resolve_redirects(
+ self,
+ resp,
+ req,
+ stream=False,
+ timeout=None,
+ verify=True,
+ cert=None,
+ proxies=None,
+ yield_requests=False,
+ **adapter_kwargs,
+ ):
"""Receives a Response. Returns a generator of Responses or Requests."""
hist = [] # keep track of history
@@ -163,19 +188,21 @@ class SessionRedirectMixin(object):
resp.raw.read(decode_content=False)
if len(resp.history) >= self.max_redirects:
- raise TooManyRedirects('Exceeded {} redirects.'.format(self.max_redirects), response=resp)
+ raise TooManyRedirects(
+ f"Exceeded {self.max_redirects} redirects.", response=resp
+ )
# Release the connection back into the pool.
resp.close()
# Handle redirection without scheme (see: RFC 1808 Section 4)
- if url.startswith('//'):
+ if url.startswith("//"):
parsed_rurl = urlparse(resp.url)
- url = ':'.join([to_native_string(parsed_rurl.scheme), url])
+ url = ":".join([to_native_string(parsed_rurl.scheme), url])
# Normalize url case and attach previous fragment if needed (RFC 7231 7.1.2)
parsed = urlparse(url)
- if parsed.fragment == '' and previous_fragment:
+ if parsed.fragment == "" and previous_fragment:
parsed = parsed._replace(fragment=previous_fragment)
elif parsed.fragment:
previous_fragment = parsed.fragment
@@ -194,15 +221,18 @@ class SessionRedirectMixin(object):
self.rebuild_method(prepared_request, resp)
# https://github.com/psf/requests/issues/1084
- if resp.status_code not in (codes.temporary_redirect, codes.permanent_redirect):
+ if resp.status_code not in (
+ codes.temporary_redirect,
+ codes.permanent_redirect,
+ ):
# https://github.com/psf/requests/issues/3490
- purged_headers = ('Content-Length', 'Content-Type', 'Transfer-Encoding')
+ purged_headers = ("Content-Length", "Content-Type", "Transfer-Encoding")
for header in purged_headers:
prepared_request.headers.pop(header, None)
prepared_request.body = None
headers = prepared_request.headers
- headers.pop('Cookie', None)
+ headers.pop("Cookie", None)
# Extract any cookies sent on the response to the cookiejar
# in the new request. Because we've mutated our copied prepared
@@ -218,9 +248,8 @@ class SessionRedirectMixin(object):
# A failed tell() sets `_body_position` to `object()`. This non-None
# value ensures `rewindable` will be True, allowing us to raise an
# UnrewindableBodyError, instead of hanging the connection.
- rewindable = (
- prepared_request._body_position is not None and
- ('Content-Length' in headers or 'Transfer-Encoding' in headers)
+ rewindable = prepared_request._body_position is not None and (
+ "Content-Length" in headers or "Transfer-Encoding" in headers
)
# Attempt to rewind consumed file-like object.
@@ -242,7 +271,7 @@ class SessionRedirectMixin(object):
cert=cert,
proxies=proxies,
allow_redirects=False,
- **adapter_kwargs
+ **adapter_kwargs,
)
extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
@@ -259,17 +288,18 @@ class SessionRedirectMixin(object):
headers = prepared_request.headers
url = prepared_request.url
- if 'Authorization' in headers and self.should_strip_auth(response.request.url, url):
+ if "Authorization" in headers and self.should_strip_auth(
+ response.request.url, url
+ ):
# If we get redirected to a new host, we should strip out any
# authentication headers.
- del headers['Authorization']
+ del headers["Authorization"]
# .netrc might have more auth for us on our new host.
new_auth = get_netrc_auth(url) if self.trust_env else None
if new_auth is not None:
prepared_request.prepare_auth(new_auth)
-
def rebuild_proxies(self, prepared_request, proxies):
"""This method re-evaluates the proxy configuration by considering the
environment variables. If we are redirected to a URL covered by
@@ -282,24 +312,12 @@ class SessionRedirectMixin(object):
:rtype: dict
"""
- proxies = proxies if proxies is not None else {}
headers = prepared_request.headers
- url = prepared_request.url
- scheme = urlparse(url).scheme
- new_proxies = proxies.copy()
- no_proxy = proxies.get('no_proxy')
+ scheme = urlparse(prepared_request.url).scheme
+ new_proxies = resolve_proxies(prepared_request, proxies, self.trust_env)
- bypass_proxy = should_bypass_proxies(url, no_proxy=no_proxy)
- if self.trust_env and not bypass_proxy:
- environ_proxies = get_environ_proxies(url, no_proxy=no_proxy)
-
- proxy = environ_proxies.get(scheme, environ_proxies.get('all'))
-
- if proxy:
- new_proxies.setdefault(scheme, proxy)
-
- if 'Proxy-Authorization' in headers:
- del headers['Proxy-Authorization']
+ if "Proxy-Authorization" in headers:
+ del headers["Proxy-Authorization"]
try:
username, password = get_auth_from_url(new_proxies[scheme])
@@ -307,7 +325,7 @@ class SessionRedirectMixin(object):
username, password = None, None
if username and password:
- headers['Proxy-Authorization'] = _basic_auth_str(username, password)
+ headers["Proxy-Authorization"] = _basic_auth_str(username, password)
return new_proxies
@@ -318,18 +336,18 @@ class SessionRedirectMixin(object):
method = prepared_request.method
# https://tools.ietf.org/html/rfc7231#section-6.4.4
- if response.status_code == codes.see_other and method != 'HEAD':
- method = 'GET'
+ if response.status_code == codes.see_other and method != "HEAD":
+ method = "GET"
# Do what the browsers do, despite standards...
# First, turn 302s into GETs.
- if response.status_code == codes.found and method != 'HEAD':
- method = 'GET'
+ if response.status_code == codes.found and method != "HEAD":
+ method = "GET"
# Second, if a POST is responded to with a 301, turn it into a GET.
# This bizarre behaviour is explained in Issue 1704.
- if response.status_code == codes.moved and method == 'POST':
- method = 'GET'
+ if response.status_code == codes.moved and method == "POST":
+ method = "GET"
prepared_request.method = method
@@ -354,9 +372,18 @@ class Session(SessionRedirectMixin):
"""
__attrs__ = [
- 'headers', 'cookies', 'auth', 'proxies', 'hooks', 'params', 'verify',
- 'cert', 'adapters', 'stream', 'trust_env',
- 'max_redirects',
+ "headers",
+ "cookies",
+ "auth",
+ "proxies",
+ "hooks",
+ "params",
+ "verify",
+ "cert",
+ "adapters",
+ "stream",
+ "trust_env",
+ "max_redirects",
]
def __init__(self):
@@ -418,8 +445,8 @@ class Session(SessionRedirectMixin):
# Default connection adapters.
self.adapters = OrderedDict()
- self.mount('https://', HTTPAdapter())
- self.mount('http://', HTTPAdapter())
+ self.mount("https://", HTTPAdapter())
+ self.mount("http://", HTTPAdapter())
def __enter__(self):
return self
@@ -445,7 +472,8 @@ class Session(SessionRedirectMixin):
# Merge with session cookies
merged_cookies = merge_cookies(
- merge_cookies(RequestsCookieJar(), self.cookies), cookies)
+ merge_cookies(RequestsCookieJar(), self.cookies), cookies
+ )
# Set environment's basic authentication if not explicitly set.
auth = request.auth
@@ -459,7 +487,9 @@ class Session(SessionRedirectMixin):
files=request.files,
data=request.data,
json=request.json,
- headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict),
+ headers=merge_setting(
+ request.headers, self.headers, dict_class=CaseInsensitiveDict
+ ),
params=merge_setting(request.params, self.params),
auth=merge_setting(auth, self.auth),
cookies=merged_cookies,
@@ -467,10 +497,25 @@ class Session(SessionRedirectMixin):
)
return p
- def request(self, method, url,
- params=None, data=None, headers=None, cookies=None, files=None,
- auth=None, timeout=None, allow_redirects=True, proxies=None,
- hooks=None, stream=None, verify=None, cert=None, json=None):
+ def request(
+ self,
+ method,
+ url,
+ params=None,
+ data=None,
+ headers=None,
+ cookies=None,
+ files=None,
+ auth=None,
+ timeout=None,
+ allow_redirects=True,
+ proxies=None,
+ hooks=None,
+ stream=None,
+ verify=None,
+ cert=None,
+ json=None,
+ ):
"""Constructs a :class:`Request <Request>`, prepares it and sends it.
Returns :class:`Response <Response>` object.
@@ -506,7 +551,7 @@ class Session(SessionRedirectMixin):
``False``, requests will accept any TLS certificate presented by
the server, and will ignore hostname mismatches and/or expired
certificates, which will make your application vulnerable to
- man-in-the-middle (MitM) attacks. Setting verify to ``False``
+ man-in-the-middle (MitM) attacks. Setting verify to ``False``
may be useful during local development or testing.
:param cert: (optional) if String, path to ssl client cert file (.pem).
If Tuple, ('cert', 'key') pair.
@@ -535,8 +580,8 @@ class Session(SessionRedirectMixin):
# Send the request.
send_kwargs = {
- 'timeout': timeout,
- 'allow_redirects': allow_redirects,
+ "timeout": timeout,
+ "allow_redirects": allow_redirects,
}
send_kwargs.update(settings)
resp = self.send(prep, **send_kwargs)
@@ -551,8 +596,8 @@ class Session(SessionRedirectMixin):
:rtype: requests.Response
"""
- kwargs.setdefault('allow_redirects', True)
- return self.request('GET', url, **kwargs)
+ kwargs.setdefault("allow_redirects", True)
+ return self.request("GET", url, **kwargs)
def options(self, url, **kwargs):
r"""Sends a OPTIONS request. Returns :class:`Response` object.
@@ -562,8 +607,8 @@ class Session(SessionRedirectMixin):
:rtype: requests.Response
"""
- kwargs.setdefault('allow_redirects', True)
- return self.request('OPTIONS', url, **kwargs)
+ kwargs.setdefault("allow_redirects", True)
+ return self.request("OPTIONS", url, **kwargs)
def head(self, url, **kwargs):
r"""Sends a HEAD request. Returns :class:`Response` object.
@@ -573,8 +618,8 @@ class Session(SessionRedirectMixin):
:rtype: requests.Response
"""
- kwargs.setdefault('allow_redirects', False)
- return self.request('HEAD', url, **kwargs)
+ kwargs.setdefault("allow_redirects", False)
+ return self.request("HEAD", url, **kwargs)
def post(self, url, data=None, json=None, **kwargs):
r"""Sends a POST request. Returns :class:`Response` object.
@@ -587,7 +632,7 @@ class Session(SessionRedirectMixin):
:rtype: requests.Response
"""
- return self.request('POST', url, data=data, json=json, **kwargs)
+ return self.request("POST", url, data=data, json=json, **kwargs)
def put(self, url, data=None, **kwargs):
r"""Sends a PUT request. Returns :class:`Response` object.
@@ -599,7 +644,7 @@ class Session(SessionRedirectMixin):
:rtype: requests.Response
"""
- return self.request('PUT', url, data=data, **kwargs)
+ return self.request("PUT", url, data=data, **kwargs)
def patch(self, url, data=None, **kwargs):
r"""Sends a PATCH request. Returns :class:`Response` object.
@@ -611,7 +656,7 @@ class Session(SessionRedirectMixin):
:rtype: requests.Response
"""
- return self.request('PATCH', url, data=data, **kwargs)
+ return self.request("PATCH", url, data=data, **kwargs)
def delete(self, url, **kwargs):
r"""Sends a DELETE request. Returns :class:`Response` object.
@@ -621,7 +666,7 @@ class Session(SessionRedirectMixin):
:rtype: requests.Response
"""
- return self.request('DELETE', url, **kwargs)
+ return self.request("DELETE", url, **kwargs)
def send(self, request, **kwargs):
"""Send a given PreparedRequest.
@@ -630,19 +675,20 @@ class Session(SessionRedirectMixin):
"""
# Set defaults that the hooks can utilize to ensure they always have
# the correct parameters to reproduce the previous request.
- kwargs.setdefault('stream', self.stream)
- kwargs.setdefault('verify', self.verify)
- kwargs.setdefault('cert', self.cert)
- kwargs.setdefault('proxies', self.rebuild_proxies(request, self.proxies))
+ kwargs.setdefault("stream", self.stream)
+ kwargs.setdefault("verify", self.verify)
+ kwargs.setdefault("cert", self.cert)
+ if "proxies" not in kwargs:
+ kwargs["proxies"] = resolve_proxies(request, self.proxies, self.trust_env)
# It's possible that users might accidentally send a Request object.
# Guard against that specific failure case.
if isinstance(request, Request):
- raise ValueError('You can only send PreparedRequests.')
+ raise ValueError("You can only send PreparedRequests.")
# Set up variables needed for resolve_redirects and dispatching of hooks
- allow_redirects = kwargs.pop('allow_redirects', True)
- stream = kwargs.get('stream')
+ allow_redirects = kwargs.pop("allow_redirects", True)
+ stream = kwargs.get("stream")
hooks = request.hooks
# Get the appropriate adapter to use
@@ -659,7 +705,7 @@ class Session(SessionRedirectMixin):
r.elapsed = timedelta(seconds=elapsed)
# Response manipulation hooks
- r = dispatch_hook('response', hooks, r, **kwargs)
+ r = dispatch_hook("response", hooks, r, **kwargs)
# Persist cookies
if r.history:
@@ -689,7 +735,9 @@ class Session(SessionRedirectMixin):
# If redirects aren't being followed, store the response on the Request for Response.next().
if not allow_redirects:
try:
- r._next = next(self.resolve_redirects(r, request, yield_requests=True, **kwargs))
+ r._next = next(
+ self.resolve_redirects(r, request, yield_requests=True, **kwargs)
+ )
except StopIteration:
pass
@@ -707,16 +755,19 @@ class Session(SessionRedirectMixin):
# Gather clues from the surrounding environment.
if self.trust_env:
# Set environment's proxies.
- no_proxy = proxies.get('no_proxy') if proxies is not None else None
+ no_proxy = proxies.get("no_proxy") if proxies is not None else None
env_proxies = get_environ_proxies(url, no_proxy=no_proxy)
for (k, v) in env_proxies.items():
proxies.setdefault(k, v)
- # Look for requests environment configuration and be compatible
- # with cURL.
+ # Look for requests environment configuration
+ # and be compatible with cURL.
if verify is True or verify is None:
- verify = (os.environ.get('REQUESTS_CA_BUNDLE') or
- os.environ.get('CURL_CA_BUNDLE'))
+ verify = (
+ os.environ.get("REQUESTS_CA_BUNDLE")
+ or os.environ.get("CURL_CA_BUNDLE")
+ or verify
+ )
# Merge all the kwargs.
proxies = merge_setting(proxies, self.proxies)
@@ -724,8 +775,7 @@ class Session(SessionRedirectMixin):
verify = merge_setting(verify, self.verify)
cert = merge_setting(cert, self.cert)
- return {'verify': verify, 'proxies': proxies, 'stream': stream,
- 'cert': cert}
+ return {"proxies": proxies, "stream": stream, "verify": verify, "cert": cert}
def get_adapter(self, url):
"""
@@ -739,7 +789,7 @@ class Session(SessionRedirectMixin):
return adapter
# Nothing matches :-/
- raise InvalidSchema("No connection adapters were found for {!r}".format(url))
+ raise InvalidSchema(f"No connection adapters were found for {url!r}")
def close(self):
"""Closes all adapters and as such the session"""
diff --git a/src/pip/_vendor/requests/status_codes.py b/src/pip/_vendor/requests/status_codes.py
index d80a7cd4d..4bd072be9 100644
--- a/src/pip/_vendor/requests/status_codes.py
+++ b/src/pip/_vendor/requests/status_codes.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
r"""
The ``codes`` object defines a mapping from common names for HTTP statuses
to their numerical codes, accessible either as attributes or as dictionary
@@ -23,101 +21,108 @@ the names are allowed. For example, ``codes.ok``, ``codes.OK``, and
from .structures import LookupDict
_codes = {
-
# Informational.
- 100: ('continue',),
- 101: ('switching_protocols',),
- 102: ('processing',),
- 103: ('checkpoint',),
- 122: ('uri_too_long', 'request_uri_too_long'),
- 200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', '\\o/', '✓'),
- 201: ('created',),
- 202: ('accepted',),
- 203: ('non_authoritative_info', 'non_authoritative_information'),
- 204: ('no_content',),
- 205: ('reset_content', 'reset'),
- 206: ('partial_content', 'partial'),
- 207: ('multi_status', 'multiple_status', 'multi_stati', 'multiple_stati'),
- 208: ('already_reported',),
- 226: ('im_used',),
-
+ 100: ("continue",),
+ 101: ("switching_protocols",),
+ 102: ("processing",),
+ 103: ("checkpoint",),
+ 122: ("uri_too_long", "request_uri_too_long"),
+ 200: ("ok", "okay", "all_ok", "all_okay", "all_good", "\\o/", "✓"),
+ 201: ("created",),
+ 202: ("accepted",),
+ 203: ("non_authoritative_info", "non_authoritative_information"),
+ 204: ("no_content",),
+ 205: ("reset_content", "reset"),
+ 206: ("partial_content", "partial"),
+ 207: ("multi_status", "multiple_status", "multi_stati", "multiple_stati"),
+ 208: ("already_reported",),
+ 226: ("im_used",),
# Redirection.
- 300: ('multiple_choices',),
- 301: ('moved_permanently', 'moved', '\\o-'),
- 302: ('found',),
- 303: ('see_other', 'other'),
- 304: ('not_modified',),
- 305: ('use_proxy',),
- 306: ('switch_proxy',),
- 307: ('temporary_redirect', 'temporary_moved', 'temporary'),
- 308: ('permanent_redirect',
- 'resume_incomplete', 'resume',), # These 2 to be removed in 3.0
-
+ 300: ("multiple_choices",),
+ 301: ("moved_permanently", "moved", "\\o-"),
+ 302: ("found",),
+ 303: ("see_other", "other"),
+ 304: ("not_modified",),
+ 305: ("use_proxy",),
+ 306: ("switch_proxy",),
+ 307: ("temporary_redirect", "temporary_moved", "temporary"),
+ 308: (
+ "permanent_redirect",
+ "resume_incomplete",
+ "resume",
+ ), # "resume" and "resume_incomplete" to be removed in 3.0
# Client Error.
- 400: ('bad_request', 'bad'),
- 401: ('unauthorized',),
- 402: ('payment_required', 'payment'),
- 403: ('forbidden',),
- 404: ('not_found', '-o-'),
- 405: ('method_not_allowed', 'not_allowed'),
- 406: ('not_acceptable',),
- 407: ('proxy_authentication_required', 'proxy_auth', 'proxy_authentication'),
- 408: ('request_timeout', 'timeout'),
- 409: ('conflict',),
- 410: ('gone',),
- 411: ('length_required',),
- 412: ('precondition_failed', 'precondition'),
- 413: ('request_entity_too_large',),
- 414: ('request_uri_too_large',),
- 415: ('unsupported_media_type', 'unsupported_media', 'media_type'),
- 416: ('requested_range_not_satisfiable', 'requested_range', 'range_not_satisfiable'),
- 417: ('expectation_failed',),
- 418: ('im_a_teapot', 'teapot', 'i_am_a_teapot'),
- 421: ('misdirected_request',),
- 422: ('unprocessable_entity', 'unprocessable'),
- 423: ('locked',),
- 424: ('failed_dependency', 'dependency'),
- 425: ('unordered_collection', 'unordered'),
- 426: ('upgrade_required', 'upgrade'),
- 428: ('precondition_required', 'precondition'),
- 429: ('too_many_requests', 'too_many'),
- 431: ('header_fields_too_large', 'fields_too_large'),
- 444: ('no_response', 'none'),
- 449: ('retry_with', 'retry'),
- 450: ('blocked_by_windows_parental_controls', 'parental_controls'),
- 451: ('unavailable_for_legal_reasons', 'legal_reasons'),
- 499: ('client_closed_request',),
-
+ 400: ("bad_request", "bad"),
+ 401: ("unauthorized",),
+ 402: ("payment_required", "payment"),
+ 403: ("forbidden",),
+ 404: ("not_found", "-o-"),
+ 405: ("method_not_allowed", "not_allowed"),
+ 406: ("not_acceptable",),
+ 407: ("proxy_authentication_required", "proxy_auth", "proxy_authentication"),
+ 408: ("request_timeout", "timeout"),
+ 409: ("conflict",),
+ 410: ("gone",),
+ 411: ("length_required",),
+ 412: ("precondition_failed", "precondition"),
+ 413: ("request_entity_too_large",),
+ 414: ("request_uri_too_large",),
+ 415: ("unsupported_media_type", "unsupported_media", "media_type"),
+ 416: (
+ "requested_range_not_satisfiable",
+ "requested_range",
+ "range_not_satisfiable",
+ ),
+ 417: ("expectation_failed",),
+ 418: ("im_a_teapot", "teapot", "i_am_a_teapot"),
+ 421: ("misdirected_request",),
+ 422: ("unprocessable_entity", "unprocessable"),
+ 423: ("locked",),
+ 424: ("failed_dependency", "dependency"),
+ 425: ("unordered_collection", "unordered"),
+ 426: ("upgrade_required", "upgrade"),
+ 428: ("precondition_required", "precondition"),
+ 429: ("too_many_requests", "too_many"),
+ 431: ("header_fields_too_large", "fields_too_large"),
+ 444: ("no_response", "none"),
+ 449: ("retry_with", "retry"),
+ 450: ("blocked_by_windows_parental_controls", "parental_controls"),
+ 451: ("unavailable_for_legal_reasons", "legal_reasons"),
+ 499: ("client_closed_request",),
# Server Error.
- 500: ('internal_server_error', 'server_error', '/o\\', '✗'),
- 501: ('not_implemented',),
- 502: ('bad_gateway',),
- 503: ('service_unavailable', 'unavailable'),
- 504: ('gateway_timeout',),
- 505: ('http_version_not_supported', 'http_version'),
- 506: ('variant_also_negotiates',),
- 507: ('insufficient_storage',),
- 509: ('bandwidth_limit_exceeded', 'bandwidth'),
- 510: ('not_extended',),
- 511: ('network_authentication_required', 'network_auth', 'network_authentication'),
+ 500: ("internal_server_error", "server_error", "/o\\", "✗"),
+ 501: ("not_implemented",),
+ 502: ("bad_gateway",),
+ 503: ("service_unavailable", "unavailable"),
+ 504: ("gateway_timeout",),
+ 505: ("http_version_not_supported", "http_version"),
+ 506: ("variant_also_negotiates",),
+ 507: ("insufficient_storage",),
+ 509: ("bandwidth_limit_exceeded", "bandwidth"),
+ 510: ("not_extended",),
+ 511: ("network_authentication_required", "network_auth", "network_authentication"),
}
-codes = LookupDict(name='status_codes')
+codes = LookupDict(name="status_codes")
+
def _init():
for code, titles in _codes.items():
for title in titles:
setattr(codes, title, code)
- if not title.startswith(('\\', '/')):
+ if not title.startswith(("\\", "/")):
setattr(codes, title.upper(), code)
def doc(code):
- names = ', '.join('``%s``' % n for n in _codes[code])
- return '* %d: %s' % (code, names)
+ names = ", ".join(f"``{n}``" for n in _codes[code])
+ return "* %d: %s" % (code, names)
global __doc__
- __doc__ = (__doc__ + '\n' +
- '\n'.join(doc(code) for code in sorted(_codes))
- if __doc__ is not None else None)
+ __doc__ = (
+ __doc__ + "\n" + "\n".join(doc(code) for code in sorted(_codes))
+ if __doc__ is not None
+ else None
+ )
+
_init()
diff --git a/src/pip/_vendor/requests/structures.py b/src/pip/_vendor/requests/structures.py
index 8ee0ba7a0..188e13e48 100644
--- a/src/pip/_vendor/requests/structures.py
+++ b/src/pip/_vendor/requests/structures.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
requests.structures
~~~~~~~~~~~~~~~~~~~
@@ -64,11 +62,7 @@ class CaseInsensitiveDict(MutableMapping):
def lower_items(self):
"""Like iteritems(), but with all lowercase keys."""
- return (
- (lowerkey, keyval[1])
- for (lowerkey, keyval)
- in self._store.items()
- )
+ return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items())
def __eq__(self, other):
if isinstance(other, Mapping):
@@ -91,10 +85,10 @@ class LookupDict(dict):
def __init__(self, name=None):
self.name = name
- super(LookupDict, self).__init__()
+ super().__init__()
def __repr__(self):
- return '<lookup \'%s\'>' % (self.name)
+ return f"<lookup '{self.name}'>"
def __getitem__(self, key):
# We allow fall-through here, so values default to None
diff --git a/src/pip/_vendor/requests/utils.py b/src/pip/_vendor/requests/utils.py
index fcb996690..33f394d26 100644
--- a/src/pip/_vendor/requests/utils.py
+++ b/src/pip/_vendor/requests/utils.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
requests.utils
~~~~~~~~~~~~~~
@@ -20,27 +18,46 @@ import tempfile
import warnings
import zipfile
from collections import OrderedDict
-from pip._vendor.urllib3.util import make_headers
-from .__version__ import __version__
+from pip._vendor.urllib3.util import make_headers, parse_url
+
from . import certs
+from .__version__ import __version__
+
# to_native_string is unused here, but imported here for backwards compatibility
-from ._internal_utils import to_native_string
+from ._internal_utils import HEADER_VALIDATORS, to_native_string # noqa: F401
+from .compat import (
+ Mapping,
+ basestring,
+ bytes,
+ getproxies,
+ getproxies_environment,
+ integer_types,
+)
from .compat import parse_http_list as _parse_list_header
from .compat import (
- quote, urlparse, bytes, str, unquote, getproxies,
- proxy_bypass, urlunparse, basestring, integer_types, is_py3,
- proxy_bypass_environment, getproxies_environment, Mapping)
+ proxy_bypass,
+ proxy_bypass_environment,
+ quote,
+ str,
+ unquote,
+ urlparse,
+ urlunparse,
+)
from .cookies import cookiejar_from_dict
-from .structures import CaseInsensitiveDict
from .exceptions import (
- InvalidURL, InvalidHeader, FileModeWarning, UnrewindableBodyError)
+ FileModeWarning,
+ InvalidHeader,
+ InvalidURL,
+ UnrewindableBodyError,
+)
+from .structures import CaseInsensitiveDict
-NETRC_FILES = ('.netrc', '_netrc')
+NETRC_FILES = (".netrc", "_netrc")
DEFAULT_CA_BUNDLE_PATH = certs.where()
-DEFAULT_PORTS = {'http': 80, 'https': 443}
+DEFAULT_PORTS = {"http": 80, "https": 443}
# Ensure that ', ' is used to preserve previous delimiter behavior.
DEFAULT_ACCEPT_ENCODING = ", ".join(
@@ -48,28 +65,25 @@ DEFAULT_ACCEPT_ENCODING = ", ".join(
)
-if sys.platform == 'win32':
+if sys.platform == "win32":
# provide a proxy_bypass version on Windows without DNS lookups
def proxy_bypass_registry(host):
try:
- if is_py3:
- import winreg
- else:
- import _winreg as winreg
+ import winreg
except ImportError:
return False
try:
- internetSettings = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
- r'Software\Microsoft\Windows\CurrentVersion\Internet Settings')
+ internetSettings = winreg.OpenKey(
+ winreg.HKEY_CURRENT_USER,
+ r"Software\Microsoft\Windows\CurrentVersion\Internet Settings",
+ )
# ProxyEnable could be REG_SZ or REG_DWORD, normalizing it
- proxyEnable = int(winreg.QueryValueEx(internetSettings,
- 'ProxyEnable')[0])
+ proxyEnable = int(winreg.QueryValueEx(internetSettings, "ProxyEnable")[0])
# ProxyOverride is almost always a string
- proxyOverride = winreg.QueryValueEx(internetSettings,
- 'ProxyOverride')[0]
- except OSError:
+ proxyOverride = winreg.QueryValueEx(internetSettings, "ProxyOverride")[0]
+ except (OSError, ValueError):
return False
if not proxyEnable or not proxyOverride:
return False
@@ -77,15 +91,15 @@ if sys.platform == 'win32':
# make a check value list from the registry entry: replace the
# '<local>' string by the localhost entry and the corresponding
# canonical entry.
- proxyOverride = proxyOverride.split(';')
+ proxyOverride = proxyOverride.split(";")
# now check if we match one of the registry values.
for test in proxyOverride:
- if test == '<local>':
- if '.' not in host:
+ if test == "<local>":
+ if "." not in host:
return True
- test = test.replace(".", r"\.") # mask dots
- test = test.replace("*", r".*") # change glob sequence
- test = test.replace("?", r".") # change glob char
+ test = test.replace(".", r"\.") # mask dots
+ test = test.replace("*", r".*") # change glob sequence
+ test = test.replace("?", r".") # change glob char
if re.match(test, host, re.I):
return True
return False
@@ -105,7 +119,7 @@ if sys.platform == 'win32':
def dict_to_sequence(d):
"""Returns an internal sequence dictionary update."""
- if hasattr(d, 'items'):
+ if hasattr(d, "items"):
d = d.items()
return d
@@ -115,37 +129,42 @@ def super_len(o):
total_length = None
current_position = 0
- if hasattr(o, '__len__'):
+ if hasattr(o, "__len__"):
total_length = len(o)
- elif hasattr(o, 'len'):
+ elif hasattr(o, "len"):
total_length = o.len
- elif hasattr(o, 'fileno'):
+ elif hasattr(o, "fileno"):
try:
fileno = o.fileno()
- except io.UnsupportedOperation:
+ except (io.UnsupportedOperation, AttributeError):
+ # AttributeError is a surprising exception, seeing as how we've just checked
+ # that `hasattr(o, 'fileno')`. It happens for objects obtained via
+ # `Tarfile.extractfile()`, per issue 5229.
pass
else:
total_length = os.fstat(fileno).st_size
# Having used fstat to determine the file length, we need to
# confirm that this file was opened up in binary mode.
- if 'b' not in o.mode:
- warnings.warn((
- "Requests has determined the content-length for this "
- "request using the binary size of the file: however, the "
- "file has been opened in text mode (i.e. without the 'b' "
- "flag in the mode). This may lead to an incorrect "
- "content-length. In Requests 3.0, support will be removed "
- "for files in text mode."),
- FileModeWarning
+ if "b" not in o.mode:
+ warnings.warn(
+ (
+ "Requests has determined the content-length for this "
+ "request using the binary size of the file: however, the "
+ "file has been opened in text mode (i.e. without the 'b' "
+ "flag in the mode). This may lead to an incorrect "
+ "content-length. In Requests 3.0, support will be removed "
+ "for files in text mode."
+ ),
+ FileModeWarning,
)
- if hasattr(o, 'tell'):
+ if hasattr(o, "tell"):
try:
current_position = o.tell()
- except (OSError, IOError):
+ except OSError:
# This can happen in some weird situations, such as when the file
# is actually a special file descriptor like stdin. In this
# instance, we don't know what the length is, so set it to zero and
@@ -153,8 +172,8 @@ def super_len(o):
if total_length is not None:
current_position = total_length
else:
- if hasattr(o, 'seek') and total_length is None:
- # StringIO and BytesIO have seek but no useable fileno
+ if hasattr(o, "seek") and total_length is None:
+ # StringIO and BytesIO have seek but no usable fileno
try:
# seek to end of file
o.seek(0, 2)
@@ -163,7 +182,7 @@ def super_len(o):
# seek back to current position to support
# partially read file-like objects
o.seek(current_position or 0)
- except (OSError, IOError):
+ except OSError:
total_length = 0
if total_length is None:
@@ -175,14 +194,14 @@ def super_len(o):
def get_netrc_auth(url, raise_errors=False):
"""Returns the Requests tuple auth for a given url from netrc."""
- netrc_file = os.environ.get('NETRC')
+ netrc_file = os.environ.get("NETRC")
if netrc_file is not None:
netrc_locations = (netrc_file,)
else:
- netrc_locations = ('~/{}'.format(f) for f in NETRC_FILES)
+ netrc_locations = (f"~/{f}" for f in NETRC_FILES)
try:
- from netrc import netrc, NetrcParseError
+ from netrc import NetrcParseError, netrc
netrc_path = None
@@ -207,18 +226,18 @@ def get_netrc_auth(url, raise_errors=False):
# Strip port numbers from netloc. This weird `if...encode`` dance is
# used for Python 3.2, which doesn't support unicode literals.
- splitstr = b':'
+ splitstr = b":"
if isinstance(url, str):
- splitstr = splitstr.decode('ascii')
+ splitstr = splitstr.decode("ascii")
host = ri.netloc.split(splitstr)[0]
try:
_netrc = netrc(netrc_path).authenticators(host)
if _netrc:
# Return with login / password
- login_i = (0 if _netrc[0] else 1)
+ login_i = 0 if _netrc[0] else 1
return (_netrc[login_i], _netrc[2])
- except (NetrcParseError, IOError):
+ except (NetrcParseError, OSError):
# If there was a parsing error or a permissions issue reading the file,
# we'll just skip netrc auth unless explicitly asked to raise errors.
if raise_errors:
@@ -231,9 +250,8 @@ def get_netrc_auth(url, raise_errors=False):
def guess_filename(obj):
"""Tries to guess the filename of the given object."""
- name = getattr(obj, 'name', None)
- if (name and isinstance(name, basestring) and name[0] != '<' and
- name[-1] != '>'):
+ name = getattr(obj, "name", None)
+ if name and isinstance(name, basestring) and name[0] != "<" and name[-1] != ">":
return os.path.basename(name)
@@ -251,7 +269,11 @@ def extract_zipped_paths(path):
archive, member = os.path.split(path)
while archive and not os.path.exists(archive):
archive, prefix = os.path.split(archive)
- member = '/'.join([prefix, member])
+ if not prefix:
+ # If we don't check for an empty prefix after the split (in other words, archive remains unchanged after the split),
+ # we _can_ end up in an infinite loop on a rare corner case affecting a small number of users
+ break
+ member = "/".join([prefix, member])
if not zipfile.is_zipfile(archive):
return path
@@ -262,7 +284,7 @@ def extract_zipped_paths(path):
# we have a valid zip archive and a valid member of that archive
tmp = tempfile.gettempdir()
- extracted_path = os.path.join(tmp, member.split('/')[-1])
+ extracted_path = os.path.join(tmp, member.split("/")[-1])
if not os.path.exists(extracted_path):
# use read + write to avoid the creating nested folders, we only want the file, avoids mkdir racing condition
with atomic_open(extracted_path) as file_handler:
@@ -273,12 +295,11 @@ def extract_zipped_paths(path):
@contextlib.contextmanager
def atomic_open(filename):
"""Write a file to the disk in an atomic fashion"""
- replacer = os.rename if sys.version_info[0] == 2 else os.replace
tmp_descriptor, tmp_name = tempfile.mkstemp(dir=os.path.dirname(filename))
try:
- with os.fdopen(tmp_descriptor, 'wb') as tmp_handler:
+ with os.fdopen(tmp_descriptor, "wb") as tmp_handler:
yield tmp_handler
- replacer(tmp_name, filename)
+ os.replace(tmp_name, filename)
except BaseException:
os.remove(tmp_name)
raise
@@ -306,7 +327,7 @@ def from_key_val_list(value):
return None
if isinstance(value, (str, bytes, bool, int)):
- raise ValueError('cannot encode objects that are not 2-tuples')
+ raise ValueError("cannot encode objects that are not 2-tuples")
return OrderedDict(value)
@@ -332,7 +353,7 @@ def to_key_val_list(value):
return None
if isinstance(value, (str, bytes, bool, int)):
- raise ValueError('cannot encode objects that are not 2-tuples')
+ raise ValueError("cannot encode objects that are not 2-tuples")
if isinstance(value, Mapping):
value = value.items()
@@ -397,10 +418,10 @@ def parse_dict_header(value):
"""
result = {}
for item in _parse_list_header(value):
- if '=' not in item:
+ if "=" not in item:
result[item] = None
continue
- name, value = item.split('=', 1)
+ name, value = item.split("=", 1)
if value[:1] == value[-1:] == '"':
value = unquote_header_value(value[1:-1])
result[name] = value
@@ -428,8 +449,8 @@ def unquote_header_value(value, is_filename=False):
# replace sequence below on a UNC path has the effect of turning
# the leading double slash into a single slash and then
# _fix_ie_filename() doesn't work correctly. See #458.
- if not is_filename or value[:2] != '\\\\':
- return value.replace('\\\\', '\\').replace('\\"', '"')
+ if not is_filename or value[:2] != "\\\\":
+ return value.replace("\\\\", "\\").replace('\\"', '"')
return value
@@ -464,19 +485,24 @@ def get_encodings_from_content(content):
:param content: bytestring to extract encodings from.
"""
- warnings.warn((
- 'In requests 3.0, get_encodings_from_content will be removed. For '
- 'more information, please see the discussion on issue #2266. (This'
- ' warning should only appear once.)'),
- DeprecationWarning)
+ warnings.warn(
+ (
+ "In requests 3.0, get_encodings_from_content will be removed. For "
+ "more information, please see the discussion on issue #2266. (This"
+ " warning should only appear once.)"
+ ),
+ DeprecationWarning,
+ )
charset_re = re.compile(r'<meta.*?charset=["\']*(.+?)["\'>]', flags=re.I)
pragma_re = re.compile(r'<meta.*?content=["\']*;?charset=(.+?)["\'>]', flags=re.I)
xml_re = re.compile(r'^<\?xml.*?encoding=["\']*(.+?)["\'>]')
- return (charset_re.findall(content) +
- pragma_re.findall(content) +
- xml_re.findall(content))
+ return (
+ charset_re.findall(content)
+ + pragma_re.findall(content)
+ + xml_re.findall(content)
+ )
def _parse_content_type_header(header):
@@ -487,7 +513,7 @@ def _parse_content_type_header(header):
parameters
"""
- tokens = header.split(';')
+ tokens = header.split(";")
content_type, params = tokens[0].strip(), tokens[1:]
params_dict = {}
items_to_strip = "\"' "
@@ -499,7 +525,7 @@ def _parse_content_type_header(header):
index_of_equals = param.find("=")
if index_of_equals != -1:
key = param[:index_of_equals].strip(items_to_strip)
- value = param[index_of_equals + 1:].strip(items_to_strip)
+ value = param[index_of_equals + 1 :].strip(items_to_strip)
params_dict[key.lower()] = value
return content_type, params_dict
@@ -511,38 +537,37 @@ def get_encoding_from_headers(headers):
:rtype: str
"""
- content_type = headers.get('content-type')
+ content_type = headers.get("content-type")
if not content_type:
return None
content_type, params = _parse_content_type_header(content_type)
- if 'charset' in params:
- return params['charset'].strip("'\"")
+ if "charset" in params:
+ return params["charset"].strip("'\"")
- if 'text' in content_type:
- return 'ISO-8859-1'
+ if "text" in content_type:
+ return "ISO-8859-1"
- if 'application/json' in content_type:
+ if "application/json" in content_type:
# Assume UTF-8 based on RFC 4627: https://www.ietf.org/rfc/rfc4627.txt since the charset was unset
- return 'utf-8'
+ return "utf-8"
def stream_decode_response_unicode(iterator, r):
- """Stream decodes a iterator."""
+ """Stream decodes an iterator."""
if r.encoding is None:
- for item in iterator:
- yield item
+ yield from iterator
return
- decoder = codecs.getincrementaldecoder(r.encoding)(errors='replace')
+ decoder = codecs.getincrementaldecoder(r.encoding)(errors="replace")
for chunk in iterator:
rv = decoder.decode(chunk)
if rv:
yield rv
- rv = decoder.decode(b'', final=True)
+ rv = decoder.decode(b"", final=True)
if rv:
yield rv
@@ -553,7 +578,7 @@ def iter_slices(string, slice_length):
if slice_length is None or slice_length <= 0:
slice_length = len(string)
while pos < len(string):
- yield string[pos:pos + slice_length]
+ yield string[pos : pos + slice_length]
pos += slice_length
@@ -569,11 +594,14 @@ def get_unicode_from_response(r):
:rtype: str
"""
- warnings.warn((
- 'In requests 3.0, get_unicode_from_response will be removed. For '
- 'more information, please see the discussion on issue #2266. (This'
- ' warning should only appear once.)'),
- DeprecationWarning)
+ warnings.warn(
+ (
+ "In requests 3.0, get_unicode_from_response will be removed. For "
+ "more information, please see the discussion on issue #2266. (This"
+ " warning should only appear once.)"
+ ),
+ DeprecationWarning,
+ )
tried_encodings = []
@@ -588,14 +616,15 @@ def get_unicode_from_response(r):
# Fall back:
try:
- return str(r.content, encoding, errors='replace')
+ return str(r.content, encoding, errors="replace")
except TypeError:
return r.content
# The unreserved URI characters (RFC 3986)
UNRESERVED_SET = frozenset(
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-._~")
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-._~"
+)
def unquote_unreserved(uri):
@@ -604,22 +633,22 @@ def unquote_unreserved(uri):
:rtype: str
"""
- parts = uri.split('%')
+ parts = uri.split("%")
for i in range(1, len(parts)):
h = parts[i][0:2]
if len(h) == 2 and h.isalnum():
try:
c = chr(int(h, 16))
except ValueError:
- raise InvalidURL("Invalid percent-escape sequence: '%s'" % h)
+ raise InvalidURL(f"Invalid percent-escape sequence: '{h}'")
if c in UNRESERVED_SET:
parts[i] = c + parts[i][2:]
else:
- parts[i] = '%' + parts[i]
+ parts[i] = f"%{parts[i]}"
else:
- parts[i] = '%' + parts[i]
- return ''.join(parts)
+ parts[i] = f"%{parts[i]}"
+ return "".join(parts)
def requote_uri(uri):
@@ -652,10 +681,10 @@ def address_in_network(ip, net):
:rtype: bool
"""
- ipaddr = struct.unpack('=L', socket.inet_aton(ip))[0]
- netaddr, bits = net.split('/')
- netmask = struct.unpack('=L', socket.inet_aton(dotted_netmask(int(bits))))[0]
- network = struct.unpack('=L', socket.inet_aton(netaddr))[0] & netmask
+ ipaddr = struct.unpack("=L", socket.inet_aton(ip))[0]
+ netaddr, bits = net.split("/")
+ netmask = struct.unpack("=L", socket.inet_aton(dotted_netmask(int(bits))))[0]
+ network = struct.unpack("=L", socket.inet_aton(netaddr))[0] & netmask
return (ipaddr & netmask) == (network & netmask)
@@ -666,8 +695,8 @@ def dotted_netmask(mask):
:rtype: str
"""
- bits = 0xffffffff ^ (1 << 32 - mask) - 1
- return socket.inet_ntoa(struct.pack('>I', bits))
+ bits = 0xFFFFFFFF ^ (1 << 32 - mask) - 1
+ return socket.inet_ntoa(struct.pack(">I", bits))
def is_ipv4_address(string_ip):
@@ -676,7 +705,7 @@ def is_ipv4_address(string_ip):
"""
try:
socket.inet_aton(string_ip)
- except socket.error:
+ except OSError:
return False
return True
@@ -687,9 +716,9 @@ def is_valid_cidr(string_network):
:rtype: bool
"""
- if string_network.count('/') == 1:
+ if string_network.count("/") == 1:
try:
- mask = int(string_network.split('/')[1])
+ mask = int(string_network.split("/")[1])
except ValueError:
return False
@@ -697,8 +726,8 @@ def is_valid_cidr(string_network):
return False
try:
- socket.inet_aton(string_network.split('/')[0])
- except socket.error:
+ socket.inet_aton(string_network.split("/")[0])
+ except OSError:
return False
else:
return False
@@ -735,13 +764,14 @@ def should_bypass_proxies(url, no_proxy):
"""
# Prioritize lowercase environment variables over uppercase
# to keep a consistent behaviour with other http projects (curl, wget).
- get_proxy = lambda k: os.environ.get(k) or os.environ.get(k.upper())
+ def get_proxy(key):
+ return os.environ.get(key) or os.environ.get(key.upper())
# First check whether no_proxy is defined. If it is, check that the URL
# we're getting isn't in the no_proxy list.
no_proxy_arg = no_proxy
if no_proxy is None:
- no_proxy = get_proxy('no_proxy')
+ no_proxy = get_proxy("no_proxy")
parsed = urlparse(url)
if parsed.hostname is None:
@@ -751,9 +781,7 @@ def should_bypass_proxies(url, no_proxy):
if no_proxy:
# We need to check whether we match here. We need to see if we match
# the end of the hostname, both with and without the port.
- no_proxy = (
- host for host in no_proxy.replace(' ', '').split(',') if host
- )
+ no_proxy = (host for host in no_proxy.replace(" ", "").split(",") if host)
if is_ipv4_address(parsed.hostname):
for proxy_ip in no_proxy:
@@ -767,7 +795,7 @@ def should_bypass_proxies(url, no_proxy):
else:
host_with_port = parsed.hostname
if parsed.port:
- host_with_port += ':{}'.format(parsed.port)
+ host_with_port += f":{parsed.port}"
for host in no_proxy:
if parsed.hostname.endswith(host) or host_with_port.endswith(host):
@@ -775,7 +803,7 @@ def should_bypass_proxies(url, no_proxy):
# to apply the proxies on this URL.
return True
- with set_environ('no_proxy', no_proxy_arg):
+ with set_environ("no_proxy", no_proxy_arg):
# parsed.hostname can be `None` in cases such as a file URI.
try:
bypass = proxy_bypass(parsed.hostname)
@@ -809,13 +837,13 @@ def select_proxy(url, proxies):
proxies = proxies or {}
urlparts = urlparse(url)
if urlparts.hostname is None:
- return proxies.get(urlparts.scheme, proxies.get('all'))
+ return proxies.get(urlparts.scheme, proxies.get("all"))
proxy_keys = [
- urlparts.scheme + '://' + urlparts.hostname,
+ urlparts.scheme + "://" + urlparts.hostname,
urlparts.scheme,
- 'all://' + urlparts.hostname,
- 'all',
+ "all://" + urlparts.hostname,
+ "all",
]
proxy = None
for proxy_key in proxy_keys:
@@ -826,25 +854,54 @@ def select_proxy(url, proxies):
return proxy
+def resolve_proxies(request, proxies, trust_env=True):
+ """This method takes proxy information from a request and configuration
+ input to resolve a mapping of target proxies. This will consider settings
+ such a NO_PROXY to strip proxy configurations.
+
+ :param request: Request or PreparedRequest
+ :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs
+ :param trust_env: Boolean declaring whether to trust environment configs
+
+ :rtype: dict
+ """
+ proxies = proxies if proxies is not None else {}
+ url = request.url
+ scheme = urlparse(url).scheme
+ no_proxy = proxies.get("no_proxy")
+ new_proxies = proxies.copy()
+
+ if trust_env and not should_bypass_proxies(url, no_proxy=no_proxy):
+ environ_proxies = get_environ_proxies(url, no_proxy=no_proxy)
+
+ proxy = environ_proxies.get(scheme, environ_proxies.get("all"))
+
+ if proxy:
+ new_proxies.setdefault(scheme, proxy)
+ return new_proxies
+
+
def default_user_agent(name="python-requests"):
"""
Return a string representing the default user agent.
:rtype: str
"""
- return '%s/%s' % (name, __version__)
+ return f"{name}/{__version__}"
def default_headers():
"""
:rtype: requests.structures.CaseInsensitiveDict
"""
- return CaseInsensitiveDict({
- 'User-Agent': default_user_agent(),
- 'Accept-Encoding': DEFAULT_ACCEPT_ENCODING,
- 'Accept': '*/*',
- 'Connection': 'keep-alive',
- })
+ return CaseInsensitiveDict(
+ {
+ "User-Agent": default_user_agent(),
+ "Accept-Encoding": DEFAULT_ACCEPT_ENCODING,
+ "Accept": "*/*",
+ "Connection": "keep-alive",
+ }
+ )
def parse_header_links(value):
@@ -857,23 +914,23 @@ def parse_header_links(value):
links = []
- replace_chars = ' \'"'
+ replace_chars = " '\""
value = value.strip(replace_chars)
if not value:
return links
- for val in re.split(', *<', value):
+ for val in re.split(", *<", value):
try:
- url, params = val.split(';', 1)
+ url, params = val.split(";", 1)
except ValueError:
- url, params = val, ''
+ url, params = val, ""
- link = {'url': url.strip('<> \'"')}
+ link = {"url": url.strip("<> '\"")}
- for param in params.split(';'):
+ for param in params.split(";"):
try:
- key, value = param.split('=')
+ key, value = param.split("=")
except ValueError:
break
@@ -885,7 +942,7 @@ def parse_header_links(value):
# Null bytes; no need to recreate these on each call to guess_json_utf
-_null = '\x00'.encode('ascii') # encoding to ASCII for Python 3
+_null = "\x00".encode("ascii") # encoding to ASCII for Python 3
_null2 = _null * 2
_null3 = _null * 3
@@ -899,25 +956,25 @@ def guess_json_utf(data):
# determine the encoding. Also detect a BOM, if present.
sample = data[:4]
if sample in (codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE):
- return 'utf-32' # BOM included
+ return "utf-32" # BOM included
if sample[:3] == codecs.BOM_UTF8:
- return 'utf-8-sig' # BOM included, MS style (discouraged)
+ return "utf-8-sig" # BOM included, MS style (discouraged)
if sample[:2] in (codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE):
- return 'utf-16' # BOM included
+ return "utf-16" # BOM included
nullcount = sample.count(_null)
if nullcount == 0:
- return 'utf-8'
+ return "utf-8"
if nullcount == 2:
- if sample[::2] == _null2: # 1st and 3rd are null
- return 'utf-16-be'
+ if sample[::2] == _null2: # 1st and 3rd are null
+ return "utf-16-be"
if sample[1::2] == _null2: # 2nd and 4th are null
- return 'utf-16-le'
+ return "utf-16-le"
# Did not detect 2 valid UTF-16 ascii-range characters
if nullcount == 3:
if sample[:3] == _null3:
- return 'utf-32-be'
+ return "utf-32-be"
if sample[1:] == _null3:
- return 'utf-32-le'
+ return "utf-32-le"
# Did not detect a valid UTF-32 ascii-range character
return None
@@ -928,15 +985,27 @@ def prepend_scheme_if_needed(url, new_scheme):
:rtype: str
"""
- scheme, netloc, path, params, query, fragment = urlparse(url, new_scheme)
-
- # urlparse is a finicky beast, and sometimes decides that there isn't a
- # netloc present. Assume that it's being over-cautious, and switch netloc
- # and path if urlparse decided there was no netloc.
+ parsed = parse_url(url)
+ scheme, auth, host, port, path, query, fragment = parsed
+
+ # A defect in urlparse determines that there isn't a netloc present in some
+ # urls. We previously assumed parsing was overly cautious, and swapped the
+ # netloc and path. Due to a lack of tests on the original defect, this is
+ # maintained with parse_url for backwards compatibility.
+ netloc = parsed.netloc
if not netloc:
netloc, path = path, netloc
- return urlunparse((scheme, netloc, path, params, query, fragment))
+ if auth:
+ # parse_url doesn't provide the netloc with auth
+ # so we'll add it ourselves.
+ netloc = "@".join([auth, netloc])
+ if scheme is None:
+ scheme = new_scheme
+ if path is None:
+ path = ""
+
+ return urlunparse((scheme, netloc, path, "", query, fragment))
def get_auth_from_url(url):
@@ -950,35 +1019,36 @@ def get_auth_from_url(url):
try:
auth = (unquote(parsed.username), unquote(parsed.password))
except (AttributeError, TypeError):
- auth = ('', '')
+ auth = ("", "")
return auth
-# Moved outside of function to avoid recompile every call
-_CLEAN_HEADER_REGEX_BYTE = re.compile(b'^\\S[^\\r\\n]*$|^$')
-_CLEAN_HEADER_REGEX_STR = re.compile(r'^\S[^\r\n]*$|^$')
-
-
def check_header_validity(header):
- """Verifies that header value is a string which doesn't contain
- leading whitespace or return characters. This prevents unintended
- header injection.
+ """Verifies that header parts don't contain leading whitespace
+ reserved characters, or return characters.
:param header: tuple, in the format (name, value).
"""
name, value = header
- if isinstance(value, bytes):
- pat = _CLEAN_HEADER_REGEX_BYTE
- else:
- pat = _CLEAN_HEADER_REGEX_STR
- try:
- if not pat.match(value):
- raise InvalidHeader("Invalid return character or leading space in header: %s" % name)
- except TypeError:
- raise InvalidHeader("Value for header {%s: %s} must be of type str or "
- "bytes, not %s" % (name, value, type(value)))
+ for part in header:
+ if type(part) not in HEADER_VALIDATORS:
+ raise InvalidHeader(
+ f"Header part ({part!r}) from {{{name!r}: {value!r}}} must be "
+ f"of type str or bytes, not {type(part)}"
+ )
+
+ _validate_header_part(name, "name", HEADER_VALIDATORS[type(name)][0])
+ _validate_header_part(value, "value", HEADER_VALIDATORS[type(value)][1])
+
+
+def _validate_header_part(header_part, header_kind, validator):
+ if not validator.match(header_part):
+ raise InvalidHeader(
+ f"Invalid leading whitespace, reserved character(s), or return"
+ f"character(s) in header {header_kind}: {header_part!r}"
+ )
def urldefragauth(url):
@@ -993,21 +1063,24 @@ def urldefragauth(url):
if not netloc:
netloc, path = path, netloc
- netloc = netloc.rsplit('@', 1)[-1]
+ netloc = netloc.rsplit("@", 1)[-1]
- return urlunparse((scheme, netloc, path, params, query, ''))
+ return urlunparse((scheme, netloc, path, params, query, ""))
def rewind_body(prepared_request):
"""Move file pointer back to its recorded starting position
so it can be read again on redirect.
"""
- body_seek = getattr(prepared_request.body, 'seek', None)
- if body_seek is not None and isinstance(prepared_request._body_position, integer_types):
+ body_seek = getattr(prepared_request.body, "seek", None)
+ if body_seek is not None and isinstance(
+ prepared_request._body_position, integer_types
+ ):
try:
body_seek(prepared_request._body_position)
- except (IOError, OSError):
- raise UnrewindableBodyError("An error occurred when rewinding request "
- "body for redirect.")
+ except OSError:
+ raise UnrewindableBodyError(
+ "An error occurred when rewinding request body for redirect."
+ )
else:
raise UnrewindableBodyError("Unable to rewind request body for redirect.")
diff --git a/src/pip/_vendor/resolvelib.pyi b/src/pip/_vendor/resolvelib.pyi
deleted file mode 100644
index b4ef4e108..000000000
--- a/src/pip/_vendor/resolvelib.pyi
+++ /dev/null
@@ -1 +0,0 @@
-from resolvelib import * \ No newline at end of file
diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py
index 1bddc2fd4..ce05fd302 100644
--- a/src/pip/_vendor/resolvelib/__init__.py
+++ b/src/pip/_vendor/resolvelib/__init__.py
@@ -11,7 +11,7 @@ __all__ = [
"ResolutionTooDeep",
]
-__version__ = "0.7.1"
+__version__ = "0.8.1"
from .providers import AbstractProvider, AbstractResolver
@@ -19,8 +19,8 @@ from .reporters import BaseReporter
from .resolvers import (
InconsistentCandidate,
RequirementsConflicted,
- Resolver,
ResolutionError,
ResolutionImpossible,
ResolutionTooDeep,
+ Resolver,
)
diff --git a/src/pip/_vendor/resolvelib/__init__.pyi b/src/pip/_vendor/resolvelib/__init__.pyi
index 4a84f8f30..d64c52ced 100644
--- a/src/pip/_vendor/resolvelib/__init__.pyi
+++ b/src/pip/_vendor/resolvelib/__init__.pyi
@@ -1,15 +1,11 @@
__version__: str
-from .providers import (
- AbstractResolver as AbstractResolver,
- AbstractProvider as AbstractProvider,
-)
+from .providers import AbstractProvider as AbstractProvider
+from .providers import AbstractResolver as AbstractResolver
from .reporters import BaseReporter as BaseReporter
-from .resolvers import (
- InconsistentCandidate as InconsistentCandidate,
- RequirementsConflicted as RequirementsConflicted,
- Resolver as Resolver,
- ResolutionError as ResolutionError,
- ResolutionImpossible as ResolutionImpossible,
- ResolutionTooDeep as ResolutionTooDeep,
-)
+from .resolvers import InconsistentCandidate as InconsistentCandidate
+from .resolvers import RequirementsConflicted as RequirementsConflicted
+from .resolvers import ResolutionError as ResolutionError
+from .resolvers import ResolutionImpossible as ResolutionImpossible
+from .resolvers import ResolutionTooDeep as ResolutionTooDeep
+from .resolvers import Resolver as Resolver
diff --git a/src/pip/_vendor/resolvelib/providers.py b/src/pip/_vendor/resolvelib/providers.py
index 4822d1665..7d0a9c22a 100644
--- a/src/pip/_vendor/resolvelib/providers.py
+++ b/src/pip/_vendor/resolvelib/providers.py
@@ -9,7 +9,14 @@ class AbstractProvider(object):
"""
raise NotImplementedError
- def get_preference(self, identifier, resolutions, candidates, information):
+ def get_preference(
+ self,
+ identifier,
+ resolutions,
+ candidates,
+ information,
+ backtrack_causes,
+ ):
"""Produce a sort key for given requirement based on preference.
The preference is defined as "I think this requirement should be
@@ -25,6 +32,8 @@ class AbstractProvider(object):
Each value is an iterator of candidates.
:param information: Mapping of requirement information of each package.
Each value is an iterator of *requirement information*.
+ :param backtrack_causes: Sequence of requirement information that were
+ the requirements that caused the resolver to most recently backtrack.
A *requirement information* instance is a named tuple with two members:
diff --git a/src/pip/_vendor/resolvelib/providers.pyi b/src/pip/_vendor/resolvelib/providers.pyi
index 86ada59c4..47d6f8aba 100644
--- a/src/pip/_vendor/resolvelib/providers.pyi
+++ b/src/pip/_vendor/resolvelib/providers.pyi
@@ -12,7 +12,7 @@ from typing import (
from .reporters import BaseReporter
from .resolvers import RequirementInformation
-from .structs import KT, RT, CT, Matches
+from .structs import CT, KT, RT, Matches
class Preference(Protocol):
def __lt__(self, __other: Any) -> bool: ...
diff --git a/src/pip/_vendor/resolvelib/reporters.py b/src/pip/_vendor/resolvelib/reporters.py
index 563489e13..6695480ff 100644
--- a/src/pip/_vendor/resolvelib/reporters.py
+++ b/src/pip/_vendor/resolvelib/reporters.py
@@ -30,6 +30,12 @@ class BaseReporter(object):
requirements passed in from ``Resolver.resolve()``.
"""
+ def resolving_conflicts(self, causes):
+ """Called when starting to attempt requirement conflict resolution.
+
+ :param causes: The information on the collision that caused the backtracking.
+ """
+
def backtracking(self, candidate):
"""Called when rejecting a candidate during backtracking."""
diff --git a/src/pip/_vendor/resolvelib/reporters.pyi b/src/pip/_vendor/resolvelib/reporters.pyi
index 55e38ab88..03d4f09a3 100644
--- a/src/pip/_vendor/resolvelib/reporters.pyi
+++ b/src/pip/_vendor/resolvelib/reporters.pyi
@@ -7,4 +7,5 @@ class BaseReporter:
def ending(self, state: Any) -> Any: ...
def adding_requirement(self, requirement: Any, parent: Any) -> Any: ...
def backtracking(self, candidate: Any) -> Any: ...
+ def resolving_conflicts(self, causes: Any) -> Any: ...
def pinning(self, candidate: Any) -> Any: ...
diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py
index 42484423c..787681b03 100644
--- a/src/pip/_vendor/resolvelib/resolvers.py
+++ b/src/pip/_vendor/resolvelib/resolvers.py
@@ -4,7 +4,6 @@ import operator
from .providers import AbstractResolver
from .structs import DirectedGraph, IteratorMapping, build_iter_view
-
RequirementInformation = collections.namedtuple(
"RequirementInformation", ["requirement", "parent"]
)
@@ -99,7 +98,7 @@ class ResolutionTooDeep(ResolutionError):
# Resolution state in a round.
-State = collections.namedtuple("State", "mapping criteria")
+State = collections.namedtuple("State", "mapping criteria backtrack_causes")
class Resolution(object):
@@ -131,6 +130,7 @@ class Resolution(object):
state = State(
mapping=base.mapping.copy(),
criteria=base.criteria.copy(),
+ backtrack_causes=base.backtrack_causes[:],
)
self._states.append(state)
@@ -185,6 +185,7 @@ class Resolution(object):
self.state.criteria,
operator.attrgetter("information"),
),
+ backtrack_causes=self.state.backtrack_causes,
)
def _is_current_pin_satisfying(self, name, criterion):
@@ -335,7 +336,13 @@ class Resolution(object):
self._r.starting()
# Initialize the root state.
- self._states = [State(mapping=collections.OrderedDict(), criteria={})]
+ self._states = [
+ State(
+ mapping=collections.OrderedDict(),
+ criteria={},
+ backtrack_causes=[],
+ )
+ ]
for r in requirements:
try:
self._add_to_criteria(self.state.criteria, r, parent=None)
@@ -366,14 +373,16 @@ class Resolution(object):
failure_causes = self._attempt_to_pin_criterion(name)
if failure_causes:
+ causes = [i for c in failure_causes for i in c.information]
# Backtrack if pinning fails. The backtrack process puts us in
# an unpinned state, so we can work on it in the next round.
+ self._r.resolving_conflicts(causes=causes)
success = self._backtrack()
+ self.state.backtrack_causes[:] = causes
# Dead ends everywhere. Give up.
if not success:
- causes = [i for c in failure_causes for i in c.information]
- raise ResolutionImpossible(causes)
+ raise ResolutionImpossible(self.state.backtrack_causes)
else:
# Pinning was successful. Push a new state to do another pin.
self._push_new_state()
diff --git a/src/pip/_vendor/resolvelib/resolvers.pyi b/src/pip/_vendor/resolvelib/resolvers.pyi
index e61b0bcb4..0eb5b2162 100644
--- a/src/pip/_vendor/resolvelib/resolvers.pyi
+++ b/src/pip/_vendor/resolvelib/resolvers.pyi
@@ -9,13 +9,7 @@ from typing import (
)
from .providers import AbstractProvider, AbstractResolver
-from .structs import (
- CT,
- KT,
- RT,
- DirectedGraph,
- IterableView,
-)
+from .structs import CT, KT, RT, DirectedGraph, IterableView
# This should be a NamedTuple, but Python 3.6 has a bug that prevents it.
# https://stackoverflow.com/a/50531189/1376863
diff --git a/src/pip/_vendor/resolvelib/structs.pyi b/src/pip/_vendor/resolvelib/structs.pyi
index 14cd46444..fae2a2fce 100644
--- a/src/pip/_vendor/resolvelib/structs.pyi
+++ b/src/pip/_vendor/resolvelib/structs.pyi
@@ -5,17 +5,22 @@ from typing import (
Generic,
Iterable,
Iterator,
+ Mapping,
Tuple,
TypeVar,
Union,
)
-KT = TypeVar("KT")
-RT = TypeVar("RT")
-CT = TypeVar("CT")
+KT = TypeVar("KT") # Identifier.
+RT = TypeVar("RT") # Requirement.
+CT = TypeVar("CT") # Candidate.
_T = TypeVar("_T")
+
Matches = Union[Iterable[CT], Callable[[], Iterator[CT]]]
+class IteratorMapping(Mapping[KT, _T], metaclass=ABCMeta):
+ pass
+
class IterableView(Container[CT], Iterable[CT], metaclass=ABCMeta):
pass
diff --git a/src/pip/_vendor/rich/LICENSE b/src/pip/_vendor/rich/LICENSE
new file mode 100644
index 000000000..441550556
--- /dev/null
+++ b/src/pip/_vendor/rich/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2020 Will McGugan
+
+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.
diff --git a/src/pip/_vendor/rich/__init__.py b/src/pip/_vendor/rich/__init__.py
new file mode 100644
index 000000000..d35875dbb
--- /dev/null
+++ b/src/pip/_vendor/rich/__init__.py
@@ -0,0 +1,176 @@
+"""Rich text and beautiful formatting in the terminal."""
+
+import os
+from typing import IO, TYPE_CHECKING, Any, Callable, Optional, Union
+
+from ._extension import load_ipython_extension # noqa: F401
+
+__all__ = ["get_console", "reconfigure", "print", "inspect"]
+
+if TYPE_CHECKING:
+ from .console import Console
+
+# Global console used by alternative print
+_console: Optional["Console"] = None
+
+try:
+ _IMPORT_CWD = os.path.abspath(os.getcwd())
+except FileNotFoundError:
+ # Can happen if the cwd has been deleted
+ _IMPORT_CWD = ""
+
+
+def get_console() -> "Console":
+ """Get a global :class:`~rich.console.Console` instance. This function is used when Rich requires a Console,
+ and hasn't been explicitly given one.
+
+ Returns:
+ Console: A console instance.
+ """
+ global _console
+ if _console is None:
+ from .console import Console
+
+ _console = Console()
+
+ return _console
+
+
+def reconfigure(*args: Any, **kwargs: Any) -> None:
+ """Reconfigures the global console by replacing it with another.
+
+ Args:
+ console (Console): Replacement console instance.
+ """
+ from pip._vendor.rich.console import Console
+
+ new_console = Console(*args, **kwargs)
+ _console = get_console()
+ _console.__dict__ = new_console.__dict__
+
+
+def print(
+ *objects: Any,
+ sep: str = " ",
+ end: str = "\n",
+ file: Optional[IO[str]] = None,
+ flush: bool = False,
+) -> None:
+ r"""Print object(s) supplied via positional arguments.
+ This function has an identical signature to the built-in print.
+ For more advanced features, see the :class:`~rich.console.Console` class.
+
+ Args:
+ sep (str, optional): Separator between printed objects. Defaults to " ".
+ end (str, optional): Character to write at end of output. Defaults to "\\n".
+ file (IO[str], optional): File to write to, or None for stdout. Defaults to None.
+ flush (bool, optional): Has no effect as Rich always flushes output. Defaults to False.
+
+ """
+ from .console import Console
+
+ write_console = get_console() if file is None else Console(file=file)
+ return write_console.print(*objects, sep=sep, end=end)
+
+
+def print_json(
+ json: Optional[str] = None,
+ *,
+ data: Any = None,
+ indent: Union[None, int, str] = 2,
+ highlight: bool = True,
+ skip_keys: bool = False,
+ ensure_ascii: bool = True,
+ check_circular: bool = True,
+ allow_nan: bool = True,
+ default: Optional[Callable[[Any], Any]] = None,
+ sort_keys: bool = False,
+) -> None:
+ """Pretty prints JSON. Output will be valid JSON.
+
+ Args:
+ json (str): A string containing JSON.
+ data (Any): If json is not supplied, then encode this data.
+ indent (int, optional): Number of spaces to indent. Defaults to 2.
+ highlight (bool, optional): Enable highlighting of output: Defaults to True.
+ skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False.
+ ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False.
+ check_circular (bool, optional): Check for circular references. Defaults to True.
+ allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True.
+ default (Callable, optional): A callable that converts values that can not be encoded
+ in to something that can be JSON encoded. Defaults to None.
+ sort_keys (bool, optional): Sort dictionary keys. Defaults to False.
+ """
+
+ get_console().print_json(
+ json,
+ data=data,
+ indent=indent,
+ highlight=highlight,
+ skip_keys=skip_keys,
+ ensure_ascii=ensure_ascii,
+ check_circular=check_circular,
+ allow_nan=allow_nan,
+ default=default,
+ sort_keys=sort_keys,
+ )
+
+
+def inspect(
+ obj: Any,
+ *,
+ console: Optional["Console"] = None,
+ title: Optional[str] = None,
+ help: bool = False,
+ methods: bool = False,
+ docs: bool = True,
+ private: bool = False,
+ dunder: bool = False,
+ sort: bool = True,
+ all: bool = False,
+ value: bool = True,
+) -> None:
+ """Inspect any Python object.
+
+ * inspect(<OBJECT>) to see summarized info.
+ * inspect(<OBJECT>, methods=True) to see methods.
+ * inspect(<OBJECT>, help=True) to see full (non-abbreviated) help.
+ * inspect(<OBJECT>, private=True) to see private attributes (single underscore).
+ * inspect(<OBJECT>, dunder=True) to see attributes beginning with double underscore.
+ * inspect(<OBJECT>, all=True) to see all attributes.
+
+ Args:
+ obj (Any): An object to inspect.
+ title (str, optional): Title to display over inspect result, or None use type. Defaults to None.
+ help (bool, optional): Show full help text rather than just first paragraph. Defaults to False.
+ methods (bool, optional): Enable inspection of callables. Defaults to False.
+ docs (bool, optional): Also render doc strings. Defaults to True.
+ private (bool, optional): Show private attributes (beginning with underscore). Defaults to False.
+ dunder (bool, optional): Show attributes starting with double underscore. Defaults to False.
+ sort (bool, optional): Sort attributes alphabetically. Defaults to True.
+ all (bool, optional): Show all attributes. Defaults to False.
+ value (bool, optional): Pretty print value. Defaults to True.
+ """
+ _console = console or get_console()
+ from pip._vendor.rich._inspect import Inspect
+
+ # Special case for inspect(inspect)
+ is_inspect = obj is inspect
+
+ _inspect = Inspect(
+ obj,
+ title=title,
+ help=is_inspect or help,
+ methods=is_inspect or methods,
+ docs=is_inspect or docs,
+ private=private,
+ dunder=dunder,
+ sort=sort,
+ all=all,
+ value=value,
+ )
+ _console.print(_inspect)
+
+
+if __name__ == "__main__": # pragma: no cover
+ print("Hello, **World**")
diff --git a/src/pip/_vendor/rich/__main__.py b/src/pip/_vendor/rich/__main__.py
new file mode 100644
index 000000000..54e6d5e8a
--- /dev/null
+++ b/src/pip/_vendor/rich/__main__.py
@@ -0,0 +1,282 @@
+import colorsys
+import io
+from time import process_time
+
+from pip._vendor.rich import box
+from pip._vendor.rich.color import Color
+from pip._vendor.rich.console import Console, ConsoleOptions, Group, RenderableType, RenderResult
+from pip._vendor.rich.markdown import Markdown
+from pip._vendor.rich.measure import Measurement
+from pip._vendor.rich.pretty import Pretty
+from pip._vendor.rich.segment import Segment
+from pip._vendor.rich.style import Style
+from pip._vendor.rich.syntax import Syntax
+from pip._vendor.rich.table import Table
+from pip._vendor.rich.text import Text
+
+
+class ColorBox:
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ for y in range(0, 5):
+ for x in range(options.max_width):
+ h = x / options.max_width
+ l = 0.1 + ((y / 5) * 0.7)
+ r1, g1, b1 = colorsys.hls_to_rgb(h, l, 1.0)
+ r2, g2, b2 = colorsys.hls_to_rgb(h, l + 0.7 / 10, 1.0)
+ bgcolor = Color.from_rgb(r1 * 255, g1 * 255, b1 * 255)
+ color = Color.from_rgb(r2 * 255, g2 * 255, b2 * 255)
+ yield Segment("â–„", Style(color=color, bgcolor=bgcolor))
+ yield Segment.line()
+
+ def __rich_measure__(
+ self, console: "Console", options: ConsoleOptions
+ ) -> Measurement:
+ return Measurement(1, options.max_width)
+
+
+def make_test_card() -> Table:
+ """Get a renderable that demonstrates a number of features."""
+ table = Table.grid(padding=1, pad_edge=True)
+ table.title = "Rich features"
+ table.add_column("Feature", no_wrap=True, justify="center", style="bold red")
+ table.add_column("Demonstration")
+
+ color_table = Table(
+ box=None,
+ expand=False,
+ show_header=False,
+ show_edge=False,
+ pad_edge=False,
+ )
+ color_table.add_row(
+ (
+ "✓ [bold green]4-bit color[/]\n"
+ "✓ [bold blue]8-bit color[/]\n"
+ "✓ [bold magenta]Truecolor (16.7 million)[/]\n"
+ "✓ [bold yellow]Dumb terminals[/]\n"
+ "✓ [bold cyan]Automatic color conversion"
+ ),
+ ColorBox(),
+ )
+
+ table.add_row("Colors", color_table)
+
+ table.add_row(
+ "Styles",
+ "All ansi styles: [bold]bold[/], [dim]dim[/], [italic]italic[/italic], [underline]underline[/], [strike]strikethrough[/], [reverse]reverse[/], and even [blink]blink[/].",
+ )
+
+ lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque in metus sed sapien ultricies pretium a at justo. Maecenas luctus velit et auctor maximus."
+ lorem_table = Table.grid(padding=1, collapse_padding=True)
+ lorem_table.pad_edge = False
+ lorem_table.add_row(
+ Text(lorem, justify="left", style="green"),
+ Text(lorem, justify="center", style="yellow"),
+ Text(lorem, justify="right", style="blue"),
+ Text(lorem, justify="full", style="red"),
+ )
+ table.add_row(
+ "Text",
+ Group(
+ Text.from_markup(
+ """Word wrap text. Justify [green]left[/], [yellow]center[/], [blue]right[/] or [red]full[/].\n"""
+ ),
+ lorem_table,
+ ),
+ )
+
+ def comparison(renderable1: RenderableType, renderable2: RenderableType) -> Table:
+ table = Table(show_header=False, pad_edge=False, box=None, expand=True)
+ table.add_column("1", ratio=1)
+ table.add_column("2", ratio=1)
+ table.add_row(renderable1, renderable2)
+ return table
+
+ table.add_row(
+ "Asian\nlanguage\nsupport",
+ ":flag_for_china: 该库支æŒä¸­æ–‡ï¼Œæ—¥æ–‡å’ŒéŸ©æ–‡æ–‡æœ¬ï¼\n:flag_for_japan: ライブラリã¯ä¸­å›½èªžã€æ—¥æœ¬èªžã€éŸ“国語ã®ãƒ†ã‚­ã‚¹ãƒˆã‚’サãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã™\n:flag_for_south_korea: ì´ ë¼ì´ë¸ŒëŸ¬ë¦¬ëŠ” 중국어, ì¼ë³¸ì–´ ë° í•œêµ­ì–´ í…스트를 지ì›í•©ë‹ˆë‹¤",
+ )
+
+ markup_example = (
+ "[bold magenta]Rich[/] supports a simple [i]bbcode[/i]-like [b]markup[/b] for [yellow]color[/], [underline]style[/], and emoji! "
+ ":+1: :apple: :ant: :bear: :baguette_bread: :bus: "
+ )
+ table.add_row("Markup", markup_example)
+
+ example_table = Table(
+ show_edge=False,
+ show_header=True,
+ expand=False,
+ row_styles=["none", "dim"],
+ box=box.SIMPLE,
+ )
+ example_table.add_column("[green]Date", style="green", no_wrap=True)
+ example_table.add_column("[blue]Title", style="blue")
+ example_table.add_column(
+ "[cyan]Production Budget",
+ style="cyan",
+ justify="right",
+ no_wrap=True,
+ )
+ example_table.add_column(
+ "[magenta]Box Office",
+ style="magenta",
+ justify="right",
+ no_wrap=True,
+ )
+ example_table.add_row(
+ "Dec 20, 2019",
+ "Star Wars: The Rise of Skywalker",
+ "$275,000,000",
+ "$375,126,118",
+ )
+ example_table.add_row(
+ "May 25, 2018",
+ "[b]Solo[/]: A Star Wars Story",
+ "$275,000,000",
+ "$393,151,347",
+ )
+ example_table.add_row(
+ "Dec 15, 2017",
+ "Star Wars Ep. VIII: The Last Jedi",
+ "$262,000,000",
+ "[bold]$1,332,539,889[/bold]",
+ )
+ example_table.add_row(
+ "May 19, 1999",
+ "Star Wars Ep. [b]I[/b]: [i]The phantom Menace",
+ "$115,000,000",
+ "$1,027,044,677",
+ )
+
+ table.add_row("Tables", example_table)
+
+ code = '''\
+def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
+ """Iterate and generate a tuple with a flag for last value."""
+ iter_values = iter(values)
+ try:
+ previous_value = next(iter_values)
+ except StopIteration:
+ return
+ for value in iter_values:
+ yield False, previous_value
+ previous_value = value
+ yield True, previous_value'''
+
+ pretty_data = {
+ "foo": [
+ 3.1427,
+ (
+ "Paul Atreides",
+ "Vladimir Harkonnen",
+ "Thufir Hawat",
+ ),
+ ],
+ "atomic": (False, True, None),
+ }
+ table.add_row(
+ "Syntax\nhighlighting\n&\npretty\nprinting",
+ comparison(
+ Syntax(code, "python3", line_numbers=True, indent_guides=True),
+ Pretty(pretty_data, indent_guides=True),
+ ),
+ )
+
+ markdown_example = """\
+# Markdown
+
+Supports much of the *markdown* __syntax__!
+
+- Headers
+- Basic formatting: **bold**, *italic*, `code`
+- Block quotes
+- Lists, and more...
+ """
+ table.add_row(
+ "Markdown", comparison("[cyan]" + markdown_example, Markdown(markdown_example))
+ )
+
+ table.add_row(
+ "+more!",
+ """Progress bars, columns, styled logging handler, tracebacks, etc...""",
+ )
+ return table
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ console = Console(
+ file=io.StringIO(),
+ force_terminal=True,
+ )
+ test_card = make_test_card()
+
+ # Print once to warm cache
+ start = process_time()
+ console.print(test_card)
+ pre_cache_taken = round((process_time() - start) * 1000.0, 1)
+
+ console.file = io.StringIO()
+
+ start = process_time()
+ console.print(test_card)
+ taken = round((process_time() - start) * 1000.0, 1)
+
+ c = Console(record=True)
+ c.print(test_card)
+ # c.save_svg(
+ # path="/Users/darrenburns/Library/Application Support/JetBrains/PyCharm2021.3/scratches/svg_export.svg",
+ # title="Rich can export to SVG",
+ # )
+
+ print(f"rendered in {pre_cache_taken}ms (cold cache)")
+ print(f"rendered in {taken}ms (warm cache)")
+
+ from pip._vendor.rich.panel import Panel
+
+ console = Console()
+
+ sponsor_message = Table.grid(padding=1)
+ sponsor_message.add_column(style="green", justify="right")
+ sponsor_message.add_column(no_wrap=True)
+
+ sponsor_message.add_row(
+ "Textualize",
+ "[u blue link=https://github.com/textualize]https://github.com/textualize",
+ )
+ sponsor_message.add_row(
+ "Buy devs a :coffee:",
+ "[u blue link=https://ko-fi.com/textualize]https://ko-fi.com/textualize",
+ )
+ sponsor_message.add_row(
+ "Twitter",
+ "[u blue link=https://twitter.com/willmcgugan]https://twitter.com/willmcgugan",
+ )
+
+ intro_message = Text.from_markup(
+ """\
+We hope you enjoy using Rich!
+
+Rich is maintained with [red]:heart:[/] by [link=https://www.textualize.io]Textualize.io[/]
+
+- Will McGugan"""
+ )
+
+ message = Table.grid(padding=2)
+ message.add_column()
+ message.add_column(no_wrap=True)
+ message.add_row(intro_message, sponsor_message)
+
+ console.print(
+ Panel.fit(
+ message,
+ box=box.ROUNDED,
+ padding=(1, 2),
+ title="[b red]Thanks for trying out Rich!",
+ border_style="bright_blue",
+ ),
+ justify="center",
+ )
diff --git a/src/pip/_vendor/rich/_cell_widths.py b/src/pip/_vendor/rich/_cell_widths.py
new file mode 100644
index 000000000..36286df37
--- /dev/null
+++ b/src/pip/_vendor/rich/_cell_widths.py
@@ -0,0 +1,451 @@
+# Auto generated by make_terminal_widths.py
+
+CELL_WIDTHS = [
+ (0, 0, 0),
+ (1, 31, -1),
+ (127, 159, -1),
+ (768, 879, 0),
+ (1155, 1161, 0),
+ (1425, 1469, 0),
+ (1471, 1471, 0),
+ (1473, 1474, 0),
+ (1476, 1477, 0),
+ (1479, 1479, 0),
+ (1552, 1562, 0),
+ (1611, 1631, 0),
+ (1648, 1648, 0),
+ (1750, 1756, 0),
+ (1759, 1764, 0),
+ (1767, 1768, 0),
+ (1770, 1773, 0),
+ (1809, 1809, 0),
+ (1840, 1866, 0),
+ (1958, 1968, 0),
+ (2027, 2035, 0),
+ (2045, 2045, 0),
+ (2070, 2073, 0),
+ (2075, 2083, 0),
+ (2085, 2087, 0),
+ (2089, 2093, 0),
+ (2137, 2139, 0),
+ (2259, 2273, 0),
+ (2275, 2306, 0),
+ (2362, 2362, 0),
+ (2364, 2364, 0),
+ (2369, 2376, 0),
+ (2381, 2381, 0),
+ (2385, 2391, 0),
+ (2402, 2403, 0),
+ (2433, 2433, 0),
+ (2492, 2492, 0),
+ (2497, 2500, 0),
+ (2509, 2509, 0),
+ (2530, 2531, 0),
+ (2558, 2558, 0),
+ (2561, 2562, 0),
+ (2620, 2620, 0),
+ (2625, 2626, 0),
+ (2631, 2632, 0),
+ (2635, 2637, 0),
+ (2641, 2641, 0),
+ (2672, 2673, 0),
+ (2677, 2677, 0),
+ (2689, 2690, 0),
+ (2748, 2748, 0),
+ (2753, 2757, 0),
+ (2759, 2760, 0),
+ (2765, 2765, 0),
+ (2786, 2787, 0),
+ (2810, 2815, 0),
+ (2817, 2817, 0),
+ (2876, 2876, 0),
+ (2879, 2879, 0),
+ (2881, 2884, 0),
+ (2893, 2893, 0),
+ (2901, 2902, 0),
+ (2914, 2915, 0),
+ (2946, 2946, 0),
+ (3008, 3008, 0),
+ (3021, 3021, 0),
+ (3072, 3072, 0),
+ (3076, 3076, 0),
+ (3134, 3136, 0),
+ (3142, 3144, 0),
+ (3146, 3149, 0),
+ (3157, 3158, 0),
+ (3170, 3171, 0),
+ (3201, 3201, 0),
+ (3260, 3260, 0),
+ (3263, 3263, 0),
+ (3270, 3270, 0),
+ (3276, 3277, 0),
+ (3298, 3299, 0),
+ (3328, 3329, 0),
+ (3387, 3388, 0),
+ (3393, 3396, 0),
+ (3405, 3405, 0),
+ (3426, 3427, 0),
+ (3457, 3457, 0),
+ (3530, 3530, 0),
+ (3538, 3540, 0),
+ (3542, 3542, 0),
+ (3633, 3633, 0),
+ (3636, 3642, 0),
+ (3655, 3662, 0),
+ (3761, 3761, 0),
+ (3764, 3772, 0),
+ (3784, 3789, 0),
+ (3864, 3865, 0),
+ (3893, 3893, 0),
+ (3895, 3895, 0),
+ (3897, 3897, 0),
+ (3953, 3966, 0),
+ (3968, 3972, 0),
+ (3974, 3975, 0),
+ (3981, 3991, 0),
+ (3993, 4028, 0),
+ (4038, 4038, 0),
+ (4141, 4144, 0),
+ (4146, 4151, 0),
+ (4153, 4154, 0),
+ (4157, 4158, 0),
+ (4184, 4185, 0),
+ (4190, 4192, 0),
+ (4209, 4212, 0),
+ (4226, 4226, 0),
+ (4229, 4230, 0),
+ (4237, 4237, 0),
+ (4253, 4253, 0),
+ (4352, 4447, 2),
+ (4957, 4959, 0),
+ (5906, 5908, 0),
+ (5938, 5940, 0),
+ (5970, 5971, 0),
+ (6002, 6003, 0),
+ (6068, 6069, 0),
+ (6071, 6077, 0),
+ (6086, 6086, 0),
+ (6089, 6099, 0),
+ (6109, 6109, 0),
+ (6155, 6157, 0),
+ (6277, 6278, 0),
+ (6313, 6313, 0),
+ (6432, 6434, 0),
+ (6439, 6440, 0),
+ (6450, 6450, 0),
+ (6457, 6459, 0),
+ (6679, 6680, 0),
+ (6683, 6683, 0),
+ (6742, 6742, 0),
+ (6744, 6750, 0),
+ (6752, 6752, 0),
+ (6754, 6754, 0),
+ (6757, 6764, 0),
+ (6771, 6780, 0),
+ (6783, 6783, 0),
+ (6832, 6848, 0),
+ (6912, 6915, 0),
+ (6964, 6964, 0),
+ (6966, 6970, 0),
+ (6972, 6972, 0),
+ (6978, 6978, 0),
+ (7019, 7027, 0),
+ (7040, 7041, 0),
+ (7074, 7077, 0),
+ (7080, 7081, 0),
+ (7083, 7085, 0),
+ (7142, 7142, 0),
+ (7144, 7145, 0),
+ (7149, 7149, 0),
+ (7151, 7153, 0),
+ (7212, 7219, 0),
+ (7222, 7223, 0),
+ (7376, 7378, 0),
+ (7380, 7392, 0),
+ (7394, 7400, 0),
+ (7405, 7405, 0),
+ (7412, 7412, 0),
+ (7416, 7417, 0),
+ (7616, 7673, 0),
+ (7675, 7679, 0),
+ (8203, 8207, 0),
+ (8232, 8238, 0),
+ (8288, 8291, 0),
+ (8400, 8432, 0),
+ (8986, 8987, 2),
+ (9001, 9002, 2),
+ (9193, 9196, 2),
+ (9200, 9200, 2),
+ (9203, 9203, 2),
+ (9725, 9726, 2),
+ (9748, 9749, 2),
+ (9800, 9811, 2),
+ (9855, 9855, 2),
+ (9875, 9875, 2),
+ (9889, 9889, 2),
+ (9898, 9899, 2),
+ (9917, 9918, 2),
+ (9924, 9925, 2),
+ (9934, 9934, 2),
+ (9940, 9940, 2),
+ (9962, 9962, 2),
+ (9970, 9971, 2),
+ (9973, 9973, 2),
+ (9978, 9978, 2),
+ (9981, 9981, 2),
+ (9989, 9989, 2),
+ (9994, 9995, 2),
+ (10024, 10024, 2),
+ (10060, 10060, 2),
+ (10062, 10062, 2),
+ (10067, 10069, 2),
+ (10071, 10071, 2),
+ (10133, 10135, 2),
+ (10160, 10160, 2),
+ (10175, 10175, 2),
+ (11035, 11036, 2),
+ (11088, 11088, 2),
+ (11093, 11093, 2),
+ (11503, 11505, 0),
+ (11647, 11647, 0),
+ (11744, 11775, 0),
+ (11904, 11929, 2),
+ (11931, 12019, 2),
+ (12032, 12245, 2),
+ (12272, 12283, 2),
+ (12288, 12329, 2),
+ (12330, 12333, 0),
+ (12334, 12350, 2),
+ (12353, 12438, 2),
+ (12441, 12442, 0),
+ (12443, 12543, 2),
+ (12549, 12591, 2),
+ (12593, 12686, 2),
+ (12688, 12771, 2),
+ (12784, 12830, 2),
+ (12832, 12871, 2),
+ (12880, 19903, 2),
+ (19968, 42124, 2),
+ (42128, 42182, 2),
+ (42607, 42610, 0),
+ (42612, 42621, 0),
+ (42654, 42655, 0),
+ (42736, 42737, 0),
+ (43010, 43010, 0),
+ (43014, 43014, 0),
+ (43019, 43019, 0),
+ (43045, 43046, 0),
+ (43052, 43052, 0),
+ (43204, 43205, 0),
+ (43232, 43249, 0),
+ (43263, 43263, 0),
+ (43302, 43309, 0),
+ (43335, 43345, 0),
+ (43360, 43388, 2),
+ (43392, 43394, 0),
+ (43443, 43443, 0),
+ (43446, 43449, 0),
+ (43452, 43453, 0),
+ (43493, 43493, 0),
+ (43561, 43566, 0),
+ (43569, 43570, 0),
+ (43573, 43574, 0),
+ (43587, 43587, 0),
+ (43596, 43596, 0),
+ (43644, 43644, 0),
+ (43696, 43696, 0),
+ (43698, 43700, 0),
+ (43703, 43704, 0),
+ (43710, 43711, 0),
+ (43713, 43713, 0),
+ (43756, 43757, 0),
+ (43766, 43766, 0),
+ (44005, 44005, 0),
+ (44008, 44008, 0),
+ (44013, 44013, 0),
+ (44032, 55203, 2),
+ (63744, 64255, 2),
+ (64286, 64286, 0),
+ (65024, 65039, 0),
+ (65040, 65049, 2),
+ (65056, 65071, 0),
+ (65072, 65106, 2),
+ (65108, 65126, 2),
+ (65128, 65131, 2),
+ (65281, 65376, 2),
+ (65504, 65510, 2),
+ (66045, 66045, 0),
+ (66272, 66272, 0),
+ (66422, 66426, 0),
+ (68097, 68099, 0),
+ (68101, 68102, 0),
+ (68108, 68111, 0),
+ (68152, 68154, 0),
+ (68159, 68159, 0),
+ (68325, 68326, 0),
+ (68900, 68903, 0),
+ (69291, 69292, 0),
+ (69446, 69456, 0),
+ (69633, 69633, 0),
+ (69688, 69702, 0),
+ (69759, 69761, 0),
+ (69811, 69814, 0),
+ (69817, 69818, 0),
+ (69888, 69890, 0),
+ (69927, 69931, 0),
+ (69933, 69940, 0),
+ (70003, 70003, 0),
+ (70016, 70017, 0),
+ (70070, 70078, 0),
+ (70089, 70092, 0),
+ (70095, 70095, 0),
+ (70191, 70193, 0),
+ (70196, 70196, 0),
+ (70198, 70199, 0),
+ (70206, 70206, 0),
+ (70367, 70367, 0),
+ (70371, 70378, 0),
+ (70400, 70401, 0),
+ (70459, 70460, 0),
+ (70464, 70464, 0),
+ (70502, 70508, 0),
+ (70512, 70516, 0),
+ (70712, 70719, 0),
+ (70722, 70724, 0),
+ (70726, 70726, 0),
+ (70750, 70750, 0),
+ (70835, 70840, 0),
+ (70842, 70842, 0),
+ (70847, 70848, 0),
+ (70850, 70851, 0),
+ (71090, 71093, 0),
+ (71100, 71101, 0),
+ (71103, 71104, 0),
+ (71132, 71133, 0),
+ (71219, 71226, 0),
+ (71229, 71229, 0),
+ (71231, 71232, 0),
+ (71339, 71339, 0),
+ (71341, 71341, 0),
+ (71344, 71349, 0),
+ (71351, 71351, 0),
+ (71453, 71455, 0),
+ (71458, 71461, 0),
+ (71463, 71467, 0),
+ (71727, 71735, 0),
+ (71737, 71738, 0),
+ (71995, 71996, 0),
+ (71998, 71998, 0),
+ (72003, 72003, 0),
+ (72148, 72151, 0),
+ (72154, 72155, 0),
+ (72160, 72160, 0),
+ (72193, 72202, 0),
+ (72243, 72248, 0),
+ (72251, 72254, 0),
+ (72263, 72263, 0),
+ (72273, 72278, 0),
+ (72281, 72283, 0),
+ (72330, 72342, 0),
+ (72344, 72345, 0),
+ (72752, 72758, 0),
+ (72760, 72765, 0),
+ (72767, 72767, 0),
+ (72850, 72871, 0),
+ (72874, 72880, 0),
+ (72882, 72883, 0),
+ (72885, 72886, 0),
+ (73009, 73014, 0),
+ (73018, 73018, 0),
+ (73020, 73021, 0),
+ (73023, 73029, 0),
+ (73031, 73031, 0),
+ (73104, 73105, 0),
+ (73109, 73109, 0),
+ (73111, 73111, 0),
+ (73459, 73460, 0),
+ (92912, 92916, 0),
+ (92976, 92982, 0),
+ (94031, 94031, 0),
+ (94095, 94098, 0),
+ (94176, 94179, 2),
+ (94180, 94180, 0),
+ (94192, 94193, 2),
+ (94208, 100343, 2),
+ (100352, 101589, 2),
+ (101632, 101640, 2),
+ (110592, 110878, 2),
+ (110928, 110930, 2),
+ (110948, 110951, 2),
+ (110960, 111355, 2),
+ (113821, 113822, 0),
+ (119143, 119145, 0),
+ (119163, 119170, 0),
+ (119173, 119179, 0),
+ (119210, 119213, 0),
+ (119362, 119364, 0),
+ (121344, 121398, 0),
+ (121403, 121452, 0),
+ (121461, 121461, 0),
+ (121476, 121476, 0),
+ (121499, 121503, 0),
+ (121505, 121519, 0),
+ (122880, 122886, 0),
+ (122888, 122904, 0),
+ (122907, 122913, 0),
+ (122915, 122916, 0),
+ (122918, 122922, 0),
+ (123184, 123190, 0),
+ (123628, 123631, 0),
+ (125136, 125142, 0),
+ (125252, 125258, 0),
+ (126980, 126980, 2),
+ (127183, 127183, 2),
+ (127374, 127374, 2),
+ (127377, 127386, 2),
+ (127488, 127490, 2),
+ (127504, 127547, 2),
+ (127552, 127560, 2),
+ (127568, 127569, 2),
+ (127584, 127589, 2),
+ (127744, 127776, 2),
+ (127789, 127797, 2),
+ (127799, 127868, 2),
+ (127870, 127891, 2),
+ (127904, 127946, 2),
+ (127951, 127955, 2),
+ (127968, 127984, 2),
+ (127988, 127988, 2),
+ (127992, 128062, 2),
+ (128064, 128064, 2),
+ (128066, 128252, 2),
+ (128255, 128317, 2),
+ (128331, 128334, 2),
+ (128336, 128359, 2),
+ (128378, 128378, 2),
+ (128405, 128406, 2),
+ (128420, 128420, 2),
+ (128507, 128591, 2),
+ (128640, 128709, 2),
+ (128716, 128716, 2),
+ (128720, 128722, 2),
+ (128725, 128727, 2),
+ (128747, 128748, 2),
+ (128756, 128764, 2),
+ (128992, 129003, 2),
+ (129292, 129338, 2),
+ (129340, 129349, 2),
+ (129351, 129400, 2),
+ (129402, 129483, 2),
+ (129485, 129535, 2),
+ (129648, 129652, 2),
+ (129656, 129658, 2),
+ (129664, 129670, 2),
+ (129680, 129704, 2),
+ (129712, 129718, 2),
+ (129728, 129730, 2),
+ (129744, 129750, 2),
+ (131072, 196605, 2),
+ (196608, 262141, 2),
+ (917760, 917999, 0),
+]
diff --git a/src/pip/_vendor/rich/_emoji_codes.py b/src/pip/_vendor/rich/_emoji_codes.py
new file mode 100644
index 000000000..1f2877bb2
--- /dev/null
+++ b/src/pip/_vendor/rich/_emoji_codes.py
@@ -0,0 +1,3610 @@
+EMOJI = {
+ "1st_place_medal": "🥇",
+ "2nd_place_medal": "🥈",
+ "3rd_place_medal": "🥉",
+ "ab_button_(blood_type)": "🆎",
+ "atm_sign": "ðŸ§",
+ "a_button_(blood_type)": "🅰",
+ "afghanistan": "🇦🇫",
+ "albania": "🇦🇱",
+ "algeria": "🇩🇿",
+ "american_samoa": "🇦🇸",
+ "andorra": "🇦🇩",
+ "angola": "🇦🇴",
+ "anguilla": "🇦🇮",
+ "antarctica": "🇦🇶",
+ "antigua_&_barbuda": "🇦🇬",
+ "aquarius": "â™’",
+ "argentina": "🇦🇷",
+ "aries": "♈",
+ "armenia": "🇦🇲",
+ "aruba": "🇦🇼",
+ "ascension_island": "🇦🇨",
+ "australia": "🇦🇺",
+ "austria": "🇦🇹",
+ "azerbaijan": "🇦🇿",
+ "back_arrow": "🔙",
+ "b_button_(blood_type)": "🅱",
+ "bahamas": "🇧🇸",
+ "bahrain": "🇧🇭",
+ "bangladesh": "🇧🇩",
+ "barbados": "🇧🇧",
+ "belarus": "🇧🇾",
+ "belgium": "🇧🇪",
+ "belize": "🇧🇿",
+ "benin": "🇧🇯",
+ "bermuda": "🇧🇲",
+ "bhutan": "🇧🇹",
+ "bolivia": "🇧🇴",
+ "bosnia_&_herzegovina": "🇧🇦",
+ "botswana": "🇧🇼",
+ "bouvet_island": "🇧🇻",
+ "brazil": "🇧🇷",
+ "british_indian_ocean_territory": "🇮🇴",
+ "british_virgin_islands": "🇻🇬",
+ "brunei": "🇧🇳",
+ "bulgaria": "🇧🇬",
+ "burkina_faso": "🇧🇫",
+ "burundi": "🇧🇮",
+ "cl_button": "🆑",
+ "cool_button": "🆒",
+ "cambodia": "🇰🇭",
+ "cameroon": "🇨🇲",
+ "canada": "🇨🇦",
+ "canary_islands": "🇮🇨",
+ "cancer": "♋",
+ "cape_verde": "🇨🇻",
+ "capricorn": "♑",
+ "caribbean_netherlands": "🇧🇶",
+ "cayman_islands": "🇰🇾",
+ "central_african_republic": "🇨🇫",
+ "ceuta_&_melilla": "🇪🇦",
+ "chad": "🇹🇩",
+ "chile": "🇨🇱",
+ "china": "🇨🇳",
+ "christmas_island": "🇨🇽",
+ "christmas_tree": "🎄",
+ "clipperton_island": "🇨🇵",
+ "cocos_(keeling)_islands": "🇨🇨",
+ "colombia": "🇨🇴",
+ "comoros": "🇰🇲",
+ "congo_-_brazzaville": "🇨🇬",
+ "congo_-_kinshasa": "🇨🇩",
+ "cook_islands": "🇨🇰",
+ "costa_rica": "🇨🇷",
+ "croatia": "🇭🇷",
+ "cuba": "🇨🇺",
+ "curaçao": "🇨🇼",
+ "cyprus": "🇨🇾",
+ "czechia": "🇨🇿",
+ "côte_d’ivoire": "🇨🇮",
+ "denmark": "🇩🇰",
+ "diego_garcia": "🇩🇬",
+ "djibouti": "🇩🇯",
+ "dominica": "🇩🇲",
+ "dominican_republic": "🇩🇴",
+ "end_arrow": "🔚",
+ "ecuador": "🇪🇨",
+ "egypt": "🇪🇬",
+ "el_salvador": "🇸🇻",
+ "england": "ðŸ´\U000e0067\U000e0062\U000e0065\U000e006e\U000e0067\U000e007f",
+ "equatorial_guinea": "🇬🇶",
+ "eritrea": "🇪🇷",
+ "estonia": "🇪🇪",
+ "ethiopia": "🇪🇹",
+ "european_union": "🇪🇺",
+ "free_button": "🆓",
+ "falkland_islands": "🇫🇰",
+ "faroe_islands": "🇫🇴",
+ "fiji": "🇫🇯",
+ "finland": "🇫🇮",
+ "france": "🇫🇷",
+ "french_guiana": "🇬🇫",
+ "french_polynesia": "🇵🇫",
+ "french_southern_territories": "🇹🇫",
+ "gabon": "🇬🇦",
+ "gambia": "🇬🇲",
+ "gemini": "♊",
+ "georgia": "🇬🇪",
+ "germany": "🇩🇪",
+ "ghana": "🇬🇭",
+ "gibraltar": "🇬🇮",
+ "greece": "🇬🇷",
+ "greenland": "🇬🇱",
+ "grenada": "🇬🇩",
+ "guadeloupe": "🇬🇵",
+ "guam": "🇬🇺",
+ "guatemala": "🇬🇹",
+ "guernsey": "🇬🇬",
+ "guinea": "🇬🇳",
+ "guinea-bissau": "🇬🇼",
+ "guyana": "🇬🇾",
+ "haiti": "🇭🇹",
+ "heard_&_mcdonald_islands": "🇭🇲",
+ "honduras": "🇭🇳",
+ "hong_kong_sar_china": "🇭🇰",
+ "hungary": "🇭🇺",
+ "id_button": "🆔",
+ "iceland": "🇮🇸",
+ "india": "🇮🇳",
+ "indonesia": "🇮🇩",
+ "iran": "🇮🇷",
+ "iraq": "🇮🇶",
+ "ireland": "🇮🇪",
+ "isle_of_man": "🇮🇲",
+ "israel": "🇮🇱",
+ "italy": "🇮🇹",
+ "jamaica": "🇯🇲",
+ "japan": "🗾",
+ "japanese_acceptable_button": "🉑",
+ "japanese_application_button": "🈸",
+ "japanese_bargain_button": "ðŸ‰",
+ "japanese_castle": "ðŸ¯",
+ "japanese_congratulations_button": "㊗",
+ "japanese_discount_button": "🈹",
+ "japanese_dolls": "🎎",
+ "japanese_free_of_charge_button": "🈚",
+ "japanese_here_button": "ðŸˆ",
+ "japanese_monthly_amount_button": "🈷",
+ "japanese_no_vacancy_button": "🈵",
+ "japanese_not_free_of_charge_button": "🈶",
+ "japanese_open_for_business_button": "🈺",
+ "japanese_passing_grade_button": "🈴",
+ "japanese_post_office": "ðŸ£",
+ "japanese_prohibited_button": "🈲",
+ "japanese_reserved_button": "🈯",
+ "japanese_secret_button": "㊙",
+ "japanese_service_charge_button": "🈂",
+ "japanese_symbol_for_beginner": "🔰",
+ "japanese_vacancy_button": "🈳",
+ "jersey": "🇯🇪",
+ "jordan": "🇯🇴",
+ "kazakhstan": "🇰🇿",
+ "kenya": "🇰🇪",
+ "kiribati": "🇰🇮",
+ "kosovo": "🇽🇰",
+ "kuwait": "🇰🇼",
+ "kyrgyzstan": "🇰🇬",
+ "laos": "🇱🇦",
+ "latvia": "🇱🇻",
+ "lebanon": "🇱🇧",
+ "leo": "♌",
+ "lesotho": "🇱🇸",
+ "liberia": "🇱🇷",
+ "libra": "♎",
+ "libya": "🇱🇾",
+ "liechtenstein": "🇱🇮",
+ "lithuania": "🇱🇹",
+ "luxembourg": "🇱🇺",
+ "macau_sar_china": "🇲🇴",
+ "macedonia": "🇲🇰",
+ "madagascar": "🇲🇬",
+ "malawi": "🇲🇼",
+ "malaysia": "🇲🇾",
+ "maldives": "🇲🇻",
+ "mali": "🇲🇱",
+ "malta": "🇲🇹",
+ "marshall_islands": "🇲🇭",
+ "martinique": "🇲🇶",
+ "mauritania": "🇲🇷",
+ "mauritius": "🇲🇺",
+ "mayotte": "🇾🇹",
+ "mexico": "🇲🇽",
+ "micronesia": "🇫🇲",
+ "moldova": "🇲🇩",
+ "monaco": "🇲🇨",
+ "mongolia": "🇲🇳",
+ "montenegro": "🇲🇪",
+ "montserrat": "🇲🇸",
+ "morocco": "🇲🇦",
+ "mozambique": "🇲🇿",
+ "mrs._claus": "🤶",
+ "mrs._claus_dark_skin_tone": "🤶ðŸ¿",
+ "mrs._claus_light_skin_tone": "🤶ðŸ»",
+ "mrs._claus_medium-dark_skin_tone": "🤶ðŸ¾",
+ "mrs._claus_medium-light_skin_tone": "🤶ðŸ¼",
+ "mrs._claus_medium_skin_tone": "🤶ðŸ½",
+ "myanmar_(burma)": "🇲🇲",
+ "new_button": "🆕",
+ "ng_button": "🆖",
+ "namibia": "🇳🇦",
+ "nauru": "🇳🇷",
+ "nepal": "🇳🇵",
+ "netherlands": "🇳🇱",
+ "new_caledonia": "🇳🇨",
+ "new_zealand": "🇳🇿",
+ "nicaragua": "🇳🇮",
+ "niger": "🇳🇪",
+ "nigeria": "🇳🇬",
+ "niue": "🇳🇺",
+ "norfolk_island": "🇳🇫",
+ "north_korea": "🇰🇵",
+ "northern_mariana_islands": "🇲🇵",
+ "norway": "🇳🇴",
+ "ok_button": "🆗",
+ "ok_hand": "👌",
+ "ok_hand_dark_skin_tone": "👌ðŸ¿",
+ "ok_hand_light_skin_tone": "👌ðŸ»",
+ "ok_hand_medium-dark_skin_tone": "👌ðŸ¾",
+ "ok_hand_medium-light_skin_tone": "👌ðŸ¼",
+ "ok_hand_medium_skin_tone": "👌ðŸ½",
+ "on!_arrow": "🔛",
+ "o_button_(blood_type)": "🅾",
+ "oman": "🇴🇲",
+ "ophiuchus": "⛎",
+ "p_button": "🅿",
+ "pakistan": "🇵🇰",
+ "palau": "🇵🇼",
+ "palestinian_territories": "🇵🇸",
+ "panama": "🇵🇦",
+ "papua_new_guinea": "🇵🇬",
+ "paraguay": "🇵🇾",
+ "peru": "🇵🇪",
+ "philippines": "🇵🇭",
+ "pisces": "♓",
+ "pitcairn_islands": "🇵🇳",
+ "poland": "🇵🇱",
+ "portugal": "🇵🇹",
+ "puerto_rico": "🇵🇷",
+ "qatar": "🇶🇦",
+ "romania": "🇷🇴",
+ "russia": "🇷🇺",
+ "rwanda": "🇷🇼",
+ "réunion": "🇷🇪",
+ "soon_arrow": "🔜",
+ "sos_button": "🆘",
+ "sagittarius": "â™",
+ "samoa": "🇼🇸",
+ "san_marino": "🇸🇲",
+ "santa_claus": "🎅",
+ "santa_claus_dark_skin_tone": "🎅ðŸ¿",
+ "santa_claus_light_skin_tone": "🎅ðŸ»",
+ "santa_claus_medium-dark_skin_tone": "🎅ðŸ¾",
+ "santa_claus_medium-light_skin_tone": "🎅ðŸ¼",
+ "santa_claus_medium_skin_tone": "🎅ðŸ½",
+ "saudi_arabia": "🇸🇦",
+ "scorpio": "â™",
+ "scotland": "ðŸ´\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f",
+ "senegal": "🇸🇳",
+ "serbia": "🇷🇸",
+ "seychelles": "🇸🇨",
+ "sierra_leone": "🇸🇱",
+ "singapore": "🇸🇬",
+ "sint_maarten": "🇸🇽",
+ "slovakia": "🇸🇰",
+ "slovenia": "🇸🇮",
+ "solomon_islands": "🇸🇧",
+ "somalia": "🇸🇴",
+ "south_africa": "🇿🇦",
+ "south_georgia_&_south_sandwich_islands": "🇬🇸",
+ "south_korea": "🇰🇷",
+ "south_sudan": "🇸🇸",
+ "spain": "🇪🇸",
+ "sri_lanka": "🇱🇰",
+ "st._barthélemy": "🇧🇱",
+ "st._helena": "🇸🇭",
+ "st._kitts_&_nevis": "🇰🇳",
+ "st._lucia": "🇱🇨",
+ "st._martin": "🇲🇫",
+ "st._pierre_&_miquelon": "🇵🇲",
+ "st._vincent_&_grenadines": "🇻🇨",
+ "statue_of_liberty": "🗽",
+ "sudan": "🇸🇩",
+ "suriname": "🇸🇷",
+ "svalbard_&_jan_mayen": "🇸🇯",
+ "swaziland": "🇸🇿",
+ "sweden": "🇸🇪",
+ "switzerland": "🇨🇭",
+ "syria": "🇸🇾",
+ "são_tomé_&_príncipe": "🇸🇹",
+ "t-rex": "🦖",
+ "top_arrow": "ðŸ”",
+ "taiwan": "🇹🇼",
+ "tajikistan": "🇹🇯",
+ "tanzania": "🇹🇿",
+ "taurus": "♉",
+ "thailand": "🇹🇭",
+ "timor-leste": "🇹🇱",
+ "togo": "🇹🇬",
+ "tokelau": "🇹🇰",
+ "tokyo_tower": "🗼",
+ "tonga": "🇹🇴",
+ "trinidad_&_tobago": "🇹🇹",
+ "tristan_da_cunha": "🇹🇦",
+ "tunisia": "🇹🇳",
+ "turkey": "🦃",
+ "turkmenistan": "🇹🇲",
+ "turks_&_caicos_islands": "🇹🇨",
+ "tuvalu": "🇹🇻",
+ "u.s._outlying_islands": "🇺🇲",
+ "u.s._virgin_islands": "🇻🇮",
+ "up!_button": "🆙",
+ "uganda": "🇺🇬",
+ "ukraine": "🇺🇦",
+ "united_arab_emirates": "🇦🇪",
+ "united_kingdom": "🇬🇧",
+ "united_nations": "🇺🇳",
+ "united_states": "🇺🇸",
+ "uruguay": "🇺🇾",
+ "uzbekistan": "🇺🇿",
+ "vs_button": "🆚",
+ "vanuatu": "🇻🇺",
+ "vatican_city": "🇻🇦",
+ "venezuela": "🇻🇪",
+ "vietnam": "🇻🇳",
+ "virgo": "â™",
+ "wales": "ðŸ´\U000e0067\U000e0062\U000e0077\U000e006c\U000e0073\U000e007f",
+ "wallis_&_futuna": "🇼🇫",
+ "western_sahara": "🇪🇭",
+ "yemen": "🇾🇪",
+ "zambia": "🇿🇲",
+ "zimbabwe": "🇿🇼",
+ "abacus": "🧮",
+ "adhesive_bandage": "🩹",
+ "admission_tickets": "🎟",
+ "adult": "🧑",
+ "adult_dark_skin_tone": "🧑ðŸ¿",
+ "adult_light_skin_tone": "🧑ðŸ»",
+ "adult_medium-dark_skin_tone": "🧑ðŸ¾",
+ "adult_medium-light_skin_tone": "🧑ðŸ¼",
+ "adult_medium_skin_tone": "🧑ðŸ½",
+ "aerial_tramway": "🚡",
+ "airplane": "✈",
+ "airplane_arrival": "🛬",
+ "airplane_departure": "🛫",
+ "alarm_clock": "â°",
+ "alembic": "âš—",
+ "alien": "👽",
+ "alien_monster": "👾",
+ "ambulance": "🚑",
+ "american_football": "ðŸˆ",
+ "amphora": "ðŸº",
+ "anchor": "âš“",
+ "anger_symbol": "💢",
+ "angry_face": "😠",
+ "angry_face_with_horns": "👿",
+ "anguished_face": "😧",
+ "ant": "ðŸœ",
+ "antenna_bars": "📶",
+ "anxious_face_with_sweat": "😰",
+ "articulated_lorry": "🚛",
+ "artist_palette": "🎨",
+ "astonished_face": "😲",
+ "atom_symbol": "âš›",
+ "auto_rickshaw": "🛺",
+ "automobile": "🚗",
+ "avocado": "🥑",
+ "axe": "🪓",
+ "baby": "👶",
+ "baby_angel": "👼",
+ "baby_angel_dark_skin_tone": "👼ðŸ¿",
+ "baby_angel_light_skin_tone": "👼ðŸ»",
+ "baby_angel_medium-dark_skin_tone": "👼ðŸ¾",
+ "baby_angel_medium-light_skin_tone": "👼ðŸ¼",
+ "baby_angel_medium_skin_tone": "👼ðŸ½",
+ "baby_bottle": "ðŸ¼",
+ "baby_chick": "ðŸ¤",
+ "baby_dark_skin_tone": "👶ðŸ¿",
+ "baby_light_skin_tone": "👶ðŸ»",
+ "baby_medium-dark_skin_tone": "👶ðŸ¾",
+ "baby_medium-light_skin_tone": "👶ðŸ¼",
+ "baby_medium_skin_tone": "👶ðŸ½",
+ "baby_symbol": "🚼",
+ "backhand_index_pointing_down": "👇",
+ "backhand_index_pointing_down_dark_skin_tone": "👇ðŸ¿",
+ "backhand_index_pointing_down_light_skin_tone": "👇ðŸ»",
+ "backhand_index_pointing_down_medium-dark_skin_tone": "👇ðŸ¾",
+ "backhand_index_pointing_down_medium-light_skin_tone": "👇ðŸ¼",
+ "backhand_index_pointing_down_medium_skin_tone": "👇ðŸ½",
+ "backhand_index_pointing_left": "👈",
+ "backhand_index_pointing_left_dark_skin_tone": "👈ðŸ¿",
+ "backhand_index_pointing_left_light_skin_tone": "👈ðŸ»",
+ "backhand_index_pointing_left_medium-dark_skin_tone": "👈ðŸ¾",
+ "backhand_index_pointing_left_medium-light_skin_tone": "👈ðŸ¼",
+ "backhand_index_pointing_left_medium_skin_tone": "👈ðŸ½",
+ "backhand_index_pointing_right": "👉",
+ "backhand_index_pointing_right_dark_skin_tone": "👉ðŸ¿",
+ "backhand_index_pointing_right_light_skin_tone": "👉ðŸ»",
+ "backhand_index_pointing_right_medium-dark_skin_tone": "👉ðŸ¾",
+ "backhand_index_pointing_right_medium-light_skin_tone": "👉ðŸ¼",
+ "backhand_index_pointing_right_medium_skin_tone": "👉ðŸ½",
+ "backhand_index_pointing_up": "👆",
+ "backhand_index_pointing_up_dark_skin_tone": "👆ðŸ¿",
+ "backhand_index_pointing_up_light_skin_tone": "👆ðŸ»",
+ "backhand_index_pointing_up_medium-dark_skin_tone": "👆ðŸ¾",
+ "backhand_index_pointing_up_medium-light_skin_tone": "👆ðŸ¼",
+ "backhand_index_pointing_up_medium_skin_tone": "👆ðŸ½",
+ "bacon": "🥓",
+ "badger": "🦡",
+ "badminton": "ðŸ¸",
+ "bagel": "🥯",
+ "baggage_claim": "🛄",
+ "baguette_bread": "🥖",
+ "balance_scale": "âš–",
+ "bald": "🦲",
+ "bald_man": "👨\u200d🦲",
+ "bald_woman": "👩\u200d🦲",
+ "ballet_shoes": "🩰",
+ "balloon": "🎈",
+ "ballot_box_with_ballot": "🗳",
+ "ballot_box_with_check": "☑",
+ "banana": "ðŸŒ",
+ "banjo": "🪕",
+ "bank": "ðŸ¦",
+ "bar_chart": "📊",
+ "barber_pole": "💈",
+ "baseball": "âš¾",
+ "basket": "🧺",
+ "basketball": "ðŸ€",
+ "bat": "🦇",
+ "bathtub": "ðŸ›",
+ "battery": "🔋",
+ "beach_with_umbrella": "ðŸ–",
+ "beaming_face_with_smiling_eyes": "ðŸ˜",
+ "bear_face": "ðŸ»",
+ "bearded_person": "🧔",
+ "bearded_person_dark_skin_tone": "🧔ðŸ¿",
+ "bearded_person_light_skin_tone": "🧔ðŸ»",
+ "bearded_person_medium-dark_skin_tone": "🧔ðŸ¾",
+ "bearded_person_medium-light_skin_tone": "🧔ðŸ¼",
+ "bearded_person_medium_skin_tone": "🧔ðŸ½",
+ "beating_heart": "💓",
+ "bed": "ðŸ›",
+ "beer_mug": "ðŸº",
+ "bell": "🔔",
+ "bell_with_slash": "🔕",
+ "bellhop_bell": "🛎",
+ "bento_box": "ðŸ±",
+ "beverage_box": "🧃",
+ "bicycle": "🚲",
+ "bikini": "👙",
+ "billed_cap": "🧢",
+ "biohazard": "☣",
+ "bird": "ðŸ¦",
+ "birthday_cake": "🎂",
+ "black_circle": "âš«",
+ "black_flag": "ðŸ´",
+ "black_heart": "🖤",
+ "black_large_square": "⬛",
+ "black_medium-small_square": "â—¾",
+ "black_medium_square": "â—¼",
+ "black_nib": "✒",
+ "black_small_square": "â–ª",
+ "black_square_button": "🔲",
+ "blond-haired_man": "👱\u200d♂ï¸",
+ "blond-haired_man_dark_skin_tone": "👱ðŸ¿\u200d♂ï¸",
+ "blond-haired_man_light_skin_tone": "👱ðŸ»\u200d♂ï¸",
+ "blond-haired_man_medium-dark_skin_tone": "👱ðŸ¾\u200d♂ï¸",
+ "blond-haired_man_medium-light_skin_tone": "👱ðŸ¼\u200d♂ï¸",
+ "blond-haired_man_medium_skin_tone": "👱ðŸ½\u200d♂ï¸",
+ "blond-haired_person": "👱",
+ "blond-haired_person_dark_skin_tone": "👱ðŸ¿",
+ "blond-haired_person_light_skin_tone": "👱ðŸ»",
+ "blond-haired_person_medium-dark_skin_tone": "👱ðŸ¾",
+ "blond-haired_person_medium-light_skin_tone": "👱ðŸ¼",
+ "blond-haired_person_medium_skin_tone": "👱ðŸ½",
+ "blond-haired_woman": "👱\u200d♀ï¸",
+ "blond-haired_woman_dark_skin_tone": "👱ðŸ¿\u200d♀ï¸",
+ "blond-haired_woman_light_skin_tone": "👱ðŸ»\u200d♀ï¸",
+ "blond-haired_woman_medium-dark_skin_tone": "👱ðŸ¾\u200d♀ï¸",
+ "blond-haired_woman_medium-light_skin_tone": "👱ðŸ¼\u200d♀ï¸",
+ "blond-haired_woman_medium_skin_tone": "👱ðŸ½\u200d♀ï¸",
+ "blossom": "🌼",
+ "blowfish": "ðŸ¡",
+ "blue_book": "📘",
+ "blue_circle": "🔵",
+ "blue_heart": "💙",
+ "blue_square": "🟦",
+ "boar": "ðŸ—",
+ "bomb": "💣",
+ "bone": "🦴",
+ "bookmark": "🔖",
+ "bookmark_tabs": "📑",
+ "books": "📚",
+ "bottle_with_popping_cork": "ðŸ¾",
+ "bouquet": "ðŸ’",
+ "bow_and_arrow": "ðŸ¹",
+ "bowl_with_spoon": "🥣",
+ "bowling": "🎳",
+ "boxing_glove": "🥊",
+ "boy": "👦",
+ "boy_dark_skin_tone": "👦ðŸ¿",
+ "boy_light_skin_tone": "👦ðŸ»",
+ "boy_medium-dark_skin_tone": "👦ðŸ¾",
+ "boy_medium-light_skin_tone": "👦ðŸ¼",
+ "boy_medium_skin_tone": "👦ðŸ½",
+ "brain": "🧠",
+ "bread": "ðŸž",
+ "breast-feeding": "🤱",
+ "breast-feeding_dark_skin_tone": "🤱ðŸ¿",
+ "breast-feeding_light_skin_tone": "🤱ðŸ»",
+ "breast-feeding_medium-dark_skin_tone": "🤱ðŸ¾",
+ "breast-feeding_medium-light_skin_tone": "🤱ðŸ¼",
+ "breast-feeding_medium_skin_tone": "🤱ðŸ½",
+ "brick": "🧱",
+ "bride_with_veil": "👰",
+ "bride_with_veil_dark_skin_tone": "👰ðŸ¿",
+ "bride_with_veil_light_skin_tone": "👰ðŸ»",
+ "bride_with_veil_medium-dark_skin_tone": "👰ðŸ¾",
+ "bride_with_veil_medium-light_skin_tone": "👰ðŸ¼",
+ "bride_with_veil_medium_skin_tone": "👰ðŸ½",
+ "bridge_at_night": "🌉",
+ "briefcase": "💼",
+ "briefs": "🩲",
+ "bright_button": "🔆",
+ "broccoli": "🥦",
+ "broken_heart": "💔",
+ "broom": "🧹",
+ "brown_circle": "🟤",
+ "brown_heart": "🤎",
+ "brown_square": "🟫",
+ "bug": "ðŸ›",
+ "building_construction": "ðŸ—",
+ "bullet_train": "🚅",
+ "burrito": "🌯",
+ "bus": "🚌",
+ "bus_stop": "ðŸš",
+ "bust_in_silhouette": "👤",
+ "busts_in_silhouette": "👥",
+ "butter": "🧈",
+ "butterfly": "🦋",
+ "cactus": "🌵",
+ "calendar": "📆",
+ "call_me_hand": "🤙",
+ "call_me_hand_dark_skin_tone": "🤙ðŸ¿",
+ "call_me_hand_light_skin_tone": "🤙ðŸ»",
+ "call_me_hand_medium-dark_skin_tone": "🤙ðŸ¾",
+ "call_me_hand_medium-light_skin_tone": "🤙ðŸ¼",
+ "call_me_hand_medium_skin_tone": "🤙ðŸ½",
+ "camel": "ðŸ«",
+ "camera": "📷",
+ "camera_with_flash": "📸",
+ "camping": "ðŸ•",
+ "candle": "🕯",
+ "candy": "ðŸ¬",
+ "canned_food": "🥫",
+ "canoe": "🛶",
+ "card_file_box": "🗃",
+ "card_index": "📇",
+ "card_index_dividers": "🗂",
+ "carousel_horse": "🎠",
+ "carp_streamer": "ðŸŽ",
+ "carrot": "🥕",
+ "castle": "ðŸ°",
+ "cat": "ðŸ±",
+ "cat_face": "ðŸ±",
+ "cat_face_with_tears_of_joy": "😹",
+ "cat_face_with_wry_smile": "😼",
+ "chains": "⛓",
+ "chair": "🪑",
+ "chart_decreasing": "📉",
+ "chart_increasing": "📈",
+ "chart_increasing_with_yen": "💹",
+ "cheese_wedge": "🧀",
+ "chequered_flag": "ðŸ",
+ "cherries": "ðŸ’",
+ "cherry_blossom": "🌸",
+ "chess_pawn": "♟",
+ "chestnut": "🌰",
+ "chicken": "ðŸ”",
+ "child": "🧒",
+ "child_dark_skin_tone": "🧒ðŸ¿",
+ "child_light_skin_tone": "🧒ðŸ»",
+ "child_medium-dark_skin_tone": "🧒ðŸ¾",
+ "child_medium-light_skin_tone": "🧒ðŸ¼",
+ "child_medium_skin_tone": "🧒ðŸ½",
+ "children_crossing": "🚸",
+ "chipmunk": "ðŸ¿",
+ "chocolate_bar": "ðŸ«",
+ "chopsticks": "🥢",
+ "church": "⛪",
+ "cigarette": "🚬",
+ "cinema": "🎦",
+ "circled_m": "â“‚",
+ "circus_tent": "🎪",
+ "cityscape": "ðŸ™",
+ "cityscape_at_dusk": "🌆",
+ "clamp": "🗜",
+ "clapper_board": "🎬",
+ "clapping_hands": "ðŸ‘",
+ "clapping_hands_dark_skin_tone": "ðŸ‘ðŸ¿",
+ "clapping_hands_light_skin_tone": "ðŸ‘ðŸ»",
+ "clapping_hands_medium-dark_skin_tone": "ðŸ‘ðŸ¾",
+ "clapping_hands_medium-light_skin_tone": "ðŸ‘ðŸ¼",
+ "clapping_hands_medium_skin_tone": "ðŸ‘ðŸ½",
+ "classical_building": "ðŸ›",
+ "clinking_beer_mugs": "ðŸ»",
+ "clinking_glasses": "🥂",
+ "clipboard": "📋",
+ "clockwise_vertical_arrows": "🔃",
+ "closed_book": "📕",
+ "closed_mailbox_with_lowered_flag": "📪",
+ "closed_mailbox_with_raised_flag": "📫",
+ "closed_umbrella": "🌂",
+ "cloud": "â˜",
+ "cloud_with_lightning": "🌩",
+ "cloud_with_lightning_and_rain": "⛈",
+ "cloud_with_rain": "🌧",
+ "cloud_with_snow": "🌨",
+ "clown_face": "🤡",
+ "club_suit": "♣",
+ "clutch_bag": "ðŸ‘",
+ "coat": "🧥",
+ "cocktail_glass": "ðŸ¸",
+ "coconut": "🥥",
+ "coffin": "âš°",
+ "cold_face": "🥶",
+ "collision": "💥",
+ "comet": "☄",
+ "compass": "🧭",
+ "computer_disk": "💽",
+ "computer_mouse": "🖱",
+ "confetti_ball": "🎊",
+ "confounded_face": "😖",
+ "confused_face": "😕",
+ "construction": "🚧",
+ "construction_worker": "👷",
+ "construction_worker_dark_skin_tone": "👷ðŸ¿",
+ "construction_worker_light_skin_tone": "👷ðŸ»",
+ "construction_worker_medium-dark_skin_tone": "👷ðŸ¾",
+ "construction_worker_medium-light_skin_tone": "👷ðŸ¼",
+ "construction_worker_medium_skin_tone": "👷ðŸ½",
+ "control_knobs": "🎛",
+ "convenience_store": "ðŸª",
+ "cooked_rice": "ðŸš",
+ "cookie": "ðŸª",
+ "cooking": "ðŸ³",
+ "copyright": "©",
+ "couch_and_lamp": "🛋",
+ "counterclockwise_arrows_button": "🔄",
+ "couple_with_heart": "💑",
+ "couple_with_heart_man_man": "👨\u200dâ¤ï¸\u200d👨",
+ "couple_with_heart_woman_man": "👩\u200dâ¤ï¸\u200d👨",
+ "couple_with_heart_woman_woman": "👩\u200dâ¤ï¸\u200d👩",
+ "cow": "ðŸ®",
+ "cow_face": "ðŸ®",
+ "cowboy_hat_face": "🤠",
+ "crab": "🦀",
+ "crayon": "ðŸ–",
+ "credit_card": "💳",
+ "crescent_moon": "🌙",
+ "cricket": "🦗",
+ "cricket_game": "ðŸ",
+ "crocodile": "ðŸŠ",
+ "croissant": "ðŸ¥",
+ "cross_mark": "âŒ",
+ "cross_mark_button": "âŽ",
+ "crossed_fingers": "🤞",
+ "crossed_fingers_dark_skin_tone": "🤞ðŸ¿",
+ "crossed_fingers_light_skin_tone": "🤞ðŸ»",
+ "crossed_fingers_medium-dark_skin_tone": "🤞ðŸ¾",
+ "crossed_fingers_medium-light_skin_tone": "🤞ðŸ¼",
+ "crossed_fingers_medium_skin_tone": "🤞ðŸ½",
+ "crossed_flags": "🎌",
+ "crossed_swords": "âš”",
+ "crown": "👑",
+ "crying_cat_face": "😿",
+ "crying_face": "😢",
+ "crystal_ball": "🔮",
+ "cucumber": "🥒",
+ "cupcake": "ðŸ§",
+ "cup_with_straw": "🥤",
+ "curling_stone": "🥌",
+ "curly_hair": "🦱",
+ "curly-haired_man": "👨\u200d🦱",
+ "curly-haired_woman": "👩\u200d🦱",
+ "curly_loop": "âž°",
+ "currency_exchange": "💱",
+ "curry_rice": "ðŸ›",
+ "custard": "ðŸ®",
+ "customs": "🛃",
+ "cut_of_meat": "🥩",
+ "cyclone": "🌀",
+ "dagger": "🗡",
+ "dango": "ðŸ¡",
+ "dashing_away": "💨",
+ "deaf_person": "ðŸ§",
+ "deciduous_tree": "🌳",
+ "deer": "🦌",
+ "delivery_truck": "🚚",
+ "department_store": "ðŸ¬",
+ "derelict_house": "ðŸš",
+ "desert": "ðŸœ",
+ "desert_island": "ðŸ",
+ "desktop_computer": "🖥",
+ "detective": "🕵",
+ "detective_dark_skin_tone": "🕵ðŸ¿",
+ "detective_light_skin_tone": "🕵ðŸ»",
+ "detective_medium-dark_skin_tone": "🕵ðŸ¾",
+ "detective_medium-light_skin_tone": "🕵ðŸ¼",
+ "detective_medium_skin_tone": "🕵ðŸ½",
+ "diamond_suit": "♦",
+ "diamond_with_a_dot": "💠",
+ "dim_button": "🔅",
+ "direct_hit": "🎯",
+ "disappointed_face": "😞",
+ "diving_mask": "🤿",
+ "diya_lamp": "🪔",
+ "dizzy": "💫",
+ "dizzy_face": "😵",
+ "dna": "🧬",
+ "dog": "ðŸ¶",
+ "dog_face": "ðŸ¶",
+ "dollar_banknote": "💵",
+ "dolphin": "ðŸ¬",
+ "door": "🚪",
+ "dotted_six-pointed_star": "🔯",
+ "double_curly_loop": "âž¿",
+ "double_exclamation_mark": "‼",
+ "doughnut": "ðŸ©",
+ "dove": "🕊",
+ "down-left_arrow": "↙",
+ "down-right_arrow": "↘",
+ "down_arrow": "⬇",
+ "downcast_face_with_sweat": "😓",
+ "downwards_button": "🔽",
+ "dragon": "ðŸ‰",
+ "dragon_face": "ðŸ²",
+ "dress": "👗",
+ "drooling_face": "🤤",
+ "drop_of_blood": "🩸",
+ "droplet": "💧",
+ "drum": "ðŸ¥",
+ "duck": "🦆",
+ "dumpling": "🥟",
+ "dvd": "📀",
+ "e-mail": "📧",
+ "eagle": "🦅",
+ "ear": "👂",
+ "ear_dark_skin_tone": "👂ðŸ¿",
+ "ear_light_skin_tone": "👂ðŸ»",
+ "ear_medium-dark_skin_tone": "👂ðŸ¾",
+ "ear_medium-light_skin_tone": "👂ðŸ¼",
+ "ear_medium_skin_tone": "👂ðŸ½",
+ "ear_of_corn": "🌽",
+ "ear_with_hearing_aid": "🦻",
+ "egg": "ðŸ³",
+ "eggplant": "ðŸ†",
+ "eight-pointed_star": "✴",
+ "eight-spoked_asterisk": "✳",
+ "eight-thirty": "🕣",
+ "eight_o’clock": "🕗",
+ "eject_button": "â",
+ "electric_plug": "🔌",
+ "elephant": "ðŸ˜",
+ "eleven-thirty": "🕦",
+ "eleven_o’clock": "🕚",
+ "elf": "ðŸ§",
+ "elf_dark_skin_tone": "ðŸ§ðŸ¿",
+ "elf_light_skin_tone": "ðŸ§ðŸ»",
+ "elf_medium-dark_skin_tone": "ðŸ§ðŸ¾",
+ "elf_medium-light_skin_tone": "ðŸ§ðŸ¼",
+ "elf_medium_skin_tone": "ðŸ§ðŸ½",
+ "envelope": "✉",
+ "envelope_with_arrow": "📩",
+ "euro_banknote": "💶",
+ "evergreen_tree": "🌲",
+ "ewe": "ðŸ‘",
+ "exclamation_mark": "â—",
+ "exclamation_question_mark": "â‰",
+ "exploding_head": "🤯",
+ "expressionless_face": "😑",
+ "eye": "ðŸ‘",
+ "eye_in_speech_bubble": "ðŸ‘ï¸\u200d🗨ï¸",
+ "eyes": "👀",
+ "face_blowing_a_kiss": "😘",
+ "face_savoring_food": "😋",
+ "face_screaming_in_fear": "😱",
+ "face_vomiting": "🤮",
+ "face_with_hand_over_mouth": "🤭",
+ "face_with_head-bandage": "🤕",
+ "face_with_medical_mask": "😷",
+ "face_with_monocle": "ðŸ§",
+ "face_with_open_mouth": "😮",
+ "face_with_raised_eyebrow": "🤨",
+ "face_with_rolling_eyes": "🙄",
+ "face_with_steam_from_nose": "😤",
+ "face_with_symbols_on_mouth": "🤬",
+ "face_with_tears_of_joy": "😂",
+ "face_with_thermometer": "🤒",
+ "face_with_tongue": "😛",
+ "face_without_mouth": "😶",
+ "factory": "ðŸ­",
+ "fairy": "🧚",
+ "fairy_dark_skin_tone": "🧚ðŸ¿",
+ "fairy_light_skin_tone": "🧚ðŸ»",
+ "fairy_medium-dark_skin_tone": "🧚ðŸ¾",
+ "fairy_medium-light_skin_tone": "🧚ðŸ¼",
+ "fairy_medium_skin_tone": "🧚ðŸ½",
+ "falafel": "🧆",
+ "fallen_leaf": "ðŸ‚",
+ "family": "👪",
+ "family_man_boy": "👨\u200d👦",
+ "family_man_boy_boy": "👨\u200d👦\u200d👦",
+ "family_man_girl": "👨\u200d👧",
+ "family_man_girl_boy": "👨\u200d👧\u200d👦",
+ "family_man_girl_girl": "👨\u200d👧\u200d👧",
+ "family_man_man_boy": "👨\u200d👨\u200d👦",
+ "family_man_man_boy_boy": "👨\u200d👨\u200d👦\u200d👦",
+ "family_man_man_girl": "👨\u200d👨\u200d👧",
+ "family_man_man_girl_boy": "👨\u200d👨\u200d👧\u200d👦",
+ "family_man_man_girl_girl": "👨\u200d👨\u200d👧\u200d👧",
+ "family_man_woman_boy": "👨\u200d👩\u200d👦",
+ "family_man_woman_boy_boy": "👨\u200d👩\u200d👦\u200d👦",
+ "family_man_woman_girl": "👨\u200d👩\u200d👧",
+ "family_man_woman_girl_boy": "👨\u200d👩\u200d👧\u200d👦",
+ "family_man_woman_girl_girl": "👨\u200d👩\u200d👧\u200d👧",
+ "family_woman_boy": "👩\u200d👦",
+ "family_woman_boy_boy": "👩\u200d👦\u200d👦",
+ "family_woman_girl": "👩\u200d👧",
+ "family_woman_girl_boy": "👩\u200d👧\u200d👦",
+ "family_woman_girl_girl": "👩\u200d👧\u200d👧",
+ "family_woman_woman_boy": "👩\u200d👩\u200d👦",
+ "family_woman_woman_boy_boy": "👩\u200d👩\u200d👦\u200d👦",
+ "family_woman_woman_girl": "👩\u200d👩\u200d👧",
+ "family_woman_woman_girl_boy": "👩\u200d👩\u200d👧\u200d👦",
+ "family_woman_woman_girl_girl": "👩\u200d👩\u200d👧\u200d👧",
+ "fast-forward_button": "â©",
+ "fast_down_button": "â¬",
+ "fast_reverse_button": "âª",
+ "fast_up_button": "â«",
+ "fax_machine": "📠",
+ "fearful_face": "😨",
+ "female_sign": "♀",
+ "ferris_wheel": "🎡",
+ "ferry": "â›´",
+ "field_hockey": "ðŸ‘",
+ "file_cabinet": "🗄",
+ "file_folder": "ðŸ“",
+ "film_frames": "🎞",
+ "film_projector": "📽",
+ "fire": "🔥",
+ "fire_extinguisher": "🧯",
+ "firecracker": "🧨",
+ "fire_engine": "🚒",
+ "fireworks": "🎆",
+ "first_quarter_moon": "🌓",
+ "first_quarter_moon_face": "🌛",
+ "fish": "ðŸŸ",
+ "fish_cake_with_swirl": "ðŸ¥",
+ "fishing_pole": "🎣",
+ "five-thirty": "🕠",
+ "five_o’clock": "🕔",
+ "flag_in_hole": "⛳",
+ "flamingo": "🦩",
+ "flashlight": "🔦",
+ "flat_shoe": "🥿",
+ "fleur-de-lis": "⚜",
+ "flexed_biceps": "💪",
+ "flexed_biceps_dark_skin_tone": "💪ðŸ¿",
+ "flexed_biceps_light_skin_tone": "💪ðŸ»",
+ "flexed_biceps_medium-dark_skin_tone": "💪ðŸ¾",
+ "flexed_biceps_medium-light_skin_tone": "💪ðŸ¼",
+ "flexed_biceps_medium_skin_tone": "💪ðŸ½",
+ "floppy_disk": "💾",
+ "flower_playing_cards": "🎴",
+ "flushed_face": "😳",
+ "flying_disc": "ðŸ¥",
+ "flying_saucer": "🛸",
+ "fog": "🌫",
+ "foggy": "ðŸŒ",
+ "folded_hands": "ðŸ™",
+ "folded_hands_dark_skin_tone": "ðŸ™ðŸ¿",
+ "folded_hands_light_skin_tone": "ðŸ™ðŸ»",
+ "folded_hands_medium-dark_skin_tone": "ðŸ™ðŸ¾",
+ "folded_hands_medium-light_skin_tone": "ðŸ™ðŸ¼",
+ "folded_hands_medium_skin_tone": "ðŸ™ðŸ½",
+ "foot": "🦶",
+ "footprints": "👣",
+ "fork_and_knife": "ðŸ´",
+ "fork_and_knife_with_plate": "ðŸ½",
+ "fortune_cookie": "🥠",
+ "fountain": "⛲",
+ "fountain_pen": "🖋",
+ "four-thirty": "🕟",
+ "four_leaf_clover": "ðŸ€",
+ "four_o’clock": "🕓",
+ "fox_face": "🦊",
+ "framed_picture": "🖼",
+ "french_fries": "ðŸŸ",
+ "fried_shrimp": "ðŸ¤",
+ "frog_face": "ðŸ¸",
+ "front-facing_baby_chick": "ðŸ¥",
+ "frowning_face": "☹",
+ "frowning_face_with_open_mouth": "😦",
+ "fuel_pump": "⛽",
+ "full_moon": "🌕",
+ "full_moon_face": "ðŸŒ",
+ "funeral_urn": "âš±",
+ "game_die": "🎲",
+ "garlic": "🧄",
+ "gear": "âš™",
+ "gem_stone": "💎",
+ "genie": "🧞",
+ "ghost": "👻",
+ "giraffe": "🦒",
+ "girl": "👧",
+ "girl_dark_skin_tone": "👧ðŸ¿",
+ "girl_light_skin_tone": "👧ðŸ»",
+ "girl_medium-dark_skin_tone": "👧ðŸ¾",
+ "girl_medium-light_skin_tone": "👧ðŸ¼",
+ "girl_medium_skin_tone": "👧ðŸ½",
+ "glass_of_milk": "🥛",
+ "glasses": "👓",
+ "globe_showing_americas": "🌎",
+ "globe_showing_asia-australia": "ðŸŒ",
+ "globe_showing_europe-africa": "ðŸŒ",
+ "globe_with_meridians": "ðŸŒ",
+ "gloves": "🧤",
+ "glowing_star": "🌟",
+ "goal_net": "🥅",
+ "goat": "ðŸ",
+ "goblin": "👺",
+ "goggles": "🥽",
+ "gorilla": "ðŸ¦",
+ "graduation_cap": "🎓",
+ "grapes": "ðŸ‡",
+ "green_apple": "ðŸ",
+ "green_book": "📗",
+ "green_circle": "🟢",
+ "green_heart": "💚",
+ "green_salad": "🥗",
+ "green_square": "🟩",
+ "grimacing_face": "😬",
+ "grinning_cat_face": "😺",
+ "grinning_cat_face_with_smiling_eyes": "😸",
+ "grinning_face": "😀",
+ "grinning_face_with_big_eyes": "😃",
+ "grinning_face_with_smiling_eyes": "😄",
+ "grinning_face_with_sweat": "😅",
+ "grinning_squinting_face": "😆",
+ "growing_heart": "💗",
+ "guard": "💂",
+ "guard_dark_skin_tone": "💂ðŸ¿",
+ "guard_light_skin_tone": "💂ðŸ»",
+ "guard_medium-dark_skin_tone": "💂ðŸ¾",
+ "guard_medium-light_skin_tone": "💂ðŸ¼",
+ "guard_medium_skin_tone": "💂ðŸ½",
+ "guide_dog": "🦮",
+ "guitar": "🎸",
+ "hamburger": "ðŸ”",
+ "hammer": "🔨",
+ "hammer_and_pick": "âš’",
+ "hammer_and_wrench": "🛠",
+ "hamster_face": "ðŸ¹",
+ "hand_with_fingers_splayed": "ðŸ–",
+ "hand_with_fingers_splayed_dark_skin_tone": "ðŸ–ðŸ¿",
+ "hand_with_fingers_splayed_light_skin_tone": "ðŸ–ðŸ»",
+ "hand_with_fingers_splayed_medium-dark_skin_tone": "ðŸ–ðŸ¾",
+ "hand_with_fingers_splayed_medium-light_skin_tone": "ðŸ–ðŸ¼",
+ "hand_with_fingers_splayed_medium_skin_tone": "ðŸ–ðŸ½",
+ "handbag": "👜",
+ "handshake": "ðŸ¤",
+ "hatching_chick": "ðŸ£",
+ "headphone": "🎧",
+ "hear-no-evil_monkey": "🙉",
+ "heart_decoration": "💟",
+ "heart_suit": "♥",
+ "heart_with_arrow": "💘",
+ "heart_with_ribbon": "ðŸ’",
+ "heavy_check_mark": "✔",
+ "heavy_division_sign": "âž—",
+ "heavy_dollar_sign": "💲",
+ "heavy_heart_exclamation": "â£",
+ "heavy_large_circle": "â­•",
+ "heavy_minus_sign": "âž–",
+ "heavy_multiplication_x": "✖",
+ "heavy_plus_sign": "âž•",
+ "hedgehog": "🦔",
+ "helicopter": "ðŸš",
+ "herb": "🌿",
+ "hibiscus": "🌺",
+ "high-heeled_shoe": "👠",
+ "high-speed_train": "🚄",
+ "high_voltage": "âš¡",
+ "hiking_boot": "🥾",
+ "hindu_temple": "🛕",
+ "hippopotamus": "🦛",
+ "hole": "🕳",
+ "honey_pot": "ðŸ¯",
+ "honeybee": "ðŸ",
+ "horizontal_traffic_light": "🚥",
+ "horse": "ðŸ´",
+ "horse_face": "ðŸ´",
+ "horse_racing": "ðŸ‡",
+ "horse_racing_dark_skin_tone": "ðŸ‡ðŸ¿",
+ "horse_racing_light_skin_tone": "ðŸ‡ðŸ»",
+ "horse_racing_medium-dark_skin_tone": "ðŸ‡ðŸ¾",
+ "horse_racing_medium-light_skin_tone": "ðŸ‡ðŸ¼",
+ "horse_racing_medium_skin_tone": "ðŸ‡ðŸ½",
+ "hospital": "ðŸ¥",
+ "hot_beverage": "☕",
+ "hot_dog": "🌭",
+ "hot_face": "🥵",
+ "hot_pepper": "🌶",
+ "hot_springs": "♨",
+ "hotel": "ðŸ¨",
+ "hourglass_done": "⌛",
+ "hourglass_not_done": "â³",
+ "house": "ðŸ ",
+ "house_with_garden": "ðŸ¡",
+ "houses": "ðŸ˜",
+ "hugging_face": "🤗",
+ "hundred_points": "💯",
+ "hushed_face": "😯",
+ "ice": "🧊",
+ "ice_cream": "ðŸ¨",
+ "ice_hockey": "ðŸ’",
+ "ice_skate": "⛸",
+ "inbox_tray": "📥",
+ "incoming_envelope": "📨",
+ "index_pointing_up": "â˜",
+ "index_pointing_up_dark_skin_tone": "â˜ðŸ¿",
+ "index_pointing_up_light_skin_tone": "â˜ðŸ»",
+ "index_pointing_up_medium-dark_skin_tone": "â˜ðŸ¾",
+ "index_pointing_up_medium-light_skin_tone": "â˜ðŸ¼",
+ "index_pointing_up_medium_skin_tone": "â˜ðŸ½",
+ "infinity": "♾",
+ "information": "ℹ",
+ "input_latin_letters": "🔤",
+ "input_latin_lowercase": "🔡",
+ "input_latin_uppercase": "🔠",
+ "input_numbers": "🔢",
+ "input_symbols": "🔣",
+ "jack-o-lantern": "🎃",
+ "jeans": "👖",
+ "jigsaw": "🧩",
+ "joker": "ðŸƒ",
+ "joystick": "🕹",
+ "kaaba": "🕋",
+ "kangaroo": "🦘",
+ "key": "🔑",
+ "keyboard": "⌨",
+ "keycap_#": "#ï¸âƒ£",
+ "keycap_*": "*ï¸âƒ£",
+ "keycap_0": "0ï¸âƒ£",
+ "keycap_1": "1ï¸âƒ£",
+ "keycap_10": "🔟",
+ "keycap_2": "2ï¸âƒ£",
+ "keycap_3": "3ï¸âƒ£",
+ "keycap_4": "4ï¸âƒ£",
+ "keycap_5": "5ï¸âƒ£",
+ "keycap_6": "6ï¸âƒ£",
+ "keycap_7": "7ï¸âƒ£",
+ "keycap_8": "8ï¸âƒ£",
+ "keycap_9": "9ï¸âƒ£",
+ "kick_scooter": "🛴",
+ "kimono": "👘",
+ "kiss": "💋",
+ "kiss_man_man": "👨\u200dâ¤ï¸\u200d💋\u200d👨",
+ "kiss_mark": "💋",
+ "kiss_woman_man": "👩\u200dâ¤ï¸\u200d💋\u200d👨",
+ "kiss_woman_woman": "👩\u200dâ¤ï¸\u200d💋\u200d👩",
+ "kissing_cat_face": "😽",
+ "kissing_face": "😗",
+ "kissing_face_with_closed_eyes": "😚",
+ "kissing_face_with_smiling_eyes": "😙",
+ "kitchen_knife": "🔪",
+ "kite": "ðŸª",
+ "kiwi_fruit": "ðŸ¥",
+ "koala": "ðŸ¨",
+ "lab_coat": "🥼",
+ "label": "ðŸ·",
+ "lacrosse": "ðŸ¥",
+ "lady_beetle": "ðŸž",
+ "laptop_computer": "💻",
+ "large_blue_diamond": "🔷",
+ "large_orange_diamond": "🔶",
+ "last_quarter_moon": "🌗",
+ "last_quarter_moon_face": "🌜",
+ "last_track_button": "â®",
+ "latin_cross": "âœ",
+ "leaf_fluttering_in_wind": "ðŸƒ",
+ "leafy_green": "🥬",
+ "ledger": "📒",
+ "left-facing_fist": "🤛",
+ "left-facing_fist_dark_skin_tone": "🤛ðŸ¿",
+ "left-facing_fist_light_skin_tone": "🤛ðŸ»",
+ "left-facing_fist_medium-dark_skin_tone": "🤛ðŸ¾",
+ "left-facing_fist_medium-light_skin_tone": "🤛ðŸ¼",
+ "left-facing_fist_medium_skin_tone": "🤛ðŸ½",
+ "left-right_arrow": "↔",
+ "left_arrow": "⬅",
+ "left_arrow_curving_right": "↪",
+ "left_luggage": "🛅",
+ "left_speech_bubble": "🗨",
+ "leg": "🦵",
+ "lemon": "ðŸ‹",
+ "leopard": "ðŸ†",
+ "level_slider": "🎚",
+ "light_bulb": "💡",
+ "light_rail": "🚈",
+ "link": "🔗",
+ "linked_paperclips": "🖇",
+ "lion_face": "ðŸ¦",
+ "lipstick": "💄",
+ "litter_in_bin_sign": "🚮",
+ "lizard": "🦎",
+ "llama": "🦙",
+ "lobster": "🦞",
+ "locked": "🔒",
+ "locked_with_key": "ðŸ”",
+ "locked_with_pen": "ðŸ”",
+ "locomotive": "🚂",
+ "lollipop": "ðŸ­",
+ "lotion_bottle": "🧴",
+ "loudly_crying_face": "😭",
+ "loudspeaker": "📢",
+ "love-you_gesture": "🤟",
+ "love-you_gesture_dark_skin_tone": "🤟ðŸ¿",
+ "love-you_gesture_light_skin_tone": "🤟ðŸ»",
+ "love-you_gesture_medium-dark_skin_tone": "🤟ðŸ¾",
+ "love-you_gesture_medium-light_skin_tone": "🤟ðŸ¼",
+ "love-you_gesture_medium_skin_tone": "🤟ðŸ½",
+ "love_hotel": "ðŸ©",
+ "love_letter": "💌",
+ "luggage": "🧳",
+ "lying_face": "🤥",
+ "mage": "🧙",
+ "mage_dark_skin_tone": "🧙ðŸ¿",
+ "mage_light_skin_tone": "🧙ðŸ»",
+ "mage_medium-dark_skin_tone": "🧙ðŸ¾",
+ "mage_medium-light_skin_tone": "🧙ðŸ¼",
+ "mage_medium_skin_tone": "🧙ðŸ½",
+ "magnet": "🧲",
+ "magnifying_glass_tilted_left": "ðŸ”",
+ "magnifying_glass_tilted_right": "🔎",
+ "mahjong_red_dragon": "🀄",
+ "male_sign": "♂",
+ "man": "👨",
+ "man_and_woman_holding_hands": "👫",
+ "man_artist": "👨\u200d🎨",
+ "man_artist_dark_skin_tone": "👨ðŸ¿\u200d🎨",
+ "man_artist_light_skin_tone": "👨ðŸ»\u200d🎨",
+ "man_artist_medium-dark_skin_tone": "👨ðŸ¾\u200d🎨",
+ "man_artist_medium-light_skin_tone": "👨ðŸ¼\u200d🎨",
+ "man_artist_medium_skin_tone": "👨ðŸ½\u200d🎨",
+ "man_astronaut": "👨\u200d🚀",
+ "man_astronaut_dark_skin_tone": "👨ðŸ¿\u200d🚀",
+ "man_astronaut_light_skin_tone": "👨ðŸ»\u200d🚀",
+ "man_astronaut_medium-dark_skin_tone": "👨ðŸ¾\u200d🚀",
+ "man_astronaut_medium-light_skin_tone": "👨ðŸ¼\u200d🚀",
+ "man_astronaut_medium_skin_tone": "👨ðŸ½\u200d🚀",
+ "man_biking": "🚴\u200d♂ï¸",
+ "man_biking_dark_skin_tone": "🚴ðŸ¿\u200d♂ï¸",
+ "man_biking_light_skin_tone": "🚴ðŸ»\u200d♂ï¸",
+ "man_biking_medium-dark_skin_tone": "🚴ðŸ¾\u200d♂ï¸",
+ "man_biking_medium-light_skin_tone": "🚴ðŸ¼\u200d♂ï¸",
+ "man_biking_medium_skin_tone": "🚴ðŸ½\u200d♂ï¸",
+ "man_bouncing_ball": "⛹ï¸\u200d♂ï¸",
+ "man_bouncing_ball_dark_skin_tone": "⛹ðŸ¿\u200d♂ï¸",
+ "man_bouncing_ball_light_skin_tone": "⛹ðŸ»\u200d♂ï¸",
+ "man_bouncing_ball_medium-dark_skin_tone": "⛹ðŸ¾\u200d♂ï¸",
+ "man_bouncing_ball_medium-light_skin_tone": "⛹ðŸ¼\u200d♂ï¸",
+ "man_bouncing_ball_medium_skin_tone": "⛹ðŸ½\u200d♂ï¸",
+ "man_bowing": "🙇\u200d♂ï¸",
+ "man_bowing_dark_skin_tone": "🙇ðŸ¿\u200d♂ï¸",
+ "man_bowing_light_skin_tone": "🙇ðŸ»\u200d♂ï¸",
+ "man_bowing_medium-dark_skin_tone": "🙇ðŸ¾\u200d♂ï¸",
+ "man_bowing_medium-light_skin_tone": "🙇ðŸ¼\u200d♂ï¸",
+ "man_bowing_medium_skin_tone": "🙇ðŸ½\u200d♂ï¸",
+ "man_cartwheeling": "🤸\u200d♂ï¸",
+ "man_cartwheeling_dark_skin_tone": "🤸ðŸ¿\u200d♂ï¸",
+ "man_cartwheeling_light_skin_tone": "🤸ðŸ»\u200d♂ï¸",
+ "man_cartwheeling_medium-dark_skin_tone": "🤸ðŸ¾\u200d♂ï¸",
+ "man_cartwheeling_medium-light_skin_tone": "🤸ðŸ¼\u200d♂ï¸",
+ "man_cartwheeling_medium_skin_tone": "🤸ðŸ½\u200d♂ï¸",
+ "man_climbing": "🧗\u200d♂ï¸",
+ "man_climbing_dark_skin_tone": "🧗ðŸ¿\u200d♂ï¸",
+ "man_climbing_light_skin_tone": "🧗ðŸ»\u200d♂ï¸",
+ "man_climbing_medium-dark_skin_tone": "🧗ðŸ¾\u200d♂ï¸",
+ "man_climbing_medium-light_skin_tone": "🧗ðŸ¼\u200d♂ï¸",
+ "man_climbing_medium_skin_tone": "🧗ðŸ½\u200d♂ï¸",
+ "man_construction_worker": "👷\u200d♂ï¸",
+ "man_construction_worker_dark_skin_tone": "👷ðŸ¿\u200d♂ï¸",
+ "man_construction_worker_light_skin_tone": "👷ðŸ»\u200d♂ï¸",
+ "man_construction_worker_medium-dark_skin_tone": "👷ðŸ¾\u200d♂ï¸",
+ "man_construction_worker_medium-light_skin_tone": "👷ðŸ¼\u200d♂ï¸",
+ "man_construction_worker_medium_skin_tone": "👷ðŸ½\u200d♂ï¸",
+ "man_cook": "👨\u200dðŸ³",
+ "man_cook_dark_skin_tone": "👨ðŸ¿\u200dðŸ³",
+ "man_cook_light_skin_tone": "👨ðŸ»\u200dðŸ³",
+ "man_cook_medium-dark_skin_tone": "👨ðŸ¾\u200dðŸ³",
+ "man_cook_medium-light_skin_tone": "👨ðŸ¼\u200dðŸ³",
+ "man_cook_medium_skin_tone": "👨ðŸ½\u200dðŸ³",
+ "man_dancing": "🕺",
+ "man_dancing_dark_skin_tone": "🕺ðŸ¿",
+ "man_dancing_light_skin_tone": "🕺ðŸ»",
+ "man_dancing_medium-dark_skin_tone": "🕺ðŸ¾",
+ "man_dancing_medium-light_skin_tone": "🕺ðŸ¼",
+ "man_dancing_medium_skin_tone": "🕺ðŸ½",
+ "man_dark_skin_tone": "👨ðŸ¿",
+ "man_detective": "🕵ï¸\u200d♂ï¸",
+ "man_detective_dark_skin_tone": "🕵ðŸ¿\u200d♂ï¸",
+ "man_detective_light_skin_tone": "🕵ðŸ»\u200d♂ï¸",
+ "man_detective_medium-dark_skin_tone": "🕵ðŸ¾\u200d♂ï¸",
+ "man_detective_medium-light_skin_tone": "🕵ðŸ¼\u200d♂ï¸",
+ "man_detective_medium_skin_tone": "🕵ðŸ½\u200d♂ï¸",
+ "man_elf": "ðŸ§\u200d♂ï¸",
+ "man_elf_dark_skin_tone": "ðŸ§ðŸ¿\u200d♂ï¸",
+ "man_elf_light_skin_tone": "ðŸ§ðŸ»\u200d♂ï¸",
+ "man_elf_medium-dark_skin_tone": "ðŸ§ðŸ¾\u200d♂ï¸",
+ "man_elf_medium-light_skin_tone": "ðŸ§ðŸ¼\u200d♂ï¸",
+ "man_elf_medium_skin_tone": "ðŸ§ðŸ½\u200d♂ï¸",
+ "man_facepalming": "🤦\u200d♂ï¸",
+ "man_facepalming_dark_skin_tone": "🤦ðŸ¿\u200d♂ï¸",
+ "man_facepalming_light_skin_tone": "🤦ðŸ»\u200d♂ï¸",
+ "man_facepalming_medium-dark_skin_tone": "🤦ðŸ¾\u200d♂ï¸",
+ "man_facepalming_medium-light_skin_tone": "🤦ðŸ¼\u200d♂ï¸",
+ "man_facepalming_medium_skin_tone": "🤦ðŸ½\u200d♂ï¸",
+ "man_factory_worker": "👨\u200dðŸ­",
+ "man_factory_worker_dark_skin_tone": "👨ðŸ¿\u200dðŸ­",
+ "man_factory_worker_light_skin_tone": "👨ðŸ»\u200dðŸ­",
+ "man_factory_worker_medium-dark_skin_tone": "👨ðŸ¾\u200dðŸ­",
+ "man_factory_worker_medium-light_skin_tone": "👨ðŸ¼\u200dðŸ­",
+ "man_factory_worker_medium_skin_tone": "👨ðŸ½\u200dðŸ­",
+ "man_fairy": "🧚\u200d♂ï¸",
+ "man_fairy_dark_skin_tone": "🧚ðŸ¿\u200d♂ï¸",
+ "man_fairy_light_skin_tone": "🧚ðŸ»\u200d♂ï¸",
+ "man_fairy_medium-dark_skin_tone": "🧚ðŸ¾\u200d♂ï¸",
+ "man_fairy_medium-light_skin_tone": "🧚ðŸ¼\u200d♂ï¸",
+ "man_fairy_medium_skin_tone": "🧚ðŸ½\u200d♂ï¸",
+ "man_farmer": "👨\u200d🌾",
+ "man_farmer_dark_skin_tone": "👨ðŸ¿\u200d🌾",
+ "man_farmer_light_skin_tone": "👨ðŸ»\u200d🌾",
+ "man_farmer_medium-dark_skin_tone": "👨ðŸ¾\u200d🌾",
+ "man_farmer_medium-light_skin_tone": "👨ðŸ¼\u200d🌾",
+ "man_farmer_medium_skin_tone": "👨ðŸ½\u200d🌾",
+ "man_firefighter": "👨\u200d🚒",
+ "man_firefighter_dark_skin_tone": "👨ðŸ¿\u200d🚒",
+ "man_firefighter_light_skin_tone": "👨ðŸ»\u200d🚒",
+ "man_firefighter_medium-dark_skin_tone": "👨ðŸ¾\u200d🚒",
+ "man_firefighter_medium-light_skin_tone": "👨ðŸ¼\u200d🚒",
+ "man_firefighter_medium_skin_tone": "👨ðŸ½\u200d🚒",
+ "man_frowning": "ðŸ™\u200d♂ï¸",
+ "man_frowning_dark_skin_tone": "ðŸ™ðŸ¿\u200d♂ï¸",
+ "man_frowning_light_skin_tone": "ðŸ™ðŸ»\u200d♂ï¸",
+ "man_frowning_medium-dark_skin_tone": "ðŸ™ðŸ¾\u200d♂ï¸",
+ "man_frowning_medium-light_skin_tone": "ðŸ™ðŸ¼\u200d♂ï¸",
+ "man_frowning_medium_skin_tone": "ðŸ™ðŸ½\u200d♂ï¸",
+ "man_genie": "🧞\u200d♂ï¸",
+ "man_gesturing_no": "🙅\u200d♂ï¸",
+ "man_gesturing_no_dark_skin_tone": "🙅ðŸ¿\u200d♂ï¸",
+ "man_gesturing_no_light_skin_tone": "🙅ðŸ»\u200d♂ï¸",
+ "man_gesturing_no_medium-dark_skin_tone": "🙅ðŸ¾\u200d♂ï¸",
+ "man_gesturing_no_medium-light_skin_tone": "🙅ðŸ¼\u200d♂ï¸",
+ "man_gesturing_no_medium_skin_tone": "🙅ðŸ½\u200d♂ï¸",
+ "man_gesturing_ok": "🙆\u200d♂ï¸",
+ "man_gesturing_ok_dark_skin_tone": "🙆ðŸ¿\u200d♂ï¸",
+ "man_gesturing_ok_light_skin_tone": "🙆ðŸ»\u200d♂ï¸",
+ "man_gesturing_ok_medium-dark_skin_tone": "🙆ðŸ¾\u200d♂ï¸",
+ "man_gesturing_ok_medium-light_skin_tone": "🙆ðŸ¼\u200d♂ï¸",
+ "man_gesturing_ok_medium_skin_tone": "🙆ðŸ½\u200d♂ï¸",
+ "man_getting_haircut": "💇\u200d♂ï¸",
+ "man_getting_haircut_dark_skin_tone": "💇ðŸ¿\u200d♂ï¸",
+ "man_getting_haircut_light_skin_tone": "💇ðŸ»\u200d♂ï¸",
+ "man_getting_haircut_medium-dark_skin_tone": "💇ðŸ¾\u200d♂ï¸",
+ "man_getting_haircut_medium-light_skin_tone": "💇ðŸ¼\u200d♂ï¸",
+ "man_getting_haircut_medium_skin_tone": "💇ðŸ½\u200d♂ï¸",
+ "man_getting_massage": "💆\u200d♂ï¸",
+ "man_getting_massage_dark_skin_tone": "💆ðŸ¿\u200d♂ï¸",
+ "man_getting_massage_light_skin_tone": "💆ðŸ»\u200d♂ï¸",
+ "man_getting_massage_medium-dark_skin_tone": "💆ðŸ¾\u200d♂ï¸",
+ "man_getting_massage_medium-light_skin_tone": "💆ðŸ¼\u200d♂ï¸",
+ "man_getting_massage_medium_skin_tone": "💆ðŸ½\u200d♂ï¸",
+ "man_golfing": "ðŸŒï¸\u200d♂ï¸",
+ "man_golfing_dark_skin_tone": "ðŸŒðŸ¿\u200d♂ï¸",
+ "man_golfing_light_skin_tone": "ðŸŒðŸ»\u200d♂ï¸",
+ "man_golfing_medium-dark_skin_tone": "ðŸŒðŸ¾\u200d♂ï¸",
+ "man_golfing_medium-light_skin_tone": "ðŸŒðŸ¼\u200d♂ï¸",
+ "man_golfing_medium_skin_tone": "ðŸŒðŸ½\u200d♂ï¸",
+ "man_guard": "💂\u200d♂ï¸",
+ "man_guard_dark_skin_tone": "💂ðŸ¿\u200d♂ï¸",
+ "man_guard_light_skin_tone": "💂ðŸ»\u200d♂ï¸",
+ "man_guard_medium-dark_skin_tone": "💂ðŸ¾\u200d♂ï¸",
+ "man_guard_medium-light_skin_tone": "💂ðŸ¼\u200d♂ï¸",
+ "man_guard_medium_skin_tone": "💂ðŸ½\u200d♂ï¸",
+ "man_health_worker": "👨\u200dâš•ï¸",
+ "man_health_worker_dark_skin_tone": "👨ðŸ¿\u200dâš•ï¸",
+ "man_health_worker_light_skin_tone": "👨ðŸ»\u200dâš•ï¸",
+ "man_health_worker_medium-dark_skin_tone": "👨ðŸ¾\u200dâš•ï¸",
+ "man_health_worker_medium-light_skin_tone": "👨ðŸ¼\u200dâš•ï¸",
+ "man_health_worker_medium_skin_tone": "👨ðŸ½\u200dâš•ï¸",
+ "man_in_lotus_position": "🧘\u200d♂ï¸",
+ "man_in_lotus_position_dark_skin_tone": "🧘ðŸ¿\u200d♂ï¸",
+ "man_in_lotus_position_light_skin_tone": "🧘ðŸ»\u200d♂ï¸",
+ "man_in_lotus_position_medium-dark_skin_tone": "🧘ðŸ¾\u200d♂ï¸",
+ "man_in_lotus_position_medium-light_skin_tone": "🧘ðŸ¼\u200d♂ï¸",
+ "man_in_lotus_position_medium_skin_tone": "🧘ðŸ½\u200d♂ï¸",
+ "man_in_manual_wheelchair": "👨\u200d🦽",
+ "man_in_motorized_wheelchair": "👨\u200d🦼",
+ "man_in_steamy_room": "🧖\u200d♂ï¸",
+ "man_in_steamy_room_dark_skin_tone": "🧖ðŸ¿\u200d♂ï¸",
+ "man_in_steamy_room_light_skin_tone": "🧖ðŸ»\u200d♂ï¸",
+ "man_in_steamy_room_medium-dark_skin_tone": "🧖ðŸ¾\u200d♂ï¸",
+ "man_in_steamy_room_medium-light_skin_tone": "🧖ðŸ¼\u200d♂ï¸",
+ "man_in_steamy_room_medium_skin_tone": "🧖ðŸ½\u200d♂ï¸",
+ "man_in_suit_levitating": "🕴",
+ "man_in_suit_levitating_dark_skin_tone": "🕴ðŸ¿",
+ "man_in_suit_levitating_light_skin_tone": "🕴ðŸ»",
+ "man_in_suit_levitating_medium-dark_skin_tone": "🕴ðŸ¾",
+ "man_in_suit_levitating_medium-light_skin_tone": "🕴ðŸ¼",
+ "man_in_suit_levitating_medium_skin_tone": "🕴ðŸ½",
+ "man_in_tuxedo": "🤵",
+ "man_in_tuxedo_dark_skin_tone": "🤵ðŸ¿",
+ "man_in_tuxedo_light_skin_tone": "🤵ðŸ»",
+ "man_in_tuxedo_medium-dark_skin_tone": "🤵ðŸ¾",
+ "man_in_tuxedo_medium-light_skin_tone": "🤵ðŸ¼",
+ "man_in_tuxedo_medium_skin_tone": "🤵ðŸ½",
+ "man_judge": "👨\u200dâš–ï¸",
+ "man_judge_dark_skin_tone": "👨ðŸ¿\u200dâš–ï¸",
+ "man_judge_light_skin_tone": "👨ðŸ»\u200dâš–ï¸",
+ "man_judge_medium-dark_skin_tone": "👨ðŸ¾\u200dâš–ï¸",
+ "man_judge_medium-light_skin_tone": "👨ðŸ¼\u200dâš–ï¸",
+ "man_judge_medium_skin_tone": "👨ðŸ½\u200dâš–ï¸",
+ "man_juggling": "🤹\u200d♂ï¸",
+ "man_juggling_dark_skin_tone": "🤹ðŸ¿\u200d♂ï¸",
+ "man_juggling_light_skin_tone": "🤹ðŸ»\u200d♂ï¸",
+ "man_juggling_medium-dark_skin_tone": "🤹ðŸ¾\u200d♂ï¸",
+ "man_juggling_medium-light_skin_tone": "🤹ðŸ¼\u200d♂ï¸",
+ "man_juggling_medium_skin_tone": "🤹ðŸ½\u200d♂ï¸",
+ "man_lifting_weights": "ðŸ‹ï¸\u200d♂ï¸",
+ "man_lifting_weights_dark_skin_tone": "ðŸ‹ðŸ¿\u200d♂ï¸",
+ "man_lifting_weights_light_skin_tone": "ðŸ‹ðŸ»\u200d♂ï¸",
+ "man_lifting_weights_medium-dark_skin_tone": "ðŸ‹ðŸ¾\u200d♂ï¸",
+ "man_lifting_weights_medium-light_skin_tone": "ðŸ‹ðŸ¼\u200d♂ï¸",
+ "man_lifting_weights_medium_skin_tone": "ðŸ‹ðŸ½\u200d♂ï¸",
+ "man_light_skin_tone": "👨ðŸ»",
+ "man_mage": "🧙\u200d♂ï¸",
+ "man_mage_dark_skin_tone": "🧙ðŸ¿\u200d♂ï¸",
+ "man_mage_light_skin_tone": "🧙ðŸ»\u200d♂ï¸",
+ "man_mage_medium-dark_skin_tone": "🧙ðŸ¾\u200d♂ï¸",
+ "man_mage_medium-light_skin_tone": "🧙ðŸ¼\u200d♂ï¸",
+ "man_mage_medium_skin_tone": "🧙ðŸ½\u200d♂ï¸",
+ "man_mechanic": "👨\u200d🔧",
+ "man_mechanic_dark_skin_tone": "👨ðŸ¿\u200d🔧",
+ "man_mechanic_light_skin_tone": "👨ðŸ»\u200d🔧",
+ "man_mechanic_medium-dark_skin_tone": "👨ðŸ¾\u200d🔧",
+ "man_mechanic_medium-light_skin_tone": "👨ðŸ¼\u200d🔧",
+ "man_mechanic_medium_skin_tone": "👨ðŸ½\u200d🔧",
+ "man_medium-dark_skin_tone": "👨ðŸ¾",
+ "man_medium-light_skin_tone": "👨ðŸ¼",
+ "man_medium_skin_tone": "👨ðŸ½",
+ "man_mountain_biking": "🚵\u200d♂ï¸",
+ "man_mountain_biking_dark_skin_tone": "🚵ðŸ¿\u200d♂ï¸",
+ "man_mountain_biking_light_skin_tone": "🚵ðŸ»\u200d♂ï¸",
+ "man_mountain_biking_medium-dark_skin_tone": "🚵ðŸ¾\u200d♂ï¸",
+ "man_mountain_biking_medium-light_skin_tone": "🚵ðŸ¼\u200d♂ï¸",
+ "man_mountain_biking_medium_skin_tone": "🚵ðŸ½\u200d♂ï¸",
+ "man_office_worker": "👨\u200d💼",
+ "man_office_worker_dark_skin_tone": "👨ðŸ¿\u200d💼",
+ "man_office_worker_light_skin_tone": "👨ðŸ»\u200d💼",
+ "man_office_worker_medium-dark_skin_tone": "👨ðŸ¾\u200d💼",
+ "man_office_worker_medium-light_skin_tone": "👨ðŸ¼\u200d💼",
+ "man_office_worker_medium_skin_tone": "👨ðŸ½\u200d💼",
+ "man_pilot": "👨\u200d✈ï¸",
+ "man_pilot_dark_skin_tone": "👨ðŸ¿\u200d✈ï¸",
+ "man_pilot_light_skin_tone": "👨ðŸ»\u200d✈ï¸",
+ "man_pilot_medium-dark_skin_tone": "👨ðŸ¾\u200d✈ï¸",
+ "man_pilot_medium-light_skin_tone": "👨ðŸ¼\u200d✈ï¸",
+ "man_pilot_medium_skin_tone": "👨ðŸ½\u200d✈ï¸",
+ "man_playing_handball": "🤾\u200d♂ï¸",
+ "man_playing_handball_dark_skin_tone": "🤾ðŸ¿\u200d♂ï¸",
+ "man_playing_handball_light_skin_tone": "🤾ðŸ»\u200d♂ï¸",
+ "man_playing_handball_medium-dark_skin_tone": "🤾ðŸ¾\u200d♂ï¸",
+ "man_playing_handball_medium-light_skin_tone": "🤾ðŸ¼\u200d♂ï¸",
+ "man_playing_handball_medium_skin_tone": "🤾ðŸ½\u200d♂ï¸",
+ "man_playing_water_polo": "🤽\u200d♂ï¸",
+ "man_playing_water_polo_dark_skin_tone": "🤽ðŸ¿\u200d♂ï¸",
+ "man_playing_water_polo_light_skin_tone": "🤽ðŸ»\u200d♂ï¸",
+ "man_playing_water_polo_medium-dark_skin_tone": "🤽ðŸ¾\u200d♂ï¸",
+ "man_playing_water_polo_medium-light_skin_tone": "🤽ðŸ¼\u200d♂ï¸",
+ "man_playing_water_polo_medium_skin_tone": "🤽ðŸ½\u200d♂ï¸",
+ "man_police_officer": "👮\u200d♂ï¸",
+ "man_police_officer_dark_skin_tone": "👮ðŸ¿\u200d♂ï¸",
+ "man_police_officer_light_skin_tone": "👮ðŸ»\u200d♂ï¸",
+ "man_police_officer_medium-dark_skin_tone": "👮ðŸ¾\u200d♂ï¸",
+ "man_police_officer_medium-light_skin_tone": "👮ðŸ¼\u200d♂ï¸",
+ "man_police_officer_medium_skin_tone": "👮ðŸ½\u200d♂ï¸",
+ "man_pouting": "🙎\u200d♂ï¸",
+ "man_pouting_dark_skin_tone": "🙎ðŸ¿\u200d♂ï¸",
+ "man_pouting_light_skin_tone": "🙎ðŸ»\u200d♂ï¸",
+ "man_pouting_medium-dark_skin_tone": "🙎ðŸ¾\u200d♂ï¸",
+ "man_pouting_medium-light_skin_tone": "🙎ðŸ¼\u200d♂ï¸",
+ "man_pouting_medium_skin_tone": "🙎ðŸ½\u200d♂ï¸",
+ "man_raising_hand": "🙋\u200d♂ï¸",
+ "man_raising_hand_dark_skin_tone": "🙋ðŸ¿\u200d♂ï¸",
+ "man_raising_hand_light_skin_tone": "🙋ðŸ»\u200d♂ï¸",
+ "man_raising_hand_medium-dark_skin_tone": "🙋ðŸ¾\u200d♂ï¸",
+ "man_raising_hand_medium-light_skin_tone": "🙋ðŸ¼\u200d♂ï¸",
+ "man_raising_hand_medium_skin_tone": "🙋ðŸ½\u200d♂ï¸",
+ "man_rowing_boat": "🚣\u200d♂ï¸",
+ "man_rowing_boat_dark_skin_tone": "🚣ðŸ¿\u200d♂ï¸",
+ "man_rowing_boat_light_skin_tone": "🚣ðŸ»\u200d♂ï¸",
+ "man_rowing_boat_medium-dark_skin_tone": "🚣ðŸ¾\u200d♂ï¸",
+ "man_rowing_boat_medium-light_skin_tone": "🚣ðŸ¼\u200d♂ï¸",
+ "man_rowing_boat_medium_skin_tone": "🚣ðŸ½\u200d♂ï¸",
+ "man_running": "ðŸƒ\u200d♂ï¸",
+ "man_running_dark_skin_tone": "ðŸƒðŸ¿\u200d♂ï¸",
+ "man_running_light_skin_tone": "ðŸƒðŸ»\u200d♂ï¸",
+ "man_running_medium-dark_skin_tone": "ðŸƒðŸ¾\u200d♂ï¸",
+ "man_running_medium-light_skin_tone": "ðŸƒðŸ¼\u200d♂ï¸",
+ "man_running_medium_skin_tone": "ðŸƒðŸ½\u200d♂ï¸",
+ "man_scientist": "👨\u200d🔬",
+ "man_scientist_dark_skin_tone": "👨ðŸ¿\u200d🔬",
+ "man_scientist_light_skin_tone": "👨ðŸ»\u200d🔬",
+ "man_scientist_medium-dark_skin_tone": "👨ðŸ¾\u200d🔬",
+ "man_scientist_medium-light_skin_tone": "👨ðŸ¼\u200d🔬",
+ "man_scientist_medium_skin_tone": "👨ðŸ½\u200d🔬",
+ "man_shrugging": "🤷\u200d♂ï¸",
+ "man_shrugging_dark_skin_tone": "🤷ðŸ¿\u200d♂ï¸",
+ "man_shrugging_light_skin_tone": "🤷ðŸ»\u200d♂ï¸",
+ "man_shrugging_medium-dark_skin_tone": "🤷ðŸ¾\u200d♂ï¸",
+ "man_shrugging_medium-light_skin_tone": "🤷ðŸ¼\u200d♂ï¸",
+ "man_shrugging_medium_skin_tone": "🤷ðŸ½\u200d♂ï¸",
+ "man_singer": "👨\u200d🎤",
+ "man_singer_dark_skin_tone": "👨ðŸ¿\u200d🎤",
+ "man_singer_light_skin_tone": "👨ðŸ»\u200d🎤",
+ "man_singer_medium-dark_skin_tone": "👨ðŸ¾\u200d🎤",
+ "man_singer_medium-light_skin_tone": "👨ðŸ¼\u200d🎤",
+ "man_singer_medium_skin_tone": "👨ðŸ½\u200d🎤",
+ "man_student": "👨\u200d🎓",
+ "man_student_dark_skin_tone": "👨ðŸ¿\u200d🎓",
+ "man_student_light_skin_tone": "👨ðŸ»\u200d🎓",
+ "man_student_medium-dark_skin_tone": "👨ðŸ¾\u200d🎓",
+ "man_student_medium-light_skin_tone": "👨ðŸ¼\u200d🎓",
+ "man_student_medium_skin_tone": "👨ðŸ½\u200d🎓",
+ "man_surfing": "ðŸ„\u200d♂ï¸",
+ "man_surfing_dark_skin_tone": "ðŸ„ðŸ¿\u200d♂ï¸",
+ "man_surfing_light_skin_tone": "ðŸ„ðŸ»\u200d♂ï¸",
+ "man_surfing_medium-dark_skin_tone": "ðŸ„ðŸ¾\u200d♂ï¸",
+ "man_surfing_medium-light_skin_tone": "ðŸ„ðŸ¼\u200d♂ï¸",
+ "man_surfing_medium_skin_tone": "ðŸ„ðŸ½\u200d♂ï¸",
+ "man_swimming": "ðŸŠ\u200d♂ï¸",
+ "man_swimming_dark_skin_tone": "ðŸŠðŸ¿\u200d♂ï¸",
+ "man_swimming_light_skin_tone": "ðŸŠðŸ»\u200d♂ï¸",
+ "man_swimming_medium-dark_skin_tone": "ðŸŠðŸ¾\u200d♂ï¸",
+ "man_swimming_medium-light_skin_tone": "ðŸŠðŸ¼\u200d♂ï¸",
+ "man_swimming_medium_skin_tone": "ðŸŠðŸ½\u200d♂ï¸",
+ "man_teacher": "👨\u200dðŸ«",
+ "man_teacher_dark_skin_tone": "👨ðŸ¿\u200dðŸ«",
+ "man_teacher_light_skin_tone": "👨ðŸ»\u200dðŸ«",
+ "man_teacher_medium-dark_skin_tone": "👨ðŸ¾\u200dðŸ«",
+ "man_teacher_medium-light_skin_tone": "👨ðŸ¼\u200dðŸ«",
+ "man_teacher_medium_skin_tone": "👨ðŸ½\u200dðŸ«",
+ "man_technologist": "👨\u200d💻",
+ "man_technologist_dark_skin_tone": "👨ðŸ¿\u200d💻",
+ "man_technologist_light_skin_tone": "👨ðŸ»\u200d💻",
+ "man_technologist_medium-dark_skin_tone": "👨ðŸ¾\u200d💻",
+ "man_technologist_medium-light_skin_tone": "👨ðŸ¼\u200d💻",
+ "man_technologist_medium_skin_tone": "👨ðŸ½\u200d💻",
+ "man_tipping_hand": "ðŸ’\u200d♂ï¸",
+ "man_tipping_hand_dark_skin_tone": "ðŸ’ðŸ¿\u200d♂ï¸",
+ "man_tipping_hand_light_skin_tone": "ðŸ’ðŸ»\u200d♂ï¸",
+ "man_tipping_hand_medium-dark_skin_tone": "ðŸ’ðŸ¾\u200d♂ï¸",
+ "man_tipping_hand_medium-light_skin_tone": "ðŸ’ðŸ¼\u200d♂ï¸",
+ "man_tipping_hand_medium_skin_tone": "ðŸ’ðŸ½\u200d♂ï¸",
+ "man_vampire": "🧛\u200d♂ï¸",
+ "man_vampire_dark_skin_tone": "🧛ðŸ¿\u200d♂ï¸",
+ "man_vampire_light_skin_tone": "🧛ðŸ»\u200d♂ï¸",
+ "man_vampire_medium-dark_skin_tone": "🧛ðŸ¾\u200d♂ï¸",
+ "man_vampire_medium-light_skin_tone": "🧛ðŸ¼\u200d♂ï¸",
+ "man_vampire_medium_skin_tone": "🧛ðŸ½\u200d♂ï¸",
+ "man_walking": "🚶\u200d♂ï¸",
+ "man_walking_dark_skin_tone": "🚶ðŸ¿\u200d♂ï¸",
+ "man_walking_light_skin_tone": "🚶ðŸ»\u200d♂ï¸",
+ "man_walking_medium-dark_skin_tone": "🚶ðŸ¾\u200d♂ï¸",
+ "man_walking_medium-light_skin_tone": "🚶ðŸ¼\u200d♂ï¸",
+ "man_walking_medium_skin_tone": "🚶ðŸ½\u200d♂ï¸",
+ "man_wearing_turban": "👳\u200d♂ï¸",
+ "man_wearing_turban_dark_skin_tone": "👳ðŸ¿\u200d♂ï¸",
+ "man_wearing_turban_light_skin_tone": "👳ðŸ»\u200d♂ï¸",
+ "man_wearing_turban_medium-dark_skin_tone": "👳ðŸ¾\u200d♂ï¸",
+ "man_wearing_turban_medium-light_skin_tone": "👳ðŸ¼\u200d♂ï¸",
+ "man_wearing_turban_medium_skin_tone": "👳ðŸ½\u200d♂ï¸",
+ "man_with_probing_cane": "👨\u200d🦯",
+ "man_with_chinese_cap": "👲",
+ "man_with_chinese_cap_dark_skin_tone": "👲ðŸ¿",
+ "man_with_chinese_cap_light_skin_tone": "👲ðŸ»",
+ "man_with_chinese_cap_medium-dark_skin_tone": "👲ðŸ¾",
+ "man_with_chinese_cap_medium-light_skin_tone": "👲ðŸ¼",
+ "man_with_chinese_cap_medium_skin_tone": "👲ðŸ½",
+ "man_zombie": "🧟\u200d♂ï¸",
+ "mango": "🥭",
+ "mantelpiece_clock": "🕰",
+ "manual_wheelchair": "🦽",
+ "man’s_shoe": "👞",
+ "map_of_japan": "🗾",
+ "maple_leaf": "ðŸ",
+ "martial_arts_uniform": "🥋",
+ "mate": "🧉",
+ "meat_on_bone": "ðŸ–",
+ "mechanical_arm": "🦾",
+ "mechanical_leg": "🦿",
+ "medical_symbol": "âš•",
+ "megaphone": "📣",
+ "melon": "ðŸˆ",
+ "memo": "ðŸ“",
+ "men_with_bunny_ears": "👯\u200d♂ï¸",
+ "men_wrestling": "🤼\u200d♂ï¸",
+ "menorah": "🕎",
+ "men’s_room": "🚹",
+ "mermaid": "🧜\u200d♀ï¸",
+ "mermaid_dark_skin_tone": "🧜ðŸ¿\u200d♀ï¸",
+ "mermaid_light_skin_tone": "🧜ðŸ»\u200d♀ï¸",
+ "mermaid_medium-dark_skin_tone": "🧜ðŸ¾\u200d♀ï¸",
+ "mermaid_medium-light_skin_tone": "🧜ðŸ¼\u200d♀ï¸",
+ "mermaid_medium_skin_tone": "🧜ðŸ½\u200d♀ï¸",
+ "merman": "🧜\u200d♂ï¸",
+ "merman_dark_skin_tone": "🧜ðŸ¿\u200d♂ï¸",
+ "merman_light_skin_tone": "🧜ðŸ»\u200d♂ï¸",
+ "merman_medium-dark_skin_tone": "🧜ðŸ¾\u200d♂ï¸",
+ "merman_medium-light_skin_tone": "🧜ðŸ¼\u200d♂ï¸",
+ "merman_medium_skin_tone": "🧜ðŸ½\u200d♂ï¸",
+ "merperson": "🧜",
+ "merperson_dark_skin_tone": "🧜ðŸ¿",
+ "merperson_light_skin_tone": "🧜ðŸ»",
+ "merperson_medium-dark_skin_tone": "🧜ðŸ¾",
+ "merperson_medium-light_skin_tone": "🧜ðŸ¼",
+ "merperson_medium_skin_tone": "🧜ðŸ½",
+ "metro": "🚇",
+ "microbe": "🦠",
+ "microphone": "🎤",
+ "microscope": "🔬",
+ "middle_finger": "🖕",
+ "middle_finger_dark_skin_tone": "🖕ðŸ¿",
+ "middle_finger_light_skin_tone": "🖕ðŸ»",
+ "middle_finger_medium-dark_skin_tone": "🖕ðŸ¾",
+ "middle_finger_medium-light_skin_tone": "🖕ðŸ¼",
+ "middle_finger_medium_skin_tone": "🖕ðŸ½",
+ "military_medal": "🎖",
+ "milky_way": "🌌",
+ "minibus": "ðŸš",
+ "moai": "🗿",
+ "mobile_phone": "📱",
+ "mobile_phone_off": "📴",
+ "mobile_phone_with_arrow": "📲",
+ "money-mouth_face": "🤑",
+ "money_bag": "💰",
+ "money_with_wings": "💸",
+ "monkey": "ðŸ’",
+ "monkey_face": "ðŸµ",
+ "monorail": "ðŸš",
+ "moon_cake": "🥮",
+ "moon_viewing_ceremony": "🎑",
+ "mosque": "🕌",
+ "mosquito": "🦟",
+ "motor_boat": "🛥",
+ "motor_scooter": "🛵",
+ "motorcycle": "ðŸ",
+ "motorized_wheelchair": "🦼",
+ "motorway": "🛣",
+ "mount_fuji": "🗻",
+ "mountain": "â›°",
+ "mountain_cableway": "🚠",
+ "mountain_railway": "🚞",
+ "mouse": "ðŸ­",
+ "mouse_face": "ðŸ­",
+ "mouth": "👄",
+ "movie_camera": "🎥",
+ "mushroom": "ðŸ„",
+ "musical_keyboard": "🎹",
+ "musical_note": "🎵",
+ "musical_notes": "🎶",
+ "musical_score": "🎼",
+ "muted_speaker": "🔇",
+ "nail_polish": "💅",
+ "nail_polish_dark_skin_tone": "💅ðŸ¿",
+ "nail_polish_light_skin_tone": "💅ðŸ»",
+ "nail_polish_medium-dark_skin_tone": "💅ðŸ¾",
+ "nail_polish_medium-light_skin_tone": "💅ðŸ¼",
+ "nail_polish_medium_skin_tone": "💅ðŸ½",
+ "name_badge": "📛",
+ "national_park": "ðŸž",
+ "nauseated_face": "🤢",
+ "nazar_amulet": "🧿",
+ "necktie": "👔",
+ "nerd_face": "🤓",
+ "neutral_face": "ðŸ˜",
+ "new_moon": "🌑",
+ "new_moon_face": "🌚",
+ "newspaper": "📰",
+ "next_track_button": "â­",
+ "night_with_stars": "🌃",
+ "nine-thirty": "🕤",
+ "nine_o’clock": "🕘",
+ "no_bicycles": "🚳",
+ "no_entry": "â›”",
+ "no_littering": "🚯",
+ "no_mobile_phones": "📵",
+ "no_one_under_eighteen": "🔞",
+ "no_pedestrians": "🚷",
+ "no_smoking": "🚭",
+ "non-potable_water": "🚱",
+ "nose": "👃",
+ "nose_dark_skin_tone": "👃ðŸ¿",
+ "nose_light_skin_tone": "👃ðŸ»",
+ "nose_medium-dark_skin_tone": "👃ðŸ¾",
+ "nose_medium-light_skin_tone": "👃ðŸ¼",
+ "nose_medium_skin_tone": "👃ðŸ½",
+ "notebook": "📓",
+ "notebook_with_decorative_cover": "📔",
+ "nut_and_bolt": "🔩",
+ "octopus": "ðŸ™",
+ "oden": "ðŸ¢",
+ "office_building": "ðŸ¢",
+ "ogre": "👹",
+ "oil_drum": "🛢",
+ "old_key": "ðŸ—",
+ "old_man": "👴",
+ "old_man_dark_skin_tone": "👴ðŸ¿",
+ "old_man_light_skin_tone": "👴ðŸ»",
+ "old_man_medium-dark_skin_tone": "👴ðŸ¾",
+ "old_man_medium-light_skin_tone": "👴ðŸ¼",
+ "old_man_medium_skin_tone": "👴ðŸ½",
+ "old_woman": "👵",
+ "old_woman_dark_skin_tone": "👵ðŸ¿",
+ "old_woman_light_skin_tone": "👵ðŸ»",
+ "old_woman_medium-dark_skin_tone": "👵ðŸ¾",
+ "old_woman_medium-light_skin_tone": "👵ðŸ¼",
+ "old_woman_medium_skin_tone": "👵ðŸ½",
+ "older_adult": "🧓",
+ "older_adult_dark_skin_tone": "🧓ðŸ¿",
+ "older_adult_light_skin_tone": "🧓ðŸ»",
+ "older_adult_medium-dark_skin_tone": "🧓ðŸ¾",
+ "older_adult_medium-light_skin_tone": "🧓ðŸ¼",
+ "older_adult_medium_skin_tone": "🧓ðŸ½",
+ "om": "🕉",
+ "oncoming_automobile": "🚘",
+ "oncoming_bus": "ðŸš",
+ "oncoming_fist": "👊",
+ "oncoming_fist_dark_skin_tone": "👊ðŸ¿",
+ "oncoming_fist_light_skin_tone": "👊ðŸ»",
+ "oncoming_fist_medium-dark_skin_tone": "👊ðŸ¾",
+ "oncoming_fist_medium-light_skin_tone": "👊ðŸ¼",
+ "oncoming_fist_medium_skin_tone": "👊ðŸ½",
+ "oncoming_police_car": "🚔",
+ "oncoming_taxi": "🚖",
+ "one-piece_swimsuit": "🩱",
+ "one-thirty": "🕜",
+ "one_o’clock": "ðŸ•",
+ "onion": "🧅",
+ "open_book": "📖",
+ "open_file_folder": "📂",
+ "open_hands": "ðŸ‘",
+ "open_hands_dark_skin_tone": "ðŸ‘ðŸ¿",
+ "open_hands_light_skin_tone": "ðŸ‘ðŸ»",
+ "open_hands_medium-dark_skin_tone": "ðŸ‘ðŸ¾",
+ "open_hands_medium-light_skin_tone": "ðŸ‘ðŸ¼",
+ "open_hands_medium_skin_tone": "ðŸ‘ðŸ½",
+ "open_mailbox_with_lowered_flag": "📭",
+ "open_mailbox_with_raised_flag": "📬",
+ "optical_disk": "💿",
+ "orange_book": "📙",
+ "orange_circle": "🟠",
+ "orange_heart": "🧡",
+ "orange_square": "🟧",
+ "orangutan": "🦧",
+ "orthodox_cross": "☦",
+ "otter": "🦦",
+ "outbox_tray": "📤",
+ "owl": "🦉",
+ "ox": "ðŸ‚",
+ "oyster": "🦪",
+ "package": "📦",
+ "page_facing_up": "📄",
+ "page_with_curl": "📃",
+ "pager": "📟",
+ "paintbrush": "🖌",
+ "palm_tree": "🌴",
+ "palms_up_together": "🤲",
+ "palms_up_together_dark_skin_tone": "🤲ðŸ¿",
+ "palms_up_together_light_skin_tone": "🤲ðŸ»",
+ "palms_up_together_medium-dark_skin_tone": "🤲ðŸ¾",
+ "palms_up_together_medium-light_skin_tone": "🤲ðŸ¼",
+ "palms_up_together_medium_skin_tone": "🤲ðŸ½",
+ "pancakes": "🥞",
+ "panda_face": "ðŸ¼",
+ "paperclip": "📎",
+ "parrot": "🦜",
+ "part_alternation_mark": "〽",
+ "party_popper": "🎉",
+ "partying_face": "🥳",
+ "passenger_ship": "🛳",
+ "passport_control": "🛂",
+ "pause_button": "â¸",
+ "paw_prints": "ðŸ¾",
+ "peace_symbol": "☮",
+ "peach": "ðŸ‘",
+ "peacock": "🦚",
+ "peanuts": "🥜",
+ "pear": "ðŸ",
+ "pen": "🖊",
+ "pencil": "ðŸ“",
+ "penguin": "ðŸ§",
+ "pensive_face": "😔",
+ "people_holding_hands": "🧑\u200dðŸ¤\u200d🧑",
+ "people_with_bunny_ears": "👯",
+ "people_wrestling": "🤼",
+ "performing_arts": "🎭",
+ "persevering_face": "😣",
+ "person_biking": "🚴",
+ "person_biking_dark_skin_tone": "🚴ðŸ¿",
+ "person_biking_light_skin_tone": "🚴ðŸ»",
+ "person_biking_medium-dark_skin_tone": "🚴ðŸ¾",
+ "person_biking_medium-light_skin_tone": "🚴ðŸ¼",
+ "person_biking_medium_skin_tone": "🚴ðŸ½",
+ "person_bouncing_ball": "⛹",
+ "person_bouncing_ball_dark_skin_tone": "⛹ðŸ¿",
+ "person_bouncing_ball_light_skin_tone": "⛹ðŸ»",
+ "person_bouncing_ball_medium-dark_skin_tone": "⛹ðŸ¾",
+ "person_bouncing_ball_medium-light_skin_tone": "⛹ðŸ¼",
+ "person_bouncing_ball_medium_skin_tone": "⛹ðŸ½",
+ "person_bowing": "🙇",
+ "person_bowing_dark_skin_tone": "🙇ðŸ¿",
+ "person_bowing_light_skin_tone": "🙇ðŸ»",
+ "person_bowing_medium-dark_skin_tone": "🙇ðŸ¾",
+ "person_bowing_medium-light_skin_tone": "🙇ðŸ¼",
+ "person_bowing_medium_skin_tone": "🙇ðŸ½",
+ "person_cartwheeling": "🤸",
+ "person_cartwheeling_dark_skin_tone": "🤸ðŸ¿",
+ "person_cartwheeling_light_skin_tone": "🤸ðŸ»",
+ "person_cartwheeling_medium-dark_skin_tone": "🤸ðŸ¾",
+ "person_cartwheeling_medium-light_skin_tone": "🤸ðŸ¼",
+ "person_cartwheeling_medium_skin_tone": "🤸ðŸ½",
+ "person_climbing": "🧗",
+ "person_climbing_dark_skin_tone": "🧗ðŸ¿",
+ "person_climbing_light_skin_tone": "🧗ðŸ»",
+ "person_climbing_medium-dark_skin_tone": "🧗ðŸ¾",
+ "person_climbing_medium-light_skin_tone": "🧗ðŸ¼",
+ "person_climbing_medium_skin_tone": "🧗ðŸ½",
+ "person_facepalming": "🤦",
+ "person_facepalming_dark_skin_tone": "🤦ðŸ¿",
+ "person_facepalming_light_skin_tone": "🤦ðŸ»",
+ "person_facepalming_medium-dark_skin_tone": "🤦ðŸ¾",
+ "person_facepalming_medium-light_skin_tone": "🤦ðŸ¼",
+ "person_facepalming_medium_skin_tone": "🤦ðŸ½",
+ "person_fencing": "🤺",
+ "person_frowning": "ðŸ™",
+ "person_frowning_dark_skin_tone": "ðŸ™ðŸ¿",
+ "person_frowning_light_skin_tone": "ðŸ™ðŸ»",
+ "person_frowning_medium-dark_skin_tone": "ðŸ™ðŸ¾",
+ "person_frowning_medium-light_skin_tone": "ðŸ™ðŸ¼",
+ "person_frowning_medium_skin_tone": "ðŸ™ðŸ½",
+ "person_gesturing_no": "🙅",
+ "person_gesturing_no_dark_skin_tone": "🙅ðŸ¿",
+ "person_gesturing_no_light_skin_tone": "🙅ðŸ»",
+ "person_gesturing_no_medium-dark_skin_tone": "🙅ðŸ¾",
+ "person_gesturing_no_medium-light_skin_tone": "🙅ðŸ¼",
+ "person_gesturing_no_medium_skin_tone": "🙅ðŸ½",
+ "person_gesturing_ok": "🙆",
+ "person_gesturing_ok_dark_skin_tone": "🙆ðŸ¿",
+ "person_gesturing_ok_light_skin_tone": "🙆ðŸ»",
+ "person_gesturing_ok_medium-dark_skin_tone": "🙆ðŸ¾",
+ "person_gesturing_ok_medium-light_skin_tone": "🙆ðŸ¼",
+ "person_gesturing_ok_medium_skin_tone": "🙆ðŸ½",
+ "person_getting_haircut": "💇",
+ "person_getting_haircut_dark_skin_tone": "💇ðŸ¿",
+ "person_getting_haircut_light_skin_tone": "💇ðŸ»",
+ "person_getting_haircut_medium-dark_skin_tone": "💇ðŸ¾",
+ "person_getting_haircut_medium-light_skin_tone": "💇ðŸ¼",
+ "person_getting_haircut_medium_skin_tone": "💇ðŸ½",
+ "person_getting_massage": "💆",
+ "person_getting_massage_dark_skin_tone": "💆ðŸ¿",
+ "person_getting_massage_light_skin_tone": "💆ðŸ»",
+ "person_getting_massage_medium-dark_skin_tone": "💆ðŸ¾",
+ "person_getting_massage_medium-light_skin_tone": "💆ðŸ¼",
+ "person_getting_massage_medium_skin_tone": "💆ðŸ½",
+ "person_golfing": "ðŸŒ",
+ "person_golfing_dark_skin_tone": "ðŸŒðŸ¿",
+ "person_golfing_light_skin_tone": "ðŸŒðŸ»",
+ "person_golfing_medium-dark_skin_tone": "ðŸŒðŸ¾",
+ "person_golfing_medium-light_skin_tone": "ðŸŒðŸ¼",
+ "person_golfing_medium_skin_tone": "ðŸŒðŸ½",
+ "person_in_bed": "🛌",
+ "person_in_bed_dark_skin_tone": "🛌ðŸ¿",
+ "person_in_bed_light_skin_tone": "🛌ðŸ»",
+ "person_in_bed_medium-dark_skin_tone": "🛌ðŸ¾",
+ "person_in_bed_medium-light_skin_tone": "🛌ðŸ¼",
+ "person_in_bed_medium_skin_tone": "🛌ðŸ½",
+ "person_in_lotus_position": "🧘",
+ "person_in_lotus_position_dark_skin_tone": "🧘ðŸ¿",
+ "person_in_lotus_position_light_skin_tone": "🧘ðŸ»",
+ "person_in_lotus_position_medium-dark_skin_tone": "🧘ðŸ¾",
+ "person_in_lotus_position_medium-light_skin_tone": "🧘ðŸ¼",
+ "person_in_lotus_position_medium_skin_tone": "🧘ðŸ½",
+ "person_in_steamy_room": "🧖",
+ "person_in_steamy_room_dark_skin_tone": "🧖ðŸ¿",
+ "person_in_steamy_room_light_skin_tone": "🧖ðŸ»",
+ "person_in_steamy_room_medium-dark_skin_tone": "🧖ðŸ¾",
+ "person_in_steamy_room_medium-light_skin_tone": "🧖ðŸ¼",
+ "person_in_steamy_room_medium_skin_tone": "🧖ðŸ½",
+ "person_juggling": "🤹",
+ "person_juggling_dark_skin_tone": "🤹ðŸ¿",
+ "person_juggling_light_skin_tone": "🤹ðŸ»",
+ "person_juggling_medium-dark_skin_tone": "🤹ðŸ¾",
+ "person_juggling_medium-light_skin_tone": "🤹ðŸ¼",
+ "person_juggling_medium_skin_tone": "🤹ðŸ½",
+ "person_kneeling": "🧎",
+ "person_lifting_weights": "ðŸ‹",
+ "person_lifting_weights_dark_skin_tone": "ðŸ‹ðŸ¿",
+ "person_lifting_weights_light_skin_tone": "ðŸ‹ðŸ»",
+ "person_lifting_weights_medium-dark_skin_tone": "ðŸ‹ðŸ¾",
+ "person_lifting_weights_medium-light_skin_tone": "ðŸ‹ðŸ¼",
+ "person_lifting_weights_medium_skin_tone": "ðŸ‹ðŸ½",
+ "person_mountain_biking": "🚵",
+ "person_mountain_biking_dark_skin_tone": "🚵ðŸ¿",
+ "person_mountain_biking_light_skin_tone": "🚵ðŸ»",
+ "person_mountain_biking_medium-dark_skin_tone": "🚵ðŸ¾",
+ "person_mountain_biking_medium-light_skin_tone": "🚵ðŸ¼",
+ "person_mountain_biking_medium_skin_tone": "🚵ðŸ½",
+ "person_playing_handball": "🤾",
+ "person_playing_handball_dark_skin_tone": "🤾ðŸ¿",
+ "person_playing_handball_light_skin_tone": "🤾ðŸ»",
+ "person_playing_handball_medium-dark_skin_tone": "🤾ðŸ¾",
+ "person_playing_handball_medium-light_skin_tone": "🤾ðŸ¼",
+ "person_playing_handball_medium_skin_tone": "🤾ðŸ½",
+ "person_playing_water_polo": "🤽",
+ "person_playing_water_polo_dark_skin_tone": "🤽ðŸ¿",
+ "person_playing_water_polo_light_skin_tone": "🤽ðŸ»",
+ "person_playing_water_polo_medium-dark_skin_tone": "🤽ðŸ¾",
+ "person_playing_water_polo_medium-light_skin_tone": "🤽ðŸ¼",
+ "person_playing_water_polo_medium_skin_tone": "🤽ðŸ½",
+ "person_pouting": "🙎",
+ "person_pouting_dark_skin_tone": "🙎ðŸ¿",
+ "person_pouting_light_skin_tone": "🙎ðŸ»",
+ "person_pouting_medium-dark_skin_tone": "🙎ðŸ¾",
+ "person_pouting_medium-light_skin_tone": "🙎ðŸ¼",
+ "person_pouting_medium_skin_tone": "🙎ðŸ½",
+ "person_raising_hand": "🙋",
+ "person_raising_hand_dark_skin_tone": "🙋ðŸ¿",
+ "person_raising_hand_light_skin_tone": "🙋ðŸ»",
+ "person_raising_hand_medium-dark_skin_tone": "🙋ðŸ¾",
+ "person_raising_hand_medium-light_skin_tone": "🙋ðŸ¼",
+ "person_raising_hand_medium_skin_tone": "🙋ðŸ½",
+ "person_rowing_boat": "🚣",
+ "person_rowing_boat_dark_skin_tone": "🚣ðŸ¿",
+ "person_rowing_boat_light_skin_tone": "🚣ðŸ»",
+ "person_rowing_boat_medium-dark_skin_tone": "🚣ðŸ¾",
+ "person_rowing_boat_medium-light_skin_tone": "🚣ðŸ¼",
+ "person_rowing_boat_medium_skin_tone": "🚣ðŸ½",
+ "person_running": "ðŸƒ",
+ "person_running_dark_skin_tone": "ðŸƒðŸ¿",
+ "person_running_light_skin_tone": "ðŸƒðŸ»",
+ "person_running_medium-dark_skin_tone": "ðŸƒðŸ¾",
+ "person_running_medium-light_skin_tone": "ðŸƒðŸ¼",
+ "person_running_medium_skin_tone": "ðŸƒðŸ½",
+ "person_shrugging": "🤷",
+ "person_shrugging_dark_skin_tone": "🤷ðŸ¿",
+ "person_shrugging_light_skin_tone": "🤷ðŸ»",
+ "person_shrugging_medium-dark_skin_tone": "🤷ðŸ¾",
+ "person_shrugging_medium-light_skin_tone": "🤷ðŸ¼",
+ "person_shrugging_medium_skin_tone": "🤷ðŸ½",
+ "person_standing": "ðŸ§",
+ "person_surfing": "ðŸ„",
+ "person_surfing_dark_skin_tone": "ðŸ„ðŸ¿",
+ "person_surfing_light_skin_tone": "ðŸ„ðŸ»",
+ "person_surfing_medium-dark_skin_tone": "ðŸ„ðŸ¾",
+ "person_surfing_medium-light_skin_tone": "ðŸ„ðŸ¼",
+ "person_surfing_medium_skin_tone": "ðŸ„ðŸ½",
+ "person_swimming": "ðŸŠ",
+ "person_swimming_dark_skin_tone": "ðŸŠðŸ¿",
+ "person_swimming_light_skin_tone": "ðŸŠðŸ»",
+ "person_swimming_medium-dark_skin_tone": "ðŸŠðŸ¾",
+ "person_swimming_medium-light_skin_tone": "ðŸŠðŸ¼",
+ "person_swimming_medium_skin_tone": "ðŸŠðŸ½",
+ "person_taking_bath": "🛀",
+ "person_taking_bath_dark_skin_tone": "🛀ðŸ¿",
+ "person_taking_bath_light_skin_tone": "🛀ðŸ»",
+ "person_taking_bath_medium-dark_skin_tone": "🛀ðŸ¾",
+ "person_taking_bath_medium-light_skin_tone": "🛀ðŸ¼",
+ "person_taking_bath_medium_skin_tone": "🛀ðŸ½",
+ "person_tipping_hand": "ðŸ’",
+ "person_tipping_hand_dark_skin_tone": "ðŸ’ðŸ¿",
+ "person_tipping_hand_light_skin_tone": "ðŸ’ðŸ»",
+ "person_tipping_hand_medium-dark_skin_tone": "ðŸ’ðŸ¾",
+ "person_tipping_hand_medium-light_skin_tone": "ðŸ’ðŸ¼",
+ "person_tipping_hand_medium_skin_tone": "ðŸ’ðŸ½",
+ "person_walking": "🚶",
+ "person_walking_dark_skin_tone": "🚶ðŸ¿",
+ "person_walking_light_skin_tone": "🚶ðŸ»",
+ "person_walking_medium-dark_skin_tone": "🚶ðŸ¾",
+ "person_walking_medium-light_skin_tone": "🚶ðŸ¼",
+ "person_walking_medium_skin_tone": "🚶ðŸ½",
+ "person_wearing_turban": "👳",
+ "person_wearing_turban_dark_skin_tone": "👳ðŸ¿",
+ "person_wearing_turban_light_skin_tone": "👳ðŸ»",
+ "person_wearing_turban_medium-dark_skin_tone": "👳ðŸ¾",
+ "person_wearing_turban_medium-light_skin_tone": "👳ðŸ¼",
+ "person_wearing_turban_medium_skin_tone": "👳ðŸ½",
+ "petri_dish": "🧫",
+ "pick": "â›",
+ "pie": "🥧",
+ "pig": "ðŸ·",
+ "pig_face": "ðŸ·",
+ "pig_nose": "ðŸ½",
+ "pile_of_poo": "💩",
+ "pill": "💊",
+ "pinching_hand": "ðŸ¤",
+ "pine_decoration": "ðŸŽ",
+ "pineapple": "ðŸ",
+ "ping_pong": "ðŸ“",
+ "pirate_flag": "ðŸ´\u200d☠ï¸",
+ "pistol": "🔫",
+ "pizza": "ðŸ•",
+ "place_of_worship": "ðŸ›",
+ "play_button": "â–¶",
+ "play_or_pause_button": "â¯",
+ "pleading_face": "🥺",
+ "police_car": "🚓",
+ "police_car_light": "🚨",
+ "police_officer": "👮",
+ "police_officer_dark_skin_tone": "👮ðŸ¿",
+ "police_officer_light_skin_tone": "👮ðŸ»",
+ "police_officer_medium-dark_skin_tone": "👮ðŸ¾",
+ "police_officer_medium-light_skin_tone": "👮ðŸ¼",
+ "police_officer_medium_skin_tone": "👮ðŸ½",
+ "poodle": "ðŸ©",
+ "pool_8_ball": "🎱",
+ "popcorn": "ðŸ¿",
+ "post_office": "ðŸ£",
+ "postal_horn": "📯",
+ "postbox": "📮",
+ "pot_of_food": "ðŸ²",
+ "potable_water": "🚰",
+ "potato": "🥔",
+ "poultry_leg": "ðŸ—",
+ "pound_banknote": "💷",
+ "pouting_cat_face": "😾",
+ "pouting_face": "😡",
+ "prayer_beads": "📿",
+ "pregnant_woman": "🤰",
+ "pregnant_woman_dark_skin_tone": "🤰ðŸ¿",
+ "pregnant_woman_light_skin_tone": "🤰ðŸ»",
+ "pregnant_woman_medium-dark_skin_tone": "🤰ðŸ¾",
+ "pregnant_woman_medium-light_skin_tone": "🤰ðŸ¼",
+ "pregnant_woman_medium_skin_tone": "🤰ðŸ½",
+ "pretzel": "🥨",
+ "probing_cane": "🦯",
+ "prince": "🤴",
+ "prince_dark_skin_tone": "🤴ðŸ¿",
+ "prince_light_skin_tone": "🤴ðŸ»",
+ "prince_medium-dark_skin_tone": "🤴ðŸ¾",
+ "prince_medium-light_skin_tone": "🤴ðŸ¼",
+ "prince_medium_skin_tone": "🤴ðŸ½",
+ "princess": "👸",
+ "princess_dark_skin_tone": "👸ðŸ¿",
+ "princess_light_skin_tone": "👸ðŸ»",
+ "princess_medium-dark_skin_tone": "👸ðŸ¾",
+ "princess_medium-light_skin_tone": "👸ðŸ¼",
+ "princess_medium_skin_tone": "👸ðŸ½",
+ "printer": "🖨",
+ "prohibited": "🚫",
+ "purple_circle": "🟣",
+ "purple_heart": "💜",
+ "purple_square": "🟪",
+ "purse": "👛",
+ "pushpin": "📌",
+ "question_mark": "â“",
+ "rabbit": "ðŸ°",
+ "rabbit_face": "ðŸ°",
+ "raccoon": "ðŸ¦",
+ "racing_car": "ðŸŽ",
+ "radio": "📻",
+ "radio_button": "🔘",
+ "radioactive": "☢",
+ "railway_car": "🚃",
+ "railway_track": "🛤",
+ "rainbow": "🌈",
+ "rainbow_flag": "ðŸ³ï¸\u200d🌈",
+ "raised_back_of_hand": "🤚",
+ "raised_back_of_hand_dark_skin_tone": "🤚ðŸ¿",
+ "raised_back_of_hand_light_skin_tone": "🤚ðŸ»",
+ "raised_back_of_hand_medium-dark_skin_tone": "🤚ðŸ¾",
+ "raised_back_of_hand_medium-light_skin_tone": "🤚ðŸ¼",
+ "raised_back_of_hand_medium_skin_tone": "🤚ðŸ½",
+ "raised_fist": "✊",
+ "raised_fist_dark_skin_tone": "✊ðŸ¿",
+ "raised_fist_light_skin_tone": "✊ðŸ»",
+ "raised_fist_medium-dark_skin_tone": "✊ðŸ¾",
+ "raised_fist_medium-light_skin_tone": "✊ðŸ¼",
+ "raised_fist_medium_skin_tone": "✊ðŸ½",
+ "raised_hand": "✋",
+ "raised_hand_dark_skin_tone": "✋ðŸ¿",
+ "raised_hand_light_skin_tone": "✋ðŸ»",
+ "raised_hand_medium-dark_skin_tone": "✋ðŸ¾",
+ "raised_hand_medium-light_skin_tone": "✋ðŸ¼",
+ "raised_hand_medium_skin_tone": "✋ðŸ½",
+ "raising_hands": "🙌",
+ "raising_hands_dark_skin_tone": "🙌ðŸ¿",
+ "raising_hands_light_skin_tone": "🙌ðŸ»",
+ "raising_hands_medium-dark_skin_tone": "🙌ðŸ¾",
+ "raising_hands_medium-light_skin_tone": "🙌ðŸ¼",
+ "raising_hands_medium_skin_tone": "🙌ðŸ½",
+ "ram": "ðŸ",
+ "rat": "ðŸ€",
+ "razor": "🪒",
+ "ringed_planet": "ðŸª",
+ "receipt": "🧾",
+ "record_button": "âº",
+ "recycling_symbol": "â™»",
+ "red_apple": "ðŸŽ",
+ "red_circle": "🔴",
+ "red_envelope": "🧧",
+ "red_hair": "🦰",
+ "red-haired_man": "👨\u200d🦰",
+ "red-haired_woman": "👩\u200d🦰",
+ "red_heart": "â¤",
+ "red_paper_lantern": "ðŸ®",
+ "red_square": "🟥",
+ "red_triangle_pointed_down": "🔻",
+ "red_triangle_pointed_up": "🔺",
+ "registered": "®",
+ "relieved_face": "😌",
+ "reminder_ribbon": "🎗",
+ "repeat_button": "ðŸ”",
+ "repeat_single_button": "🔂",
+ "rescue_worker’s_helmet": "⛑",
+ "restroom": "🚻",
+ "reverse_button": "â—€",
+ "revolving_hearts": "💞",
+ "rhinoceros": "ðŸ¦",
+ "ribbon": "🎀",
+ "rice_ball": "ðŸ™",
+ "rice_cracker": "ðŸ˜",
+ "right-facing_fist": "🤜",
+ "right-facing_fist_dark_skin_tone": "🤜ðŸ¿",
+ "right-facing_fist_light_skin_tone": "🤜ðŸ»",
+ "right-facing_fist_medium-dark_skin_tone": "🤜ðŸ¾",
+ "right-facing_fist_medium-light_skin_tone": "🤜ðŸ¼",
+ "right-facing_fist_medium_skin_tone": "🤜ðŸ½",
+ "right_anger_bubble": "🗯",
+ "right_arrow": "âž¡",
+ "right_arrow_curving_down": "⤵",
+ "right_arrow_curving_left": "↩",
+ "right_arrow_curving_up": "⤴",
+ "ring": "ðŸ’",
+ "roasted_sweet_potato": "ðŸ ",
+ "robot_face": "🤖",
+ "rocket": "🚀",
+ "roll_of_paper": "🧻",
+ "rolled-up_newspaper": "🗞",
+ "roller_coaster": "🎢",
+ "rolling_on_the_floor_laughing": "🤣",
+ "rooster": "ðŸ“",
+ "rose": "🌹",
+ "rosette": "ðŸµ",
+ "round_pushpin": "ðŸ“",
+ "rugby_football": "ðŸ‰",
+ "running_shirt": "🎽",
+ "running_shoe": "👟",
+ "sad_but_relieved_face": "😥",
+ "safety_pin": "🧷",
+ "safety_vest": "🦺",
+ "salt": "🧂",
+ "sailboat": "⛵",
+ "sake": "ðŸ¶",
+ "sandwich": "🥪",
+ "sari": "🥻",
+ "satellite": "📡",
+ "satellite_antenna": "📡",
+ "sauropod": "🦕",
+ "saxophone": "🎷",
+ "scarf": "🧣",
+ "school": "ðŸ«",
+ "school_backpack": "🎒",
+ "scissors": "✂",
+ "scorpion": "🦂",
+ "scroll": "📜",
+ "seat": "💺",
+ "see-no-evil_monkey": "🙈",
+ "seedling": "🌱",
+ "selfie": "🤳",
+ "selfie_dark_skin_tone": "🤳ðŸ¿",
+ "selfie_light_skin_tone": "🤳ðŸ»",
+ "selfie_medium-dark_skin_tone": "🤳ðŸ¾",
+ "selfie_medium-light_skin_tone": "🤳ðŸ¼",
+ "selfie_medium_skin_tone": "🤳ðŸ½",
+ "service_dog": "ðŸ•\u200d🦺",
+ "seven-thirty": "🕢",
+ "seven_o’clock": "🕖",
+ "shallow_pan_of_food": "🥘",
+ "shamrock": "☘",
+ "shark": "🦈",
+ "shaved_ice": "ðŸ§",
+ "sheaf_of_rice": "🌾",
+ "shield": "🛡",
+ "shinto_shrine": "⛩",
+ "ship": "🚢",
+ "shooting_star": "🌠",
+ "shopping_bags": "ðŸ›",
+ "shopping_cart": "🛒",
+ "shortcake": "ðŸ°",
+ "shorts": "🩳",
+ "shower": "🚿",
+ "shrimp": "ðŸ¦",
+ "shuffle_tracks_button": "🔀",
+ "shushing_face": "🤫",
+ "sign_of_the_horns": "🤘",
+ "sign_of_the_horns_dark_skin_tone": "🤘ðŸ¿",
+ "sign_of_the_horns_light_skin_tone": "🤘ðŸ»",
+ "sign_of_the_horns_medium-dark_skin_tone": "🤘ðŸ¾",
+ "sign_of_the_horns_medium-light_skin_tone": "🤘ðŸ¼",
+ "sign_of_the_horns_medium_skin_tone": "🤘ðŸ½",
+ "six-thirty": "🕡",
+ "six_o’clock": "🕕",
+ "skateboard": "🛹",
+ "skier": "â›·",
+ "skis": "🎿",
+ "skull": "💀",
+ "skull_and_crossbones": "☠",
+ "skunk": "🦨",
+ "sled": "🛷",
+ "sleeping_face": "😴",
+ "sleepy_face": "😪",
+ "slightly_frowning_face": "ðŸ™",
+ "slightly_smiling_face": "🙂",
+ "slot_machine": "🎰",
+ "sloth": "🦥",
+ "small_airplane": "🛩",
+ "small_blue_diamond": "🔹",
+ "small_orange_diamond": "🔸",
+ "smiling_cat_face_with_heart-eyes": "😻",
+ "smiling_face": "☺",
+ "smiling_face_with_halo": "😇",
+ "smiling_face_with_3_hearts": "🥰",
+ "smiling_face_with_heart-eyes": "ðŸ˜",
+ "smiling_face_with_horns": "😈",
+ "smiling_face_with_smiling_eyes": "😊",
+ "smiling_face_with_sunglasses": "😎",
+ "smirking_face": "ðŸ˜",
+ "snail": "ðŸŒ",
+ "snake": "ðŸ",
+ "sneezing_face": "🤧",
+ "snow-capped_mountain": "ðŸ”",
+ "snowboarder": "ðŸ‚",
+ "snowboarder_dark_skin_tone": "ðŸ‚ðŸ¿",
+ "snowboarder_light_skin_tone": "ðŸ‚ðŸ»",
+ "snowboarder_medium-dark_skin_tone": "ðŸ‚ðŸ¾",
+ "snowboarder_medium-light_skin_tone": "ðŸ‚ðŸ¼",
+ "snowboarder_medium_skin_tone": "ðŸ‚ðŸ½",
+ "snowflake": "â„",
+ "snowman": "☃",
+ "snowman_without_snow": "⛄",
+ "soap": "🧼",
+ "soccer_ball": "âš½",
+ "socks": "🧦",
+ "softball": "🥎",
+ "soft_ice_cream": "ðŸ¦",
+ "spade_suit": "â™ ",
+ "spaghetti": "ðŸ",
+ "sparkle": "â‡",
+ "sparkler": "🎇",
+ "sparkles": "✨",
+ "sparkling_heart": "💖",
+ "speak-no-evil_monkey": "🙊",
+ "speaker_high_volume": "🔊",
+ "speaker_low_volume": "🔈",
+ "speaker_medium_volume": "🔉",
+ "speaking_head": "🗣",
+ "speech_balloon": "💬",
+ "speedboat": "🚤",
+ "spider": "🕷",
+ "spider_web": "🕸",
+ "spiral_calendar": "🗓",
+ "spiral_notepad": "🗒",
+ "spiral_shell": "ðŸš",
+ "spoon": "🥄",
+ "sponge": "🧽",
+ "sport_utility_vehicle": "🚙",
+ "sports_medal": "ðŸ…",
+ "spouting_whale": "ðŸ³",
+ "squid": "🦑",
+ "squinting_face_with_tongue": "ðŸ˜",
+ "stadium": "ðŸŸ",
+ "star-struck": "🤩",
+ "star_and_crescent": "☪",
+ "star_of_david": "✡",
+ "station": "🚉",
+ "steaming_bowl": "ðŸœ",
+ "stethoscope": "🩺",
+ "stop_button": "â¹",
+ "stop_sign": "🛑",
+ "stopwatch": "â±",
+ "straight_ruler": "ðŸ“",
+ "strawberry": "ðŸ“",
+ "studio_microphone": "🎙",
+ "stuffed_flatbread": "🥙",
+ "sun": "☀",
+ "sun_behind_cloud": "â›…",
+ "sun_behind_large_cloud": "🌥",
+ "sun_behind_rain_cloud": "🌦",
+ "sun_behind_small_cloud": "🌤",
+ "sun_with_face": "🌞",
+ "sunflower": "🌻",
+ "sunglasses": "😎",
+ "sunrise": "🌅",
+ "sunrise_over_mountains": "🌄",
+ "sunset": "🌇",
+ "superhero": "🦸",
+ "supervillain": "🦹",
+ "sushi": "ðŸ£",
+ "suspension_railway": "🚟",
+ "swan": "🦢",
+ "sweat_droplets": "💦",
+ "synagogue": "ðŸ•",
+ "syringe": "💉",
+ "t-shirt": "👕",
+ "taco": "🌮",
+ "takeout_box": "🥡",
+ "tanabata_tree": "🎋",
+ "tangerine": "ðŸŠ",
+ "taxi": "🚕",
+ "teacup_without_handle": "ðŸµ",
+ "tear-off_calendar": "📆",
+ "teddy_bear": "🧸",
+ "telephone": "☎",
+ "telephone_receiver": "📞",
+ "telescope": "🔭",
+ "television": "📺",
+ "ten-thirty": "🕥",
+ "ten_o’clock": "🕙",
+ "tennis": "🎾",
+ "tent": "⛺",
+ "test_tube": "🧪",
+ "thermometer": "🌡",
+ "thinking_face": "🤔",
+ "thought_balloon": "💭",
+ "thread": "🧵",
+ "three-thirty": "🕞",
+ "three_o’clock": "🕒",
+ "thumbs_down": "👎",
+ "thumbs_down_dark_skin_tone": "👎ðŸ¿",
+ "thumbs_down_light_skin_tone": "👎ðŸ»",
+ "thumbs_down_medium-dark_skin_tone": "👎ðŸ¾",
+ "thumbs_down_medium-light_skin_tone": "👎ðŸ¼",
+ "thumbs_down_medium_skin_tone": "👎ðŸ½",
+ "thumbs_up": "ðŸ‘",
+ "thumbs_up_dark_skin_tone": "ðŸ‘ðŸ¿",
+ "thumbs_up_light_skin_tone": "ðŸ‘ðŸ»",
+ "thumbs_up_medium-dark_skin_tone": "ðŸ‘ðŸ¾",
+ "thumbs_up_medium-light_skin_tone": "ðŸ‘ðŸ¼",
+ "thumbs_up_medium_skin_tone": "ðŸ‘ðŸ½",
+ "ticket": "🎫",
+ "tiger": "ðŸ¯",
+ "tiger_face": "ðŸ¯",
+ "timer_clock": "â²",
+ "tired_face": "😫",
+ "toolbox": "🧰",
+ "toilet": "🚽",
+ "tomato": "ðŸ…",
+ "tongue": "👅",
+ "tooth": "🦷",
+ "top_hat": "🎩",
+ "tornado": "🌪",
+ "trackball": "🖲",
+ "tractor": "🚜",
+ "trade_mark": "â„¢",
+ "train": "🚋",
+ "tram": "🚊",
+ "tram_car": "🚋",
+ "triangular_flag": "🚩",
+ "triangular_ruler": "ðŸ“",
+ "trident_emblem": "🔱",
+ "trolleybus": "🚎",
+ "trophy": "ðŸ†",
+ "tropical_drink": "ðŸ¹",
+ "tropical_fish": "ðŸ ",
+ "trumpet": "🎺",
+ "tulip": "🌷",
+ "tumbler_glass": "🥃",
+ "turtle": "ðŸ¢",
+ "twelve-thirty": "🕧",
+ "twelve_o’clock": "🕛",
+ "two-hump_camel": "ðŸ«",
+ "two-thirty": "ðŸ•",
+ "two_hearts": "💕",
+ "two_men_holding_hands": "👬",
+ "two_o’clock": "🕑",
+ "two_women_holding_hands": "👭",
+ "umbrella": "☂",
+ "umbrella_on_ground": "â›±",
+ "umbrella_with_rain_drops": "☔",
+ "unamused_face": "😒",
+ "unicorn_face": "🦄",
+ "unlocked": "🔓",
+ "up-down_arrow": "↕",
+ "up-left_arrow": "↖",
+ "up-right_arrow": "↗",
+ "up_arrow": "⬆",
+ "upside-down_face": "🙃",
+ "upwards_button": "🔼",
+ "vampire": "🧛",
+ "vampire_dark_skin_tone": "🧛ðŸ¿",
+ "vampire_light_skin_tone": "🧛ðŸ»",
+ "vampire_medium-dark_skin_tone": "🧛ðŸ¾",
+ "vampire_medium-light_skin_tone": "🧛ðŸ¼",
+ "vampire_medium_skin_tone": "🧛ðŸ½",
+ "vertical_traffic_light": "🚦",
+ "vibration_mode": "📳",
+ "victory_hand": "✌",
+ "victory_hand_dark_skin_tone": "✌ðŸ¿",
+ "victory_hand_light_skin_tone": "✌ðŸ»",
+ "victory_hand_medium-dark_skin_tone": "✌ðŸ¾",
+ "victory_hand_medium-light_skin_tone": "✌ðŸ¼",
+ "victory_hand_medium_skin_tone": "✌ðŸ½",
+ "video_camera": "📹",
+ "video_game": "🎮",
+ "videocassette": "📼",
+ "violin": "🎻",
+ "volcano": "🌋",
+ "volleyball": "ðŸ",
+ "vulcan_salute": "🖖",
+ "vulcan_salute_dark_skin_tone": "🖖ðŸ¿",
+ "vulcan_salute_light_skin_tone": "🖖ðŸ»",
+ "vulcan_salute_medium-dark_skin_tone": "🖖ðŸ¾",
+ "vulcan_salute_medium-light_skin_tone": "🖖ðŸ¼",
+ "vulcan_salute_medium_skin_tone": "🖖ðŸ½",
+ "waffle": "🧇",
+ "waning_crescent_moon": "🌘",
+ "waning_gibbous_moon": "🌖",
+ "warning": "âš ",
+ "wastebasket": "🗑",
+ "watch": "⌚",
+ "water_buffalo": "ðŸƒ",
+ "water_closet": "🚾",
+ "water_wave": "🌊",
+ "watermelon": "ðŸ‰",
+ "waving_hand": "👋",
+ "waving_hand_dark_skin_tone": "👋ðŸ¿",
+ "waving_hand_light_skin_tone": "👋ðŸ»",
+ "waving_hand_medium-dark_skin_tone": "👋ðŸ¾",
+ "waving_hand_medium-light_skin_tone": "👋ðŸ¼",
+ "waving_hand_medium_skin_tone": "👋ðŸ½",
+ "wavy_dash": "〰",
+ "waxing_crescent_moon": "🌒",
+ "waxing_gibbous_moon": "🌔",
+ "weary_cat_face": "🙀",
+ "weary_face": "😩",
+ "wedding": "💒",
+ "whale": "ðŸ³",
+ "wheel_of_dharma": "☸",
+ "wheelchair_symbol": "♿",
+ "white_circle": "⚪",
+ "white_exclamation_mark": "â•",
+ "white_flag": "ðŸ³",
+ "white_flower": "💮",
+ "white_hair": "🦳",
+ "white-haired_man": "👨\u200d🦳",
+ "white-haired_woman": "👩\u200d🦳",
+ "white_heart": "ðŸ¤",
+ "white_heavy_check_mark": "✅",
+ "white_large_square": "⬜",
+ "white_medium-small_square": "â—½",
+ "white_medium_square": "â—»",
+ "white_medium_star": "â­",
+ "white_question_mark": "â”",
+ "white_small_square": "â–«",
+ "white_square_button": "🔳",
+ "wilted_flower": "🥀",
+ "wind_chime": "ðŸŽ",
+ "wind_face": "🌬",
+ "wine_glass": "ðŸ·",
+ "winking_face": "😉",
+ "winking_face_with_tongue": "😜",
+ "wolf_face": "ðŸº",
+ "woman": "👩",
+ "woman_artist": "👩\u200d🎨",
+ "woman_artist_dark_skin_tone": "👩ðŸ¿\u200d🎨",
+ "woman_artist_light_skin_tone": "👩ðŸ»\u200d🎨",
+ "woman_artist_medium-dark_skin_tone": "👩ðŸ¾\u200d🎨",
+ "woman_artist_medium-light_skin_tone": "👩ðŸ¼\u200d🎨",
+ "woman_artist_medium_skin_tone": "👩ðŸ½\u200d🎨",
+ "woman_astronaut": "👩\u200d🚀",
+ "woman_astronaut_dark_skin_tone": "👩ðŸ¿\u200d🚀",
+ "woman_astronaut_light_skin_tone": "👩ðŸ»\u200d🚀",
+ "woman_astronaut_medium-dark_skin_tone": "👩ðŸ¾\u200d🚀",
+ "woman_astronaut_medium-light_skin_tone": "👩ðŸ¼\u200d🚀",
+ "woman_astronaut_medium_skin_tone": "👩ðŸ½\u200d🚀",
+ "woman_biking": "🚴\u200d♀ï¸",
+ "woman_biking_dark_skin_tone": "🚴ðŸ¿\u200d♀ï¸",
+ "woman_biking_light_skin_tone": "🚴ðŸ»\u200d♀ï¸",
+ "woman_biking_medium-dark_skin_tone": "🚴ðŸ¾\u200d♀ï¸",
+ "woman_biking_medium-light_skin_tone": "🚴ðŸ¼\u200d♀ï¸",
+ "woman_biking_medium_skin_tone": "🚴ðŸ½\u200d♀ï¸",
+ "woman_bouncing_ball": "⛹ï¸\u200d♀ï¸",
+ "woman_bouncing_ball_dark_skin_tone": "⛹ðŸ¿\u200d♀ï¸",
+ "woman_bouncing_ball_light_skin_tone": "⛹ðŸ»\u200d♀ï¸",
+ "woman_bouncing_ball_medium-dark_skin_tone": "⛹ðŸ¾\u200d♀ï¸",
+ "woman_bouncing_ball_medium-light_skin_tone": "⛹ðŸ¼\u200d♀ï¸",
+ "woman_bouncing_ball_medium_skin_tone": "⛹ðŸ½\u200d♀ï¸",
+ "woman_bowing": "🙇\u200d♀ï¸",
+ "woman_bowing_dark_skin_tone": "🙇ðŸ¿\u200d♀ï¸",
+ "woman_bowing_light_skin_tone": "🙇ðŸ»\u200d♀ï¸",
+ "woman_bowing_medium-dark_skin_tone": "🙇ðŸ¾\u200d♀ï¸",
+ "woman_bowing_medium-light_skin_tone": "🙇ðŸ¼\u200d♀ï¸",
+ "woman_bowing_medium_skin_tone": "🙇ðŸ½\u200d♀ï¸",
+ "woman_cartwheeling": "🤸\u200d♀ï¸",
+ "woman_cartwheeling_dark_skin_tone": "🤸ðŸ¿\u200d♀ï¸",
+ "woman_cartwheeling_light_skin_tone": "🤸ðŸ»\u200d♀ï¸",
+ "woman_cartwheeling_medium-dark_skin_tone": "🤸ðŸ¾\u200d♀ï¸",
+ "woman_cartwheeling_medium-light_skin_tone": "🤸ðŸ¼\u200d♀ï¸",
+ "woman_cartwheeling_medium_skin_tone": "🤸ðŸ½\u200d♀ï¸",
+ "woman_climbing": "🧗\u200d♀ï¸",
+ "woman_climbing_dark_skin_tone": "🧗ðŸ¿\u200d♀ï¸",
+ "woman_climbing_light_skin_tone": "🧗ðŸ»\u200d♀ï¸",
+ "woman_climbing_medium-dark_skin_tone": "🧗ðŸ¾\u200d♀ï¸",
+ "woman_climbing_medium-light_skin_tone": "🧗ðŸ¼\u200d♀ï¸",
+ "woman_climbing_medium_skin_tone": "🧗ðŸ½\u200d♀ï¸",
+ "woman_construction_worker": "👷\u200d♀ï¸",
+ "woman_construction_worker_dark_skin_tone": "👷ðŸ¿\u200d♀ï¸",
+ "woman_construction_worker_light_skin_tone": "👷ðŸ»\u200d♀ï¸",
+ "woman_construction_worker_medium-dark_skin_tone": "👷ðŸ¾\u200d♀ï¸",
+ "woman_construction_worker_medium-light_skin_tone": "👷ðŸ¼\u200d♀ï¸",
+ "woman_construction_worker_medium_skin_tone": "👷ðŸ½\u200d♀ï¸",
+ "woman_cook": "👩\u200dðŸ³",
+ "woman_cook_dark_skin_tone": "👩ðŸ¿\u200dðŸ³",
+ "woman_cook_light_skin_tone": "👩ðŸ»\u200dðŸ³",
+ "woman_cook_medium-dark_skin_tone": "👩ðŸ¾\u200dðŸ³",
+ "woman_cook_medium-light_skin_tone": "👩ðŸ¼\u200dðŸ³",
+ "woman_cook_medium_skin_tone": "👩ðŸ½\u200dðŸ³",
+ "woman_dancing": "💃",
+ "woman_dancing_dark_skin_tone": "💃ðŸ¿",
+ "woman_dancing_light_skin_tone": "💃ðŸ»",
+ "woman_dancing_medium-dark_skin_tone": "💃ðŸ¾",
+ "woman_dancing_medium-light_skin_tone": "💃ðŸ¼",
+ "woman_dancing_medium_skin_tone": "💃ðŸ½",
+ "woman_dark_skin_tone": "👩ðŸ¿",
+ "woman_detective": "🕵ï¸\u200d♀ï¸",
+ "woman_detective_dark_skin_tone": "🕵ðŸ¿\u200d♀ï¸",
+ "woman_detective_light_skin_tone": "🕵ðŸ»\u200d♀ï¸",
+ "woman_detective_medium-dark_skin_tone": "🕵ðŸ¾\u200d♀ï¸",
+ "woman_detective_medium-light_skin_tone": "🕵ðŸ¼\u200d♀ï¸",
+ "woman_detective_medium_skin_tone": "🕵ðŸ½\u200d♀ï¸",
+ "woman_elf": "ðŸ§\u200d♀ï¸",
+ "woman_elf_dark_skin_tone": "ðŸ§ðŸ¿\u200d♀ï¸",
+ "woman_elf_light_skin_tone": "ðŸ§ðŸ»\u200d♀ï¸",
+ "woman_elf_medium-dark_skin_tone": "ðŸ§ðŸ¾\u200d♀ï¸",
+ "woman_elf_medium-light_skin_tone": "ðŸ§ðŸ¼\u200d♀ï¸",
+ "woman_elf_medium_skin_tone": "ðŸ§ðŸ½\u200d♀ï¸",
+ "woman_facepalming": "🤦\u200d♀ï¸",
+ "woman_facepalming_dark_skin_tone": "🤦ðŸ¿\u200d♀ï¸",
+ "woman_facepalming_light_skin_tone": "🤦ðŸ»\u200d♀ï¸",
+ "woman_facepalming_medium-dark_skin_tone": "🤦ðŸ¾\u200d♀ï¸",
+ "woman_facepalming_medium-light_skin_tone": "🤦ðŸ¼\u200d♀ï¸",
+ "woman_facepalming_medium_skin_tone": "🤦ðŸ½\u200d♀ï¸",
+ "woman_factory_worker": "👩\u200dðŸ­",
+ "woman_factory_worker_dark_skin_tone": "👩ðŸ¿\u200dðŸ­",
+ "woman_factory_worker_light_skin_tone": "👩ðŸ»\u200dðŸ­",
+ "woman_factory_worker_medium-dark_skin_tone": "👩ðŸ¾\u200dðŸ­",
+ "woman_factory_worker_medium-light_skin_tone": "👩ðŸ¼\u200dðŸ­",
+ "woman_factory_worker_medium_skin_tone": "👩ðŸ½\u200dðŸ­",
+ "woman_fairy": "🧚\u200d♀ï¸",
+ "woman_fairy_dark_skin_tone": "🧚ðŸ¿\u200d♀ï¸",
+ "woman_fairy_light_skin_tone": "🧚ðŸ»\u200d♀ï¸",
+ "woman_fairy_medium-dark_skin_tone": "🧚ðŸ¾\u200d♀ï¸",
+ "woman_fairy_medium-light_skin_tone": "🧚ðŸ¼\u200d♀ï¸",
+ "woman_fairy_medium_skin_tone": "🧚ðŸ½\u200d♀ï¸",
+ "woman_farmer": "👩\u200d🌾",
+ "woman_farmer_dark_skin_tone": "👩ðŸ¿\u200d🌾",
+ "woman_farmer_light_skin_tone": "👩ðŸ»\u200d🌾",
+ "woman_farmer_medium-dark_skin_tone": "👩ðŸ¾\u200d🌾",
+ "woman_farmer_medium-light_skin_tone": "👩ðŸ¼\u200d🌾",
+ "woman_farmer_medium_skin_tone": "👩ðŸ½\u200d🌾",
+ "woman_firefighter": "👩\u200d🚒",
+ "woman_firefighter_dark_skin_tone": "👩ðŸ¿\u200d🚒",
+ "woman_firefighter_light_skin_tone": "👩ðŸ»\u200d🚒",
+ "woman_firefighter_medium-dark_skin_tone": "👩ðŸ¾\u200d🚒",
+ "woman_firefighter_medium-light_skin_tone": "👩ðŸ¼\u200d🚒",
+ "woman_firefighter_medium_skin_tone": "👩ðŸ½\u200d🚒",
+ "woman_frowning": "ðŸ™\u200d♀ï¸",
+ "woman_frowning_dark_skin_tone": "ðŸ™ðŸ¿\u200d♀ï¸",
+ "woman_frowning_light_skin_tone": "ðŸ™ðŸ»\u200d♀ï¸",
+ "woman_frowning_medium-dark_skin_tone": "ðŸ™ðŸ¾\u200d♀ï¸",
+ "woman_frowning_medium-light_skin_tone": "ðŸ™ðŸ¼\u200d♀ï¸",
+ "woman_frowning_medium_skin_tone": "ðŸ™ðŸ½\u200d♀ï¸",
+ "woman_genie": "🧞\u200d♀ï¸",
+ "woman_gesturing_no": "🙅\u200d♀ï¸",
+ "woman_gesturing_no_dark_skin_tone": "🙅ðŸ¿\u200d♀ï¸",
+ "woman_gesturing_no_light_skin_tone": "🙅ðŸ»\u200d♀ï¸",
+ "woman_gesturing_no_medium-dark_skin_tone": "🙅ðŸ¾\u200d♀ï¸",
+ "woman_gesturing_no_medium-light_skin_tone": "🙅ðŸ¼\u200d♀ï¸",
+ "woman_gesturing_no_medium_skin_tone": "🙅ðŸ½\u200d♀ï¸",
+ "woman_gesturing_ok": "🙆\u200d♀ï¸",
+ "woman_gesturing_ok_dark_skin_tone": "🙆ðŸ¿\u200d♀ï¸",
+ "woman_gesturing_ok_light_skin_tone": "🙆ðŸ»\u200d♀ï¸",
+ "woman_gesturing_ok_medium-dark_skin_tone": "🙆ðŸ¾\u200d♀ï¸",
+ "woman_gesturing_ok_medium-light_skin_tone": "🙆ðŸ¼\u200d♀ï¸",
+ "woman_gesturing_ok_medium_skin_tone": "🙆ðŸ½\u200d♀ï¸",
+ "woman_getting_haircut": "💇\u200d♀ï¸",
+ "woman_getting_haircut_dark_skin_tone": "💇ðŸ¿\u200d♀ï¸",
+ "woman_getting_haircut_light_skin_tone": "💇ðŸ»\u200d♀ï¸",
+ "woman_getting_haircut_medium-dark_skin_tone": "💇ðŸ¾\u200d♀ï¸",
+ "woman_getting_haircut_medium-light_skin_tone": "💇ðŸ¼\u200d♀ï¸",
+ "woman_getting_haircut_medium_skin_tone": "💇ðŸ½\u200d♀ï¸",
+ "woman_getting_massage": "💆\u200d♀ï¸",
+ "woman_getting_massage_dark_skin_tone": "💆ðŸ¿\u200d♀ï¸",
+ "woman_getting_massage_light_skin_tone": "💆ðŸ»\u200d♀ï¸",
+ "woman_getting_massage_medium-dark_skin_tone": "💆ðŸ¾\u200d♀ï¸",
+ "woman_getting_massage_medium-light_skin_tone": "💆ðŸ¼\u200d♀ï¸",
+ "woman_getting_massage_medium_skin_tone": "💆ðŸ½\u200d♀ï¸",
+ "woman_golfing": "ðŸŒï¸\u200d♀ï¸",
+ "woman_golfing_dark_skin_tone": "ðŸŒðŸ¿\u200d♀ï¸",
+ "woman_golfing_light_skin_tone": "ðŸŒðŸ»\u200d♀ï¸",
+ "woman_golfing_medium-dark_skin_tone": "ðŸŒðŸ¾\u200d♀ï¸",
+ "woman_golfing_medium-light_skin_tone": "ðŸŒðŸ¼\u200d♀ï¸",
+ "woman_golfing_medium_skin_tone": "ðŸŒðŸ½\u200d♀ï¸",
+ "woman_guard": "💂\u200d♀ï¸",
+ "woman_guard_dark_skin_tone": "💂ðŸ¿\u200d♀ï¸",
+ "woman_guard_light_skin_tone": "💂ðŸ»\u200d♀ï¸",
+ "woman_guard_medium-dark_skin_tone": "💂ðŸ¾\u200d♀ï¸",
+ "woman_guard_medium-light_skin_tone": "💂ðŸ¼\u200d♀ï¸",
+ "woman_guard_medium_skin_tone": "💂ðŸ½\u200d♀ï¸",
+ "woman_health_worker": "👩\u200dâš•ï¸",
+ "woman_health_worker_dark_skin_tone": "👩ðŸ¿\u200dâš•ï¸",
+ "woman_health_worker_light_skin_tone": "👩ðŸ»\u200dâš•ï¸",
+ "woman_health_worker_medium-dark_skin_tone": "👩ðŸ¾\u200dâš•ï¸",
+ "woman_health_worker_medium-light_skin_tone": "👩ðŸ¼\u200dâš•ï¸",
+ "woman_health_worker_medium_skin_tone": "👩ðŸ½\u200dâš•ï¸",
+ "woman_in_lotus_position": "🧘\u200d♀ï¸",
+ "woman_in_lotus_position_dark_skin_tone": "🧘ðŸ¿\u200d♀ï¸",
+ "woman_in_lotus_position_light_skin_tone": "🧘ðŸ»\u200d♀ï¸",
+ "woman_in_lotus_position_medium-dark_skin_tone": "🧘ðŸ¾\u200d♀ï¸",
+ "woman_in_lotus_position_medium-light_skin_tone": "🧘ðŸ¼\u200d♀ï¸",
+ "woman_in_lotus_position_medium_skin_tone": "🧘ðŸ½\u200d♀ï¸",
+ "woman_in_manual_wheelchair": "👩\u200d🦽",
+ "woman_in_motorized_wheelchair": "👩\u200d🦼",
+ "woman_in_steamy_room": "🧖\u200d♀ï¸",
+ "woman_in_steamy_room_dark_skin_tone": "🧖ðŸ¿\u200d♀ï¸",
+ "woman_in_steamy_room_light_skin_tone": "🧖ðŸ»\u200d♀ï¸",
+ "woman_in_steamy_room_medium-dark_skin_tone": "🧖ðŸ¾\u200d♀ï¸",
+ "woman_in_steamy_room_medium-light_skin_tone": "🧖ðŸ¼\u200d♀ï¸",
+ "woman_in_steamy_room_medium_skin_tone": "🧖ðŸ½\u200d♀ï¸",
+ "woman_judge": "👩\u200dâš–ï¸",
+ "woman_judge_dark_skin_tone": "👩ðŸ¿\u200dâš–ï¸",
+ "woman_judge_light_skin_tone": "👩ðŸ»\u200dâš–ï¸",
+ "woman_judge_medium-dark_skin_tone": "👩ðŸ¾\u200dâš–ï¸",
+ "woman_judge_medium-light_skin_tone": "👩ðŸ¼\u200dâš–ï¸",
+ "woman_judge_medium_skin_tone": "👩ðŸ½\u200dâš–ï¸",
+ "woman_juggling": "🤹\u200d♀ï¸",
+ "woman_juggling_dark_skin_tone": "🤹ðŸ¿\u200d♀ï¸",
+ "woman_juggling_light_skin_tone": "🤹ðŸ»\u200d♀ï¸",
+ "woman_juggling_medium-dark_skin_tone": "🤹ðŸ¾\u200d♀ï¸",
+ "woman_juggling_medium-light_skin_tone": "🤹ðŸ¼\u200d♀ï¸",
+ "woman_juggling_medium_skin_tone": "🤹ðŸ½\u200d♀ï¸",
+ "woman_lifting_weights": "ðŸ‹ï¸\u200d♀ï¸",
+ "woman_lifting_weights_dark_skin_tone": "ðŸ‹ðŸ¿\u200d♀ï¸",
+ "woman_lifting_weights_light_skin_tone": "ðŸ‹ðŸ»\u200d♀ï¸",
+ "woman_lifting_weights_medium-dark_skin_tone": "ðŸ‹ðŸ¾\u200d♀ï¸",
+ "woman_lifting_weights_medium-light_skin_tone": "ðŸ‹ðŸ¼\u200d♀ï¸",
+ "woman_lifting_weights_medium_skin_tone": "ðŸ‹ðŸ½\u200d♀ï¸",
+ "woman_light_skin_tone": "👩ðŸ»",
+ "woman_mage": "🧙\u200d♀ï¸",
+ "woman_mage_dark_skin_tone": "🧙ðŸ¿\u200d♀ï¸",
+ "woman_mage_light_skin_tone": "🧙ðŸ»\u200d♀ï¸",
+ "woman_mage_medium-dark_skin_tone": "🧙ðŸ¾\u200d♀ï¸",
+ "woman_mage_medium-light_skin_tone": "🧙ðŸ¼\u200d♀ï¸",
+ "woman_mage_medium_skin_tone": "🧙ðŸ½\u200d♀ï¸",
+ "woman_mechanic": "👩\u200d🔧",
+ "woman_mechanic_dark_skin_tone": "👩ðŸ¿\u200d🔧",
+ "woman_mechanic_light_skin_tone": "👩ðŸ»\u200d🔧",
+ "woman_mechanic_medium-dark_skin_tone": "👩ðŸ¾\u200d🔧",
+ "woman_mechanic_medium-light_skin_tone": "👩ðŸ¼\u200d🔧",
+ "woman_mechanic_medium_skin_tone": "👩ðŸ½\u200d🔧",
+ "woman_medium-dark_skin_tone": "👩ðŸ¾",
+ "woman_medium-light_skin_tone": "👩ðŸ¼",
+ "woman_medium_skin_tone": "👩ðŸ½",
+ "woman_mountain_biking": "🚵\u200d♀ï¸",
+ "woman_mountain_biking_dark_skin_tone": "🚵ðŸ¿\u200d♀ï¸",
+ "woman_mountain_biking_light_skin_tone": "🚵ðŸ»\u200d♀ï¸",
+ "woman_mountain_biking_medium-dark_skin_tone": "🚵ðŸ¾\u200d♀ï¸",
+ "woman_mountain_biking_medium-light_skin_tone": "🚵ðŸ¼\u200d♀ï¸",
+ "woman_mountain_biking_medium_skin_tone": "🚵ðŸ½\u200d♀ï¸",
+ "woman_office_worker": "👩\u200d💼",
+ "woman_office_worker_dark_skin_tone": "👩ðŸ¿\u200d💼",
+ "woman_office_worker_light_skin_tone": "👩ðŸ»\u200d💼",
+ "woman_office_worker_medium-dark_skin_tone": "👩ðŸ¾\u200d💼",
+ "woman_office_worker_medium-light_skin_tone": "👩ðŸ¼\u200d💼",
+ "woman_office_worker_medium_skin_tone": "👩ðŸ½\u200d💼",
+ "woman_pilot": "👩\u200d✈ï¸",
+ "woman_pilot_dark_skin_tone": "👩ðŸ¿\u200d✈ï¸",
+ "woman_pilot_light_skin_tone": "👩ðŸ»\u200d✈ï¸",
+ "woman_pilot_medium-dark_skin_tone": "👩ðŸ¾\u200d✈ï¸",
+ "woman_pilot_medium-light_skin_tone": "👩ðŸ¼\u200d✈ï¸",
+ "woman_pilot_medium_skin_tone": "👩ðŸ½\u200d✈ï¸",
+ "woman_playing_handball": "🤾\u200d♀ï¸",
+ "woman_playing_handball_dark_skin_tone": "🤾ðŸ¿\u200d♀ï¸",
+ "woman_playing_handball_light_skin_tone": "🤾ðŸ»\u200d♀ï¸",
+ "woman_playing_handball_medium-dark_skin_tone": "🤾ðŸ¾\u200d♀ï¸",
+ "woman_playing_handball_medium-light_skin_tone": "🤾ðŸ¼\u200d♀ï¸",
+ "woman_playing_handball_medium_skin_tone": "🤾ðŸ½\u200d♀ï¸",
+ "woman_playing_water_polo": "🤽\u200d♀ï¸",
+ "woman_playing_water_polo_dark_skin_tone": "🤽ðŸ¿\u200d♀ï¸",
+ "woman_playing_water_polo_light_skin_tone": "🤽ðŸ»\u200d♀ï¸",
+ "woman_playing_water_polo_medium-dark_skin_tone": "🤽ðŸ¾\u200d♀ï¸",
+ "woman_playing_water_polo_medium-light_skin_tone": "🤽ðŸ¼\u200d♀ï¸",
+ "woman_playing_water_polo_medium_skin_tone": "🤽ðŸ½\u200d♀ï¸",
+ "woman_police_officer": "👮\u200d♀ï¸",
+ "woman_police_officer_dark_skin_tone": "👮ðŸ¿\u200d♀ï¸",
+ "woman_police_officer_light_skin_tone": "👮ðŸ»\u200d♀ï¸",
+ "woman_police_officer_medium-dark_skin_tone": "👮ðŸ¾\u200d♀ï¸",
+ "woman_police_officer_medium-light_skin_tone": "👮ðŸ¼\u200d♀ï¸",
+ "woman_police_officer_medium_skin_tone": "👮ðŸ½\u200d♀ï¸",
+ "woman_pouting": "🙎\u200d♀ï¸",
+ "woman_pouting_dark_skin_tone": "🙎ðŸ¿\u200d♀ï¸",
+ "woman_pouting_light_skin_tone": "🙎ðŸ»\u200d♀ï¸",
+ "woman_pouting_medium-dark_skin_tone": "🙎ðŸ¾\u200d♀ï¸",
+ "woman_pouting_medium-light_skin_tone": "🙎ðŸ¼\u200d♀ï¸",
+ "woman_pouting_medium_skin_tone": "🙎ðŸ½\u200d♀ï¸",
+ "woman_raising_hand": "🙋\u200d♀ï¸",
+ "woman_raising_hand_dark_skin_tone": "🙋ðŸ¿\u200d♀ï¸",
+ "woman_raising_hand_light_skin_tone": "🙋ðŸ»\u200d♀ï¸",
+ "woman_raising_hand_medium-dark_skin_tone": "🙋ðŸ¾\u200d♀ï¸",
+ "woman_raising_hand_medium-light_skin_tone": "🙋ðŸ¼\u200d♀ï¸",
+ "woman_raising_hand_medium_skin_tone": "🙋ðŸ½\u200d♀ï¸",
+ "woman_rowing_boat": "🚣\u200d♀ï¸",
+ "woman_rowing_boat_dark_skin_tone": "🚣ðŸ¿\u200d♀ï¸",
+ "woman_rowing_boat_light_skin_tone": "🚣ðŸ»\u200d♀ï¸",
+ "woman_rowing_boat_medium-dark_skin_tone": "🚣ðŸ¾\u200d♀ï¸",
+ "woman_rowing_boat_medium-light_skin_tone": "🚣ðŸ¼\u200d♀ï¸",
+ "woman_rowing_boat_medium_skin_tone": "🚣ðŸ½\u200d♀ï¸",
+ "woman_running": "ðŸƒ\u200d♀ï¸",
+ "woman_running_dark_skin_tone": "ðŸƒðŸ¿\u200d♀ï¸",
+ "woman_running_light_skin_tone": "ðŸƒðŸ»\u200d♀ï¸",
+ "woman_running_medium-dark_skin_tone": "ðŸƒðŸ¾\u200d♀ï¸",
+ "woman_running_medium-light_skin_tone": "ðŸƒðŸ¼\u200d♀ï¸",
+ "woman_running_medium_skin_tone": "ðŸƒðŸ½\u200d♀ï¸",
+ "woman_scientist": "👩\u200d🔬",
+ "woman_scientist_dark_skin_tone": "👩ðŸ¿\u200d🔬",
+ "woman_scientist_light_skin_tone": "👩ðŸ»\u200d🔬",
+ "woman_scientist_medium-dark_skin_tone": "👩ðŸ¾\u200d🔬",
+ "woman_scientist_medium-light_skin_tone": "👩ðŸ¼\u200d🔬",
+ "woman_scientist_medium_skin_tone": "👩ðŸ½\u200d🔬",
+ "woman_shrugging": "🤷\u200d♀ï¸",
+ "woman_shrugging_dark_skin_tone": "🤷ðŸ¿\u200d♀ï¸",
+ "woman_shrugging_light_skin_tone": "🤷ðŸ»\u200d♀ï¸",
+ "woman_shrugging_medium-dark_skin_tone": "🤷ðŸ¾\u200d♀ï¸",
+ "woman_shrugging_medium-light_skin_tone": "🤷ðŸ¼\u200d♀ï¸",
+ "woman_shrugging_medium_skin_tone": "🤷ðŸ½\u200d♀ï¸",
+ "woman_singer": "👩\u200d🎤",
+ "woman_singer_dark_skin_tone": "👩ðŸ¿\u200d🎤",
+ "woman_singer_light_skin_tone": "👩ðŸ»\u200d🎤",
+ "woman_singer_medium-dark_skin_tone": "👩ðŸ¾\u200d🎤",
+ "woman_singer_medium-light_skin_tone": "👩ðŸ¼\u200d🎤",
+ "woman_singer_medium_skin_tone": "👩ðŸ½\u200d🎤",
+ "woman_student": "👩\u200d🎓",
+ "woman_student_dark_skin_tone": "👩ðŸ¿\u200d🎓",
+ "woman_student_light_skin_tone": "👩ðŸ»\u200d🎓",
+ "woman_student_medium-dark_skin_tone": "👩ðŸ¾\u200d🎓",
+ "woman_student_medium-light_skin_tone": "👩ðŸ¼\u200d🎓",
+ "woman_student_medium_skin_tone": "👩ðŸ½\u200d🎓",
+ "woman_surfing": "ðŸ„\u200d♀ï¸",
+ "woman_surfing_dark_skin_tone": "ðŸ„ðŸ¿\u200d♀ï¸",
+ "woman_surfing_light_skin_tone": "ðŸ„ðŸ»\u200d♀ï¸",
+ "woman_surfing_medium-dark_skin_tone": "ðŸ„ðŸ¾\u200d♀ï¸",
+ "woman_surfing_medium-light_skin_tone": "ðŸ„ðŸ¼\u200d♀ï¸",
+ "woman_surfing_medium_skin_tone": "ðŸ„ðŸ½\u200d♀ï¸",
+ "woman_swimming": "ðŸŠ\u200d♀ï¸",
+ "woman_swimming_dark_skin_tone": "ðŸŠðŸ¿\u200d♀ï¸",
+ "woman_swimming_light_skin_tone": "ðŸŠðŸ»\u200d♀ï¸",
+ "woman_swimming_medium-dark_skin_tone": "ðŸŠðŸ¾\u200d♀ï¸",
+ "woman_swimming_medium-light_skin_tone": "ðŸŠðŸ¼\u200d♀ï¸",
+ "woman_swimming_medium_skin_tone": "ðŸŠðŸ½\u200d♀ï¸",
+ "woman_teacher": "👩\u200dðŸ«",
+ "woman_teacher_dark_skin_tone": "👩ðŸ¿\u200dðŸ«",
+ "woman_teacher_light_skin_tone": "👩ðŸ»\u200dðŸ«",
+ "woman_teacher_medium-dark_skin_tone": "👩ðŸ¾\u200dðŸ«",
+ "woman_teacher_medium-light_skin_tone": "👩ðŸ¼\u200dðŸ«",
+ "woman_teacher_medium_skin_tone": "👩ðŸ½\u200dðŸ«",
+ "woman_technologist": "👩\u200d💻",
+ "woman_technologist_dark_skin_tone": "👩ðŸ¿\u200d💻",
+ "woman_technologist_light_skin_tone": "👩ðŸ»\u200d💻",
+ "woman_technologist_medium-dark_skin_tone": "👩ðŸ¾\u200d💻",
+ "woman_technologist_medium-light_skin_tone": "👩ðŸ¼\u200d💻",
+ "woman_technologist_medium_skin_tone": "👩ðŸ½\u200d💻",
+ "woman_tipping_hand": "ðŸ’\u200d♀ï¸",
+ "woman_tipping_hand_dark_skin_tone": "ðŸ’ðŸ¿\u200d♀ï¸",
+ "woman_tipping_hand_light_skin_tone": "ðŸ’ðŸ»\u200d♀ï¸",
+ "woman_tipping_hand_medium-dark_skin_tone": "ðŸ’ðŸ¾\u200d♀ï¸",
+ "woman_tipping_hand_medium-light_skin_tone": "ðŸ’ðŸ¼\u200d♀ï¸",
+ "woman_tipping_hand_medium_skin_tone": "ðŸ’ðŸ½\u200d♀ï¸",
+ "woman_vampire": "🧛\u200d♀ï¸",
+ "woman_vampire_dark_skin_tone": "🧛ðŸ¿\u200d♀ï¸",
+ "woman_vampire_light_skin_tone": "🧛ðŸ»\u200d♀ï¸",
+ "woman_vampire_medium-dark_skin_tone": "🧛ðŸ¾\u200d♀ï¸",
+ "woman_vampire_medium-light_skin_tone": "🧛ðŸ¼\u200d♀ï¸",
+ "woman_vampire_medium_skin_tone": "🧛ðŸ½\u200d♀ï¸",
+ "woman_walking": "🚶\u200d♀ï¸",
+ "woman_walking_dark_skin_tone": "🚶ðŸ¿\u200d♀ï¸",
+ "woman_walking_light_skin_tone": "🚶ðŸ»\u200d♀ï¸",
+ "woman_walking_medium-dark_skin_tone": "🚶ðŸ¾\u200d♀ï¸",
+ "woman_walking_medium-light_skin_tone": "🚶ðŸ¼\u200d♀ï¸",
+ "woman_walking_medium_skin_tone": "🚶ðŸ½\u200d♀ï¸",
+ "woman_wearing_turban": "👳\u200d♀ï¸",
+ "woman_wearing_turban_dark_skin_tone": "👳ðŸ¿\u200d♀ï¸",
+ "woman_wearing_turban_light_skin_tone": "👳ðŸ»\u200d♀ï¸",
+ "woman_wearing_turban_medium-dark_skin_tone": "👳ðŸ¾\u200d♀ï¸",
+ "woman_wearing_turban_medium-light_skin_tone": "👳ðŸ¼\u200d♀ï¸",
+ "woman_wearing_turban_medium_skin_tone": "👳ðŸ½\u200d♀ï¸",
+ "woman_with_headscarf": "🧕",
+ "woman_with_headscarf_dark_skin_tone": "🧕ðŸ¿",
+ "woman_with_headscarf_light_skin_tone": "🧕ðŸ»",
+ "woman_with_headscarf_medium-dark_skin_tone": "🧕ðŸ¾",
+ "woman_with_headscarf_medium-light_skin_tone": "🧕ðŸ¼",
+ "woman_with_headscarf_medium_skin_tone": "🧕ðŸ½",
+ "woman_with_probing_cane": "👩\u200d🦯",
+ "woman_zombie": "🧟\u200d♀ï¸",
+ "woman’s_boot": "👢",
+ "woman’s_clothes": "👚",
+ "woman’s_hat": "👒",
+ "woman’s_sandal": "👡",
+ "women_with_bunny_ears": "👯\u200d♀ï¸",
+ "women_wrestling": "🤼\u200d♀ï¸",
+ "women’s_room": "🚺",
+ "woozy_face": "🥴",
+ "world_map": "🗺",
+ "worried_face": "😟",
+ "wrapped_gift": "ðŸŽ",
+ "wrench": "🔧",
+ "writing_hand": "âœ",
+ "writing_hand_dark_skin_tone": "âœðŸ¿",
+ "writing_hand_light_skin_tone": "âœðŸ»",
+ "writing_hand_medium-dark_skin_tone": "âœðŸ¾",
+ "writing_hand_medium-light_skin_tone": "âœðŸ¼",
+ "writing_hand_medium_skin_tone": "âœðŸ½",
+ "yarn": "🧶",
+ "yawning_face": "🥱",
+ "yellow_circle": "🟡",
+ "yellow_heart": "💛",
+ "yellow_square": "🟨",
+ "yen_banknote": "💴",
+ "yo-yo": "🪀",
+ "yin_yang": "☯",
+ "zany_face": "🤪",
+ "zebra": "🦓",
+ "zipper-mouth_face": "ðŸ¤",
+ "zombie": "🧟",
+ "zzz": "💤",
+ "åland_islands": "🇦🇽",
+ "keycap_asterisk": "*⃣",
+ "keycap_digit_eight": "8⃣",
+ "keycap_digit_five": "5⃣",
+ "keycap_digit_four": "4⃣",
+ "keycap_digit_nine": "9⃣",
+ "keycap_digit_one": "1⃣",
+ "keycap_digit_seven": "7⃣",
+ "keycap_digit_six": "6⃣",
+ "keycap_digit_three": "3⃣",
+ "keycap_digit_two": "2⃣",
+ "keycap_digit_zero": "0⃣",
+ "keycap_number_sign": "#⃣",
+ "light_skin_tone": "ðŸ»",
+ "medium_light_skin_tone": "ðŸ¼",
+ "medium_skin_tone": "ðŸ½",
+ "medium_dark_skin_tone": "ðŸ¾",
+ "dark_skin_tone": "ðŸ¿",
+ "regional_indicator_symbol_letter_a": "🇦",
+ "regional_indicator_symbol_letter_b": "🇧",
+ "regional_indicator_symbol_letter_c": "🇨",
+ "regional_indicator_symbol_letter_d": "🇩",
+ "regional_indicator_symbol_letter_e": "🇪",
+ "regional_indicator_symbol_letter_f": "🇫",
+ "regional_indicator_symbol_letter_g": "🇬",
+ "regional_indicator_symbol_letter_h": "🇭",
+ "regional_indicator_symbol_letter_i": "🇮",
+ "regional_indicator_symbol_letter_j": "🇯",
+ "regional_indicator_symbol_letter_k": "🇰",
+ "regional_indicator_symbol_letter_l": "🇱",
+ "regional_indicator_symbol_letter_m": "🇲",
+ "regional_indicator_symbol_letter_n": "🇳",
+ "regional_indicator_symbol_letter_o": "🇴",
+ "regional_indicator_symbol_letter_p": "🇵",
+ "regional_indicator_symbol_letter_q": "🇶",
+ "regional_indicator_symbol_letter_r": "🇷",
+ "regional_indicator_symbol_letter_s": "🇸",
+ "regional_indicator_symbol_letter_t": "🇹",
+ "regional_indicator_symbol_letter_u": "🇺",
+ "regional_indicator_symbol_letter_v": "🇻",
+ "regional_indicator_symbol_letter_w": "🇼",
+ "regional_indicator_symbol_letter_x": "🇽",
+ "regional_indicator_symbol_letter_y": "🇾",
+ "regional_indicator_symbol_letter_z": "🇿",
+ "airplane_arriving": "🛬",
+ "space_invader": "👾",
+ "football": "ðŸˆ",
+ "anger": "💢",
+ "angry": "😠",
+ "anguished": "😧",
+ "signal_strength": "📶",
+ "arrows_counterclockwise": "🔄",
+ "arrow_heading_down": "⤵",
+ "arrow_heading_up": "⤴",
+ "art": "🎨",
+ "astonished": "😲",
+ "athletic_shoe": "👟",
+ "atm": "ðŸ§",
+ "car": "🚗",
+ "red_car": "🚗",
+ "angel": "👼",
+ "back": "🔙",
+ "badminton_racquet_and_shuttlecock": "ðŸ¸",
+ "dollar": "💵",
+ "euro": "💶",
+ "pound": "💷",
+ "yen": "💴",
+ "barber": "💈",
+ "bath": "🛀",
+ "bear": "ðŸ»",
+ "heartbeat": "💓",
+ "beer": "ðŸº",
+ "no_bell": "🔕",
+ "bento": "ðŸ±",
+ "bike": "🚲",
+ "bicyclist": "🚴",
+ "8ball": "🎱",
+ "biohazard_sign": "☣",
+ "birthday": "🎂",
+ "black_circle_for_record": "âº",
+ "clubs": "♣",
+ "diamonds": "♦",
+ "arrow_double_down": "â¬",
+ "hearts": "♥",
+ "rewind": "âª",
+ "black_left__pointing_double_triangle_with_vertical_bar": "â®",
+ "arrow_backward": "â—€",
+ "black_medium_small_square": "â—¾",
+ "question": "â“",
+ "fast_forward": "â©",
+ "black_right__pointing_double_triangle_with_vertical_bar": "â­",
+ "arrow_forward": "â–¶",
+ "black_right__pointing_triangle_with_double_vertical_bar": "â¯",
+ "arrow_right": "âž¡",
+ "spades": "â™ ",
+ "black_square_for_stop": "â¹",
+ "sunny": "☀",
+ "phone": "☎",
+ "recycle": "â™»",
+ "arrow_double_up": "â«",
+ "busstop": "ðŸš",
+ "date": "📅",
+ "flags": "ðŸŽ",
+ "cat2": "ðŸˆ",
+ "joy_cat": "😹",
+ "smirk_cat": "😼",
+ "chart_with_downwards_trend": "📉",
+ "chart_with_upwards_trend": "📈",
+ "chart": "💹",
+ "mega": "📣",
+ "checkered_flag": "ðŸ",
+ "accept": "🉑",
+ "ideograph_advantage": "ðŸ‰",
+ "congratulations": "㊗",
+ "secret": "㊙",
+ "m": "â“‚",
+ "city_sunset": "🌆",
+ "clapper": "🎬",
+ "clap": "ðŸ‘",
+ "beers": "ðŸ»",
+ "clock830": "🕣",
+ "clock8": "🕗",
+ "clock1130": "🕦",
+ "clock11": "🕚",
+ "clock530": "🕠",
+ "clock5": "🕔",
+ "clock430": "🕟",
+ "clock4": "🕓",
+ "clock930": "🕤",
+ "clock9": "🕘",
+ "clock130": "🕜",
+ "clock1": "ðŸ•",
+ "clock730": "🕢",
+ "clock7": "🕖",
+ "clock630": "🕡",
+ "clock6": "🕕",
+ "clock1030": "🕥",
+ "clock10": "🕙",
+ "clock330": "🕞",
+ "clock3": "🕒",
+ "clock1230": "🕧",
+ "clock12": "🕛",
+ "clock230": "ðŸ•",
+ "clock2": "🕑",
+ "arrows_clockwise": "🔃",
+ "repeat": "ðŸ”",
+ "repeat_one": "🔂",
+ "closed_lock_with_key": "ðŸ”",
+ "mailbox_closed": "📪",
+ "mailbox": "📫",
+ "cloud_with_tornado": "🌪",
+ "cocktail": "ðŸ¸",
+ "boom": "💥",
+ "compression": "🗜",
+ "confounded": "😖",
+ "confused": "😕",
+ "rice": "ðŸš",
+ "cow2": "ðŸ„",
+ "cricket_bat_and_ball": "ðŸ",
+ "x": "âŒ",
+ "cry": "😢",
+ "curry": "ðŸ›",
+ "dagger_knife": "🗡",
+ "dancer": "💃",
+ "dark_sunglasses": "🕶",
+ "dash": "💨",
+ "truck": "🚚",
+ "derelict_house_building": "ðŸš",
+ "diamond_shape_with_a_dot_inside": "💠",
+ "dart": "🎯",
+ "disappointed_relieved": "😥",
+ "disappointed": "😞",
+ "do_not_litter": "🚯",
+ "dog2": "ðŸ•",
+ "flipper": "ðŸ¬",
+ "loop": "âž¿",
+ "bangbang": "‼",
+ "double_vertical_bar": "â¸",
+ "dove_of_peace": "🕊",
+ "small_red_triangle_down": "🔻",
+ "arrow_down_small": "🔽",
+ "arrow_down": "⬇",
+ "dromedary_camel": "ðŸª",
+ "e__mail": "📧",
+ "corn": "🌽",
+ "ear_of_rice": "🌾",
+ "earth_americas": "🌎",
+ "earth_asia": "ðŸŒ",
+ "earth_africa": "ðŸŒ",
+ "eight_pointed_black_star": "✴",
+ "eight_spoked_asterisk": "✳",
+ "eject_symbol": "â",
+ "bulb": "💡",
+ "emoji_modifier_fitzpatrick_type__1__2": "ðŸ»",
+ "emoji_modifier_fitzpatrick_type__3": "ðŸ¼",
+ "emoji_modifier_fitzpatrick_type__4": "ðŸ½",
+ "emoji_modifier_fitzpatrick_type__5": "ðŸ¾",
+ "emoji_modifier_fitzpatrick_type__6": "ðŸ¿",
+ "end": "🔚",
+ "email": "✉",
+ "european_castle": "ðŸ°",
+ "european_post_office": "ðŸ¤",
+ "interrobang": "â‰",
+ "expressionless": "😑",
+ "eyeglasses": "👓",
+ "massage": "💆",
+ "yum": "😋",
+ "scream": "😱",
+ "kissing_heart": "😘",
+ "sweat": "😓",
+ "face_with_head__bandage": "🤕",
+ "triumph": "😤",
+ "mask": "😷",
+ "no_good": "🙅",
+ "ok_woman": "🙆",
+ "open_mouth": "😮",
+ "cold_sweat": "😰",
+ "stuck_out_tongue": "😛",
+ "stuck_out_tongue_closed_eyes": "ðŸ˜",
+ "stuck_out_tongue_winking_eye": "😜",
+ "joy": "😂",
+ "no_mouth": "😶",
+ "santa": "🎅",
+ "fax": "📠",
+ "fearful": "😨",
+ "field_hockey_stick_and_ball": "ðŸ‘",
+ "first_quarter_moon_with_face": "🌛",
+ "fish_cake": "ðŸ¥",
+ "fishing_pole_and_fish": "🎣",
+ "facepunch": "👊",
+ "punch": "👊",
+ "flag_for_afghanistan": "🇦🇫",
+ "flag_for_albania": "🇦🇱",
+ "flag_for_algeria": "🇩🇿",
+ "flag_for_american_samoa": "🇦🇸",
+ "flag_for_andorra": "🇦🇩",
+ "flag_for_angola": "🇦🇴",
+ "flag_for_anguilla": "🇦🇮",
+ "flag_for_antarctica": "🇦🇶",
+ "flag_for_antigua_&_barbuda": "🇦🇬",
+ "flag_for_argentina": "🇦🇷",
+ "flag_for_armenia": "🇦🇲",
+ "flag_for_aruba": "🇦🇼",
+ "flag_for_ascension_island": "🇦🇨",
+ "flag_for_australia": "🇦🇺",
+ "flag_for_austria": "🇦🇹",
+ "flag_for_azerbaijan": "🇦🇿",
+ "flag_for_bahamas": "🇧🇸",
+ "flag_for_bahrain": "🇧🇭",
+ "flag_for_bangladesh": "🇧🇩",
+ "flag_for_barbados": "🇧🇧",
+ "flag_for_belarus": "🇧🇾",
+ "flag_for_belgium": "🇧🇪",
+ "flag_for_belize": "🇧🇿",
+ "flag_for_benin": "🇧🇯",
+ "flag_for_bermuda": "🇧🇲",
+ "flag_for_bhutan": "🇧🇹",
+ "flag_for_bolivia": "🇧🇴",
+ "flag_for_bosnia_&_herzegovina": "🇧🇦",
+ "flag_for_botswana": "🇧🇼",
+ "flag_for_bouvet_island": "🇧🇻",
+ "flag_for_brazil": "🇧🇷",
+ "flag_for_british_indian_ocean_territory": "🇮🇴",
+ "flag_for_british_virgin_islands": "🇻🇬",
+ "flag_for_brunei": "🇧🇳",
+ "flag_for_bulgaria": "🇧🇬",
+ "flag_for_burkina_faso": "🇧🇫",
+ "flag_for_burundi": "🇧🇮",
+ "flag_for_cambodia": "🇰🇭",
+ "flag_for_cameroon": "🇨🇲",
+ "flag_for_canada": "🇨🇦",
+ "flag_for_canary_islands": "🇮🇨",
+ "flag_for_cape_verde": "🇨🇻",
+ "flag_for_caribbean_netherlands": "🇧🇶",
+ "flag_for_cayman_islands": "🇰🇾",
+ "flag_for_central_african_republic": "🇨🇫",
+ "flag_for_ceuta_&_melilla": "🇪🇦",
+ "flag_for_chad": "🇹🇩",
+ "flag_for_chile": "🇨🇱",
+ "flag_for_china": "🇨🇳",
+ "flag_for_christmas_island": "🇨🇽",
+ "flag_for_clipperton_island": "🇨🇵",
+ "flag_for_cocos__islands": "🇨🇨",
+ "flag_for_colombia": "🇨🇴",
+ "flag_for_comoros": "🇰🇲",
+ "flag_for_congo____brazzaville": "🇨🇬",
+ "flag_for_congo____kinshasa": "🇨🇩",
+ "flag_for_cook_islands": "🇨🇰",
+ "flag_for_costa_rica": "🇨🇷",
+ "flag_for_croatia": "🇭🇷",
+ "flag_for_cuba": "🇨🇺",
+ "flag_for_curaçao": "🇨🇼",
+ "flag_for_cyprus": "🇨🇾",
+ "flag_for_czech_republic": "🇨🇿",
+ "flag_for_côte_d’ivoire": "🇨🇮",
+ "flag_for_denmark": "🇩🇰",
+ "flag_for_diego_garcia": "🇩🇬",
+ "flag_for_djibouti": "🇩🇯",
+ "flag_for_dominica": "🇩🇲",
+ "flag_for_dominican_republic": "🇩🇴",
+ "flag_for_ecuador": "🇪🇨",
+ "flag_for_egypt": "🇪🇬",
+ "flag_for_el_salvador": "🇸🇻",
+ "flag_for_equatorial_guinea": "🇬🇶",
+ "flag_for_eritrea": "🇪🇷",
+ "flag_for_estonia": "🇪🇪",
+ "flag_for_ethiopia": "🇪🇹",
+ "flag_for_european_union": "🇪🇺",
+ "flag_for_falkland_islands": "🇫🇰",
+ "flag_for_faroe_islands": "🇫🇴",
+ "flag_for_fiji": "🇫🇯",
+ "flag_for_finland": "🇫🇮",
+ "flag_for_france": "🇫🇷",
+ "flag_for_french_guiana": "🇬🇫",
+ "flag_for_french_polynesia": "🇵🇫",
+ "flag_for_french_southern_territories": "🇹🇫",
+ "flag_for_gabon": "🇬🇦",
+ "flag_for_gambia": "🇬🇲",
+ "flag_for_georgia": "🇬🇪",
+ "flag_for_germany": "🇩🇪",
+ "flag_for_ghana": "🇬🇭",
+ "flag_for_gibraltar": "🇬🇮",
+ "flag_for_greece": "🇬🇷",
+ "flag_for_greenland": "🇬🇱",
+ "flag_for_grenada": "🇬🇩",
+ "flag_for_guadeloupe": "🇬🇵",
+ "flag_for_guam": "🇬🇺",
+ "flag_for_guatemala": "🇬🇹",
+ "flag_for_guernsey": "🇬🇬",
+ "flag_for_guinea": "🇬🇳",
+ "flag_for_guinea__bissau": "🇬🇼",
+ "flag_for_guyana": "🇬🇾",
+ "flag_for_haiti": "🇭🇹",
+ "flag_for_heard_&_mcdonald_islands": "🇭🇲",
+ "flag_for_honduras": "🇭🇳",
+ "flag_for_hong_kong": "🇭🇰",
+ "flag_for_hungary": "🇭🇺",
+ "flag_for_iceland": "🇮🇸",
+ "flag_for_india": "🇮🇳",
+ "flag_for_indonesia": "🇮🇩",
+ "flag_for_iran": "🇮🇷",
+ "flag_for_iraq": "🇮🇶",
+ "flag_for_ireland": "🇮🇪",
+ "flag_for_isle_of_man": "🇮🇲",
+ "flag_for_israel": "🇮🇱",
+ "flag_for_italy": "🇮🇹",
+ "flag_for_jamaica": "🇯🇲",
+ "flag_for_japan": "🇯🇵",
+ "flag_for_jersey": "🇯🇪",
+ "flag_for_jordan": "🇯🇴",
+ "flag_for_kazakhstan": "🇰🇿",
+ "flag_for_kenya": "🇰🇪",
+ "flag_for_kiribati": "🇰🇮",
+ "flag_for_kosovo": "🇽🇰",
+ "flag_for_kuwait": "🇰🇼",
+ "flag_for_kyrgyzstan": "🇰🇬",
+ "flag_for_laos": "🇱🇦",
+ "flag_for_latvia": "🇱🇻",
+ "flag_for_lebanon": "🇱🇧",
+ "flag_for_lesotho": "🇱🇸",
+ "flag_for_liberia": "🇱🇷",
+ "flag_for_libya": "🇱🇾",
+ "flag_for_liechtenstein": "🇱🇮",
+ "flag_for_lithuania": "🇱🇹",
+ "flag_for_luxembourg": "🇱🇺",
+ "flag_for_macau": "🇲🇴",
+ "flag_for_macedonia": "🇲🇰",
+ "flag_for_madagascar": "🇲🇬",
+ "flag_for_malawi": "🇲🇼",
+ "flag_for_malaysia": "🇲🇾",
+ "flag_for_maldives": "🇲🇻",
+ "flag_for_mali": "🇲🇱",
+ "flag_for_malta": "🇲🇹",
+ "flag_for_marshall_islands": "🇲🇭",
+ "flag_for_martinique": "🇲🇶",
+ "flag_for_mauritania": "🇲🇷",
+ "flag_for_mauritius": "🇲🇺",
+ "flag_for_mayotte": "🇾🇹",
+ "flag_for_mexico": "🇲🇽",
+ "flag_for_micronesia": "🇫🇲",
+ "flag_for_moldova": "🇲🇩",
+ "flag_for_monaco": "🇲🇨",
+ "flag_for_mongolia": "🇲🇳",
+ "flag_for_montenegro": "🇲🇪",
+ "flag_for_montserrat": "🇲🇸",
+ "flag_for_morocco": "🇲🇦",
+ "flag_for_mozambique": "🇲🇿",
+ "flag_for_myanmar": "🇲🇲",
+ "flag_for_namibia": "🇳🇦",
+ "flag_for_nauru": "🇳🇷",
+ "flag_for_nepal": "🇳🇵",
+ "flag_for_netherlands": "🇳🇱",
+ "flag_for_new_caledonia": "🇳🇨",
+ "flag_for_new_zealand": "🇳🇿",
+ "flag_for_nicaragua": "🇳🇮",
+ "flag_for_niger": "🇳🇪",
+ "flag_for_nigeria": "🇳🇬",
+ "flag_for_niue": "🇳🇺",
+ "flag_for_norfolk_island": "🇳🇫",
+ "flag_for_north_korea": "🇰🇵",
+ "flag_for_northern_mariana_islands": "🇲🇵",
+ "flag_for_norway": "🇳🇴",
+ "flag_for_oman": "🇴🇲",
+ "flag_for_pakistan": "🇵🇰",
+ "flag_for_palau": "🇵🇼",
+ "flag_for_palestinian_territories": "🇵🇸",
+ "flag_for_panama": "🇵🇦",
+ "flag_for_papua_new_guinea": "🇵🇬",
+ "flag_for_paraguay": "🇵🇾",
+ "flag_for_peru": "🇵🇪",
+ "flag_for_philippines": "🇵🇭",
+ "flag_for_pitcairn_islands": "🇵🇳",
+ "flag_for_poland": "🇵🇱",
+ "flag_for_portugal": "🇵🇹",
+ "flag_for_puerto_rico": "🇵🇷",
+ "flag_for_qatar": "🇶🇦",
+ "flag_for_romania": "🇷🇴",
+ "flag_for_russia": "🇷🇺",
+ "flag_for_rwanda": "🇷🇼",
+ "flag_for_réunion": "🇷🇪",
+ "flag_for_samoa": "🇼🇸",
+ "flag_for_san_marino": "🇸🇲",
+ "flag_for_saudi_arabia": "🇸🇦",
+ "flag_for_senegal": "🇸🇳",
+ "flag_for_serbia": "🇷🇸",
+ "flag_for_seychelles": "🇸🇨",
+ "flag_for_sierra_leone": "🇸🇱",
+ "flag_for_singapore": "🇸🇬",
+ "flag_for_sint_maarten": "🇸🇽",
+ "flag_for_slovakia": "🇸🇰",
+ "flag_for_slovenia": "🇸🇮",
+ "flag_for_solomon_islands": "🇸🇧",
+ "flag_for_somalia": "🇸🇴",
+ "flag_for_south_africa": "🇿🇦",
+ "flag_for_south_georgia_&_south_sandwich_islands": "🇬🇸",
+ "flag_for_south_korea": "🇰🇷",
+ "flag_for_south_sudan": "🇸🇸",
+ "flag_for_spain": "🇪🇸",
+ "flag_for_sri_lanka": "🇱🇰",
+ "flag_for_st._barthélemy": "🇧🇱",
+ "flag_for_st._helena": "🇸🇭",
+ "flag_for_st._kitts_&_nevis": "🇰🇳",
+ "flag_for_st._lucia": "🇱🇨",
+ "flag_for_st._martin": "🇲🇫",
+ "flag_for_st._pierre_&_miquelon": "🇵🇲",
+ "flag_for_st._vincent_&_grenadines": "🇻🇨",
+ "flag_for_sudan": "🇸🇩",
+ "flag_for_suriname": "🇸🇷",
+ "flag_for_svalbard_&_jan_mayen": "🇸🇯",
+ "flag_for_swaziland": "🇸🇿",
+ "flag_for_sweden": "🇸🇪",
+ "flag_for_switzerland": "🇨🇭",
+ "flag_for_syria": "🇸🇾",
+ "flag_for_são_tomé_&_príncipe": "🇸🇹",
+ "flag_for_taiwan": "🇹🇼",
+ "flag_for_tajikistan": "🇹🇯",
+ "flag_for_tanzania": "🇹🇿",
+ "flag_for_thailand": "🇹🇭",
+ "flag_for_timor__leste": "🇹🇱",
+ "flag_for_togo": "🇹🇬",
+ "flag_for_tokelau": "🇹🇰",
+ "flag_for_tonga": "🇹🇴",
+ "flag_for_trinidad_&_tobago": "🇹🇹",
+ "flag_for_tristan_da_cunha": "🇹🇦",
+ "flag_for_tunisia": "🇹🇳",
+ "flag_for_turkey": "🇹🇷",
+ "flag_for_turkmenistan": "🇹🇲",
+ "flag_for_turks_&_caicos_islands": "🇹🇨",
+ "flag_for_tuvalu": "🇹🇻",
+ "flag_for_u.s._outlying_islands": "🇺🇲",
+ "flag_for_u.s._virgin_islands": "🇻🇮",
+ "flag_for_uganda": "🇺🇬",
+ "flag_for_ukraine": "🇺🇦",
+ "flag_for_united_arab_emirates": "🇦🇪",
+ "flag_for_united_kingdom": "🇬🇧",
+ "flag_for_united_states": "🇺🇸",
+ "flag_for_uruguay": "🇺🇾",
+ "flag_for_uzbekistan": "🇺🇿",
+ "flag_for_vanuatu": "🇻🇺",
+ "flag_for_vatican_city": "🇻🇦",
+ "flag_for_venezuela": "🇻🇪",
+ "flag_for_vietnam": "🇻🇳",
+ "flag_for_wallis_&_futuna": "🇼🇫",
+ "flag_for_western_sahara": "🇪🇭",
+ "flag_for_yemen": "🇾🇪",
+ "flag_for_zambia": "🇿🇲",
+ "flag_for_zimbabwe": "🇿🇼",
+ "flag_for_åland_islands": "🇦🇽",
+ "golf": "⛳",
+ "fleur__de__lis": "⚜",
+ "muscle": "💪",
+ "flushed": "😳",
+ "frame_with_picture": "🖼",
+ "fries": "ðŸŸ",
+ "frog": "ðŸ¸",
+ "hatched_chick": "ðŸ¥",
+ "frowning": "😦",
+ "fuelpump": "⛽",
+ "full_moon_with_face": "ðŸŒ",
+ "gem": "💎",
+ "star2": "🌟",
+ "golfer": "ðŸŒ",
+ "mortar_board": "🎓",
+ "grimacing": "😬",
+ "smile_cat": "😸",
+ "grinning": "😀",
+ "grin": "ðŸ˜",
+ "heartpulse": "💗",
+ "guardsman": "💂",
+ "haircut": "💇",
+ "hamster": "ðŸ¹",
+ "raising_hand": "🙋",
+ "headphones": "🎧",
+ "hear_no_evil": "🙉",
+ "cupid": "💘",
+ "gift_heart": "ðŸ’",
+ "heart": "â¤",
+ "exclamation": "â—",
+ "heavy_exclamation_mark": "â—",
+ "heavy_heart_exclamation_mark_ornament": "â£",
+ "o": "â­•",
+ "helm_symbol": "⎈",
+ "helmet_with_white_cross": "⛑",
+ "high_heel": "👠",
+ "bullettrain_side": "🚄",
+ "bullettrain_front": "🚅",
+ "high_brightness": "🔆",
+ "zap": "âš¡",
+ "hocho": "🔪",
+ "knife": "🔪",
+ "bee": "ðŸ",
+ "traffic_light": "🚥",
+ "racehorse": "ðŸŽ",
+ "coffee": "☕",
+ "hotsprings": "♨",
+ "hourglass": "⌛",
+ "hourglass_flowing_sand": "â³",
+ "house_buildings": "ðŸ˜",
+ "100": "💯",
+ "hushed": "😯",
+ "ice_hockey_stick_and_puck": "ðŸ’",
+ "imp": "👿",
+ "information_desk_person": "ðŸ’",
+ "information_source": "ℹ",
+ "capital_abcd": "🔠",
+ "abc": "🔤",
+ "abcd": "🔡",
+ "1234": "🔢",
+ "symbols": "🔣",
+ "izakaya_lantern": "ðŸ®",
+ "lantern": "ðŸ®",
+ "jack_o_lantern": "🎃",
+ "dolls": "🎎",
+ "japanese_goblin": "👺",
+ "japanese_ogre": "👹",
+ "beginner": "🔰",
+ "zero": "0ï¸âƒ£",
+ "one": "1ï¸âƒ£",
+ "ten": "🔟",
+ "two": "2ï¸âƒ£",
+ "three": "3ï¸âƒ£",
+ "four": "4ï¸âƒ£",
+ "five": "5ï¸âƒ£",
+ "six": "6ï¸âƒ£",
+ "seven": "7ï¸âƒ£",
+ "eight": "8ï¸âƒ£",
+ "nine": "9ï¸âƒ£",
+ "couplekiss": "ðŸ’",
+ "kissing_cat": "😽",
+ "kissing": "😗",
+ "kissing_closed_eyes": "😚",
+ "kissing_smiling_eyes": "😙",
+ "beetle": "ðŸž",
+ "large_blue_circle": "🔵",
+ "last_quarter_moon_with_face": "🌜",
+ "leaves": "ðŸƒ",
+ "mag": "ðŸ”",
+ "left_right_arrow": "↔",
+ "leftwards_arrow_with_hook": "↩",
+ "arrow_left": "⬅",
+ "lock": "🔒",
+ "lock_with_ink_pen": "ðŸ”",
+ "sob": "😭",
+ "low_brightness": "🔅",
+ "lower_left_ballpoint_pen": "🖊",
+ "lower_left_crayon": "ðŸ–",
+ "lower_left_fountain_pen": "🖋",
+ "lower_left_paintbrush": "🖌",
+ "mahjong": "🀄",
+ "couple": "👫",
+ "man_in_business_suit_levitating": "🕴",
+ "man_with_gua_pi_mao": "👲",
+ "man_with_turban": "👳",
+ "mans_shoe": "👞",
+ "shoe": "👞",
+ "menorah_with_nine_branches": "🕎",
+ "mens": "🚹",
+ "minidisc": "💽",
+ "iphone": "📱",
+ "calling": "📲",
+ "money__mouth_face": "🤑",
+ "moneybag": "💰",
+ "rice_scene": "🎑",
+ "mountain_bicyclist": "🚵",
+ "mouse2": "ðŸ",
+ "lips": "👄",
+ "moyai": "🗿",
+ "notes": "🎶",
+ "nail_care": "💅",
+ "ab": "🆎",
+ "negative_squared_cross_mark": "âŽ",
+ "a": "🅰",
+ "b": "🅱",
+ "o2": "🅾",
+ "parking": "🅿",
+ "new_moon_with_face": "🌚",
+ "no_entry_sign": "🚫",
+ "underage": "🔞",
+ "non__potable_water": "🚱",
+ "arrow_upper_right": "↗",
+ "arrow_upper_left": "↖",
+ "office": "ðŸ¢",
+ "older_man": "👴",
+ "older_woman": "👵",
+ "om_symbol": "🕉",
+ "on": "🔛",
+ "book": "📖",
+ "unlock": "🔓",
+ "mailbox_with_no_mail": "📭",
+ "mailbox_with_mail": "📬",
+ "cd": "💿",
+ "tada": "🎉",
+ "feet": "ðŸ¾",
+ "walking": "🚶",
+ "pencil2": "âœ",
+ "pensive": "😔",
+ "persevere": "😣",
+ "bow": "🙇",
+ "raised_hands": "🙌",
+ "person_with_ball": "⛹",
+ "person_with_blond_hair": "👱",
+ "pray": "ðŸ™",
+ "person_with_pouting_face": "🙎",
+ "computer": "💻",
+ "pig2": "ðŸ–",
+ "hankey": "💩",
+ "poop": "💩",
+ "shit": "💩",
+ "bamboo": "ðŸŽ",
+ "gun": "🔫",
+ "black_joker": "ðŸƒ",
+ "rotating_light": "🚨",
+ "cop": "👮",
+ "stew": "ðŸ²",
+ "pouch": "ðŸ‘",
+ "pouting_cat": "😾",
+ "rage": "😡",
+ "put_litter_in_its_place": "🚮",
+ "rabbit2": "ðŸ‡",
+ "racing_motorcycle": "ðŸ",
+ "radioactive_sign": "☢",
+ "fist": "✊",
+ "hand": "✋",
+ "raised_hand_with_fingers_splayed": "ðŸ–",
+ "raised_hand_with_part_between_middle_and_ring_fingers": "🖖",
+ "blue_car": "🚙",
+ "apple": "ðŸŽ",
+ "relieved": "😌",
+ "reversed_hand_with_middle_finger_extended": "🖕",
+ "mag_right": "🔎",
+ "arrow_right_hook": "↪",
+ "sweet_potato": "ðŸ ",
+ "robot": "🤖",
+ "rolled__up_newspaper": "🗞",
+ "rowboat": "🚣",
+ "runner": "ðŸƒ",
+ "running": "ðŸƒ",
+ "running_shirt_with_sash": "🎽",
+ "boat": "⛵",
+ "scales": "âš–",
+ "school_satchel": "🎒",
+ "scorpius": "â™",
+ "see_no_evil": "🙈",
+ "sheep": "ðŸ‘",
+ "stars": "🌠",
+ "cake": "ðŸ°",
+ "six_pointed_star": "🔯",
+ "ski": "🎿",
+ "sleeping_accommodation": "🛌",
+ "sleeping": "😴",
+ "sleepy": "😪",
+ "sleuth_or_spy": "🕵",
+ "heart_eyes_cat": "😻",
+ "smiley_cat": "😺",
+ "innocent": "😇",
+ "heart_eyes": "ðŸ˜",
+ "smiling_imp": "😈",
+ "smiley": "😃",
+ "sweat_smile": "😅",
+ "smile": "😄",
+ "laughing": "😆",
+ "satisfied": "😆",
+ "blush": "😊",
+ "smirk": "ðŸ˜",
+ "smoking": "🚬",
+ "snow_capped_mountain": "ðŸ”",
+ "soccer": "âš½",
+ "icecream": "ðŸ¦",
+ "soon": "🔜",
+ "arrow_lower_right": "↘",
+ "arrow_lower_left": "↙",
+ "speak_no_evil": "🙊",
+ "speaker": "🔈",
+ "mute": "🔇",
+ "sound": "🔉",
+ "loud_sound": "🔊",
+ "speaking_head_in_silhouette": "🗣",
+ "spiral_calendar_pad": "🗓",
+ "spiral_note_pad": "🗒",
+ "shell": "ðŸš",
+ "sweat_drops": "💦",
+ "u5272": "🈹",
+ "u5408": "🈴",
+ "u55b6": "🈺",
+ "u6307": "🈯",
+ "u6708": "🈷",
+ "u6709": "🈶",
+ "u6e80": "🈵",
+ "u7121": "🈚",
+ "u7533": "🈸",
+ "u7981": "🈲",
+ "u7a7a": "🈳",
+ "cl": "🆑",
+ "cool": "🆒",
+ "free": "🆓",
+ "id": "🆔",
+ "koko": "ðŸˆ",
+ "sa": "🈂",
+ "new": "🆕",
+ "ng": "🆖",
+ "ok": "🆗",
+ "sos": "🆘",
+ "up": "🆙",
+ "vs": "🆚",
+ "steam_locomotive": "🚂",
+ "ramen": "ðŸœ",
+ "partly_sunny": "â›…",
+ "city_sunrise": "🌇",
+ "surfer": "ðŸ„",
+ "swimmer": "ðŸŠ",
+ "shirt": "👕",
+ "tshirt": "👕",
+ "table_tennis_paddle_and_ball": "ðŸ“",
+ "tea": "ðŸµ",
+ "tv": "📺",
+ "three_button_mouse": "🖱",
+ "+1": "ðŸ‘",
+ "thumbsup": "ðŸ‘",
+ "__1": "👎",
+ "-1": "👎",
+ "thumbsdown": "👎",
+ "thunder_cloud_and_rain": "⛈",
+ "tiger2": "ðŸ…",
+ "tophat": "🎩",
+ "top": "ðŸ”",
+ "tm": "â„¢",
+ "train2": "🚆",
+ "triangular_flag_on_post": "🚩",
+ "trident": "🔱",
+ "twisted_rightwards_arrows": "🔀",
+ "unamused": "😒",
+ "small_red_triangle": "🔺",
+ "arrow_up_small": "🔼",
+ "arrow_up_down": "↕",
+ "upside__down_face": "🙃",
+ "arrow_up": "⬆",
+ "v": "✌",
+ "vhs": "📼",
+ "wc": "🚾",
+ "ocean": "🌊",
+ "waving_black_flag": "ðŸ´",
+ "wave": "👋",
+ "waving_white_flag": "ðŸ³",
+ "moon": "🌔",
+ "scream_cat": "🙀",
+ "weary": "😩",
+ "weight_lifter": "ðŸ‹",
+ "whale2": "ðŸ‹",
+ "wheelchair": "♿",
+ "point_down": "👇",
+ "grey_exclamation": "â•",
+ "white_frowning_face": "☹",
+ "white_check_mark": "✅",
+ "point_left": "👈",
+ "white_medium_small_square": "â—½",
+ "star": "â­",
+ "grey_question": "â”",
+ "point_right": "👉",
+ "relaxed": "☺",
+ "white_sun_behind_cloud": "🌥",
+ "white_sun_behind_cloud_with_rain": "🌦",
+ "white_sun_with_small_cloud": "🌤",
+ "point_up_2": "👆",
+ "point_up": "â˜",
+ "wind_blowing_face": "🌬",
+ "wink": "😉",
+ "wolf": "ðŸº",
+ "dancers": "👯",
+ "boot": "👢",
+ "womans_clothes": "👚",
+ "womans_hat": "👒",
+ "sandal": "👡",
+ "womens": "🚺",
+ "worried": "😟",
+ "gift": "ðŸŽ",
+ "zipper__mouth_face": "ðŸ¤",
+ "regional_indicator_a": "🇦",
+ "regional_indicator_b": "🇧",
+ "regional_indicator_c": "🇨",
+ "regional_indicator_d": "🇩",
+ "regional_indicator_e": "🇪",
+ "regional_indicator_f": "🇫",
+ "regional_indicator_g": "🇬",
+ "regional_indicator_h": "🇭",
+ "regional_indicator_i": "🇮",
+ "regional_indicator_j": "🇯",
+ "regional_indicator_k": "🇰",
+ "regional_indicator_l": "🇱",
+ "regional_indicator_m": "🇲",
+ "regional_indicator_n": "🇳",
+ "regional_indicator_o": "🇴",
+ "regional_indicator_p": "🇵",
+ "regional_indicator_q": "🇶",
+ "regional_indicator_r": "🇷",
+ "regional_indicator_s": "🇸",
+ "regional_indicator_t": "🇹",
+ "regional_indicator_u": "🇺",
+ "regional_indicator_v": "🇻",
+ "regional_indicator_w": "🇼",
+ "regional_indicator_x": "🇽",
+ "regional_indicator_y": "🇾",
+ "regional_indicator_z": "🇿",
+}
diff --git a/src/pip/_vendor/rich/_emoji_replace.py b/src/pip/_vendor/rich/_emoji_replace.py
new file mode 100644
index 000000000..bb2cafa18
--- /dev/null
+++ b/src/pip/_vendor/rich/_emoji_replace.py
@@ -0,0 +1,32 @@
+from typing import Callable, Match, Optional
+import re
+
+from ._emoji_codes import EMOJI
+
+
+_ReStringMatch = Match[str] # regex match object
+_ReSubCallable = Callable[[_ReStringMatch], str] # Callable invoked by re.sub
+_EmojiSubMethod = Callable[[_ReSubCallable, str], str] # Sub method of a compiled re
+
+
+def _emoji_replace(
+ text: str,
+ default_variant: Optional[str] = None,
+ _emoji_sub: _EmojiSubMethod = re.compile(r"(:(\S*?)(?:(?:\-)(emoji|text))?:)").sub,
+) -> str:
+ """Replace emoji code in text."""
+ get_emoji = EMOJI.__getitem__
+ variants = {"text": "\uFE0E", "emoji": "\uFE0F"}
+ get_variant = variants.get
+ default_variant_code = variants.get(default_variant, "") if default_variant else ""
+
+ def do_replace(match: Match[str]) -> str:
+ emoji_code, emoji_name, variant = match.groups()
+ try:
+ return get_emoji(emoji_name.lower()) + get_variant(
+ variant, default_variant_code
+ )
+ except KeyError:
+ return emoji_code
+
+ return _emoji_sub(do_replace, text)
diff --git a/src/pip/_vendor/rich/_export_format.py b/src/pip/_vendor/rich/_export_format.py
new file mode 100644
index 000000000..b79c13069
--- /dev/null
+++ b/src/pip/_vendor/rich/_export_format.py
@@ -0,0 +1,78 @@
+CONSOLE_HTML_FORMAT = """\
+<!DOCTYPE html>
+<head>
+<meta charset="UTF-8">
+<style>
+{stylesheet}
+body {{
+ color: {foreground};
+ background-color: {background};
+}}
+</style>
+</head>
+<html>
+<body>
+ <code>
+ <pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace">{code}</pre>
+ </code>
+</body>
+</html>
+"""
+
+CONSOLE_SVG_FORMAT = """\
+<svg class="rich-terminal" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">
+ <!-- Generated with Rich https://www.textualize.io -->
+ <style>
+
+ @font-face {{
+ font-family: "Fira Code";
+ src: local("FiraCode-Regular"),
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
+ font-style: normal;
+ font-weight: 400;
+ }}
+ @font-face {{
+ font-family: "Fira Code";
+ src: local("FiraCode-Bold"),
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
+ font-style: bold;
+ font-weight: 700;
+ }}
+
+ .{unique_id}-matrix {{
+ font-family: Fira Code, monospace;
+ font-size: {char_height}px;
+ line-height: {line_height}px;
+ font-variant-east-asian: full-width;
+ }}
+
+ .{unique_id}-title {{
+ font-size: 18px;
+ font-weight: bold;
+ font-family: arial;
+ }}
+
+ {styles}
+ </style>
+
+ <defs>
+ <clipPath id="{unique_id}-clip-terminal">
+ <rect x="0" y="0" width="{terminal_width}" height="{terminal_height}" />
+ </clipPath>
+ {lines}
+ </defs>
+
+ {chrome}
+ <g transform="translate({terminal_x}, {terminal_y})" clip-path="url(#{unique_id}-clip-terminal)">
+ {backgrounds}
+ <g class="{unique_id}-matrix">
+ {matrix}
+ </g>
+ </g>
+</svg>
+"""
+
+_SVG_FONT_FAMILY = "Rich Fira Code"
+_SVG_CLASSES_PREFIX = "rich-svg"
diff --git a/src/pip/_vendor/rich/_extension.py b/src/pip/_vendor/rich/_extension.py
new file mode 100644
index 000000000..cbd6da9be
--- /dev/null
+++ b/src/pip/_vendor/rich/_extension.py
@@ -0,0 +1,10 @@
+from typing import Any
+
+
+def load_ipython_extension(ip: Any) -> None: # pragma: no cover
+ # prevent circular import
+ from pip._vendor.rich.pretty import install
+ from pip._vendor.rich.traceback import install as tr_install
+
+ install()
+ tr_install()
diff --git a/src/pip/_vendor/rich/_inspect.py b/src/pip/_vendor/rich/_inspect.py
new file mode 100644
index 000000000..30446ceb3
--- /dev/null
+++ b/src/pip/_vendor/rich/_inspect.py
@@ -0,0 +1,270 @@
+from __future__ import absolute_import
+
+import inspect
+from inspect import cleandoc, getdoc, getfile, isclass, ismodule, signature
+from typing import Any, Collection, Iterable, Optional, Tuple, Type, Union
+
+from .console import Group, RenderableType
+from .control import escape_control_codes
+from .highlighter import ReprHighlighter
+from .jupyter import JupyterMixin
+from .panel import Panel
+from .pretty import Pretty
+from .table import Table
+from .text import Text, TextType
+
+
+def _first_paragraph(doc: str) -> str:
+ """Get the first paragraph from a docstring."""
+ paragraph, _, _ = doc.partition("\n\n")
+ return paragraph
+
+
+class Inspect(JupyterMixin):
+ """A renderable to inspect any Python Object.
+
+ Args:
+ obj (Any): An object to inspect.
+ title (str, optional): Title to display over inspect result, or None use type. Defaults to None.
+ help (bool, optional): Show full help text rather than just first paragraph. Defaults to False.
+ methods (bool, optional): Enable inspection of callables. Defaults to False.
+ docs (bool, optional): Also render doc strings. Defaults to True.
+ private (bool, optional): Show private attributes (beginning with underscore). Defaults to False.
+ dunder (bool, optional): Show attributes starting with double underscore. Defaults to False.
+ sort (bool, optional): Sort attributes alphabetically. Defaults to True.
+ all (bool, optional): Show all attributes. Defaults to False.
+ value (bool, optional): Pretty print value of object. Defaults to True.
+ """
+
+ def __init__(
+ self,
+ obj: Any,
+ *,
+ title: Optional[TextType] = None,
+ help: bool = False,
+ methods: bool = False,
+ docs: bool = True,
+ private: bool = False,
+ dunder: bool = False,
+ sort: bool = True,
+ all: bool = True,
+ value: bool = True,
+ ) -> None:
+ self.highlighter = ReprHighlighter()
+ self.obj = obj
+ self.title = title or self._make_title(obj)
+ if all:
+ methods = private = dunder = True
+ self.help = help
+ self.methods = methods
+ self.docs = docs or help
+ self.private = private or dunder
+ self.dunder = dunder
+ self.sort = sort
+ self.value = value
+
+ def _make_title(self, obj: Any) -> Text:
+ """Make a default title."""
+ title_str = (
+ str(obj)
+ if (isclass(obj) or callable(obj) or ismodule(obj))
+ else str(type(obj))
+ )
+ title_text = self.highlighter(title_str)
+ return title_text
+
+ def __rich__(self) -> Panel:
+ return Panel.fit(
+ Group(*self._render()),
+ title=self.title,
+ border_style="scope.border",
+ padding=(0, 1),
+ )
+
+ def _get_signature(self, name: str, obj: Any) -> Optional[Text]:
+ """Get a signature for a callable."""
+ try:
+ _signature = str(signature(obj)) + ":"
+ except ValueError:
+ _signature = "(...)"
+ except TypeError:
+ return None
+
+ source_filename: Optional[str] = None
+ try:
+ source_filename = getfile(obj)
+ except (OSError, TypeError):
+ # OSError is raised if obj has no source file, e.g. when defined in REPL.
+ pass
+
+ callable_name = Text(name, style="inspect.callable")
+ if source_filename:
+ callable_name.stylize(f"link file://{source_filename}")
+ signature_text = self.highlighter(_signature)
+
+ qualname = name or getattr(obj, "__qualname__", name)
+
+ # If obj is a module, there may be classes (which are callable) to display
+ if inspect.isclass(obj):
+ prefix = "class"
+ elif inspect.iscoroutinefunction(obj):
+ prefix = "async def"
+ else:
+ prefix = "def"
+
+ qual_signature = Text.assemble(
+ (f"{prefix} ", f"inspect.{prefix.replace(' ', '_')}"),
+ (qualname, "inspect.callable"),
+ signature_text,
+ )
+
+ return qual_signature
+
+ def _render(self) -> Iterable[RenderableType]:
+ """Render object."""
+
+ def sort_items(item: Tuple[str, Any]) -> Tuple[bool, str]:
+ key, (_error, value) = item
+ return (callable(value), key.strip("_").lower())
+
+ def safe_getattr(attr_name: str) -> Tuple[Any, Any]:
+ """Get attribute or any exception."""
+ try:
+ return (None, getattr(obj, attr_name))
+ except Exception as error:
+ return (error, None)
+
+ obj = self.obj
+ keys = dir(obj)
+ total_items = len(keys)
+ if not self.dunder:
+ keys = [key for key in keys if not key.startswith("__")]
+ if not self.private:
+ keys = [key for key in keys if not key.startswith("_")]
+ not_shown_count = total_items - len(keys)
+ items = [(key, safe_getattr(key)) for key in keys]
+ if self.sort:
+ items.sort(key=sort_items)
+
+ items_table = Table.grid(padding=(0, 1), expand=False)
+ items_table.add_column(justify="right")
+ add_row = items_table.add_row
+ highlighter = self.highlighter
+
+ if callable(obj):
+ signature = self._get_signature("", obj)
+ if signature is not None:
+ yield signature
+ yield ""
+
+ if self.docs:
+ _doc = self._get_formatted_doc(obj)
+ if _doc is not None:
+ doc_text = Text(_doc, style="inspect.help")
+ doc_text = highlighter(doc_text)
+ yield doc_text
+ yield ""
+
+ if self.value and not (isclass(obj) or callable(obj) or ismodule(obj)):
+ yield Panel(
+ Pretty(obj, indent_guides=True, max_length=10, max_string=60),
+ border_style="inspect.value.border",
+ )
+ yield ""
+
+ for key, (error, value) in items:
+ key_text = Text.assemble(
+ (
+ key,
+ "inspect.attr.dunder" if key.startswith("__") else "inspect.attr",
+ ),
+ (" =", "inspect.equals"),
+ )
+ if error is not None:
+ warning = key_text.copy()
+ warning.stylize("inspect.error")
+ add_row(warning, highlighter(repr(error)))
+ continue
+
+ if callable(value):
+ if not self.methods:
+ continue
+
+ _signature_text = self._get_signature(key, value)
+ if _signature_text is None:
+ add_row(key_text, Pretty(value, highlighter=highlighter))
+ else:
+ if self.docs:
+ docs = self._get_formatted_doc(value)
+ if docs is not None:
+ _signature_text.append("\n" if "\n" in docs else " ")
+ doc = highlighter(docs)
+ doc.stylize("inspect.doc")
+ _signature_text.append(doc)
+
+ add_row(key_text, _signature_text)
+ else:
+ add_row(key_text, Pretty(value, highlighter=highlighter))
+ if items_table.row_count:
+ yield items_table
+ elif not_shown_count:
+ yield Text.from_markup(
+ f"[b cyan]{not_shown_count}[/][i] attribute(s) not shown.[/i] "
+ f"Run [b][magenta]inspect[/]([not b]inspect[/])[/b] for options."
+ )
+
+ def _get_formatted_doc(self, object_: Any) -> Optional[str]:
+ """
+ Extract the docstring of an object, process it and returns it.
+ The processing consists in cleaning up the doctring's indentation,
+ taking only its 1st paragraph if `self.help` is not True,
+ and escape its control codes.
+
+ Args:
+ object_ (Any): the object to get the docstring from.
+
+ Returns:
+ Optional[str]: the processed docstring, or None if no docstring was found.
+ """
+ docs = getdoc(object_)
+ if docs is None:
+ return None
+ docs = cleandoc(docs).strip()
+ if not self.help:
+ docs = _first_paragraph(docs)
+ return escape_control_codes(docs)
+
+
+def get_object_types_mro(obj: Union[object, Type[Any]]) -> Tuple[type, ...]:
+ """Returns the MRO of an object's class, or of the object itself if it's a class."""
+ if not hasattr(obj, "__mro__"):
+ # N.B. we cannot use `if type(obj) is type` here because it doesn't work with
+ # some types of classes, such as the ones that use abc.ABCMeta.
+ obj = type(obj)
+ return getattr(obj, "__mro__", ())
+
+
+def get_object_types_mro_as_strings(obj: object) -> Collection[str]:
+ """
+ Returns the MRO of an object's class as full qualified names, or of the object itself if it's a class.
+
+ Examples:
+ `object_types_mro_as_strings(JSONDecoder)` will return `['json.decoder.JSONDecoder', 'builtins.object']`
+ """
+ return [
+ f'{getattr(type_, "__module__", "")}.{getattr(type_, "__qualname__", "")}'
+ for type_ in get_object_types_mro(obj)
+ ]
+
+
+def is_object_one_of_types(
+ obj: object, fully_qualified_types_names: Collection[str]
+) -> bool:
+ """
+ Returns `True` if the given object's class (or the object itself, if it's a class) has one of the
+ fully qualified names in its MRO.
+ """
+ for type_name in get_object_types_mro_as_strings(obj):
+ if type_name in fully_qualified_types_names:
+ return True
+ return False
diff --git a/src/pip/_vendor/rich/_log_render.py b/src/pip/_vendor/rich/_log_render.py
new file mode 100644
index 000000000..fc16c8443
--- /dev/null
+++ b/src/pip/_vendor/rich/_log_render.py
@@ -0,0 +1,94 @@
+from datetime import datetime
+from typing import Iterable, List, Optional, TYPE_CHECKING, Union, Callable
+
+
+from .text import Text, TextType
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleRenderable, RenderableType
+ from .table import Table
+
+FormatTimeCallable = Callable[[datetime], Text]
+
+
+class LogRender:
+ def __init__(
+ self,
+ show_time: bool = True,
+ show_level: bool = False,
+ show_path: bool = True,
+ time_format: Union[str, FormatTimeCallable] = "[%x %X]",
+ omit_repeated_times: bool = True,
+ level_width: Optional[int] = 8,
+ ) -> None:
+ self.show_time = show_time
+ self.show_level = show_level
+ self.show_path = show_path
+ self.time_format = time_format
+ self.omit_repeated_times = omit_repeated_times
+ self.level_width = level_width
+ self._last_time: Optional[Text] = None
+
+ def __call__(
+ self,
+ console: "Console",
+ renderables: Iterable["ConsoleRenderable"],
+ log_time: Optional[datetime] = None,
+ time_format: Optional[Union[str, FormatTimeCallable]] = None,
+ level: TextType = "",
+ path: Optional[str] = None,
+ line_no: Optional[int] = None,
+ link_path: Optional[str] = None,
+ ) -> "Table":
+ from .containers import Renderables
+ from .table import Table
+
+ output = Table.grid(padding=(0, 1))
+ output.expand = True
+ if self.show_time:
+ output.add_column(style="log.time")
+ if self.show_level:
+ output.add_column(style="log.level", width=self.level_width)
+ output.add_column(ratio=1, style="log.message", overflow="fold")
+ if self.show_path and path:
+ output.add_column(style="log.path")
+ row: List["RenderableType"] = []
+ if self.show_time:
+ log_time = log_time or console.get_datetime()
+ time_format = time_format or self.time_format
+ if callable(time_format):
+ log_time_display = time_format(log_time)
+ else:
+ log_time_display = Text(log_time.strftime(time_format))
+ if log_time_display == self._last_time and self.omit_repeated_times:
+ row.append(Text(" " * len(log_time_display)))
+ else:
+ row.append(log_time_display)
+ self._last_time = log_time_display
+ if self.show_level:
+ row.append(level)
+
+ row.append(Renderables(renderables))
+ if self.show_path and path:
+ path_text = Text()
+ path_text.append(
+ path, style=f"link file://{link_path}" if link_path else ""
+ )
+ if line_no:
+ path_text.append(":")
+ path_text.append(
+ f"{line_no}",
+ style=f"link file://{link_path}#{line_no}" if link_path else "",
+ )
+ row.append(path_text)
+
+ output.add_row(*row)
+ return output
+
+
+if __name__ == "__main__": # pragma: no cover
+ from pip._vendor.rich.console import Console
+
+ c = Console()
+ c.print("[on blue]Hello", justify="right")
+ c.log("[on blue]hello", justify="right")
diff --git a/src/pip/_vendor/rich/_loop.py b/src/pip/_vendor/rich/_loop.py
new file mode 100644
index 000000000..01c6cafbe
--- /dev/null
+++ b/src/pip/_vendor/rich/_loop.py
@@ -0,0 +1,43 @@
+from typing import Iterable, Tuple, TypeVar
+
+T = TypeVar("T")
+
+
+def loop_first(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
+ """Iterate and generate a tuple with a flag for first value."""
+ iter_values = iter(values)
+ try:
+ value = next(iter_values)
+ except StopIteration:
+ return
+ yield True, value
+ for value in iter_values:
+ yield False, value
+
+
+def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
+ """Iterate and generate a tuple with a flag for last value."""
+ iter_values = iter(values)
+ try:
+ previous_value = next(iter_values)
+ except StopIteration:
+ return
+ for value in iter_values:
+ yield False, previous_value
+ previous_value = value
+ yield True, previous_value
+
+
+def loop_first_last(values: Iterable[T]) -> Iterable[Tuple[bool, bool, T]]:
+ """Iterate and generate a tuple with a flag for first and last value."""
+ iter_values = iter(values)
+ try:
+ previous_value = next(iter_values)
+ except StopIteration:
+ return
+ first = True
+ for value in iter_values:
+ yield first, False, previous_value
+ first = False
+ previous_value = value
+ yield first, True, previous_value
diff --git a/src/pip/_vendor/rich/_palettes.py b/src/pip/_vendor/rich/_palettes.py
new file mode 100644
index 000000000..3c748d33e
--- /dev/null
+++ b/src/pip/_vendor/rich/_palettes.py
@@ -0,0 +1,309 @@
+from .palette import Palette
+
+
+# Taken from https://en.wikipedia.org/wiki/ANSI_escape_code (Windows 10 column)
+WINDOWS_PALETTE = Palette(
+ [
+ (12, 12, 12),
+ (197, 15, 31),
+ (19, 161, 14),
+ (193, 156, 0),
+ (0, 55, 218),
+ (136, 23, 152),
+ (58, 150, 221),
+ (204, 204, 204),
+ (118, 118, 118),
+ (231, 72, 86),
+ (22, 198, 12),
+ (249, 241, 165),
+ (59, 120, 255),
+ (180, 0, 158),
+ (97, 214, 214),
+ (242, 242, 242),
+ ]
+)
+
+# # The standard ansi colors (including bright variants)
+STANDARD_PALETTE = Palette(
+ [
+ (0, 0, 0),
+ (170, 0, 0),
+ (0, 170, 0),
+ (170, 85, 0),
+ (0, 0, 170),
+ (170, 0, 170),
+ (0, 170, 170),
+ (170, 170, 170),
+ (85, 85, 85),
+ (255, 85, 85),
+ (85, 255, 85),
+ (255, 255, 85),
+ (85, 85, 255),
+ (255, 85, 255),
+ (85, 255, 255),
+ (255, 255, 255),
+ ]
+)
+
+
+# The 256 color palette
+EIGHT_BIT_PALETTE = Palette(
+ [
+ (0, 0, 0),
+ (128, 0, 0),
+ (0, 128, 0),
+ (128, 128, 0),
+ (0, 0, 128),
+ (128, 0, 128),
+ (0, 128, 128),
+ (192, 192, 192),
+ (128, 128, 128),
+ (255, 0, 0),
+ (0, 255, 0),
+ (255, 255, 0),
+ (0, 0, 255),
+ (255, 0, 255),
+ (0, 255, 255),
+ (255, 255, 255),
+ (0, 0, 0),
+ (0, 0, 95),
+ (0, 0, 135),
+ (0, 0, 175),
+ (0, 0, 215),
+ (0, 0, 255),
+ (0, 95, 0),
+ (0, 95, 95),
+ (0, 95, 135),
+ (0, 95, 175),
+ (0, 95, 215),
+ (0, 95, 255),
+ (0, 135, 0),
+ (0, 135, 95),
+ (0, 135, 135),
+ (0, 135, 175),
+ (0, 135, 215),
+ (0, 135, 255),
+ (0, 175, 0),
+ (0, 175, 95),
+ (0, 175, 135),
+ (0, 175, 175),
+ (0, 175, 215),
+ (0, 175, 255),
+ (0, 215, 0),
+ (0, 215, 95),
+ (0, 215, 135),
+ (0, 215, 175),
+ (0, 215, 215),
+ (0, 215, 255),
+ (0, 255, 0),
+ (0, 255, 95),
+ (0, 255, 135),
+ (0, 255, 175),
+ (0, 255, 215),
+ (0, 255, 255),
+ (95, 0, 0),
+ (95, 0, 95),
+ (95, 0, 135),
+ (95, 0, 175),
+ (95, 0, 215),
+ (95, 0, 255),
+ (95, 95, 0),
+ (95, 95, 95),
+ (95, 95, 135),
+ (95, 95, 175),
+ (95, 95, 215),
+ (95, 95, 255),
+ (95, 135, 0),
+ (95, 135, 95),
+ (95, 135, 135),
+ (95, 135, 175),
+ (95, 135, 215),
+ (95, 135, 255),
+ (95, 175, 0),
+ (95, 175, 95),
+ (95, 175, 135),
+ (95, 175, 175),
+ (95, 175, 215),
+ (95, 175, 255),
+ (95, 215, 0),
+ (95, 215, 95),
+ (95, 215, 135),
+ (95, 215, 175),
+ (95, 215, 215),
+ (95, 215, 255),
+ (95, 255, 0),
+ (95, 255, 95),
+ (95, 255, 135),
+ (95, 255, 175),
+ (95, 255, 215),
+ (95, 255, 255),
+ (135, 0, 0),
+ (135, 0, 95),
+ (135, 0, 135),
+ (135, 0, 175),
+ (135, 0, 215),
+ (135, 0, 255),
+ (135, 95, 0),
+ (135, 95, 95),
+ (135, 95, 135),
+ (135, 95, 175),
+ (135, 95, 215),
+ (135, 95, 255),
+ (135, 135, 0),
+ (135, 135, 95),
+ (135, 135, 135),
+ (135, 135, 175),
+ (135, 135, 215),
+ (135, 135, 255),
+ (135, 175, 0),
+ (135, 175, 95),
+ (135, 175, 135),
+ (135, 175, 175),
+ (135, 175, 215),
+ (135, 175, 255),
+ (135, 215, 0),
+ (135, 215, 95),
+ (135, 215, 135),
+ (135, 215, 175),
+ (135, 215, 215),
+ (135, 215, 255),
+ (135, 255, 0),
+ (135, 255, 95),
+ (135, 255, 135),
+ (135, 255, 175),
+ (135, 255, 215),
+ (135, 255, 255),
+ (175, 0, 0),
+ (175, 0, 95),
+ (175, 0, 135),
+ (175, 0, 175),
+ (175, 0, 215),
+ (175, 0, 255),
+ (175, 95, 0),
+ (175, 95, 95),
+ (175, 95, 135),
+ (175, 95, 175),
+ (175, 95, 215),
+ (175, 95, 255),
+ (175, 135, 0),
+ (175, 135, 95),
+ (175, 135, 135),
+ (175, 135, 175),
+ (175, 135, 215),
+ (175, 135, 255),
+ (175, 175, 0),
+ (175, 175, 95),
+ (175, 175, 135),
+ (175, 175, 175),
+ (175, 175, 215),
+ (175, 175, 255),
+ (175, 215, 0),
+ (175, 215, 95),
+ (175, 215, 135),
+ (175, 215, 175),
+ (175, 215, 215),
+ (175, 215, 255),
+ (175, 255, 0),
+ (175, 255, 95),
+ (175, 255, 135),
+ (175, 255, 175),
+ (175, 255, 215),
+ (175, 255, 255),
+ (215, 0, 0),
+ (215, 0, 95),
+ (215, 0, 135),
+ (215, 0, 175),
+ (215, 0, 215),
+ (215, 0, 255),
+ (215, 95, 0),
+ (215, 95, 95),
+ (215, 95, 135),
+ (215, 95, 175),
+ (215, 95, 215),
+ (215, 95, 255),
+ (215, 135, 0),
+ (215, 135, 95),
+ (215, 135, 135),
+ (215, 135, 175),
+ (215, 135, 215),
+ (215, 135, 255),
+ (215, 175, 0),
+ (215, 175, 95),
+ (215, 175, 135),
+ (215, 175, 175),
+ (215, 175, 215),
+ (215, 175, 255),
+ (215, 215, 0),
+ (215, 215, 95),
+ (215, 215, 135),
+ (215, 215, 175),
+ (215, 215, 215),
+ (215, 215, 255),
+ (215, 255, 0),
+ (215, 255, 95),
+ (215, 255, 135),
+ (215, 255, 175),
+ (215, 255, 215),
+ (215, 255, 255),
+ (255, 0, 0),
+ (255, 0, 95),
+ (255, 0, 135),
+ (255, 0, 175),
+ (255, 0, 215),
+ (255, 0, 255),
+ (255, 95, 0),
+ (255, 95, 95),
+ (255, 95, 135),
+ (255, 95, 175),
+ (255, 95, 215),
+ (255, 95, 255),
+ (255, 135, 0),
+ (255, 135, 95),
+ (255, 135, 135),
+ (255, 135, 175),
+ (255, 135, 215),
+ (255, 135, 255),
+ (255, 175, 0),
+ (255, 175, 95),
+ (255, 175, 135),
+ (255, 175, 175),
+ (255, 175, 215),
+ (255, 175, 255),
+ (255, 215, 0),
+ (255, 215, 95),
+ (255, 215, 135),
+ (255, 215, 175),
+ (255, 215, 215),
+ (255, 215, 255),
+ (255, 255, 0),
+ (255, 255, 95),
+ (255, 255, 135),
+ (255, 255, 175),
+ (255, 255, 215),
+ (255, 255, 255),
+ (8, 8, 8),
+ (18, 18, 18),
+ (28, 28, 28),
+ (38, 38, 38),
+ (48, 48, 48),
+ (58, 58, 58),
+ (68, 68, 68),
+ (78, 78, 78),
+ (88, 88, 88),
+ (98, 98, 98),
+ (108, 108, 108),
+ (118, 118, 118),
+ (128, 128, 128),
+ (138, 138, 138),
+ (148, 148, 148),
+ (158, 158, 158),
+ (168, 168, 168),
+ (178, 178, 178),
+ (188, 188, 188),
+ (198, 198, 198),
+ (208, 208, 208),
+ (218, 218, 218),
+ (228, 228, 228),
+ (238, 238, 238),
+ ]
+)
diff --git a/src/pip/_vendor/rich/_pick.py b/src/pip/_vendor/rich/_pick.py
new file mode 100644
index 000000000..4f6d8b2d7
--- /dev/null
+++ b/src/pip/_vendor/rich/_pick.py
@@ -0,0 +1,17 @@
+from typing import Optional
+
+
+def pick_bool(*values: Optional[bool]) -> bool:
+ """Pick the first non-none bool or return the last value.
+
+ Args:
+ *values (bool): Any number of boolean or None values.
+
+ Returns:
+ bool: First non-none boolean.
+ """
+ assert values, "1 or more values required"
+ for value in values:
+ if value is not None:
+ return value
+ return bool(value)
diff --git a/src/pip/_vendor/rich/_ratio.py b/src/pip/_vendor/rich/_ratio.py
new file mode 100644
index 000000000..e8a3a674e
--- /dev/null
+++ b/src/pip/_vendor/rich/_ratio.py
@@ -0,0 +1,160 @@
+import sys
+from fractions import Fraction
+from math import ceil
+from typing import cast, List, Optional, Sequence
+
+if sys.version_info >= (3, 8):
+ from typing import Protocol
+else:
+ from pip._vendor.typing_extensions import Protocol # pragma: no cover
+
+
+class Edge(Protocol):
+ """Any object that defines an edge (such as Layout)."""
+
+ size: Optional[int] = None
+ ratio: int = 1
+ minimum_size: int = 1
+
+
+def ratio_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
+ """Divide total space to satisfy size, ratio, and minimum_size, constraints.
+
+ The returned list of integers should add up to total in most cases, unless it is
+ impossible to satisfy all the constraints. For instance, if there are two edges
+ with a minimum size of 20 each and `total` is 30 then the returned list will be
+ greater than total. In practice, this would mean that a Layout object would
+ clip the rows that would overflow the screen height.
+
+ Args:
+ total (int): Total number of characters.
+ edges (List[Edge]): Edges within total space.
+
+ Returns:
+ List[int]: Number of characters for each edge.
+ """
+ # Size of edge or None for yet to be determined
+ sizes = [(edge.size or None) for edge in edges]
+
+ _Fraction = Fraction
+
+ # While any edges haven't been calculated
+ while None in sizes:
+ # Get flexible edges and index to map these back on to sizes list
+ flexible_edges = [
+ (index, edge)
+ for index, (size, edge) in enumerate(zip(sizes, edges))
+ if size is None
+ ]
+ # Remaining space in total
+ remaining = total - sum(size or 0 for size in sizes)
+ if remaining <= 0:
+ # No room for flexible edges
+ return [
+ ((edge.minimum_size or 1) if size is None else size)
+ for size, edge in zip(sizes, edges)
+ ]
+ # Calculate number of characters in a ratio portion
+ portion = _Fraction(
+ remaining, sum((edge.ratio or 1) for _, edge in flexible_edges)
+ )
+
+ # If any edges will be less than their minimum, replace size with the minimum
+ for index, edge in flexible_edges:
+ if portion * edge.ratio <= edge.minimum_size:
+ sizes[index] = edge.minimum_size
+ # New fixed size will invalidate calculations, so we need to repeat the process
+ break
+ else:
+ # Distribute flexible space and compensate for rounding error
+ # Since edge sizes can only be integers we need to add the remainder
+ # to the following line
+ remainder = _Fraction(0)
+ for index, edge in flexible_edges:
+ size, remainder = divmod(portion * edge.ratio + remainder, 1)
+ sizes[index] = size
+ break
+ # Sizes now contains integers only
+ return cast(List[int], sizes)
+
+
+def ratio_reduce(
+ total: int, ratios: List[int], maximums: List[int], values: List[int]
+) -> List[int]:
+ """Divide an integer total in to parts based on ratios.
+
+ Args:
+ total (int): The total to divide.
+ ratios (List[int]): A list of integer ratios.
+ maximums (List[int]): List of maximums values for each slot.
+ values (List[int]): List of values
+
+ Returns:
+ List[int]: A list of integers guaranteed to sum to total.
+ """
+ ratios = [ratio if _max else 0 for ratio, _max in zip(ratios, maximums)]
+ total_ratio = sum(ratios)
+ if not total_ratio:
+ return values[:]
+ total_remaining = total
+ result: List[int] = []
+ append = result.append
+ for ratio, maximum, value in zip(ratios, maximums, values):
+ if ratio and total_ratio > 0:
+ distributed = min(maximum, round(ratio * total_remaining / total_ratio))
+ append(value - distributed)
+ total_remaining -= distributed
+ total_ratio -= ratio
+ else:
+ append(value)
+ return result
+
+
+def ratio_distribute(
+ total: int, ratios: List[int], minimums: Optional[List[int]] = None
+) -> List[int]:
+ """Distribute an integer total in to parts based on ratios.
+
+ Args:
+ total (int): The total to divide.
+ ratios (List[int]): A list of integer ratios.
+ minimums (List[int]): List of minimum values for each slot.
+
+ Returns:
+ List[int]: A list of integers guaranteed to sum to total.
+ """
+ if minimums:
+ ratios = [ratio if _min else 0 for ratio, _min in zip(ratios, minimums)]
+ total_ratio = sum(ratios)
+ assert total_ratio > 0, "Sum of ratios must be > 0"
+
+ total_remaining = total
+ distributed_total: List[int] = []
+ append = distributed_total.append
+ if minimums is None:
+ _minimums = [0] * len(ratios)
+ else:
+ _minimums = minimums
+ for ratio, minimum in zip(ratios, _minimums):
+ if total_ratio > 0:
+ distributed = max(minimum, ceil(ratio * total_remaining / total_ratio))
+ else:
+ distributed = total_remaining
+ append(distributed)
+ total_ratio -= ratio
+ total_remaining -= distributed
+ return distributed_total
+
+
+if __name__ == "__main__":
+ from dataclasses import dataclass
+
+ @dataclass
+ class E:
+
+ size: Optional[int] = None
+ ratio: int = 1
+ minimum_size: int = 1
+
+ resolved = ratio_resolve(110, [E(None, 1, 1), E(None, 1, 1), E(None, 1, 1)])
+ print(sum(resolved))
diff --git a/src/pip/_vendor/rich/_spinners.py b/src/pip/_vendor/rich/_spinners.py
new file mode 100644
index 000000000..d0bb1fe75
--- /dev/null
+++ b/src/pip/_vendor/rich/_spinners.py
@@ -0,0 +1,482 @@
+"""
+Spinners are from:
+* cli-spinners:
+ MIT License
+ Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
+ 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.
+"""
+
+SPINNERS = {
+ "dots": {
+ "interval": 80,
+ "frames": "⠋⠙⠹⠸⠼⠴⠦⠧⠇â ",
+ },
+ "dots2": {"interval": 80, "frames": "⣾⣽⣻⢿⡿⣟⣯⣷"},
+ "dots3": {
+ "interval": 80,
+ "frames": "⠋⠙⠚⠞⠖⠦⠴⠲⠳⠓",
+ },
+ "dots4": {
+ "interval": 80,
+ "frames": "⠄⠆⠇⠋⠙⠸⠰⠠⠰⠸⠙⠋⠇⠆",
+ },
+ "dots5": {
+ "interval": 80,
+ "frames": "⠋⠙⠚⠒⠂⠂⠒⠲⠴⠦⠖⠒â â â ’â “â ‹",
+ },
+ "dots6": {
+ "interval": 80,
+ "frames": "â â ‰â ™â šâ ’⠂⠂⠒⠲⠴⠤⠄⠄⠤⠴⠲⠒⠂⠂⠒⠚⠙⠉â ",
+ },
+ "dots7": {
+ "interval": 80,
+ "frames": "⠈⠉⠋⠓⠒â â â ’⠖⠦⠤⠠⠠⠤⠦⠖⠒â â â ’⠓⠋⠉⠈",
+ },
+ "dots8": {
+ "interval": 80,
+ "frames": "â â â ‰â ™â šâ ’⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒â â â ’⠓⠋⠉⠈⠈",
+ },
+ "dots9": {"interval": 80, "frames": "⢹⢺⢼⣸⣇⡧⡗â¡"},
+ "dots10": {"interval": 80, "frames": "⢄⢂â¢â¡â¡ˆâ¡â¡ "},
+ "dots11": {"interval": 100, "frames": "â â ‚⠄⡀⢀⠠â â ˆ"},
+ "dots12": {
+ "interval": 80,
+ "frames": [
+ "⢀⠀",
+ "⡀⠀",
+ "â „â €",
+ "⢂⠀",
+ "â¡‚â €",
+ "â …â €",
+ "⢃⠀",
+ "⡃⠀",
+ "â â €",
+ "⢋⠀",
+ "â¡‹â €",
+ "â â ",
+ "⢋â ",
+ "â¡‹â ",
+ "â â ‰",
+ "â ‹â ‰",
+ "â ‹â ‰",
+ "⠉⠙",
+ "⠉⠙",
+ "⠉⠩",
+ "⠈⢙",
+ "⠈⡙",
+ "⢈⠩",
+ "⡀⢙",
+ "â „â¡™",
+ "⢂⠩",
+ "⡂⢘",
+ "⠅⡘",
+ "⢃⠨",
+ "⡃â¢",
+ "â â¡",
+ "⢋⠠",
+ "⡋⢀",
+ "â â¡",
+ "⢋â ",
+ "â¡‹â ",
+ "â â ‰",
+ "â ‹â ‰",
+ "â ‹â ‰",
+ "⠉⠙",
+ "⠉⠙",
+ "⠉⠩",
+ "⠈⢙",
+ "⠈⡙",
+ "⠈⠩",
+ "⠀⢙",
+ "⠀⡙",
+ "⠀⠩",
+ "⠀⢘",
+ "⠀⡘",
+ "⠀⠨",
+ "â €â¢",
+ "â €â¡",
+ "⠀⠠",
+ "⠀⢀",
+ "⠀⡀",
+ ],
+ },
+ "dots8Bit": {
+ "interval": 80,
+ "frames": "â €â â ‚⠃⠄⠅⠆⠇⡀â¡â¡‚⡃⡄⡅⡆⡇⠈⠉⠊⠋⠌â â Žâ â¡ˆâ¡‰â¡Šâ¡‹â¡Œâ¡â¡Žâ¡â â ‘⠒⠓⠔⠕⠖⠗â¡â¡‘⡒⡓⡔⡕⡖⡗⠘⠙⠚⠛⠜â â žâ Ÿâ¡˜â¡™"
+ "⡚⡛⡜â¡â¡žâ¡Ÿâ  â ¡â ¢â £â ¤â ¥â ¦â §â¡ â¡¡â¡¢â¡£â¡¤â¡¥â¡¦â¡§â ¨â ©â ªâ «â ¬â ­â ®â ¯â¡¨â¡©â¡ªâ¡«â¡¬â¡­â¡®â¡¯â °â ±â ²â ³â ´â µâ ¶â ·â¡°â¡±â¡²â¡³â¡´â¡µâ¡¶â¡·â ¸â ¹â ºâ »"
+ "⠼⠽⠾⠿⡸⡹⡺⡻⡼⡽⡾⡿⢀â¢â¢‚⢃⢄⢅⢆⢇⣀â£â£‚⣃⣄⣅⣆⣇⢈⢉⢊⢋⢌â¢â¢Žâ¢â£ˆâ£‰â£Šâ£‹â£Œâ£â£Žâ£â¢â¢‘⢒⢓⢔⢕⢖⢗â£â£‘⣒⣓⣔⣕"
+ "⣖⣗⢘⢙⢚⢛⢜â¢â¢žâ¢Ÿâ£˜â£™â£šâ£›â£œâ£â£žâ£Ÿâ¢ â¢¡â¢¢â¢£â¢¤â¢¥â¢¦â¢§â£ â£¡â£¢â££â£¤â£¥â£¦â£§â¢¨â¢©â¢ªâ¢«â¢¬â¢­â¢®â¢¯â£¨â£©â£ªâ£«â£¬â£­â£®â£¯â¢°â¢±â¢²â¢³â¢´â¢µâ¢¶â¢·"
+ "⣰⣱⣲⣳⣴⣵⣶⣷⢸⢹⢺⢻⢼⢽⢾⢿⣸⣹⣺⣻⣼⣽⣾⣿",
+ },
+ "line": {"interval": 130, "frames": ["-", "\\", "|", "/"]},
+ "line2": {"interval": 100, "frames": "⠂-–—–-"},
+ "pipe": {"interval": 100, "frames": "┤┘┴└├┌┬â”"},
+ "simpleDots": {"interval": 400, "frames": [". ", ".. ", "...", " "]},
+ "simpleDotsScrolling": {
+ "interval": 200,
+ "frames": [". ", ".. ", "...", " ..", " .", " "],
+ },
+ "star": {"interval": 70, "frames": "✶✸✹✺✹✷"},
+ "star2": {"interval": 80, "frames": "+x*"},
+ "flip": {
+ "interval": 70,
+ "frames": "___-``'´-___",
+ },
+ "hamburger": {"interval": 100, "frames": "☱☲☴"},
+ "growVertical": {
+ "interval": 120,
+ "frames": "â–▃▄▅▆▇▆▅▄▃",
+ },
+ "growHorizontal": {
+ "interval": 120,
+ "frames": "â–â–Žâ–▌▋▊▉▊▋▌â–â–Ž",
+ },
+ "balloon": {"interval": 140, "frames": " .oO@* "},
+ "balloon2": {"interval": 120, "frames": ".oO°Oo."},
+ "noise": {"interval": 100, "frames": "â–“â–’â–‘"},
+ "bounce": {"interval": 120, "frames": "â â ‚â „â ‚"},
+ "boxBounce": {"interval": 120, "frames": "â––â–˜â–â–—"},
+ "boxBounce2": {"interval": 100, "frames": "▌▀â–â–„"},
+ "triangle": {"interval": 50, "frames": "◢◣◤◥"},
+ "arc": {"interval": 100, "frames": "◜◠â—â—žâ—¡â—Ÿ"},
+ "circle": {"interval": 120, "frames": "◡⊙◠"},
+ "squareCorners": {"interval": 180, "frames": "◰◳◲◱"},
+ "circleQuarters": {"interval": 120, "frames": "◴◷◶◵"},
+ "circleHalves": {"interval": 50, "frames": "â—â—“â—‘â—’"},
+ "squish": {"interval": 100, "frames": "╫╪"},
+ "toggle": {"interval": 250, "frames": "⊶⊷"},
+ "toggle2": {"interval": 80, "frames": "â–«â–ª"},
+ "toggle3": {"interval": 120, "frames": "â–¡â– "},
+ "toggle4": {"interval": 100, "frames": "■□▪▫"},
+ "toggle5": {"interval": 100, "frames": "▮▯"},
+ "toggle6": {"interval": 300, "frames": "á€á€"},
+ "toggle7": {"interval": 80, "frames": "⦾⦿"},
+ "toggle8": {"interval": 100, "frames": "â—â—Œ"},
+ "toggle9": {"interval": 100, "frames": "◉◎"},
+ "toggle10": {"interval": 100, "frames": "㊂㊀ãŠ"},
+ "toggle11": {"interval": 50, "frames": "⧇⧆"},
+ "toggle12": {"interval": 120, "frames": "☗☖"},
+ "toggle13": {"interval": 80, "frames": "=*-"},
+ "arrow": {"interval": 100, "frames": "â†â†–↑↗→↘↓↙"},
+ "arrow2": {
+ "interval": 80,
+ "frames": ["â¬†ï¸ ", "â†—ï¸ ", "âž¡ï¸ ", "â†˜ï¸ ", "â¬‡ï¸ ", "â†™ï¸ ", "â¬…ï¸ ", "â†–ï¸ "],
+ },
+ "arrow3": {
+ "interval": 120,
+ "frames": ["▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"],
+ },
+ "bouncingBar": {
+ "interval": 80,
+ "frames": [
+ "[ ]",
+ "[= ]",
+ "[== ]",
+ "[=== ]",
+ "[ ===]",
+ "[ ==]",
+ "[ =]",
+ "[ ]",
+ "[ =]",
+ "[ ==]",
+ "[ ===]",
+ "[====]",
+ "[=== ]",
+ "[== ]",
+ "[= ]",
+ ],
+ },
+ "bouncingBall": {
+ "interval": 80,
+ "frames": [
+ "( â— )",
+ "( â— )",
+ "( â— )",
+ "( â— )",
+ "( â—)",
+ "( â— )",
+ "( â— )",
+ "( â— )",
+ "( â— )",
+ "(â— )",
+ ],
+ },
+ "smiley": {"interval": 200, "frames": ["😄 ", "😠"]},
+ "monkey": {"interval": 300, "frames": ["🙈 ", "🙈 ", "🙉 ", "🙊 "]},
+ "hearts": {"interval": 100, "frames": ["💛 ", "💙 ", "💜 ", "💚 ", "â¤ï¸ "]},
+ "clock": {
+ "interval": 100,
+ "frames": [
+ "🕛 ",
+ "🕠",
+ "🕑 ",
+ "🕒 ",
+ "🕓 ",
+ "🕔 ",
+ "🕕 ",
+ "🕖 ",
+ "🕗 ",
+ "🕘 ",
+ "🕙 ",
+ "🕚 ",
+ ],
+ },
+ "earth": {"interval": 180, "frames": ["🌠", "🌎 ", "🌠"]},
+ "material": {
+ "interval": 17,
+ "frames": [
+ "â–ˆâ–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–",
+ "██â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–",
+ "███â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–",
+ "████â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–",
+ "██████â–â–â–â–â–â–â–â–â–â–â–â–â–â–",
+ "██████â–â–â–â–â–â–â–â–â–â–â–â–â–â–",
+ "███████â–â–â–â–â–â–â–â–â–â–â–â–â–",
+ "████████â–â–â–â–â–â–â–â–â–â–â–â–",
+ "█████████â–â–â–â–â–â–â–â–â–â–â–",
+ "█████████â–â–â–â–â–â–â–â–â–â–â–",
+ "██████████â–â–â–â–â–â–â–â–â–â–",
+ "███████████â–â–â–â–â–â–â–â–â–",
+ "█████████████â–â–â–â–â–â–â–",
+ "██████████████â–â–â–â–â–â–",
+ "██████████████â–â–â–â–â–â–",
+ "â–██████████████â–â–â–â–â–",
+ "â–██████████████â–â–â–â–â–",
+ "â–██████████████â–â–â–â–â–",
+ "â–â–██████████████â–â–â–â–",
+ "â–â–â–██████████████â–â–â–",
+ "â–â–â–â–█████████████â–â–â–",
+ "â–â–â–â–██████████████â–â–",
+ "â–â–â–â–██████████████â–â–",
+ "â–â–â–â–â–██████████████â–",
+ "â–â–â–â–â–██████████████â–",
+ "â–â–â–â–â–██████████████â–",
+ "â–â–â–â–â–â–██████████████",
+ "â–â–â–â–â–â–██████████████",
+ "â–â–â–â–â–â–â–█████████████",
+ "â–â–â–â–â–â–â–█████████████",
+ "â–â–â–â–â–â–â–â–████████████",
+ "â–â–â–â–â–â–â–â–████████████",
+ "â–â–â–â–â–â–â–â–â–███████████",
+ "â–â–â–â–â–â–â–â–â–███████████",
+ "â–â–â–â–â–â–â–â–â–â–██████████",
+ "â–â–â–â–â–â–â–â–â–â–██████████",
+ "â–â–â–â–â–â–â–â–â–â–â–â–████████",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–███████",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–██████",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–█████",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–█████",
+ "â–ˆâ–â–â–â–â–â–â–â–â–â–â–â–â–â–â–████",
+ "██â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–███",
+ "██â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–███",
+ "███â–â–â–â–â–â–â–â–â–â–â–â–â–â–███",
+ "████â–â–â–â–â–â–â–â–â–â–â–â–â–â–██",
+ "█████â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–ˆ",
+ "█████â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–ˆ",
+ "██████â–â–â–â–â–â–â–â–â–â–â–â–â–â–ˆ",
+ "████████â–â–â–â–â–â–â–â–â–â–â–â–",
+ "█████████â–â–â–â–â–â–â–â–â–â–â–",
+ "█████████â–â–â–â–â–â–â–â–â–â–â–",
+ "█████████â–â–â–â–â–â–â–â–â–â–â–",
+ "█████████â–â–â–â–â–â–â–â–â–â–â–",
+ "███████████â–â–â–â–â–â–â–â–â–",
+ "████████████â–â–â–â–â–â–â–â–",
+ "████████████â–â–â–â–â–â–â–â–",
+ "██████████████â–â–â–â–â–â–",
+ "██████████████â–â–â–â–â–â–",
+ "â–██████████████â–â–â–â–â–",
+ "â–██████████████â–â–â–â–â–",
+ "â–â–â–█████████████â–â–â–â–",
+ "â–â–â–â–â–████████████â–â–â–",
+ "â–â–â–â–â–████████████â–â–â–",
+ "â–â–â–â–â–â–███████████â–â–â–",
+ "â–â–â–â–â–â–â–â–█████████â–â–â–",
+ "â–â–â–â–â–â–â–â–█████████â–â–â–",
+ "â–â–â–â–â–â–â–â–â–█████████â–â–",
+ "â–â–â–â–â–â–â–â–â–█████████â–â–",
+ "â–â–â–â–â–â–â–â–â–â–█████████â–",
+ "â–â–â–â–â–â–â–â–â–â–â–████████â–",
+ "â–â–â–â–â–â–â–â–â–â–â–████████â–",
+ "â–â–â–â–â–â–â–â–â–â–â–â–███████â–",
+ "â–â–â–â–â–â–â–â–â–â–â–â–███████â–",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–███████",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–███████",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–█████",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–████",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–████",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–████",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–███",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–███",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–██",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–██",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–██",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–ˆ",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–ˆ",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–ˆ",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–",
+ "â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–â–",
+ ],
+ },
+ "moon": {
+ "interval": 80,
+ "frames": ["🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "],
+ },
+ "runner": {"interval": 140, "frames": ["🚶 ", "🃠"]},
+ "pong": {
+ "interval": 80,
+ "frames": [
+ "â–â ‚ â–Œ",
+ "â–â ˆ â–Œ",
+ "■⠂ ▌",
+ "■⠠ ▌",
+ "■⡀ ▌",
+ "■⠠ ▌",
+ "■⠂ ▌",
+ "■⠈ ▌",
+ "■⠂ ▌",
+ "■⠠ ▌",
+ "■⡀ ▌",
+ "■⠠ ▌",
+ "■⠂ ▌",
+ "■⠈ ▌",
+ "■⠂▌",
+ "■⠠▌",
+ "■⡀▌",
+ "■⠠ ▌",
+ "■⠂ ▌",
+ "■⠈ ▌",
+ "■⠂ ▌",
+ "■⠠ ▌",
+ "■⡀ ▌",
+ "■⠠ ▌",
+ "■⠂ ▌",
+ "■⠈ ▌",
+ "■⠂ ▌",
+ "■⠠ ▌",
+ "■⡀ ▌",
+ "â–â   â–Œ",
+ ],
+ },
+ "shark": {
+ "interval": 120,
+ "frames": [
+ "â–|\\____________â–Œ",
+ "â–_|\\___________â–Œ",
+ "â–__|\\__________â–Œ",
+ "â–___|\\_________â–Œ",
+ "â–____|\\________â–Œ",
+ "â–_____|\\_______â–Œ",
+ "â–______|\\______â–Œ",
+ "â–_______|\\_____â–Œ",
+ "â–________|\\____â–Œ",
+ "â–_________|\\___â–Œ",
+ "â–__________|\\__â–Œ",
+ "â–___________|\\_â–Œ",
+ "â–____________|\\â–Œ",
+ "â–____________/|â–Œ",
+ "â–___________/|_â–Œ",
+ "â–__________/|__â–Œ",
+ "â–_________/|___â–Œ",
+ "â–________/|____â–Œ",
+ "â–_______/|_____â–Œ",
+ "â–______/|______â–Œ",
+ "â–_____/|_______â–Œ",
+ "â–____/|________â–Œ",
+ "â–___/|_________â–Œ",
+ "â–__/|__________â–Œ",
+ "â–_/|___________â–Œ",
+ "â–/|____________â–Œ",
+ ],
+ },
+ "dqpb": {"interval": 100, "frames": "dqpb"},
+ "weather": {
+ "interval": 100,
+ "frames": [
+ "â˜€ï¸ ",
+ "â˜€ï¸ ",
+ "â˜€ï¸ ",
+ "🌤 ",
+ "â›…ï¸ ",
+ "🌥 ",
+ "â˜ï¸ ",
+ "🌧 ",
+ "🌨 ",
+ "🌧 ",
+ "🌨 ",
+ "🌧 ",
+ "🌨 ",
+ "⛈ ",
+ "🌨 ",
+ "🌧 ",
+ "🌨 ",
+ "â˜ï¸ ",
+ "🌥 ",
+ "â›…ï¸ ",
+ "🌤 ",
+ "â˜€ï¸ ",
+ "â˜€ï¸ ",
+ ],
+ },
+ "christmas": {"interval": 400, "frames": "🌲🎄"},
+ "grenade": {
+ "interval": 80,
+ "frames": [
+ "، ",
+ "′ ",
+ " ´ ",
+ " ‾ ",
+ " ⸌",
+ " ⸊",
+ " |",
+ " âŽ",
+ " â•",
+ " à·´ ",
+ " â“",
+ " ",
+ " ",
+ " ",
+ ],
+ },
+ "point": {"interval": 125, "frames": ["∙∙∙", "â—∙∙", "∙â—∙", "∙∙â—", "∙∙∙"]},
+ "layer": {"interval": 150, "frames": "-=≡"},
+ "betaWave": {
+ "interval": 80,
+ "frames": [
+ "Ïββββββ",
+ "βÏβββββ",
+ "ββÏββββ",
+ "βββÏβββ",
+ "ββββÏββ",
+ "βββββÏβ",
+ "ββββββÏ",
+ ],
+ },
+ "aesthetic": {
+ "interval": 80,
+ "frames": [
+ "▰▱▱▱▱▱▱",
+ "▰▰▱▱▱▱▱",
+ "▰▰▰▱▱▱▱",
+ "▰▰▰▰▱▱▱",
+ "▰▰▰▰▰▱▱",
+ "â–°â–°â–°â–°â–°â–°â–±",
+ "â–°â–°â–°â–°â–°â–°â–°",
+ "▰▱▱▱▱▱▱",
+ ],
+ },
+}
diff --git a/src/pip/_vendor/rich/_stack.py b/src/pip/_vendor/rich/_stack.py
new file mode 100644
index 000000000..194564e76
--- /dev/null
+++ b/src/pip/_vendor/rich/_stack.py
@@ -0,0 +1,16 @@
+from typing import List, TypeVar
+
+T = TypeVar("T")
+
+
+class Stack(List[T]):
+ """A small shim over builtin list."""
+
+ @property
+ def top(self) -> T:
+ """Get top of stack."""
+ return self[-1]
+
+ def push(self, item: T) -> None:
+ """Push an item on to the stack (append in stack nomenclature)."""
+ self.append(item)
diff --git a/src/pip/_vendor/rich/_timer.py b/src/pip/_vendor/rich/_timer.py
new file mode 100644
index 000000000..a2ca6be03
--- /dev/null
+++ b/src/pip/_vendor/rich/_timer.py
@@ -0,0 +1,19 @@
+"""
+Timer context manager, only used in debug.
+
+"""
+
+from time import time
+
+import contextlib
+from typing import Generator
+
+
+@contextlib.contextmanager
+def timer(subject: str = "time") -> Generator[None, None, None]:
+ """print the elapsed time. (only used in debugging)"""
+ start = time()
+ yield
+ elapsed = time() - start
+ elapsed_ms = elapsed * 1000
+ print(f"{subject} elapsed {elapsed_ms:.1f}ms")
diff --git a/src/pip/_vendor/rich/_win32_console.py b/src/pip/_vendor/rich/_win32_console.py
new file mode 100644
index 000000000..81b108290
--- /dev/null
+++ b/src/pip/_vendor/rich/_win32_console.py
@@ -0,0 +1,662 @@
+"""Light wrapper around the Win32 Console API - this module should only be imported on Windows
+
+The API that this module wraps is documented at https://docs.microsoft.com/en-us/windows/console/console-functions
+"""
+import ctypes
+import sys
+from typing import Any
+
+windll: Any = None
+if sys.platform == "win32":
+ windll = ctypes.LibraryLoader(ctypes.WinDLL)
+else:
+ raise ImportError(f"{__name__} can only be imported on Windows")
+
+import time
+from ctypes import Structure, byref, wintypes
+from typing import IO, NamedTuple, Type, cast
+
+from pip._vendor.rich.color import ColorSystem
+from pip._vendor.rich.style import Style
+
+STDOUT = -11
+ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
+
+COORD = wintypes._COORD
+
+
+class LegacyWindowsError(Exception):
+ pass
+
+
+class WindowsCoordinates(NamedTuple):
+ """Coordinates in the Windows Console API are (y, x), not (x, y).
+ This class is intended to prevent that confusion.
+ Rows and columns are indexed from 0.
+ This class can be used in place of wintypes._COORD in arguments and argtypes.
+ """
+
+ row: int
+ col: int
+
+ @classmethod
+ def from_param(cls, value: "WindowsCoordinates") -> COORD:
+ """Converts a WindowsCoordinates into a wintypes _COORD structure.
+ This classmethod is internally called by ctypes to perform the conversion.
+
+ Args:
+ value (WindowsCoordinates): The input coordinates to convert.
+
+ Returns:
+ wintypes._COORD: The converted coordinates struct.
+ """
+ return COORD(value.col, value.row)
+
+
+class CONSOLE_SCREEN_BUFFER_INFO(Structure):
+ _fields_ = [
+ ("dwSize", COORD),
+ ("dwCursorPosition", COORD),
+ ("wAttributes", wintypes.WORD),
+ ("srWindow", wintypes.SMALL_RECT),
+ ("dwMaximumWindowSize", COORD),
+ ]
+
+
+class CONSOLE_CURSOR_INFO(ctypes.Structure):
+ _fields_ = [("dwSize", wintypes.DWORD), ("bVisible", wintypes.BOOL)]
+
+
+_GetStdHandle = windll.kernel32.GetStdHandle
+_GetStdHandle.argtypes = [
+ wintypes.DWORD,
+]
+_GetStdHandle.restype = wintypes.HANDLE
+
+
+def GetStdHandle(handle: int = STDOUT) -> wintypes.HANDLE:
+ """Retrieves a handle to the specified standard device (standard input, standard output, or standard error).
+
+ Args:
+ handle (int): Integer identifier for the handle. Defaults to -11 (stdout).
+
+ Returns:
+ wintypes.HANDLE: The handle
+ """
+ return cast(wintypes.HANDLE, _GetStdHandle(handle))
+
+
+_GetConsoleMode = windll.kernel32.GetConsoleMode
+_GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD]
+_GetConsoleMode.restype = wintypes.BOOL
+
+
+def GetConsoleMode(std_handle: wintypes.HANDLE) -> int:
+ """Retrieves the current input mode of a console's input buffer
+ or the current output mode of a console screen buffer.
+
+ Args:
+ std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+
+ Raises:
+ LegacyWindowsError: If any error occurs while calling the Windows console API.
+
+ Returns:
+ int: Value representing the current console mode as documented at
+ https://docs.microsoft.com/en-us/windows/console/getconsolemode#parameters
+ """
+
+ console_mode = wintypes.DWORD()
+ success = bool(_GetConsoleMode(std_handle, console_mode))
+ if not success:
+ raise LegacyWindowsError("Unable to get legacy Windows Console Mode")
+ return console_mode.value
+
+
+_FillConsoleOutputCharacterW = windll.kernel32.FillConsoleOutputCharacterW
+_FillConsoleOutputCharacterW.argtypes = [
+ wintypes.HANDLE,
+ ctypes.c_char,
+ wintypes.DWORD,
+ cast(Type[COORD], WindowsCoordinates),
+ ctypes.POINTER(wintypes.DWORD),
+]
+_FillConsoleOutputCharacterW.restype = wintypes.BOOL
+
+
+def FillConsoleOutputCharacter(
+ std_handle: wintypes.HANDLE,
+ char: str,
+ length: int,
+ start: WindowsCoordinates,
+) -> int:
+ """Writes a character to the console screen buffer a specified number of times, beginning at the specified coordinates.
+
+ Args:
+ std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+ char (str): The character to write. Must be a string of length 1.
+ length (int): The number of times to write the character.
+ start (WindowsCoordinates): The coordinates to start writing at.
+
+ Returns:
+ int: The number of characters written.
+ """
+ character = ctypes.c_char(char.encode())
+ num_characters = wintypes.DWORD(length)
+ num_written = wintypes.DWORD(0)
+ _FillConsoleOutputCharacterW(
+ std_handle,
+ character,
+ num_characters,
+ start,
+ byref(num_written),
+ )
+ return num_written.value
+
+
+_FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute
+_FillConsoleOutputAttribute.argtypes = [
+ wintypes.HANDLE,
+ wintypes.WORD,
+ wintypes.DWORD,
+ cast(Type[COORD], WindowsCoordinates),
+ ctypes.POINTER(wintypes.DWORD),
+]
+_FillConsoleOutputAttribute.restype = wintypes.BOOL
+
+
+def FillConsoleOutputAttribute(
+ std_handle: wintypes.HANDLE,
+ attributes: int,
+ length: int,
+ start: WindowsCoordinates,
+) -> int:
+ """Sets the character attributes for a specified number of character cells,
+ beginning at the specified coordinates in a screen buffer.
+
+ Args:
+ std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+ attributes (int): Integer value representing the foreground and background colours of the cells.
+ length (int): The number of cells to set the output attribute of.
+ start (WindowsCoordinates): The coordinates of the first cell whose attributes are to be set.
+
+ Returns:
+ int: The number of cells whose attributes were actually set.
+ """
+ num_cells = wintypes.DWORD(length)
+ style_attrs = wintypes.WORD(attributes)
+ num_written = wintypes.DWORD(0)
+ _FillConsoleOutputAttribute(
+ std_handle, style_attrs, num_cells, start, byref(num_written)
+ )
+ return num_written.value
+
+
+_SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute
+_SetConsoleTextAttribute.argtypes = [
+ wintypes.HANDLE,
+ wintypes.WORD,
+]
+_SetConsoleTextAttribute.restype = wintypes.BOOL
+
+
+def SetConsoleTextAttribute(
+ std_handle: wintypes.HANDLE, attributes: wintypes.WORD
+) -> bool:
+ """Set the colour attributes for all text written after this function is called.
+
+ Args:
+ std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+ attributes (int): Integer value representing the foreground and background colours.
+
+
+ Returns:
+ bool: True if the attribute was set successfully, otherwise False.
+ """
+ return bool(_SetConsoleTextAttribute(std_handle, attributes))
+
+
+_GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo
+_GetConsoleScreenBufferInfo.argtypes = [
+ wintypes.HANDLE,
+ ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO),
+]
+_GetConsoleScreenBufferInfo.restype = wintypes.BOOL
+
+
+def GetConsoleScreenBufferInfo(
+ std_handle: wintypes.HANDLE,
+) -> CONSOLE_SCREEN_BUFFER_INFO:
+ """Retrieves information about the specified console screen buffer.
+
+ Args:
+ std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+
+ Returns:
+ CONSOLE_SCREEN_BUFFER_INFO: A CONSOLE_SCREEN_BUFFER_INFO ctype struct contain information about
+ screen size, cursor position, colour attributes, and more."""
+ console_screen_buffer_info = CONSOLE_SCREEN_BUFFER_INFO()
+ _GetConsoleScreenBufferInfo(std_handle, byref(console_screen_buffer_info))
+ return console_screen_buffer_info
+
+
+_SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition
+_SetConsoleCursorPosition.argtypes = [
+ wintypes.HANDLE,
+ cast(Type[COORD], WindowsCoordinates),
+]
+_SetConsoleCursorPosition.restype = wintypes.BOOL
+
+
+def SetConsoleCursorPosition(
+ std_handle: wintypes.HANDLE, coords: WindowsCoordinates
+) -> bool:
+ """Set the position of the cursor in the console screen
+
+ Args:
+ std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+ coords (WindowsCoordinates): The coordinates to move the cursor to.
+
+ Returns:
+ bool: True if the function succeeds, otherwise False.
+ """
+ return bool(_SetConsoleCursorPosition(std_handle, coords))
+
+
+_GetConsoleCursorInfo = windll.kernel32.GetConsoleCursorInfo
+_GetConsoleCursorInfo.argtypes = [
+ wintypes.HANDLE,
+ ctypes.POINTER(CONSOLE_CURSOR_INFO),
+]
+_GetConsoleCursorInfo.restype = wintypes.BOOL
+
+
+def GetConsoleCursorInfo(
+ std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO
+) -> bool:
+ """Get the cursor info - used to get cursor visibility and width
+
+ Args:
+ std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+ cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct that receives information
+ about the console's cursor.
+
+ Returns:
+ bool: True if the function succeeds, otherwise False.
+ """
+ return bool(_GetConsoleCursorInfo(std_handle, byref(cursor_info)))
+
+
+_SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo
+_SetConsoleCursorInfo.argtypes = [
+ wintypes.HANDLE,
+ ctypes.POINTER(CONSOLE_CURSOR_INFO),
+]
+_SetConsoleCursorInfo.restype = wintypes.BOOL
+
+
+def SetConsoleCursorInfo(
+ std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO
+) -> bool:
+ """Set the cursor info - used for adjusting cursor visibility and width
+
+ Args:
+ std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer.
+ cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct containing the new cursor info.
+
+ Returns:
+ bool: True if the function succeeds, otherwise False.
+ """
+ return bool(_SetConsoleCursorInfo(std_handle, byref(cursor_info)))
+
+
+_SetConsoleTitle = windll.kernel32.SetConsoleTitleW
+_SetConsoleTitle.argtypes = [wintypes.LPCWSTR]
+_SetConsoleTitle.restype = wintypes.BOOL
+
+
+def SetConsoleTitle(title: str) -> bool:
+ """Sets the title of the current console window
+
+ Args:
+ title (str): The new title of the console window.
+
+ Returns:
+ bool: True if the function succeeds, otherwise False.
+ """
+ return bool(_SetConsoleTitle(title))
+
+
+class LegacyWindowsTerm:
+ """This class allows interaction with the legacy Windows Console API. It should only be used in the context
+ of environments where virtual terminal processing is not available. However, if it is used in a Windows environment,
+ the entire API should work.
+
+ Args:
+ file (IO[str]): The file which the Windows Console API HANDLE is retrieved from, defaults to sys.stdout.
+ """
+
+ BRIGHT_BIT = 8
+
+ # Indices are ANSI color numbers, values are the corresponding Windows Console API color numbers
+ ANSI_TO_WINDOWS = [
+ 0, # black The Windows colours are defined in wincon.h as follows:
+ 4, # red define FOREGROUND_BLUE 0x0001 -- 0000 0001
+ 2, # green define FOREGROUND_GREEN 0x0002 -- 0000 0010
+ 6, # yellow define FOREGROUND_RED 0x0004 -- 0000 0100
+ 1, # blue define FOREGROUND_INTENSITY 0x0008 -- 0000 1000
+ 5, # magenta define BACKGROUND_BLUE 0x0010 -- 0001 0000
+ 3, # cyan define BACKGROUND_GREEN 0x0020 -- 0010 0000
+ 7, # white define BACKGROUND_RED 0x0040 -- 0100 0000
+ 8, # bright black (grey) define BACKGROUND_INTENSITY 0x0080 -- 1000 0000
+ 12, # bright red
+ 10, # bright green
+ 14, # bright yellow
+ 9, # bright blue
+ 13, # bright magenta
+ 11, # bright cyan
+ 15, # bright white
+ ]
+
+ def __init__(self, file: "IO[str]") -> None:
+ handle = GetStdHandle(STDOUT)
+ self._handle = handle
+ default_text = GetConsoleScreenBufferInfo(handle).wAttributes
+ self._default_text = default_text
+
+ self._default_fore = default_text & 7
+ self._default_back = (default_text >> 4) & 7
+ self._default_attrs = self._default_fore | (self._default_back << 4)
+
+ self._file = file
+ self.write = file.write
+ self.flush = file.flush
+
+ @property
+ def cursor_position(self) -> WindowsCoordinates:
+ """Returns the current position of the cursor (0-based)
+
+ Returns:
+ WindowsCoordinates: The current cursor position.
+ """
+ coord: COORD = GetConsoleScreenBufferInfo(self._handle).dwCursorPosition
+ return WindowsCoordinates(row=cast(int, coord.Y), col=cast(int, coord.X))
+
+ @property
+ def screen_size(self) -> WindowsCoordinates:
+ """Returns the current size of the console screen buffer, in character columns and rows
+
+ Returns:
+ WindowsCoordinates: The width and height of the screen as WindowsCoordinates.
+ """
+ screen_size: COORD = GetConsoleScreenBufferInfo(self._handle).dwSize
+ return WindowsCoordinates(
+ row=cast(int, screen_size.Y), col=cast(int, screen_size.X)
+ )
+
+ def write_text(self, text: str) -> None:
+ """Write text directly to the terminal without any modification of styles
+
+ Args:
+ text (str): The text to write to the console
+ """
+ self.write(text)
+ self.flush()
+
+ def write_styled(self, text: str, style: Style) -> None:
+ """Write styled text to the terminal.
+
+ Args:
+ text (str): The text to write
+ style (Style): The style of the text
+ """
+ color = style.color
+ bgcolor = style.bgcolor
+ if style.reverse:
+ color, bgcolor = bgcolor, color
+
+ if color:
+ fore = color.downgrade(ColorSystem.WINDOWS).number
+ fore = fore if fore is not None else 7 # Default to ANSI 7: White
+ if style.bold:
+ fore = fore | self.BRIGHT_BIT
+ if style.dim:
+ fore = fore & ~self.BRIGHT_BIT
+ fore = self.ANSI_TO_WINDOWS[fore]
+ else:
+ fore = self._default_fore
+
+ if bgcolor:
+ back = bgcolor.downgrade(ColorSystem.WINDOWS).number
+ back = back if back is not None else 0 # Default to ANSI 0: Black
+ back = self.ANSI_TO_WINDOWS[back]
+ else:
+ back = self._default_back
+
+ assert fore is not None
+ assert back is not None
+
+ SetConsoleTextAttribute(
+ self._handle, attributes=ctypes.c_ushort(fore | (back << 4))
+ )
+ self.write_text(text)
+ SetConsoleTextAttribute(self._handle, attributes=self._default_text)
+
+ def move_cursor_to(self, new_position: WindowsCoordinates) -> None:
+ """Set the position of the cursor
+
+ Args:
+ new_position (WindowsCoordinates): The WindowsCoordinates representing the new position of the cursor.
+ """
+ if new_position.col < 0 or new_position.row < 0:
+ return
+ SetConsoleCursorPosition(self._handle, coords=new_position)
+
+ def erase_line(self) -> None:
+ """Erase all content on the line the cursor is currently located at"""
+ screen_size = self.screen_size
+ cursor_position = self.cursor_position
+ cells_to_erase = screen_size.col
+ start_coordinates = WindowsCoordinates(row=cursor_position.row, col=0)
+ FillConsoleOutputCharacter(
+ self._handle, " ", length=cells_to_erase, start=start_coordinates
+ )
+ FillConsoleOutputAttribute(
+ self._handle,
+ self._default_attrs,
+ length=cells_to_erase,
+ start=start_coordinates,
+ )
+
+ def erase_end_of_line(self) -> None:
+ """Erase all content from the cursor position to the end of that line"""
+ cursor_position = self.cursor_position
+ cells_to_erase = self.screen_size.col - cursor_position.col
+ FillConsoleOutputCharacter(
+ self._handle, " ", length=cells_to_erase, start=cursor_position
+ )
+ FillConsoleOutputAttribute(
+ self._handle,
+ self._default_attrs,
+ length=cells_to_erase,
+ start=cursor_position,
+ )
+
+ def erase_start_of_line(self) -> None:
+ """Erase all content from the cursor position to the start of that line"""
+ row, col = self.cursor_position
+ start = WindowsCoordinates(row, 0)
+ FillConsoleOutputCharacter(self._handle, " ", length=col, start=start)
+ FillConsoleOutputAttribute(
+ self._handle, self._default_attrs, length=col, start=start
+ )
+
+ def move_cursor_up(self) -> None:
+ """Move the cursor up a single cell"""
+ cursor_position = self.cursor_position
+ SetConsoleCursorPosition(
+ self._handle,
+ coords=WindowsCoordinates(
+ row=cursor_position.row - 1, col=cursor_position.col
+ ),
+ )
+
+ def move_cursor_down(self) -> None:
+ """Move the cursor down a single cell"""
+ cursor_position = self.cursor_position
+ SetConsoleCursorPosition(
+ self._handle,
+ coords=WindowsCoordinates(
+ row=cursor_position.row + 1,
+ col=cursor_position.col,
+ ),
+ )
+
+ def move_cursor_forward(self) -> None:
+ """Move the cursor forward a single cell. Wrap to the next line if required."""
+ row, col = self.cursor_position
+ if col == self.screen_size.col - 1:
+ row += 1
+ col = 0
+ else:
+ col += 1
+ SetConsoleCursorPosition(
+ self._handle, coords=WindowsCoordinates(row=row, col=col)
+ )
+
+ def move_cursor_to_column(self, column: int) -> None:
+ """Move cursor to the column specified by the zero-based column index, staying on the same row
+
+ Args:
+ column (int): The zero-based column index to move the cursor to.
+ """
+ row, _ = self.cursor_position
+ SetConsoleCursorPosition(self._handle, coords=WindowsCoordinates(row, column))
+
+ def move_cursor_backward(self) -> None:
+ """Move the cursor backward a single cell. Wrap to the previous line if required."""
+ row, col = self.cursor_position
+ if col == 0:
+ row -= 1
+ col = self.screen_size.col - 1
+ else:
+ col -= 1
+ SetConsoleCursorPosition(
+ self._handle, coords=WindowsCoordinates(row=row, col=col)
+ )
+
+ def hide_cursor(self) -> None:
+ """Hide the cursor"""
+ current_cursor_size = self._get_cursor_size()
+ invisible_cursor = CONSOLE_CURSOR_INFO(dwSize=current_cursor_size, bVisible=0)
+ SetConsoleCursorInfo(self._handle, cursor_info=invisible_cursor)
+
+ def show_cursor(self) -> None:
+ """Show the cursor"""
+ current_cursor_size = self._get_cursor_size()
+ visible_cursor = CONSOLE_CURSOR_INFO(dwSize=current_cursor_size, bVisible=1)
+ SetConsoleCursorInfo(self._handle, cursor_info=visible_cursor)
+
+ def set_title(self, title: str) -> None:
+ """Set the title of the terminal window
+
+ Args:
+ title (str): The new title of the console window
+ """
+ assert len(title) < 255, "Console title must be less than 255 characters"
+ SetConsoleTitle(title)
+
+ def _get_cursor_size(self) -> int:
+ """Get the percentage of the character cell that is filled by the cursor"""
+ cursor_info = CONSOLE_CURSOR_INFO()
+ GetConsoleCursorInfo(self._handle, cursor_info=cursor_info)
+ return int(cursor_info.dwSize)
+
+
+if __name__ == "__main__":
+ handle = GetStdHandle()
+
+ from pip._vendor.rich.console import Console
+
+ console = Console()
+
+ term = LegacyWindowsTerm(sys.stdout)
+ term.set_title("Win32 Console Examples")
+
+ style = Style(color="black", bgcolor="red")
+
+ heading = Style.parse("black on green")
+
+ # Check colour output
+ console.rule("Checking colour output")
+ console.print("[on red]on red!")
+ console.print("[blue]blue!")
+ console.print("[yellow]yellow!")
+ console.print("[bold yellow]bold yellow!")
+ console.print("[bright_yellow]bright_yellow!")
+ console.print("[dim bright_yellow]dim bright_yellow!")
+ console.print("[italic cyan]italic cyan!")
+ console.print("[bold white on blue]bold white on blue!")
+ console.print("[reverse bold white on blue]reverse bold white on blue!")
+ console.print("[bold black on cyan]bold black on cyan!")
+ console.print("[black on green]black on green!")
+ console.print("[blue on green]blue on green!")
+ console.print("[white on black]white on black!")
+ console.print("[black on white]black on white!")
+ console.print("[#1BB152 on #DA812D]#1BB152 on #DA812D!")
+
+ # Check cursor movement
+ console.rule("Checking cursor movement")
+ console.print()
+ term.move_cursor_backward()
+ term.move_cursor_backward()
+ term.write_text("went back and wrapped to prev line")
+ time.sleep(1)
+ term.move_cursor_up()
+ term.write_text("we go up")
+ time.sleep(1)
+ term.move_cursor_down()
+ term.write_text("and down")
+ time.sleep(1)
+ term.move_cursor_up()
+ term.move_cursor_backward()
+ term.move_cursor_backward()
+ term.write_text("we went up and back 2")
+ time.sleep(1)
+ term.move_cursor_down()
+ term.move_cursor_backward()
+ term.move_cursor_backward()
+ term.write_text("we went down and back 2")
+ time.sleep(1)
+
+ # Check erasing of lines
+ term.hide_cursor()
+ console.print()
+ console.rule("Checking line erasing")
+ console.print("\n...Deleting to the start of the line...")
+ term.write_text("The red arrow shows the cursor location, and direction of erase")
+ time.sleep(1)
+ term.move_cursor_to_column(16)
+ term.write_styled("<", Style.parse("black on red"))
+ term.move_cursor_backward()
+ time.sleep(1)
+ term.erase_start_of_line()
+ time.sleep(1)
+
+ console.print("\n\n...And to the end of the line...")
+ term.write_text("The red arrow shows the cursor location, and direction of erase")
+ time.sleep(1)
+
+ term.move_cursor_to_column(16)
+ term.write_styled(">", Style.parse("black on red"))
+ time.sleep(1)
+ term.erase_end_of_line()
+ time.sleep(1)
+
+ console.print("\n\n...Now the whole line will be erased...")
+ term.write_styled("I'm going to disappear!", style=Style.parse("black on cyan"))
+ time.sleep(1)
+ term.erase_line()
+
+ term.show_cursor()
+ print("\n")
diff --git a/src/pip/_vendor/rich/_windows.py b/src/pip/_vendor/rich/_windows.py
new file mode 100644
index 000000000..10fc0d7e9
--- /dev/null
+++ b/src/pip/_vendor/rich/_windows.py
@@ -0,0 +1,72 @@
+import sys
+from dataclasses import dataclass
+
+
+@dataclass
+class WindowsConsoleFeatures:
+ """Windows features available."""
+
+ vt: bool = False
+ """The console supports VT codes."""
+ truecolor: bool = False
+ """The console supports truecolor."""
+
+
+try:
+ import ctypes
+ from ctypes import LibraryLoader
+
+ if sys.platform == "win32":
+ windll = LibraryLoader(ctypes.WinDLL)
+ else:
+ windll = None
+ raise ImportError("Not windows")
+
+ from pip._vendor.rich._win32_console import (
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING,
+ GetConsoleMode,
+ GetStdHandle,
+ LegacyWindowsError,
+ )
+
+except (AttributeError, ImportError, ValueError):
+
+ # Fallback if we can't load the Windows DLL
+ def get_windows_console_features() -> WindowsConsoleFeatures:
+ features = WindowsConsoleFeatures()
+ return features
+
+else:
+
+ def get_windows_console_features() -> WindowsConsoleFeatures:
+ """Get windows console features.
+
+ Returns:
+ WindowsConsoleFeatures: An instance of WindowsConsoleFeatures.
+ """
+ handle = GetStdHandle()
+ try:
+ console_mode = GetConsoleMode(handle)
+ success = True
+ except LegacyWindowsError:
+ console_mode = 0
+ success = False
+ vt = bool(success and console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+ truecolor = False
+ if vt:
+ win_version = sys.getwindowsversion()
+ truecolor = win_version.major > 10 or (
+ win_version.major == 10 and win_version.build >= 15063
+ )
+ features = WindowsConsoleFeatures(vt=vt, truecolor=truecolor)
+ return features
+
+
+if __name__ == "__main__":
+ import platform
+
+ features = get_windows_console_features()
+ from pip._vendor.rich import print
+
+ print(f'platform="{platform.system()}"')
+ print(repr(features))
diff --git a/src/pip/_vendor/rich/_windows_renderer.py b/src/pip/_vendor/rich/_windows_renderer.py
new file mode 100644
index 000000000..5ece05649
--- /dev/null
+++ b/src/pip/_vendor/rich/_windows_renderer.py
@@ -0,0 +1,56 @@
+from typing import Iterable, Sequence, Tuple, cast
+
+from pip._vendor.rich._win32_console import LegacyWindowsTerm, WindowsCoordinates
+from pip._vendor.rich.segment import ControlCode, ControlType, Segment
+
+
+def legacy_windows_render(buffer: Iterable[Segment], term: LegacyWindowsTerm) -> None:
+ """Makes appropriate Windows Console API calls based on the segments in the buffer.
+
+ Args:
+ buffer (Iterable[Segment]): Iterable of Segments to convert to Win32 API calls.
+ term (LegacyWindowsTerm): Used to call the Windows Console API.
+ """
+ for text, style, control in buffer:
+ if not control:
+ if style:
+ term.write_styled(text, style)
+ else:
+ term.write_text(text)
+ else:
+ control_codes: Sequence[ControlCode] = control
+ for control_code in control_codes:
+ control_type = control_code[0]
+ if control_type == ControlType.CURSOR_MOVE_TO:
+ _, x, y = cast(Tuple[ControlType, int, int], control_code)
+ term.move_cursor_to(WindowsCoordinates(row=y - 1, col=x - 1))
+ elif control_type == ControlType.CARRIAGE_RETURN:
+ term.write_text("\r")
+ elif control_type == ControlType.HOME:
+ term.move_cursor_to(WindowsCoordinates(0, 0))
+ elif control_type == ControlType.CURSOR_UP:
+ term.move_cursor_up()
+ elif control_type == ControlType.CURSOR_DOWN:
+ term.move_cursor_down()
+ elif control_type == ControlType.CURSOR_FORWARD:
+ term.move_cursor_forward()
+ elif control_type == ControlType.CURSOR_BACKWARD:
+ term.move_cursor_backward()
+ elif control_type == ControlType.CURSOR_MOVE_TO_COLUMN:
+ _, column = cast(Tuple[ControlType, int], control_code)
+ term.move_cursor_to_column(column - 1)
+ elif control_type == ControlType.HIDE_CURSOR:
+ term.hide_cursor()
+ elif control_type == ControlType.SHOW_CURSOR:
+ term.show_cursor()
+ elif control_type == ControlType.ERASE_IN_LINE:
+ _, mode = cast(Tuple[ControlType, int], control_code)
+ if mode == 0:
+ term.erase_end_of_line()
+ elif mode == 1:
+ term.erase_start_of_line()
+ elif mode == 2:
+ term.erase_line()
+ elif control_type == ControlType.SET_WINDOW_TITLE:
+ _, title = cast(Tuple[ControlType, str], control_code)
+ term.set_title(title)
diff --git a/src/pip/_vendor/rich/_wrap.py b/src/pip/_vendor/rich/_wrap.py
new file mode 100644
index 000000000..c45f193f7
--- /dev/null
+++ b/src/pip/_vendor/rich/_wrap.py
@@ -0,0 +1,56 @@
+import re
+from typing import Iterable, List, Tuple
+
+from ._loop import loop_last
+from .cells import cell_len, chop_cells
+
+re_word = re.compile(r"\s*\S+\s*")
+
+
+def words(text: str) -> Iterable[Tuple[int, int, str]]:
+ position = 0
+ word_match = re_word.match(text, position)
+ while word_match is not None:
+ start, end = word_match.span()
+ word = word_match.group(0)
+ yield start, end, word
+ word_match = re_word.match(text, end)
+
+
+def divide_line(text: str, width: int, fold: bool = True) -> List[int]:
+ divides: List[int] = []
+ append = divides.append
+ line_position = 0
+ _cell_len = cell_len
+ for start, _end, word in words(text):
+ word_length = _cell_len(word.rstrip())
+ if line_position + word_length > width:
+ if word_length > width:
+ if fold:
+ chopped_words = chop_cells(word, max_size=width, position=0)
+ for last, line in loop_last(chopped_words):
+ if start:
+ append(start)
+
+ if last:
+ line_position = _cell_len(line)
+ else:
+ start += len(line)
+ else:
+ if start:
+ append(start)
+ line_position = _cell_len(word)
+ elif line_position and start:
+ append(start)
+ line_position = _cell_len(word)
+ else:
+ line_position += _cell_len(word)
+ return divides
+
+
+if __name__ == "__main__": # pragma: no cover
+ from .console import Console
+
+ console = Console(width=10)
+ console.print("12345 abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ 12345")
+ print(chop_cells("abcdefghijklmnopqrstuvwxyz", 10, position=2))
diff --git a/src/pip/_vendor/rich/abc.py b/src/pip/_vendor/rich/abc.py
new file mode 100644
index 000000000..e6e498efa
--- /dev/null
+++ b/src/pip/_vendor/rich/abc.py
@@ -0,0 +1,33 @@
+from abc import ABC
+
+
+class RichRenderable(ABC):
+ """An abstract base class for Rich renderables.
+
+ Note that there is no need to extend this class, the intended use is to check if an
+ object supports the Rich renderable protocol. For example::
+
+ if isinstance(my_object, RichRenderable):
+ console.print(my_object)
+
+ """
+
+ @classmethod
+ def __subclasshook__(cls, other: type) -> bool:
+ """Check if this class supports the rich render protocol."""
+ return hasattr(other, "__rich_console__") or hasattr(other, "__rich__")
+
+
+if __name__ == "__main__": # pragma: no cover
+ from pip._vendor.rich.text import Text
+
+ t = Text()
+ print(isinstance(Text, RichRenderable))
+ print(isinstance(t, RichRenderable))
+
+ class Foo:
+ pass
+
+ f = Foo()
+ print(isinstance(f, RichRenderable))
+ print(isinstance("", RichRenderable))
diff --git a/src/pip/_vendor/rich/align.py b/src/pip/_vendor/rich/align.py
new file mode 100644
index 000000000..d5abb5947
--- /dev/null
+++ b/src/pip/_vendor/rich/align.py
@@ -0,0 +1,311 @@
+import sys
+from itertools import chain
+from typing import TYPE_CHECKING, Iterable, Optional
+
+if sys.version_info >= (3, 8):
+ from typing import Literal
+else:
+ from pip._vendor.typing_extensions import Literal # pragma: no cover
+
+from .constrain import Constrain
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .segment import Segment
+from .style import StyleType
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderableType, RenderResult
+
+AlignMethod = Literal["left", "center", "right"]
+VerticalAlignMethod = Literal["top", "middle", "bottom"]
+
+
+class Align(JupyterMixin):
+ """Align a renderable by adding spaces if necessary.
+
+ Args:
+ renderable (RenderableType): A console renderable.
+ align (AlignMethod): One of "left", "center", or "right""
+ style (StyleType, optional): An optional style to apply to the background.
+ vertical (Optional[VerticalAlginMethod], optional): Optional vertical align, one of "top", "middle", or "bottom". Defaults to None.
+ pad (bool, optional): Pad the right with spaces. Defaults to True.
+ width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None.
+ height (int, optional): Set height of align renderable, or None to fit to contents. Defaults to None.
+
+ Raises:
+ ValueError: if ``align`` is not one of the expected values.
+ """
+
+ def __init__(
+ self,
+ renderable: "RenderableType",
+ align: AlignMethod = "left",
+ style: Optional[StyleType] = None,
+ *,
+ vertical: Optional[VerticalAlignMethod] = None,
+ pad: bool = True,
+ width: Optional[int] = None,
+ height: Optional[int] = None,
+ ) -> None:
+ if align not in ("left", "center", "right"):
+ raise ValueError(
+ f'invalid value for align, expected "left", "center", or "right" (not {align!r})'
+ )
+ if vertical is not None and vertical not in ("top", "middle", "bottom"):
+ raise ValueError(
+ f'invalid value for vertical, expected "top", "middle", or "bottom" (not {vertical!r})'
+ )
+ self.renderable = renderable
+ self.align = align
+ self.style = style
+ self.vertical = vertical
+ self.pad = pad
+ self.width = width
+ self.height = height
+
+ def __repr__(self) -> str:
+ return f"Align({self.renderable!r}, {self.align!r})"
+
+ @classmethod
+ def left(
+ cls,
+ renderable: "RenderableType",
+ style: Optional[StyleType] = None,
+ *,
+ vertical: Optional[VerticalAlignMethod] = None,
+ pad: bool = True,
+ width: Optional[int] = None,
+ height: Optional[int] = None,
+ ) -> "Align":
+ """Align a renderable to the left."""
+ return cls(
+ renderable,
+ "left",
+ style=style,
+ vertical=vertical,
+ pad=pad,
+ width=width,
+ height=height,
+ )
+
+ @classmethod
+ def center(
+ cls,
+ renderable: "RenderableType",
+ style: Optional[StyleType] = None,
+ *,
+ vertical: Optional[VerticalAlignMethod] = None,
+ pad: bool = True,
+ width: Optional[int] = None,
+ height: Optional[int] = None,
+ ) -> "Align":
+ """Align a renderable to the center."""
+ return cls(
+ renderable,
+ "center",
+ style=style,
+ vertical=vertical,
+ pad=pad,
+ width=width,
+ height=height,
+ )
+
+ @classmethod
+ def right(
+ cls,
+ renderable: "RenderableType",
+ style: Optional[StyleType] = None,
+ *,
+ vertical: Optional[VerticalAlignMethod] = None,
+ pad: bool = True,
+ width: Optional[int] = None,
+ height: Optional[int] = None,
+ ) -> "Align":
+ """Align a renderable to the right."""
+ return cls(
+ renderable,
+ "right",
+ style=style,
+ vertical=vertical,
+ pad=pad,
+ width=width,
+ height=height,
+ )
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ align = self.align
+ width = console.measure(self.renderable, options=options).maximum
+ rendered = console.render(
+ Constrain(
+ self.renderable, width if self.width is None else min(width, self.width)
+ ),
+ options.update(height=None),
+ )
+ lines = list(Segment.split_lines(rendered))
+ width, height = Segment.get_shape(lines)
+ lines = Segment.set_shape(lines, width, height)
+ new_line = Segment.line()
+ excess_space = options.max_width - width
+ style = console.get_style(self.style) if self.style is not None else None
+
+ def generate_segments() -> Iterable[Segment]:
+ if excess_space <= 0:
+ # Exact fit
+ for line in lines:
+ yield from line
+ yield new_line
+
+ elif align == "left":
+ # Pad on the right
+ pad = Segment(" " * excess_space, style) if self.pad else None
+ for line in lines:
+ yield from line
+ if pad:
+ yield pad
+ yield new_line
+
+ elif align == "center":
+ # Pad left and right
+ left = excess_space // 2
+ pad = Segment(" " * left, style)
+ pad_right = (
+ Segment(" " * (excess_space - left), style) if self.pad else None
+ )
+ for line in lines:
+ if left:
+ yield pad
+ yield from line
+ if pad_right:
+ yield pad_right
+ yield new_line
+
+ elif align == "right":
+ # Padding on left
+ pad = Segment(" " * excess_space, style)
+ for line in lines:
+ yield pad
+ yield from line
+ yield new_line
+
+ blank_line = (
+ Segment(f"{' ' * (self.width or options.max_width)}\n", style)
+ if self.pad
+ else Segment("\n")
+ )
+
+ def blank_lines(count: int) -> Iterable[Segment]:
+ if count > 0:
+ for _ in range(count):
+ yield blank_line
+
+ vertical_height = self.height or options.height
+ iter_segments: Iterable[Segment]
+ if self.vertical and vertical_height is not None:
+ if self.vertical == "top":
+ bottom_space = vertical_height - height
+ iter_segments = chain(generate_segments(), blank_lines(bottom_space))
+ elif self.vertical == "middle":
+ top_space = (vertical_height - height) // 2
+ bottom_space = vertical_height - top_space - height
+ iter_segments = chain(
+ blank_lines(top_space),
+ generate_segments(),
+ blank_lines(bottom_space),
+ )
+ else: # self.vertical == "bottom":
+ top_space = vertical_height - height
+ iter_segments = chain(blank_lines(top_space), generate_segments())
+ else:
+ iter_segments = generate_segments()
+ if self.style:
+ style = console.get_style(self.style)
+ iter_segments = Segment.apply_style(iter_segments, style)
+ yield from iter_segments
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> Measurement:
+ measurement = Measurement.get(console, options, self.renderable)
+ return measurement
+
+
+class VerticalCenter(JupyterMixin):
+ """Vertically aligns a renderable.
+
+ Warn:
+ This class is deprecated and may be removed in a future version. Use Align class with
+ `vertical="middle"`.
+
+ Args:
+ renderable (RenderableType): A renderable object.
+ """
+
+ def __init__(
+ self,
+ renderable: "RenderableType",
+ style: Optional[StyleType] = None,
+ ) -> None:
+ self.renderable = renderable
+ self.style = style
+
+ def __repr__(self) -> str:
+ return f"VerticalCenter({self.renderable!r})"
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ style = console.get_style(self.style) if self.style is not None else None
+ lines = console.render_lines(
+ self.renderable, options.update(height=None), pad=False
+ )
+ width, _height = Segment.get_shape(lines)
+ new_line = Segment.line()
+ height = options.height or options.size.height
+ top_space = (height - len(lines)) // 2
+ bottom_space = height - top_space - len(lines)
+ blank_line = Segment(f"{' ' * width}", style)
+
+ def blank_lines(count: int) -> Iterable[Segment]:
+ for _ in range(count):
+ yield blank_line
+ yield new_line
+
+ if top_space > 0:
+ yield from blank_lines(top_space)
+ for line in lines:
+ yield from line
+ yield new_line
+ if bottom_space > 0:
+ yield from blank_lines(bottom_space)
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> Measurement:
+ measurement = Measurement.get(console, options, self.renderable)
+ return measurement
+
+
+if __name__ == "__main__": # pragma: no cover
+ from pip._vendor.rich.console import Console, Group
+ from pip._vendor.rich.highlighter import ReprHighlighter
+ from pip._vendor.rich.panel import Panel
+
+ highlighter = ReprHighlighter()
+ console = Console()
+
+ panel = Panel(
+ Group(
+ Align.left(highlighter("align='left'")),
+ Align.center(highlighter("align='center'")),
+ Align.right(highlighter("align='right'")),
+ ),
+ width=60,
+ style="on dark_blue",
+ title="Algin",
+ )
+
+ console.print(
+ Align.center(panel, vertical="middle", style="on red", height=console.height)
+ )
diff --git a/src/pip/_vendor/rich/ansi.py b/src/pip/_vendor/rich/ansi.py
new file mode 100644
index 000000000..d4c32cef1
--- /dev/null
+++ b/src/pip/_vendor/rich/ansi.py
@@ -0,0 +1,237 @@
+import re
+import sys
+from contextlib import suppress
+from typing import Iterable, NamedTuple, Optional
+
+from .color import Color
+from .style import Style
+from .text import Text
+
+re_ansi = re.compile(
+ r"""
+(?:\x1b\](.*?)\x1b\\)|
+(?:\x1b([(@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))
+""",
+ re.VERBOSE,
+)
+
+
+class _AnsiToken(NamedTuple):
+ """Result of ansi tokenized string."""
+
+ plain: str = ""
+ sgr: Optional[str] = ""
+ osc: Optional[str] = ""
+
+
+def _ansi_tokenize(ansi_text: str) -> Iterable[_AnsiToken]:
+ """Tokenize a string in to plain text and ANSI codes.
+
+ Args:
+ ansi_text (str): A String containing ANSI codes.
+
+ Yields:
+ AnsiToken: A named tuple of (plain, sgr, osc)
+ """
+
+ position = 0
+ sgr: Optional[str]
+ osc: Optional[str]
+ for match in re_ansi.finditer(ansi_text):
+ start, end = match.span(0)
+ osc, sgr = match.groups()
+ if start > position:
+ yield _AnsiToken(ansi_text[position:start])
+ if sgr:
+ if sgr.endswith("m"):
+ yield _AnsiToken("", sgr[1:-1], osc)
+ else:
+ yield _AnsiToken("", sgr, osc)
+ position = end
+ if position < len(ansi_text):
+ yield _AnsiToken(ansi_text[position:])
+
+
+SGR_STYLE_MAP = {
+ 1: "bold",
+ 2: "dim",
+ 3: "italic",
+ 4: "underline",
+ 5: "blink",
+ 6: "blink2",
+ 7: "reverse",
+ 8: "conceal",
+ 9: "strike",
+ 21: "underline2",
+ 22: "not dim not bold",
+ 23: "not italic",
+ 24: "not underline",
+ 25: "not blink",
+ 26: "not blink2",
+ 27: "not reverse",
+ 28: "not conceal",
+ 29: "not strike",
+ 30: "color(0)",
+ 31: "color(1)",
+ 32: "color(2)",
+ 33: "color(3)",
+ 34: "color(4)",
+ 35: "color(5)",
+ 36: "color(6)",
+ 37: "color(7)",
+ 39: "default",
+ 40: "on color(0)",
+ 41: "on color(1)",
+ 42: "on color(2)",
+ 43: "on color(3)",
+ 44: "on color(4)",
+ 45: "on color(5)",
+ 46: "on color(6)",
+ 47: "on color(7)",
+ 49: "on default",
+ 51: "frame",
+ 52: "encircle",
+ 53: "overline",
+ 54: "not frame not encircle",
+ 55: "not overline",
+ 90: "color(8)",
+ 91: "color(9)",
+ 92: "color(10)",
+ 93: "color(11)",
+ 94: "color(12)",
+ 95: "color(13)",
+ 96: "color(14)",
+ 97: "color(15)",
+ 100: "on color(8)",
+ 101: "on color(9)",
+ 102: "on color(10)",
+ 103: "on color(11)",
+ 104: "on color(12)",
+ 105: "on color(13)",
+ 106: "on color(14)",
+ 107: "on color(15)",
+}
+
+
+class AnsiDecoder:
+ """Translate ANSI code in to styled Text."""
+
+ def __init__(self) -> None:
+ self.style = Style.null()
+
+ def decode(self, terminal_text: str) -> Iterable[Text]:
+ """Decode ANSI codes in an interable of lines.
+
+ Args:
+ lines (Iterable[str]): An iterable of lines of terminal output.
+
+ Yields:
+ Text: Marked up Text.
+ """
+ for line in terminal_text.splitlines():
+ yield self.decode_line(line)
+
+ def decode_line(self, line: str) -> Text:
+ """Decode a line containing ansi codes.
+
+ Args:
+ line (str): A line of terminal output.
+
+ Returns:
+ Text: A Text instance marked up according to ansi codes.
+ """
+ from_ansi = Color.from_ansi
+ from_rgb = Color.from_rgb
+ _Style = Style
+ text = Text()
+ append = text.append
+ line = line.rsplit("\r", 1)[-1]
+ for plain_text, sgr, osc in _ansi_tokenize(line):
+ if plain_text:
+ append(plain_text, self.style or None)
+ elif osc is not None:
+ if osc.startswith("8;"):
+ _params, semicolon, link = osc[2:].partition(";")
+ if semicolon:
+ self.style = self.style.update_link(link or None)
+ elif sgr is not None:
+ # Translate in to semi-colon separated codes
+ # Ignore invalid codes, because we want to be lenient
+ codes = [
+ min(255, int(_code) if _code else 0)
+ for _code in sgr.split(";")
+ if _code.isdigit() or _code == ""
+ ]
+ iter_codes = iter(codes)
+ for code in iter_codes:
+ if code == 0:
+ # reset
+ self.style = _Style.null()
+ elif code in SGR_STYLE_MAP:
+ # styles
+ self.style += _Style.parse(SGR_STYLE_MAP[code])
+ elif code == 38:
+ #  Foreground
+ with suppress(StopIteration):
+ color_type = next(iter_codes)
+ if color_type == 5:
+ self.style += _Style.from_color(
+ from_ansi(next(iter_codes))
+ )
+ elif color_type == 2:
+ self.style += _Style.from_color(
+ from_rgb(
+ next(iter_codes),
+ next(iter_codes),
+ next(iter_codes),
+ )
+ )
+ elif code == 48:
+ # Background
+ with suppress(StopIteration):
+ color_type = next(iter_codes)
+ if color_type == 5:
+ self.style += _Style.from_color(
+ None, from_ansi(next(iter_codes))
+ )
+ elif color_type == 2:
+ self.style += _Style.from_color(
+ None,
+ from_rgb(
+ next(iter_codes),
+ next(iter_codes),
+ next(iter_codes),
+ ),
+ )
+
+ return text
+
+
+if sys.platform != "win32" and __name__ == "__main__": # pragma: no cover
+ import io
+ import os
+ import pty
+ import sys
+
+ decoder = AnsiDecoder()
+
+ stdout = io.BytesIO()
+
+ def read(fd: int) -> bytes:
+ data = os.read(fd, 1024)
+ stdout.write(data)
+ return data
+
+ pty.spawn(sys.argv[1:], read)
+
+ from .console import Console
+
+ console = Console(record=True)
+
+ stdout_result = stdout.getvalue().decode("utf-8")
+ print(stdout_result)
+
+ for line in decoder.decode(stdout_result):
+ console.print(line)
+
+ console.save_html("stdout.html")
diff --git a/src/pip/_vendor/rich/bar.py b/src/pip/_vendor/rich/bar.py
new file mode 100644
index 000000000..ed86a552d
--- /dev/null
+++ b/src/pip/_vendor/rich/bar.py
@@ -0,0 +1,94 @@
+from typing import Optional, Union
+
+from .color import Color
+from .console import Console, ConsoleOptions, RenderResult
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .segment import Segment
+from .style import Style
+
+# There are left-aligned characters for 1/8 to 7/8, but
+# the right-aligned characters exist only for 1/8 and 4/8.
+BEGIN_BLOCK_ELEMENTS = ["â–ˆ", "â–ˆ", "â–ˆ", "â–", "â–", "â–", "â–•", "â–•"]
+END_BLOCK_ELEMENTS = [" ", "â–", "â–Ž", "â–", "â–Œ", "â–‹", "â–Š", "â–‰"]
+FULL_BLOCK = "â–ˆ"
+
+
+class Bar(JupyterMixin):
+ """Renders a solid block bar.
+
+ Args:
+ size (float): Value for the end of the bar.
+ begin (float): Begin point (between 0 and size, inclusive).
+ end (float): End point (between 0 and size, inclusive).
+ width (int, optional): Width of the bar, or ``None`` for maximum width. Defaults to None.
+ color (Union[Color, str], optional): Color of the bar. Defaults to "default".
+ bgcolor (Union[Color, str], optional): Color of bar background. Defaults to "default".
+ """
+
+ def __init__(
+ self,
+ size: float,
+ begin: float,
+ end: float,
+ *,
+ width: Optional[int] = None,
+ color: Union[Color, str] = "default",
+ bgcolor: Union[Color, str] = "default",
+ ):
+ self.size = size
+ self.begin = max(begin, 0)
+ self.end = min(end, size)
+ self.width = width
+ self.style = Style(color=color, bgcolor=bgcolor)
+
+ def __repr__(self) -> str:
+ return f"Bar({self.size}, {self.begin}, {self.end})"
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+
+ width = min(
+ self.width if self.width is not None else options.max_width,
+ options.max_width,
+ )
+
+ if self.begin >= self.end:
+ yield Segment(" " * width, self.style)
+ yield Segment.line()
+ return
+
+ prefix_complete_eights = int(width * 8 * self.begin / self.size)
+ prefix_bar_count = prefix_complete_eights // 8
+ prefix_eights_count = prefix_complete_eights % 8
+
+ body_complete_eights = int(width * 8 * self.end / self.size)
+ body_bar_count = body_complete_eights // 8
+ body_eights_count = body_complete_eights % 8
+
+ # When start and end fall into the same cell, we ideally should render
+ # a symbol that's "center-aligned", but there is no good symbol in Unicode.
+ # In this case, we fall back to right-aligned block symbol for simplicity.
+
+ prefix = " " * prefix_bar_count
+ if prefix_eights_count:
+ prefix += BEGIN_BLOCK_ELEMENTS[prefix_eights_count]
+
+ body = FULL_BLOCK * body_bar_count
+ if body_eights_count:
+ body += END_BLOCK_ELEMENTS[body_eights_count]
+
+ suffix = " " * (width - len(body))
+
+ yield Segment(prefix + body[len(prefix) :] + suffix, self.style)
+ yield Segment.line()
+
+ def __rich_measure__(
+ self, console: Console, options: ConsoleOptions
+ ) -> Measurement:
+ return (
+ Measurement(self.width, self.width)
+ if self.width is not None
+ else Measurement(4, options.max_width)
+ )
diff --git a/src/pip/_vendor/rich/box.py b/src/pip/_vendor/rich/box.py
new file mode 100644
index 000000000..d0b07cf57
--- /dev/null
+++ b/src/pip/_vendor/rich/box.py
@@ -0,0 +1,517 @@
+import sys
+from typing import TYPE_CHECKING, Iterable, List
+
+if sys.version_info >= (3, 8):
+ from typing import Literal
+else:
+ from pip._vendor.typing_extensions import Literal # pragma: no cover
+
+
+from ._loop import loop_last
+
+if TYPE_CHECKING:
+ from pip._vendor.rich.console import ConsoleOptions
+
+
+class Box:
+ """Defines characters to render boxes.
+
+ ┌─┬┠top
+ │ ││ head
+ ├─┼┤ head_row
+ │ ││ mid
+ ├─┼┤ row
+ ├─┼┤ foot_row
+ │ ││ foot
+ └─┴┘ bottom
+
+ Args:
+ box (str): Characters making up box.
+ ascii (bool, optional): True if this box uses ascii characters only. Default is False.
+ """
+
+ def __init__(self, box: str, *, ascii: bool = False) -> None:
+ self._box = box
+ self.ascii = ascii
+ line1, line2, line3, line4, line5, line6, line7, line8 = box.splitlines()
+ # top
+ self.top_left, self.top, self.top_divider, self.top_right = iter(line1)
+ # head
+ self.head_left, _, self.head_vertical, self.head_right = iter(line2)
+ # head_row
+ (
+ self.head_row_left,
+ self.head_row_horizontal,
+ self.head_row_cross,
+ self.head_row_right,
+ ) = iter(line3)
+
+ # mid
+ self.mid_left, _, self.mid_vertical, self.mid_right = iter(line4)
+ # row
+ self.row_left, self.row_horizontal, self.row_cross, self.row_right = iter(line5)
+ # foot_row
+ (
+ self.foot_row_left,
+ self.foot_row_horizontal,
+ self.foot_row_cross,
+ self.foot_row_right,
+ ) = iter(line6)
+ # foot
+ self.foot_left, _, self.foot_vertical, self.foot_right = iter(line7)
+ # bottom
+ self.bottom_left, self.bottom, self.bottom_divider, self.bottom_right = iter(
+ line8
+ )
+
+ def __repr__(self) -> str:
+ return "Box(...)"
+
+ def __str__(self) -> str:
+ return self._box
+
+ def substitute(self, options: "ConsoleOptions", safe: bool = True) -> "Box":
+ """Substitute this box for another if it won't render due to platform issues.
+
+ Args:
+ options (ConsoleOptions): Console options used in rendering.
+ safe (bool, optional): Substitute this for another Box if there are known problems
+ displaying on the platform (currently only relevant on Windows). Default is True.
+
+ Returns:
+ Box: A different Box or the same Box.
+ """
+ box = self
+ if options.legacy_windows and safe:
+ box = LEGACY_WINDOWS_SUBSTITUTIONS.get(box, box)
+ if options.ascii_only and not box.ascii:
+ box = ASCII
+ return box
+
+ def get_plain_headed_box(self) -> "Box":
+ """If this box uses special characters for the borders of the header, then
+ return the equivalent box that does not.
+
+ Returns:
+ Box: The most similar Box that doesn't use header-specific box characters.
+ If the current Box already satisfies this criterion, then it's returned.
+ """
+ return PLAIN_HEADED_SUBSTITUTIONS.get(self, self)
+
+ def get_top(self, widths: Iterable[int]) -> str:
+ """Get the top of a simple box.
+
+ Args:
+ widths (List[int]): Widths of columns.
+
+ Returns:
+ str: A string of box characters.
+ """
+
+ parts: List[str] = []
+ append = parts.append
+ append(self.top_left)
+ for last, width in loop_last(widths):
+ append(self.top * width)
+ if not last:
+ append(self.top_divider)
+ append(self.top_right)
+ return "".join(parts)
+
+ def get_row(
+ self,
+ widths: Iterable[int],
+ level: Literal["head", "row", "foot", "mid"] = "row",
+ edge: bool = True,
+ ) -> str:
+ """Get the top of a simple box.
+
+ Args:
+ width (List[int]): Widths of columns.
+
+ Returns:
+ str: A string of box characters.
+ """
+ if level == "head":
+ left = self.head_row_left
+ horizontal = self.head_row_horizontal
+ cross = self.head_row_cross
+ right = self.head_row_right
+ elif level == "row":
+ left = self.row_left
+ horizontal = self.row_horizontal
+ cross = self.row_cross
+ right = self.row_right
+ elif level == "mid":
+ left = self.mid_left
+ horizontal = " "
+ cross = self.mid_vertical
+ right = self.mid_right
+ elif level == "foot":
+ left = self.foot_row_left
+ horizontal = self.foot_row_horizontal
+ cross = self.foot_row_cross
+ right = self.foot_row_right
+ else:
+ raise ValueError("level must be 'head', 'row' or 'foot'")
+
+ parts: List[str] = []
+ append = parts.append
+ if edge:
+ append(left)
+ for last, width in loop_last(widths):
+ append(horizontal * width)
+ if not last:
+ append(cross)
+ if edge:
+ append(right)
+ return "".join(parts)
+
+ def get_bottom(self, widths: Iterable[int]) -> str:
+ """Get the bottom of a simple box.
+
+ Args:
+ widths (List[int]): Widths of columns.
+
+ Returns:
+ str: A string of box characters.
+ """
+
+ parts: List[str] = []
+ append = parts.append
+ append(self.bottom_left)
+ for last, width in loop_last(widths):
+ append(self.bottom * width)
+ if not last:
+ append(self.bottom_divider)
+ append(self.bottom_right)
+ return "".join(parts)
+
+
+ASCII: Box = Box(
+ """\
++--+
+| ||
+|-+|
+| ||
+|-+|
+|-+|
+| ||
++--+
+""",
+ ascii=True,
+)
+
+ASCII2: Box = Box(
+ """\
++-++
+| ||
++-++
+| ||
++-++
++-++
+| ||
++-++
+""",
+ ascii=True,
+)
+
+ASCII_DOUBLE_HEAD: Box = Box(
+ """\
++-++
+| ||
++=++
+| ||
++-++
++-++
+| ||
++-++
+""",
+ ascii=True,
+)
+
+SQUARE: Box = Box(
+ """\
+┌─┬â”
+│ ││
+├─┼┤
+│ ││
+├─┼┤
+├─┼┤
+│ ││
+└─┴┘
+"""
+)
+
+SQUARE_DOUBLE_HEAD: Box = Box(
+ """\
+┌─┬â”
+│ ││
+â•žâ•â•ªâ•¡
+│ ││
+├─┼┤
+├─┼┤
+│ ││
+└─┴┘
+"""
+)
+
+MINIMAL: Box = Box(
+ """\
+ â•·
+ │
+╶─┼╴
+ │
+╶─┼╴
+╶─┼╴
+ │
+ ╵
+"""
+)
+
+
+MINIMAL_HEAVY_HEAD: Box = Box(
+ """\
+ â•·
+ │
+╺â”┿╸
+ │
+╶─┼╴
+╶─┼╴
+ │
+ ╵
+"""
+)
+
+MINIMAL_DOUBLE_HEAD: Box = Box(
+ """\
+ â•·
+ │
+ â•â•ª
+ │
+ ─┼
+ ─┼
+ │
+ ╵
+"""
+)
+
+
+SIMPLE: Box = Box(
+ """\
+
+
+ ──
+
+
+ ──
+
+
+"""
+)
+
+SIMPLE_HEAD: Box = Box(
+ """\
+
+
+ ──
+
+
+
+
+
+"""
+)
+
+
+SIMPLE_HEAVY: Box = Box(
+ """\
+
+
+ â”â”
+
+
+ â”â”
+
+
+"""
+)
+
+
+HORIZONTALS: Box = Box(
+ """\
+ ──
+
+ ──
+
+ ──
+ ──
+
+ ──
+"""
+)
+
+ROUNDED: Box = Box(
+ """\
+╭─┬╮
+│ ││
+├─┼┤
+│ ││
+├─┼┤
+├─┼┤
+│ ││
+╰─┴╯
+"""
+)
+
+HEAVY: Box = Box(
+ """\
+â”â”┳┓
+┃ ┃┃
+┣â”╋┫
+┃ ┃┃
+┣â”╋┫
+┣â”╋┫
+┃ ┃┃
+â”—â”┻┛
+"""
+)
+
+HEAVY_EDGE: Box = Box(
+ """\
+â”â”┯┓
+┃ │┃
+┠─┼┨
+┃ │┃
+┠─┼┨
+┠─┼┨
+┃ │┃
+â”—â”â”·â”›
+"""
+)
+
+HEAVY_HEAD: Box = Box(
+ """\
+â”â”┳┓
+┃ ┃┃
+┡â”╇┩
+│ ││
+├─┼┤
+├─┼┤
+│ ││
+└─┴┘
+"""
+)
+
+DOUBLE: Box = Box(
+ """\
+â•”â•â•¦â•—
+â•‘ â•‘â•‘
+â• â•â•¬â•£
+â•‘ â•‘â•‘
+â• â•â•¬â•£
+â• â•â•¬â•£
+â•‘ â•‘â•‘
+â•šâ•â•©â•
+"""
+)
+
+DOUBLE_EDGE: Box = Box(
+ """\
+â•”â•â•¤â•—
+║ │║
+╟─┼╢
+║ │║
+╟─┼╢
+╟─┼╢
+║ │║
+â•šâ•â•§â•
+"""
+)
+
+MARKDOWN: Box = Box(
+ """\
+
+| ||
+|-||
+| ||
+|-||
+|-||
+| ||
+
+""",
+ ascii=True,
+)
+
+# Map Boxes that don't render with raster fonts on to equivalent that do
+LEGACY_WINDOWS_SUBSTITUTIONS = {
+ ROUNDED: SQUARE,
+ MINIMAL_HEAVY_HEAD: MINIMAL,
+ SIMPLE_HEAVY: SIMPLE,
+ HEAVY: SQUARE,
+ HEAVY_EDGE: SQUARE,
+ HEAVY_HEAD: SQUARE,
+}
+
+# Map headed boxes to their headerless equivalents
+PLAIN_HEADED_SUBSTITUTIONS = {
+ HEAVY_HEAD: SQUARE,
+ SQUARE_DOUBLE_HEAD: SQUARE,
+ MINIMAL_DOUBLE_HEAD: MINIMAL,
+ MINIMAL_HEAVY_HEAD: MINIMAL,
+ ASCII_DOUBLE_HEAD: ASCII2,
+}
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from pip._vendor.rich.columns import Columns
+ from pip._vendor.rich.panel import Panel
+
+ from . import box as box
+ from .console import Console
+ from .table import Table
+ from .text import Text
+
+ console = Console(record=True)
+
+ BOXES = [
+ "ASCII",
+ "ASCII2",
+ "ASCII_DOUBLE_HEAD",
+ "SQUARE",
+ "SQUARE_DOUBLE_HEAD",
+ "MINIMAL",
+ "MINIMAL_HEAVY_HEAD",
+ "MINIMAL_DOUBLE_HEAD",
+ "SIMPLE",
+ "SIMPLE_HEAD",
+ "SIMPLE_HEAVY",
+ "HORIZONTALS",
+ "ROUNDED",
+ "HEAVY",
+ "HEAVY_EDGE",
+ "HEAVY_HEAD",
+ "DOUBLE",
+ "DOUBLE_EDGE",
+ "MARKDOWN",
+ ]
+
+ console.print(Panel("[bold green]Box Constants", style="green"), justify="center")
+ console.print()
+
+ columns = Columns(expand=True, padding=2)
+ for box_name in sorted(BOXES):
+ table = Table(
+ show_footer=True, style="dim", border_style="not dim", expand=True
+ )
+ table.add_column("Header 1", "Footer 1")
+ table.add_column("Header 2", "Footer 2")
+ table.add_row("Cell", "Cell")
+ table.add_row("Cell", "Cell")
+ table.box = getattr(box, box_name)
+ table.title = Text(f"box.{box_name}", style="magenta")
+ columns.add_renderable(table)
+ console.print(columns)
+
+ # console.save_html("box.html", inline_styles=True)
diff --git a/src/pip/_vendor/rich/cells.py b/src/pip/_vendor/rich/cells.py
new file mode 100644
index 000000000..139b949f7
--- /dev/null
+++ b/src/pip/_vendor/rich/cells.py
@@ -0,0 +1,154 @@
+import re
+from functools import lru_cache
+from typing import Callable, List
+
+from ._cell_widths import CELL_WIDTHS
+
+# Regex to match sequence of the most common character ranges
+_is_single_cell_widths = re.compile("^[\u0020-\u006f\u00a0\u02ff\u0370-\u0482]*$").match
+
+
+@lru_cache(4096)
+def cached_cell_len(text: str) -> int:
+ """Get the number of cells required to display text.
+
+ This method always caches, which may use up a lot of memory. It is recommended to use
+ `cell_len` over this method.
+
+ Args:
+ text (str): Text to display.
+
+ Returns:
+ int: Get the number of cells required to display text.
+ """
+ _get_size = get_character_cell_size
+ total_size = sum(_get_size(character) for character in text)
+ return total_size
+
+
+def cell_len(text: str, _cell_len: Callable[[str], int] = cached_cell_len) -> int:
+ """Get the number of cells required to display text.
+
+ Args:
+ text (str): Text to display.
+
+ Returns:
+ int: Get the number of cells required to display text.
+ """
+ if len(text) < 512:
+ return _cell_len(text)
+ _get_size = get_character_cell_size
+ total_size = sum(_get_size(character) for character in text)
+ return total_size
+
+
+@lru_cache(maxsize=4096)
+def get_character_cell_size(character: str) -> int:
+ """Get the cell size of a character.
+
+ Args:
+ character (str): A single character.
+
+ Returns:
+ int: Number of cells (0, 1 or 2) occupied by that character.
+ """
+ return _get_codepoint_cell_size(ord(character))
+
+
+@lru_cache(maxsize=4096)
+def _get_codepoint_cell_size(codepoint: int) -> int:
+ """Get the cell size of a character.
+
+ Args:
+ character (str): A single character.
+
+ Returns:
+ int: Number of cells (0, 1 or 2) occupied by that character.
+ """
+
+ _table = CELL_WIDTHS
+ lower_bound = 0
+ upper_bound = len(_table) - 1
+ index = (lower_bound + upper_bound) // 2
+ while True:
+ start, end, width = _table[index]
+ if codepoint < start:
+ upper_bound = index - 1
+ elif codepoint > end:
+ lower_bound = index + 1
+ else:
+ return 0 if width == -1 else width
+ if upper_bound < lower_bound:
+ break
+ index = (lower_bound + upper_bound) // 2
+ return 1
+
+
+def set_cell_size(text: str, total: int) -> str:
+ """Set the length of a string to fit within given number of cells."""
+
+ if _is_single_cell_widths(text):
+ size = len(text)
+ if size < total:
+ return text + " " * (total - size)
+ return text[:total]
+
+ if total <= 0:
+ return ""
+ cell_size = cell_len(text)
+ if cell_size == total:
+ return text
+ if cell_size < total:
+ return text + " " * (total - cell_size)
+
+ start = 0
+ end = len(text)
+
+ # Binary search until we find the right size
+ while True:
+ pos = (start + end) // 2
+ before = text[: pos + 1]
+ before_len = cell_len(before)
+ if before_len == total + 1 and cell_len(before[-1]) == 2:
+ return before[:-1] + " "
+ if before_len == total:
+ return before
+ if before_len > total:
+ end = pos
+ else:
+ start = pos
+
+
+# TODO: This is inefficient
+# TODO: This might not work with CWJ type characters
+def chop_cells(text: str, max_size: int, position: int = 0) -> List[str]:
+ """Break text in to equal (cell) length strings, returning the characters in reverse
+ order"""
+ _get_character_cell_size = get_character_cell_size
+ characters = [
+ (character, _get_character_cell_size(character)) for character in text
+ ]
+ total_size = position
+ lines: List[List[str]] = [[]]
+ append = lines[-1].append
+
+ for character, size in reversed(characters):
+ if total_size + size > max_size:
+ lines.append([character])
+ append = lines[-1].append
+ total_size = size
+ else:
+ total_size += size
+ append(character)
+
+ return ["".join(line) for line in lines]
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ print(get_character_cell_size("😽"))
+ for line in chop_cells("""这是对亚洲语言支æŒçš„测试。é¢å¯¹æ¨¡æ£±ä¸¤å¯çš„想法,拒ç»çŒœæµ‹çš„诱惑。""", 8):
+ print(line)
+ for n in range(80, 1, -1):
+ print(set_cell_size("""这是对亚洲语言支æŒçš„测试。é¢å¯¹æ¨¡æ£±ä¸¤å¯çš„想法,拒ç»çŒœæµ‹çš„诱惑。""", n) + "|")
+ print("x" * n)
diff --git a/src/pip/_vendor/rich/color.py b/src/pip/_vendor/rich/color.py
new file mode 100644
index 000000000..6bca2da92
--- /dev/null
+++ b/src/pip/_vendor/rich/color.py
@@ -0,0 +1,615 @@
+import platform
+import re
+from colorsys import rgb_to_hls
+from enum import IntEnum
+from functools import lru_cache
+from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple
+
+from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE
+from .color_triplet import ColorTriplet
+from .repr import Result, rich_repr
+from .terminal_theme import DEFAULT_TERMINAL_THEME
+
+if TYPE_CHECKING: # pragma: no cover
+ from .terminal_theme import TerminalTheme
+ from .text import Text
+
+
+WINDOWS = platform.system() == "Windows"
+
+
+class ColorSystem(IntEnum):
+ """One of the 3 color system supported by terminals."""
+
+ STANDARD = 1
+ EIGHT_BIT = 2
+ TRUECOLOR = 3
+ WINDOWS = 4
+
+ def __repr__(self) -> str:
+ return f"ColorSystem.{self.name}"
+
+
+class ColorType(IntEnum):
+ """Type of color stored in Color class."""
+
+ DEFAULT = 0
+ STANDARD = 1
+ EIGHT_BIT = 2
+ TRUECOLOR = 3
+ WINDOWS = 4
+
+ def __repr__(self) -> str:
+ return f"ColorType.{self.name}"
+
+
+ANSI_COLOR_NAMES = {
+ "black": 0,
+ "red": 1,
+ "green": 2,
+ "yellow": 3,
+ "blue": 4,
+ "magenta": 5,
+ "cyan": 6,
+ "white": 7,
+ "bright_black": 8,
+ "bright_red": 9,
+ "bright_green": 10,
+ "bright_yellow": 11,
+ "bright_blue": 12,
+ "bright_magenta": 13,
+ "bright_cyan": 14,
+ "bright_white": 15,
+ "grey0": 16,
+ "gray0": 16,
+ "navy_blue": 17,
+ "dark_blue": 18,
+ "blue3": 20,
+ "blue1": 21,
+ "dark_green": 22,
+ "deep_sky_blue4": 25,
+ "dodger_blue3": 26,
+ "dodger_blue2": 27,
+ "green4": 28,
+ "spring_green4": 29,
+ "turquoise4": 30,
+ "deep_sky_blue3": 32,
+ "dodger_blue1": 33,
+ "green3": 40,
+ "spring_green3": 41,
+ "dark_cyan": 36,
+ "light_sea_green": 37,
+ "deep_sky_blue2": 38,
+ "deep_sky_blue1": 39,
+ "spring_green2": 47,
+ "cyan3": 43,
+ "dark_turquoise": 44,
+ "turquoise2": 45,
+ "green1": 46,
+ "spring_green1": 48,
+ "medium_spring_green": 49,
+ "cyan2": 50,
+ "cyan1": 51,
+ "dark_red": 88,
+ "deep_pink4": 125,
+ "purple4": 55,
+ "purple3": 56,
+ "blue_violet": 57,
+ "orange4": 94,
+ "grey37": 59,
+ "gray37": 59,
+ "medium_purple4": 60,
+ "slate_blue3": 62,
+ "royal_blue1": 63,
+ "chartreuse4": 64,
+ "dark_sea_green4": 71,
+ "pale_turquoise4": 66,
+ "steel_blue": 67,
+ "steel_blue3": 68,
+ "cornflower_blue": 69,
+ "chartreuse3": 76,
+ "cadet_blue": 73,
+ "sky_blue3": 74,
+ "steel_blue1": 81,
+ "pale_green3": 114,
+ "sea_green3": 78,
+ "aquamarine3": 79,
+ "medium_turquoise": 80,
+ "chartreuse2": 112,
+ "sea_green2": 83,
+ "sea_green1": 85,
+ "aquamarine1": 122,
+ "dark_slate_gray2": 87,
+ "dark_magenta": 91,
+ "dark_violet": 128,
+ "purple": 129,
+ "light_pink4": 95,
+ "plum4": 96,
+ "medium_purple3": 98,
+ "slate_blue1": 99,
+ "yellow4": 106,
+ "wheat4": 101,
+ "grey53": 102,
+ "gray53": 102,
+ "light_slate_grey": 103,
+ "light_slate_gray": 103,
+ "medium_purple": 104,
+ "light_slate_blue": 105,
+ "dark_olive_green3": 149,
+ "dark_sea_green": 108,
+ "light_sky_blue3": 110,
+ "sky_blue2": 111,
+ "dark_sea_green3": 150,
+ "dark_slate_gray3": 116,
+ "sky_blue1": 117,
+ "chartreuse1": 118,
+ "light_green": 120,
+ "pale_green1": 156,
+ "dark_slate_gray1": 123,
+ "red3": 160,
+ "medium_violet_red": 126,
+ "magenta3": 164,
+ "dark_orange3": 166,
+ "indian_red": 167,
+ "hot_pink3": 168,
+ "medium_orchid3": 133,
+ "medium_orchid": 134,
+ "medium_purple2": 140,
+ "dark_goldenrod": 136,
+ "light_salmon3": 173,
+ "rosy_brown": 138,
+ "grey63": 139,
+ "gray63": 139,
+ "medium_purple1": 141,
+ "gold3": 178,
+ "dark_khaki": 143,
+ "navajo_white3": 144,
+ "grey69": 145,
+ "gray69": 145,
+ "light_steel_blue3": 146,
+ "light_steel_blue": 147,
+ "yellow3": 184,
+ "dark_sea_green2": 157,
+ "light_cyan3": 152,
+ "light_sky_blue1": 153,
+ "green_yellow": 154,
+ "dark_olive_green2": 155,
+ "dark_sea_green1": 193,
+ "pale_turquoise1": 159,
+ "deep_pink3": 162,
+ "magenta2": 200,
+ "hot_pink2": 169,
+ "orchid": 170,
+ "medium_orchid1": 207,
+ "orange3": 172,
+ "light_pink3": 174,
+ "pink3": 175,
+ "plum3": 176,
+ "violet": 177,
+ "light_goldenrod3": 179,
+ "tan": 180,
+ "misty_rose3": 181,
+ "thistle3": 182,
+ "plum2": 183,
+ "khaki3": 185,
+ "light_goldenrod2": 222,
+ "light_yellow3": 187,
+ "grey84": 188,
+ "gray84": 188,
+ "light_steel_blue1": 189,
+ "yellow2": 190,
+ "dark_olive_green1": 192,
+ "honeydew2": 194,
+ "light_cyan1": 195,
+ "red1": 196,
+ "deep_pink2": 197,
+ "deep_pink1": 199,
+ "magenta1": 201,
+ "orange_red1": 202,
+ "indian_red1": 204,
+ "hot_pink": 206,
+ "dark_orange": 208,
+ "salmon1": 209,
+ "light_coral": 210,
+ "pale_violet_red1": 211,
+ "orchid2": 212,
+ "orchid1": 213,
+ "orange1": 214,
+ "sandy_brown": 215,
+ "light_salmon1": 216,
+ "light_pink1": 217,
+ "pink1": 218,
+ "plum1": 219,
+ "gold1": 220,
+ "navajo_white1": 223,
+ "misty_rose1": 224,
+ "thistle1": 225,
+ "yellow1": 226,
+ "light_goldenrod1": 227,
+ "khaki1": 228,
+ "wheat1": 229,
+ "cornsilk1": 230,
+ "grey100": 231,
+ "gray100": 231,
+ "grey3": 232,
+ "gray3": 232,
+ "grey7": 233,
+ "gray7": 233,
+ "grey11": 234,
+ "gray11": 234,
+ "grey15": 235,
+ "gray15": 235,
+ "grey19": 236,
+ "gray19": 236,
+ "grey23": 237,
+ "gray23": 237,
+ "grey27": 238,
+ "gray27": 238,
+ "grey30": 239,
+ "gray30": 239,
+ "grey35": 240,
+ "gray35": 240,
+ "grey39": 241,
+ "gray39": 241,
+ "grey42": 242,
+ "gray42": 242,
+ "grey46": 243,
+ "gray46": 243,
+ "grey50": 244,
+ "gray50": 244,
+ "grey54": 245,
+ "gray54": 245,
+ "grey58": 246,
+ "gray58": 246,
+ "grey62": 247,
+ "gray62": 247,
+ "grey66": 248,
+ "gray66": 248,
+ "grey70": 249,
+ "gray70": 249,
+ "grey74": 250,
+ "gray74": 250,
+ "grey78": 251,
+ "gray78": 251,
+ "grey82": 252,
+ "gray82": 252,
+ "grey85": 253,
+ "gray85": 253,
+ "grey89": 254,
+ "gray89": 254,
+ "grey93": 255,
+ "gray93": 255,
+}
+
+
+class ColorParseError(Exception):
+ """The color could not be parsed."""
+
+
+RE_COLOR = re.compile(
+ r"""^
+\#([0-9a-f]{6})$|
+color\(([0-9]{1,3})\)$|
+rgb\(([\d\s,]+)\)$
+""",
+ re.VERBOSE,
+)
+
+
+@rich_repr
+class Color(NamedTuple):
+ """Terminal color definition."""
+
+ name: str
+ """The name of the color (typically the input to Color.parse)."""
+ type: ColorType
+ """The type of the color."""
+ number: Optional[int] = None
+ """The color number, if a standard color, or None."""
+ triplet: Optional[ColorTriplet] = None
+ """A triplet of color components, if an RGB color."""
+
+ def __rich__(self) -> "Text":
+ """Dispays the actual color if Rich printed."""
+ from .style import Style
+ from .text import Text
+
+ return Text.assemble(
+ f"<color {self.name!r} ({self.type.name.lower()})",
+ ("⬤", Style(color=self)),
+ " >",
+ )
+
+ def __rich_repr__(self) -> Result:
+ yield self.name
+ yield self.type
+ yield "number", self.number, None
+ yield "triplet", self.triplet, None
+
+ @property
+ def system(self) -> ColorSystem:
+ """Get the native color system for this color."""
+ if self.type == ColorType.DEFAULT:
+ return ColorSystem.STANDARD
+ return ColorSystem(int(self.type))
+
+ @property
+ def is_system_defined(self) -> bool:
+ """Check if the color is ultimately defined by the system."""
+ return self.system not in (ColorSystem.EIGHT_BIT, ColorSystem.TRUECOLOR)
+
+ @property
+ def is_default(self) -> bool:
+ """Check if the color is a default color."""
+ return self.type == ColorType.DEFAULT
+
+ def get_truecolor(
+ self, theme: Optional["TerminalTheme"] = None, foreground: bool = True
+ ) -> ColorTriplet:
+ """Get an equivalent color triplet for this color.
+
+ Args:
+ theme (TerminalTheme, optional): Optional terminal theme, or None to use default. Defaults to None.
+ foreground (bool, optional): True for a foreground color, or False for background. Defaults to True.
+
+ Returns:
+ ColorTriplet: A color triplet containing RGB components.
+ """
+
+ if theme is None:
+ theme = DEFAULT_TERMINAL_THEME
+ if self.type == ColorType.TRUECOLOR:
+ assert self.triplet is not None
+ return self.triplet
+ elif self.type == ColorType.EIGHT_BIT:
+ assert self.number is not None
+ return EIGHT_BIT_PALETTE[self.number]
+ elif self.type == ColorType.STANDARD:
+ assert self.number is not None
+ return theme.ansi_colors[self.number]
+ elif self.type == ColorType.WINDOWS:
+ assert self.number is not None
+ return WINDOWS_PALETTE[self.number]
+ else: # self.type == ColorType.DEFAULT:
+ assert self.number is None
+ return theme.foreground_color if foreground else theme.background_color
+
+ @classmethod
+ def from_ansi(cls, number: int) -> "Color":
+ """Create a Color number from it's 8-bit ansi number.
+
+ Args:
+ number (int): A number between 0-255 inclusive.
+
+ Returns:
+ Color: A new Color instance.
+ """
+ return cls(
+ name=f"color({number})",
+ type=(ColorType.STANDARD if number < 16 else ColorType.EIGHT_BIT),
+ number=number,
+ )
+
+ @classmethod
+ def from_triplet(cls, triplet: "ColorTriplet") -> "Color":
+ """Create a truecolor RGB color from a triplet of values.
+
+ Args:
+ triplet (ColorTriplet): A color triplet containing red, green and blue components.
+
+ Returns:
+ Color: A new color object.
+ """
+ return cls(name=triplet.hex, type=ColorType.TRUECOLOR, triplet=triplet)
+
+ @classmethod
+ def from_rgb(cls, red: float, green: float, blue: float) -> "Color":
+ """Create a truecolor from three color components in the range(0->255).
+
+ Args:
+ red (float): Red component in range 0-255.
+ green (float): Green component in range 0-255.
+ blue (float): Blue component in range 0-255.
+
+ Returns:
+ Color: A new color object.
+ """
+ return cls.from_triplet(ColorTriplet(int(red), int(green), int(blue)))
+
+ @classmethod
+ def default(cls) -> "Color":
+ """Get a Color instance representing the default color.
+
+ Returns:
+ Color: Default color.
+ """
+ return cls(name="default", type=ColorType.DEFAULT)
+
+ @classmethod
+ @lru_cache(maxsize=1024)
+ def parse(cls, color: str) -> "Color":
+ """Parse a color definition."""
+ original_color = color
+ color = color.lower().strip()
+
+ if color == "default":
+ return cls(color, type=ColorType.DEFAULT)
+
+ color_number = ANSI_COLOR_NAMES.get(color)
+ if color_number is not None:
+ return cls(
+ color,
+ type=(ColorType.STANDARD if color_number < 16 else ColorType.EIGHT_BIT),
+ number=color_number,
+ )
+
+ color_match = RE_COLOR.match(color)
+ if color_match is None:
+ raise ColorParseError(f"{original_color!r} is not a valid color")
+
+ color_24, color_8, color_rgb = color_match.groups()
+ if color_24:
+ triplet = ColorTriplet(
+ int(color_24[0:2], 16), int(color_24[2:4], 16), int(color_24[4:6], 16)
+ )
+ return cls(color, ColorType.TRUECOLOR, triplet=triplet)
+
+ elif color_8:
+ number = int(color_8)
+ if number > 255:
+ raise ColorParseError(f"color number must be <= 255 in {color!r}")
+ return cls(
+ color,
+ type=(ColorType.STANDARD if number < 16 else ColorType.EIGHT_BIT),
+ number=number,
+ )
+
+ else: # color_rgb:
+ components = color_rgb.split(",")
+ if len(components) != 3:
+ raise ColorParseError(
+ f"expected three components in {original_color!r}"
+ )
+ red, green, blue = components
+ triplet = ColorTriplet(int(red), int(green), int(blue))
+ if not all(component <= 255 for component in triplet):
+ raise ColorParseError(
+ f"color components must be <= 255 in {original_color!r}"
+ )
+ return cls(color, ColorType.TRUECOLOR, triplet=triplet)
+
+ @lru_cache(maxsize=1024)
+ def get_ansi_codes(self, foreground: bool = True) -> Tuple[str, ...]:
+ """Get the ANSI escape codes for this color."""
+ _type = self.type
+ if _type == ColorType.DEFAULT:
+ return ("39" if foreground else "49",)
+
+ elif _type == ColorType.WINDOWS:
+ number = self.number
+ assert number is not None
+ fore, back = (30, 40) if number < 8 else (82, 92)
+ return (str(fore + number if foreground else back + number),)
+
+ elif _type == ColorType.STANDARD:
+ number = self.number
+ assert number is not None
+ fore, back = (30, 40) if number < 8 else (82, 92)
+ return (str(fore + number if foreground else back + number),)
+
+ elif _type == ColorType.EIGHT_BIT:
+ assert self.number is not None
+ return ("38" if foreground else "48", "5", str(self.number))
+
+ else: # self.standard == ColorStandard.TRUECOLOR:
+ assert self.triplet is not None
+ red, green, blue = self.triplet
+ return ("38" if foreground else "48", "2", str(red), str(green), str(blue))
+
+ @lru_cache(maxsize=1024)
+ def downgrade(self, system: ColorSystem) -> "Color":
+ """Downgrade a color system to a system with fewer colors."""
+
+ if self.type in [ColorType.DEFAULT, system]:
+ return self
+ # Convert to 8-bit color from truecolor color
+ if system == ColorSystem.EIGHT_BIT and self.system == ColorSystem.TRUECOLOR:
+ assert self.triplet is not None
+ red, green, blue = self.triplet.normalized
+ _h, l, s = rgb_to_hls(red, green, blue)
+ # If saturation is under 10% assume it is grayscale
+ if s < 0.1:
+ gray = round(l * 25.0)
+ if gray == 0:
+ color_number = 16
+ elif gray == 25:
+ color_number = 231
+ else:
+ color_number = 231 + gray
+ return Color(self.name, ColorType.EIGHT_BIT, number=color_number)
+
+ color_number = (
+ 16 + 36 * round(red * 5.0) + 6 * round(green * 5.0) + round(blue * 5.0)
+ )
+ return Color(self.name, ColorType.EIGHT_BIT, number=color_number)
+
+ # Convert to standard from truecolor or 8-bit
+ elif system == ColorSystem.STANDARD:
+ if self.system == ColorSystem.TRUECOLOR:
+ assert self.triplet is not None
+ triplet = self.triplet
+ else: # self.system == ColorSystem.EIGHT_BIT
+ assert self.number is not None
+ triplet = ColorTriplet(*EIGHT_BIT_PALETTE[self.number])
+
+ color_number = STANDARD_PALETTE.match(triplet)
+ return Color(self.name, ColorType.STANDARD, number=color_number)
+
+ elif system == ColorSystem.WINDOWS:
+ if self.system == ColorSystem.TRUECOLOR:
+ assert self.triplet is not None
+ triplet = self.triplet
+ else: # self.system == ColorSystem.EIGHT_BIT
+ assert self.number is not None
+ if self.number < 16:
+ return Color(self.name, ColorType.WINDOWS, number=self.number)
+ triplet = ColorTriplet(*EIGHT_BIT_PALETTE[self.number])
+
+ color_number = WINDOWS_PALETTE.match(triplet)
+ return Color(self.name, ColorType.WINDOWS, number=color_number)
+
+ return self
+
+
+def parse_rgb_hex(hex_color: str) -> ColorTriplet:
+ """Parse six hex characters in to RGB triplet."""
+ assert len(hex_color) == 6, "must be 6 characters"
+ color = ColorTriplet(
+ int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
+ )
+ return color
+
+
+def blend_rgb(
+ color1: ColorTriplet, color2: ColorTriplet, cross_fade: float = 0.5
+) -> ColorTriplet:
+ """Blend one RGB color in to another."""
+ r1, g1, b1 = color1
+ r2, g2, b2 = color2
+ new_color = ColorTriplet(
+ int(r1 + (r2 - r1) * cross_fade),
+ int(g1 + (g2 - g1) * cross_fade),
+ int(b1 + (b2 - b1) * cross_fade),
+ )
+ return new_color
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from .console import Console
+ from .table import Table
+ from .text import Text
+
+ console = Console()
+
+ table = Table(show_footer=False, show_edge=True)
+ table.add_column("Color", width=10, overflow="ellipsis")
+ table.add_column("Number", justify="right", style="yellow")
+ table.add_column("Name", style="green")
+ table.add_column("Hex", style="blue")
+ table.add_column("RGB", style="magenta")
+
+ colors = sorted((v, k) for k, v in ANSI_COLOR_NAMES.items())
+ for color_number, name in colors:
+ if "grey" in name:
+ continue
+ color_cell = Text(" " * 10, style=f"on {name}")
+ if color_number < 16:
+ table.add_row(color_cell, f"{color_number}", Text(f'"{name}"'))
+ else:
+ color = EIGHT_BIT_PALETTE[color_number] # type: ignore[has-type]
+ table.add_row(
+ color_cell, str(color_number), Text(f'"{name}"'), color.hex, color.rgb
+ )
+
+ console.print(table)
diff --git a/src/pip/_vendor/rich/color_triplet.py b/src/pip/_vendor/rich/color_triplet.py
new file mode 100644
index 000000000..02cab3282
--- /dev/null
+++ b/src/pip/_vendor/rich/color_triplet.py
@@ -0,0 +1,38 @@
+from typing import NamedTuple, Tuple
+
+
+class ColorTriplet(NamedTuple):
+ """The red, green, and blue components of a color."""
+
+ red: int
+ """Red component in 0 to 255 range."""
+ green: int
+ """Green component in 0 to 255 range."""
+ blue: int
+ """Blue component in 0 to 255 range."""
+
+ @property
+ def hex(self) -> str:
+ """get the color triplet in CSS style."""
+ red, green, blue = self
+ return f"#{red:02x}{green:02x}{blue:02x}"
+
+ @property
+ def rgb(self) -> str:
+ """The color in RGB format.
+
+ Returns:
+ str: An rgb color, e.g. ``"rgb(100,23,255)"``.
+ """
+ red, green, blue = self
+ return f"rgb({red},{green},{blue})"
+
+ @property
+ def normalized(self) -> Tuple[float, float, float]:
+ """Convert components into floats between 0 and 1.
+
+ Returns:
+ Tuple[float, float, float]: A tuple of three normalized colour components.
+ """
+ red, green, blue = self
+ return red / 255.0, green / 255.0, blue / 255.0
diff --git a/src/pip/_vendor/rich/columns.py b/src/pip/_vendor/rich/columns.py
new file mode 100644
index 000000000..669a3a707
--- /dev/null
+++ b/src/pip/_vendor/rich/columns.py
@@ -0,0 +1,187 @@
+from collections import defaultdict
+from itertools import chain
+from operator import itemgetter
+from typing import Dict, Iterable, List, Optional, Tuple
+
+from .align import Align, AlignMethod
+from .console import Console, ConsoleOptions, RenderableType, RenderResult
+from .constrain import Constrain
+from .measure import Measurement
+from .padding import Padding, PaddingDimensions
+from .table import Table
+from .text import TextType
+from .jupyter import JupyterMixin
+
+
+class Columns(JupyterMixin):
+ """Display renderables in neat columns.
+
+ Args:
+ renderables (Iterable[RenderableType]): Any number of Rich renderables (including str).
+ width (int, optional): The desired width of the columns, or None to auto detect. Defaults to None.
+ padding (PaddingDimensions, optional): Optional padding around cells. Defaults to (0, 1).
+ expand (bool, optional): Expand columns to full width. Defaults to False.
+ equal (bool, optional): Arrange in to equal sized columns. Defaults to False.
+ column_first (bool, optional): Align items from top to bottom (rather than left to right). Defaults to False.
+ right_to_left (bool, optional): Start column from right hand side. Defaults to False.
+ align (str, optional): Align value ("left", "right", or "center") or None for default. Defaults to None.
+ title (TextType, optional): Optional title for Columns.
+ """
+
+ def __init__(
+ self,
+ renderables: Optional[Iterable[RenderableType]] = None,
+ padding: PaddingDimensions = (0, 1),
+ *,
+ width: Optional[int] = None,
+ expand: bool = False,
+ equal: bool = False,
+ column_first: bool = False,
+ right_to_left: bool = False,
+ align: Optional[AlignMethod] = None,
+ title: Optional[TextType] = None,
+ ) -> None:
+ self.renderables = list(renderables or [])
+ self.width = width
+ self.padding = padding
+ self.expand = expand
+ self.equal = equal
+ self.column_first = column_first
+ self.right_to_left = right_to_left
+ self.align: Optional[AlignMethod] = align
+ self.title = title
+
+ def add_renderable(self, renderable: RenderableType) -> None:
+ """Add a renderable to the columns.
+
+ Args:
+ renderable (RenderableType): Any renderable object.
+ """
+ self.renderables.append(renderable)
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ render_str = console.render_str
+ renderables = [
+ render_str(renderable) if isinstance(renderable, str) else renderable
+ for renderable in self.renderables
+ ]
+ if not renderables:
+ return
+ _top, right, _bottom, left = Padding.unpack(self.padding)
+ width_padding = max(left, right)
+ max_width = options.max_width
+ widths: Dict[int, int] = defaultdict(int)
+ column_count = len(renderables)
+
+ get_measurement = Measurement.get
+ renderable_widths = [
+ get_measurement(console, options, renderable).maximum
+ for renderable in renderables
+ ]
+ if self.equal:
+ renderable_widths = [max(renderable_widths)] * len(renderable_widths)
+
+ def iter_renderables(
+ column_count: int,
+ ) -> Iterable[Tuple[int, Optional[RenderableType]]]:
+ item_count = len(renderables)
+ if self.column_first:
+ width_renderables = list(zip(renderable_widths, renderables))
+
+ column_lengths: List[int] = [item_count // column_count] * column_count
+ for col_no in range(item_count % column_count):
+ column_lengths[col_no] += 1
+
+ row_count = (item_count + column_count - 1) // column_count
+ cells = [[-1] * column_count for _ in range(row_count)]
+ row = col = 0
+ for index in range(item_count):
+ cells[row][col] = index
+ column_lengths[col] -= 1
+ if column_lengths[col]:
+ row += 1
+ else:
+ col += 1
+ row = 0
+ for index in chain.from_iterable(cells):
+ if index == -1:
+ break
+ yield width_renderables[index]
+ else:
+ yield from zip(renderable_widths, renderables)
+ # Pad odd elements with spaces
+ if item_count % column_count:
+ for _ in range(column_count - (item_count % column_count)):
+ yield 0, None
+
+ table = Table.grid(padding=self.padding, collapse_padding=True, pad_edge=False)
+ table.expand = self.expand
+ table.title = self.title
+
+ if self.width is not None:
+ column_count = (max_width) // (self.width + width_padding)
+ for _ in range(column_count):
+ table.add_column(width=self.width)
+ else:
+ while column_count > 1:
+ widths.clear()
+ column_no = 0
+ for renderable_width, _ in iter_renderables(column_count):
+ widths[column_no] = max(widths[column_no], renderable_width)
+ total_width = sum(widths.values()) + width_padding * (
+ len(widths) - 1
+ )
+ if total_width > max_width:
+ column_count = len(widths) - 1
+ break
+ else:
+ column_no = (column_no + 1) % column_count
+ else:
+ break
+
+ get_renderable = itemgetter(1)
+ _renderables = [
+ get_renderable(_renderable)
+ for _renderable in iter_renderables(column_count)
+ ]
+ if self.equal:
+ _renderables = [
+ None
+ if renderable is None
+ else Constrain(renderable, renderable_widths[0])
+ for renderable in _renderables
+ ]
+ if self.align:
+ align = self.align
+ _Align = Align
+ _renderables = [
+ None if renderable is None else _Align(renderable, align)
+ for renderable in _renderables
+ ]
+
+ right_to_left = self.right_to_left
+ add_row = table.add_row
+ for start in range(0, len(_renderables), column_count):
+ row = _renderables[start : start + column_count]
+ if right_to_left:
+ row = row[::-1]
+ add_row(*row)
+ yield table
+
+
+if __name__ == "__main__": # pragma: no cover
+ import os
+
+ console = Console()
+
+ files = [f"{i} {s}" for i, s in enumerate(sorted(os.listdir()))]
+ columns = Columns(files, padding=(0, 1), expand=False, equal=False)
+ console.print(columns)
+ console.rule()
+ columns.column_first = True
+ console.print(columns)
+ columns.right_to_left = True
+ console.rule()
+ console.print(columns)
diff --git a/src/pip/_vendor/rich/console.py b/src/pip/_vendor/rich/console.py
new file mode 100644
index 000000000..93a10b0b5
--- /dev/null
+++ b/src/pip/_vendor/rich/console.py
@@ -0,0 +1,2572 @@
+import inspect
+import io
+import os
+import platform
+import sys
+import threading
+import zlib
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from datetime import datetime
+from functools import wraps
+from getpass import getpass
+from html import escape
+from inspect import isclass
+from itertools import islice
+from math import ceil
+from time import monotonic
+from types import FrameType, ModuleType, TracebackType
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ List,
+ Mapping,
+ NamedTuple,
+ Optional,
+ TextIO,
+ Tuple,
+ Type,
+ Union,
+ cast,
+)
+
+if sys.version_info >= (3, 8):
+ from typing import Literal, Protocol, runtime_checkable
+else:
+ from pip._vendor.typing_extensions import (
+ Literal,
+ Protocol,
+ runtime_checkable,
+ ) # pragma: no cover
+
+from . import errors, themes
+from ._emoji_replace import _emoji_replace
+from ._export_format import CONSOLE_HTML_FORMAT, CONSOLE_SVG_FORMAT
+from ._log_render import FormatTimeCallable, LogRender
+from .align import Align, AlignMethod
+from .color import ColorSystem, blend_rgb
+from .control import Control
+from .emoji import EmojiVariant
+from .highlighter import NullHighlighter, ReprHighlighter
+from .markup import render as render_markup
+from .measure import Measurement, measure_renderables
+from .pager import Pager, SystemPager
+from .pretty import Pretty, is_expandable
+from .protocol import rich_cast
+from .region import Region
+from .scope import render_scope
+from .screen import Screen
+from .segment import Segment
+from .style import Style, StyleType
+from .styled import Styled
+from .terminal_theme import DEFAULT_TERMINAL_THEME, SVG_EXPORT_THEME, TerminalTheme
+from .text import Text, TextType
+from .theme import Theme, ThemeStack
+
+if TYPE_CHECKING:
+ from ._windows import WindowsConsoleFeatures
+ from .live import Live
+ from .status import Status
+
+JUPYTER_DEFAULT_COLUMNS = 115
+JUPYTER_DEFAULT_LINES = 100
+WINDOWS = platform.system() == "Windows"
+
+HighlighterType = Callable[[Union[str, "Text"]], "Text"]
+JustifyMethod = Literal["default", "left", "center", "right", "full"]
+OverflowMethod = Literal["fold", "crop", "ellipsis", "ignore"]
+
+
+class NoChange:
+ pass
+
+
+NO_CHANGE = NoChange()
+
+try:
+ _STDIN_FILENO = sys.__stdin__.fileno()
+except Exception:
+ _STDIN_FILENO = 0
+try:
+ _STDOUT_FILENO = sys.__stdout__.fileno()
+except Exception:
+ _STDOUT_FILENO = 1
+try:
+ _STDERR_FILENO = sys.__stderr__.fileno()
+except Exception:
+ _STDERR_FILENO = 2
+
+_STD_STREAMS = (_STDIN_FILENO, _STDOUT_FILENO, _STDERR_FILENO)
+_STD_STREAMS_OUTPUT = (_STDOUT_FILENO, _STDERR_FILENO)
+
+
+_TERM_COLORS = {"256color": ColorSystem.EIGHT_BIT, "16color": ColorSystem.STANDARD}
+
+
+class ConsoleDimensions(NamedTuple):
+ """Size of the terminal."""
+
+ width: int
+ """The width of the console in 'cells'."""
+ height: int
+ """The height of the console in lines."""
+
+
+@dataclass
+class ConsoleOptions:
+ """Options for __rich_console__ method."""
+
+ size: ConsoleDimensions
+ """Size of console."""
+ legacy_windows: bool
+ """legacy_windows: flag for legacy windows."""
+ min_width: int
+ """Minimum width of renderable."""
+ max_width: int
+ """Maximum width of renderable."""
+ is_terminal: bool
+ """True if the target is a terminal, otherwise False."""
+ encoding: str
+ """Encoding of terminal."""
+ max_height: int
+ """Height of container (starts as terminal)"""
+ justify: Optional[JustifyMethod] = None
+ """Justify value override for renderable."""
+ overflow: Optional[OverflowMethod] = None
+ """Overflow value override for renderable."""
+ no_wrap: Optional[bool] = False
+ """Disable wrapping for text."""
+ highlight: Optional[bool] = None
+ """Highlight override for render_str."""
+ markup: Optional[bool] = None
+ """Enable markup when rendering strings."""
+ height: Optional[int] = None
+
+ @property
+ def ascii_only(self) -> bool:
+ """Check if renderables should use ascii only."""
+ return not self.encoding.startswith("utf")
+
+ def copy(self) -> "ConsoleOptions":
+ """Return a copy of the options.
+
+ Returns:
+ ConsoleOptions: a copy of self.
+ """
+ options: ConsoleOptions = ConsoleOptions.__new__(ConsoleOptions)
+ options.__dict__ = self.__dict__.copy()
+ return options
+
+ def update(
+ self,
+ *,
+ width: Union[int, NoChange] = NO_CHANGE,
+ min_width: Union[int, NoChange] = NO_CHANGE,
+ max_width: Union[int, NoChange] = NO_CHANGE,
+ justify: Union[Optional[JustifyMethod], NoChange] = NO_CHANGE,
+ overflow: Union[Optional[OverflowMethod], NoChange] = NO_CHANGE,
+ no_wrap: Union[Optional[bool], NoChange] = NO_CHANGE,
+ highlight: Union[Optional[bool], NoChange] = NO_CHANGE,
+ markup: Union[Optional[bool], NoChange] = NO_CHANGE,
+ height: Union[Optional[int], NoChange] = NO_CHANGE,
+ ) -> "ConsoleOptions":
+ """Update values, return a copy."""
+ options = self.copy()
+ if not isinstance(width, NoChange):
+ options.min_width = options.max_width = max(0, width)
+ if not isinstance(min_width, NoChange):
+ options.min_width = min_width
+ if not isinstance(max_width, NoChange):
+ options.max_width = max_width
+ if not isinstance(justify, NoChange):
+ options.justify = justify
+ if not isinstance(overflow, NoChange):
+ options.overflow = overflow
+ if not isinstance(no_wrap, NoChange):
+ options.no_wrap = no_wrap
+ if not isinstance(highlight, NoChange):
+ options.highlight = highlight
+ if not isinstance(markup, NoChange):
+ options.markup = markup
+ if not isinstance(height, NoChange):
+ if height is not None:
+ options.max_height = height
+ options.height = None if height is None else max(0, height)
+ return options
+
+ def update_width(self, width: int) -> "ConsoleOptions":
+ """Update just the width, return a copy.
+
+ Args:
+ width (int): New width (sets both min_width and max_width)
+
+ Returns:
+ ~ConsoleOptions: New console options instance.
+ """
+ options = self.copy()
+ options.min_width = options.max_width = max(0, width)
+ return options
+
+ def update_height(self, height: int) -> "ConsoleOptions":
+ """Update the height, and return a copy.
+
+ Args:
+ height (int): New height
+
+ Returns:
+ ~ConsoleOptions: New Console options instance.
+ """
+ options = self.copy()
+ options.max_height = options.height = height
+ return options
+
+ def reset_height(self) -> "ConsoleOptions":
+ """Return a copy of the options with height set to ``None``.
+
+ Returns:
+ ~ConsoleOptions: New console options instance.
+ """
+ options = self.copy()
+ options.height = None
+ return options
+
+ def update_dimensions(self, width: int, height: int) -> "ConsoleOptions":
+ """Update the width and height, and return a copy.
+
+ Args:
+ width (int): New width (sets both min_width and max_width).
+ height (int): New height.
+
+ Returns:
+ ~ConsoleOptions: New console options instance.
+ """
+ options = self.copy()
+ options.min_width = options.max_width = max(0, width)
+ options.height = options.max_height = height
+ return options
+
+
+@runtime_checkable
+class RichCast(Protocol):
+ """An object that may be 'cast' to a console renderable."""
+
+ def __rich__(
+ self,
+ ) -> Union["ConsoleRenderable", "RichCast", str]: # pragma: no cover
+ ...
+
+
+@runtime_checkable
+class ConsoleRenderable(Protocol):
+ """An object that supports the console protocol."""
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult": # pragma: no cover
+ ...
+
+
+# A type that may be rendered by Console.
+RenderableType = Union[ConsoleRenderable, RichCast, str]
+
+# The result of calling a __rich_console__ method.
+RenderResult = Iterable[Union[RenderableType, Segment]]
+
+_null_highlighter = NullHighlighter()
+
+
+class CaptureError(Exception):
+ """An error in the Capture context manager."""
+
+
+class NewLine:
+ """A renderable to generate new line(s)"""
+
+ def __init__(self, count: int = 1) -> None:
+ self.count = count
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> Iterable[Segment]:
+ yield Segment("\n" * self.count)
+
+
+class ScreenUpdate:
+ """Render a list of lines at a given offset."""
+
+ def __init__(self, lines: List[List[Segment]], x: int, y: int) -> None:
+ self._lines = lines
+ self.x = x
+ self.y = y
+
+ def __rich_console__(
+ self, console: "Console", options: ConsoleOptions
+ ) -> RenderResult:
+ x = self.x
+ move_to = Control.move_to
+ for offset, line in enumerate(self._lines, self.y):
+ yield move_to(x, offset)
+ yield from line
+
+
+class Capture:
+ """Context manager to capture the result of printing to the console.
+ See :meth:`~rich.console.Console.capture` for how to use.
+
+ Args:
+ console (Console): A console instance to capture output.
+ """
+
+ def __init__(self, console: "Console") -> None:
+ self._console = console
+ self._result: Optional[str] = None
+
+ def __enter__(self) -> "Capture":
+ self._console.begin_capture()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ self._result = self._console.end_capture()
+
+ def get(self) -> str:
+ """Get the result of the capture."""
+ if self._result is None:
+ raise CaptureError(
+ "Capture result is not available until context manager exits."
+ )
+ return self._result
+
+
+class ThemeContext:
+ """A context manager to use a temporary theme. See :meth:`~rich.console.Console.use_theme` for usage."""
+
+ def __init__(self, console: "Console", theme: Theme, inherit: bool = True) -> None:
+ self.console = console
+ self.theme = theme
+ self.inherit = inherit
+
+ def __enter__(self) -> "ThemeContext":
+ self.console.push_theme(self.theme)
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ self.console.pop_theme()
+
+
+class PagerContext:
+ """A context manager that 'pages' content. See :meth:`~rich.console.Console.pager` for usage."""
+
+ def __init__(
+ self,
+ console: "Console",
+ pager: Optional[Pager] = None,
+ styles: bool = False,
+ links: bool = False,
+ ) -> None:
+ self._console = console
+ self.pager = SystemPager() if pager is None else pager
+ self.styles = styles
+ self.links = links
+
+ def __enter__(self) -> "PagerContext":
+ self._console._enter_buffer()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ if exc_type is None:
+ with self._console._lock:
+ buffer: List[Segment] = self._console._buffer[:]
+ del self._console._buffer[:]
+ segments: Iterable[Segment] = buffer
+ if not self.styles:
+ segments = Segment.strip_styles(segments)
+ elif not self.links:
+ segments = Segment.strip_links(segments)
+ content = self._console._render_buffer(segments)
+ self.pager.show(content)
+ self._console._exit_buffer()
+
+
+class ScreenContext:
+ """A context manager that enables an alternative screen. See :meth:`~rich.console.Console.screen` for usage."""
+
+ def __init__(
+ self, console: "Console", hide_cursor: bool, style: StyleType = ""
+ ) -> None:
+ self.console = console
+ self.hide_cursor = hide_cursor
+ self.screen = Screen(style=style)
+ self._changed = False
+
+ def update(
+ self, *renderables: RenderableType, style: Optional[StyleType] = None
+ ) -> None:
+ """Update the screen.
+
+ Args:
+ renderable (RenderableType, optional): Optional renderable to replace current renderable,
+ or None for no change. Defaults to None.
+ style: (Style, optional): Replacement style, or None for no change. Defaults to None.
+ """
+ if renderables:
+ self.screen.renderable = (
+ Group(*renderables) if len(renderables) > 1 else renderables[0]
+ )
+ if style is not None:
+ self.screen.style = style
+ self.console.print(self.screen, end="")
+
+ def __enter__(self) -> "ScreenContext":
+ self._changed = self.console.set_alt_screen(True)
+ if self._changed and self.hide_cursor:
+ self.console.show_cursor(False)
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ if self._changed:
+ self.console.set_alt_screen(False)
+ if self.hide_cursor:
+ self.console.show_cursor(True)
+
+
+class Group:
+ """Takes a group of renderables and returns a renderable object that renders the group.
+
+ Args:
+ renderables (Iterable[RenderableType]): An iterable of renderable objects.
+ fit (bool, optional): Fit dimension of group to contents, or fill available space. Defaults to True.
+ """
+
+ def __init__(self, *renderables: "RenderableType", fit: bool = True) -> None:
+ self._renderables = renderables
+ self.fit = fit
+ self._render: Optional[List[RenderableType]] = None
+
+ @property
+ def renderables(self) -> List["RenderableType"]:
+ if self._render is None:
+ self._render = list(self._renderables)
+ return self._render
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "Measurement":
+ if self.fit:
+ return measure_renderables(console, options, self.renderables)
+ else:
+ return Measurement(options.max_width, options.max_width)
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> RenderResult:
+ yield from self.renderables
+
+
+def group(fit: bool = True) -> Callable[..., Callable[..., Group]]:
+ """A decorator that turns an iterable of renderables in to a group.
+
+ Args:
+ fit (bool, optional): Fit dimension of group to contents, or fill available space. Defaults to True.
+ """
+
+ def decorator(
+ method: Callable[..., Iterable[RenderableType]]
+ ) -> Callable[..., Group]:
+ """Convert a method that returns an iterable of renderables in to a Group."""
+
+ @wraps(method)
+ def _replace(*args: Any, **kwargs: Any) -> Group:
+ renderables = method(*args, **kwargs)
+ return Group(*renderables, fit=fit)
+
+ return _replace
+
+ return decorator
+
+
+def _is_jupyter() -> bool: # pragma: no cover
+ """Check if we're running in a Jupyter notebook."""
+ try:
+ get_ipython # type: ignore[name-defined]
+ except NameError:
+ return False
+ ipython = get_ipython() # type: ignore[name-defined]
+ shell = ipython.__class__.__name__
+ if "google.colab" in str(ipython.__class__) or shell == "ZMQInteractiveShell":
+ return True # Jupyter notebook or qtconsole
+ elif shell == "TerminalInteractiveShell":
+ return False # Terminal running IPython
+ else:
+ return False # Other type (?)
+
+
+COLOR_SYSTEMS = {
+ "standard": ColorSystem.STANDARD,
+ "256": ColorSystem.EIGHT_BIT,
+ "truecolor": ColorSystem.TRUECOLOR,
+ "windows": ColorSystem.WINDOWS,
+}
+
+_COLOR_SYSTEMS_NAMES = {system: name for name, system in COLOR_SYSTEMS.items()}
+
+
+@dataclass
+class ConsoleThreadLocals(threading.local):
+ """Thread local values for Console context."""
+
+ theme_stack: ThemeStack
+ buffer: List[Segment] = field(default_factory=list)
+ buffer_index: int = 0
+
+
+class RenderHook(ABC):
+ """Provides hooks in to the render process."""
+
+ @abstractmethod
+ def process_renderables(
+ self, renderables: List[ConsoleRenderable]
+ ) -> List[ConsoleRenderable]:
+ """Called with a list of objects to render.
+
+ This method can return a new list of renderables, or modify and return the same list.
+
+ Args:
+ renderables (List[ConsoleRenderable]): A number of renderable objects.
+
+ Returns:
+ List[ConsoleRenderable]: A replacement list of renderables.
+ """
+
+
+_windows_console_features: Optional["WindowsConsoleFeatures"] = None
+
+
+def get_windows_console_features() -> "WindowsConsoleFeatures": # pragma: no cover
+ global _windows_console_features
+ if _windows_console_features is not None:
+ return _windows_console_features
+ from ._windows import get_windows_console_features
+
+ _windows_console_features = get_windows_console_features()
+ return _windows_console_features
+
+
+def detect_legacy_windows() -> bool:
+ """Detect legacy Windows."""
+ return WINDOWS and not get_windows_console_features().vt
+
+
+class Console:
+ """A high level console interface.
+
+ Args:
+ color_system (str, optional): The color system supported by your terminal,
+ either ``"standard"``, ``"256"`` or ``"truecolor"``. Leave as ``"auto"`` to autodetect.
+ force_terminal (Optional[bool], optional): Enable/disable terminal control codes, or None to auto-detect terminal. Defaults to None.
+ force_jupyter (Optional[bool], optional): Enable/disable Jupyter rendering, or None to auto-detect Jupyter. Defaults to None.
+ force_interactive (Optional[bool], optional): Enable/disable interactive mode, or None to auto detect. Defaults to None.
+ soft_wrap (Optional[bool], optional): Set soft wrap default on print method. Defaults to False.
+ theme (Theme, optional): An optional style theme object, or ``None`` for default theme.
+ stderr (bool, optional): Use stderr rather than stdout if ``file`` is not specified. Defaults to False.
+ file (IO, optional): A file object where the console should write to. Defaults to stdout.
+ quiet (bool, Optional): Boolean to suppress all output. Defaults to False.
+ width (int, optional): The width of the terminal. Leave as default to auto-detect width.
+ height (int, optional): The height of the terminal. Leave as default to auto-detect height.
+ style (StyleType, optional): Style to apply to all output, or None for no style. Defaults to None.
+ no_color (Optional[bool], optional): Enabled no color mode, or None to auto detect. Defaults to None.
+ tab_size (int, optional): Number of spaces used to replace a tab character. Defaults to 8.
+ record (bool, optional): Boolean to enable recording of terminal output,
+ required to call :meth:`export_html`, :meth:`export_svg`, and :meth:`export_text`. Defaults to False.
+ markup (bool, optional): Boolean to enable :ref:`console_markup`. Defaults to True.
+ emoji (bool, optional): Enable emoji code. Defaults to True.
+ emoji_variant (str, optional): Optional emoji variant, either "text" or "emoji". Defaults to None.
+ highlight (bool, optional): Enable automatic highlighting. Defaults to True.
+ log_time (bool, optional): Boolean to enable logging of time by :meth:`log` methods. Defaults to True.
+ log_path (bool, optional): Boolean to enable the logging of the caller by :meth:`log`. Defaults to True.
+ log_time_format (Union[str, TimeFormatterCallable], optional): If ``log_time`` is enabled, either string for strftime or callable that formats the time. Defaults to "[%X] ".
+ highlighter (HighlighterType, optional): Default highlighter.
+ legacy_windows (bool, optional): Enable legacy Windows mode, or ``None`` to auto detect. Defaults to ``None``.
+ safe_box (bool, optional): Restrict box options that don't render on legacy Windows.
+ get_datetime (Callable[[], datetime], optional): Callable that gets the current time as a datetime.datetime object (used by Console.log),
+ or None for datetime.now.
+ get_time (Callable[[], time], optional): Callable that gets the current time in seconds, default uses time.monotonic.
+ """
+
+ _environ: Mapping[str, str] = os.environ
+
+ def __init__(
+ self,
+ *,
+ color_system: Optional[
+ Literal["auto", "standard", "256", "truecolor", "windows"]
+ ] = "auto",
+ force_terminal: Optional[bool] = None,
+ force_jupyter: Optional[bool] = None,
+ force_interactive: Optional[bool] = None,
+ soft_wrap: bool = False,
+ theme: Optional[Theme] = None,
+ stderr: bool = False,
+ file: Optional[IO[str]] = None,
+ quiet: bool = False,
+ width: Optional[int] = None,
+ height: Optional[int] = None,
+ style: Optional[StyleType] = None,
+ no_color: Optional[bool] = None,
+ tab_size: int = 8,
+ record: bool = False,
+ markup: bool = True,
+ emoji: bool = True,
+ emoji_variant: Optional[EmojiVariant] = None,
+ highlight: bool = True,
+ log_time: bool = True,
+ log_path: bool = True,
+ log_time_format: Union[str, FormatTimeCallable] = "[%X]",
+ highlighter: Optional["HighlighterType"] = ReprHighlighter(),
+ legacy_windows: Optional[bool] = None,
+ safe_box: bool = True,
+ get_datetime: Optional[Callable[[], datetime]] = None,
+ get_time: Optional[Callable[[], float]] = None,
+ _environ: Optional[Mapping[str, str]] = None,
+ ):
+ # Copy of os.environ allows us to replace it for testing
+ if _environ is not None:
+ self._environ = _environ
+
+ self.is_jupyter = _is_jupyter() if force_jupyter is None else force_jupyter
+ if self.is_jupyter:
+ if width is None:
+ jupyter_columns = self._environ.get("JUPYTER_COLUMNS")
+ if jupyter_columns is not None and jupyter_columns.isdigit():
+ width = int(jupyter_columns)
+ else:
+ width = JUPYTER_DEFAULT_COLUMNS
+ if height is None:
+ jupyter_lines = self._environ.get("JUPYTER_LINES")
+ if jupyter_lines is not None and jupyter_lines.isdigit():
+ height = int(jupyter_lines)
+ else:
+ height = JUPYTER_DEFAULT_LINES
+
+ self.tab_size = tab_size
+ self.record = record
+ self._markup = markup
+ self._emoji = emoji
+ self._emoji_variant: Optional[EmojiVariant] = emoji_variant
+ self._highlight = highlight
+ self.legacy_windows: bool = (
+ (detect_legacy_windows() and not self.is_jupyter)
+ if legacy_windows is None
+ else legacy_windows
+ )
+
+ if width is None:
+ columns = self._environ.get("COLUMNS")
+ if columns is not None and columns.isdigit():
+ width = int(columns) - self.legacy_windows
+ if height is None:
+ lines = self._environ.get("LINES")
+ if lines is not None and lines.isdigit():
+ height = int(lines)
+
+ self.soft_wrap = soft_wrap
+ self._width = width
+ self._height = height
+
+ self._color_system: Optional[ColorSystem]
+ self._force_terminal = force_terminal
+ self._file = file
+ self.quiet = quiet
+ self.stderr = stderr
+
+ if color_system is None:
+ self._color_system = None
+ elif color_system == "auto":
+ self._color_system = self._detect_color_system()
+ else:
+ self._color_system = COLOR_SYSTEMS[color_system]
+
+ self._lock = threading.RLock()
+ self._log_render = LogRender(
+ show_time=log_time,
+ show_path=log_path,
+ time_format=log_time_format,
+ )
+ self.highlighter: HighlighterType = highlighter or _null_highlighter
+ self.safe_box = safe_box
+ self.get_datetime = get_datetime or datetime.now
+ self.get_time = get_time or monotonic
+ self.style = style
+ self.no_color = (
+ no_color if no_color is not None else "NO_COLOR" in self._environ
+ )
+ self.is_interactive = (
+ (self.is_terminal and not self.is_dumb_terminal)
+ if force_interactive is None
+ else force_interactive
+ )
+
+ self._record_buffer_lock = threading.RLock()
+ self._thread_locals = ConsoleThreadLocals(
+ theme_stack=ThemeStack(themes.DEFAULT if theme is None else theme)
+ )
+ self._record_buffer: List[Segment] = []
+ self._render_hooks: List[RenderHook] = []
+ self._live: Optional["Live"] = None
+ self._is_alt_screen = False
+
+ def __repr__(self) -> str:
+ return f"<console width={self.width} {str(self._color_system)}>"
+
+ @property
+ def file(self) -> IO[str]:
+ """Get the file object to write to."""
+ file = self._file or (sys.stderr if self.stderr else sys.stdout)
+ file = getattr(file, "rich_proxied_file", file)
+ return file
+
+ @file.setter
+ def file(self, new_file: IO[str]) -> None:
+ """Set a new file object."""
+ self._file = new_file
+
+ @property
+ def _buffer(self) -> List[Segment]:
+ """Get a thread local buffer."""
+ return self._thread_locals.buffer
+
+ @property
+ def _buffer_index(self) -> int:
+ """Get a thread local buffer."""
+ return self._thread_locals.buffer_index
+
+ @_buffer_index.setter
+ def _buffer_index(self, value: int) -> None:
+ self._thread_locals.buffer_index = value
+
+ @property
+ def _theme_stack(self) -> ThemeStack:
+ """Get the thread local theme stack."""
+ return self._thread_locals.theme_stack
+
+ def _detect_color_system(self) -> Optional[ColorSystem]:
+ """Detect color system from env vars."""
+ if self.is_jupyter:
+ return ColorSystem.TRUECOLOR
+ if not self.is_terminal or self.is_dumb_terminal:
+ return None
+ if WINDOWS: # pragma: no cover
+ if self.legacy_windows: # pragma: no cover
+ return ColorSystem.WINDOWS
+ windows_console_features = get_windows_console_features()
+ return (
+ ColorSystem.TRUECOLOR
+ if windows_console_features.truecolor
+ else ColorSystem.EIGHT_BIT
+ )
+ else:
+ color_term = self._environ.get("COLORTERM", "").strip().lower()
+ if color_term in ("truecolor", "24bit"):
+ return ColorSystem.TRUECOLOR
+ term = self._environ.get("TERM", "").strip().lower()
+ _term_name, _hyphen, colors = term.rpartition("-")
+ color_system = _TERM_COLORS.get(colors, ColorSystem.STANDARD)
+ return color_system
+
+ def _enter_buffer(self) -> None:
+ """Enter in to a buffer context, and buffer all output."""
+ self._buffer_index += 1
+
+ def _exit_buffer(self) -> None:
+ """Leave buffer context, and render content if required."""
+ self._buffer_index -= 1
+ self._check_buffer()
+
+ def set_live(self, live: "Live") -> None:
+ """Set Live instance. Used by Live context manager.
+
+ Args:
+ live (Live): Live instance using this Console.
+
+ Raises:
+ errors.LiveError: If this Console has a Live context currently active.
+ """
+ with self._lock:
+ if self._live is not None:
+ raise errors.LiveError("Only one live display may be active at once")
+ self._live = live
+
+ def clear_live(self) -> None:
+ """Clear the Live instance."""
+ with self._lock:
+ self._live = None
+
+ def push_render_hook(self, hook: RenderHook) -> None:
+ """Add a new render hook to the stack.
+
+ Args:
+ hook (RenderHook): Render hook instance.
+ """
+ with self._lock:
+ self._render_hooks.append(hook)
+
+ def pop_render_hook(self) -> None:
+ """Pop the last renderhook from the stack."""
+ with self._lock:
+ self._render_hooks.pop()
+
+ def __enter__(self) -> "Console":
+ """Own context manager to enter buffer context."""
+ self._enter_buffer()
+ return self
+
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
+ """Exit buffer context."""
+ self._exit_buffer()
+
+ def begin_capture(self) -> None:
+ """Begin capturing console output. Call :meth:`end_capture` to exit capture mode and return output."""
+ self._enter_buffer()
+
+ def end_capture(self) -> str:
+ """End capture mode and return captured string.
+
+ Returns:
+ str: Console output.
+ """
+ render_result = self._render_buffer(self._buffer)
+ del self._buffer[:]
+ self._exit_buffer()
+ return render_result
+
+ def push_theme(self, theme: Theme, *, inherit: bool = True) -> None:
+ """Push a new theme on to the top of the stack, replacing the styles from the previous theme.
+ Generally speaking, you should call :meth:`~rich.console.Console.use_theme` to get a context manager, rather
+ than calling this method directly.
+
+ Args:
+ theme (Theme): A theme instance.
+ inherit (bool, optional): Inherit existing styles. Defaults to True.
+ """
+ self._theme_stack.push_theme(theme, inherit=inherit)
+
+ def pop_theme(self) -> None:
+ """Remove theme from top of stack, restoring previous theme."""
+ self._theme_stack.pop_theme()
+
+ def use_theme(self, theme: Theme, *, inherit: bool = True) -> ThemeContext:
+ """Use a different theme for the duration of the context manager.
+
+ Args:
+ theme (Theme): Theme instance to user.
+ inherit (bool, optional): Inherit existing console styles. Defaults to True.
+
+ Returns:
+ ThemeContext: [description]
+ """
+ return ThemeContext(self, theme, inherit)
+
+ @property
+ def color_system(self) -> Optional[str]:
+ """Get color system string.
+
+ Returns:
+ Optional[str]: "standard", "256" or "truecolor".
+ """
+
+ if self._color_system is not None:
+ return _COLOR_SYSTEMS_NAMES[self._color_system]
+ else:
+ return None
+
+ @property
+ def encoding(self) -> str:
+ """Get the encoding of the console file, e.g. ``"utf-8"``.
+
+ Returns:
+ str: A standard encoding string.
+ """
+ return (getattr(self.file, "encoding", "utf-8") or "utf-8").lower()
+
+ @property
+ def is_terminal(self) -> bool:
+ """Check if the console is writing to a terminal.
+
+ Returns:
+ bool: True if the console writing to a device capable of
+ understanding terminal codes, otherwise False.
+ """
+ if self._force_terminal is not None:
+ return self._force_terminal
+
+ if hasattr(sys.stdin, "__module__") and sys.stdin.__module__.startswith(
+ "idlelib"
+ ):
+ # Return False for Idle which claims to be a tty but can't handle ansi codes
+ return False
+
+ isatty: Optional[Callable[[], bool]] = getattr(self.file, "isatty", None)
+ try:
+ return False if isatty is None else isatty()
+ except ValueError:
+ # in some situation (at the end of a pytest run for example) isatty() can raise
+ # ValueError: I/O operation on closed file
+ # return False because we aren't in a terminal anymore
+ return False
+
+ @property
+ def is_dumb_terminal(self) -> bool:
+ """Detect dumb terminal.
+
+ Returns:
+ bool: True if writing to a dumb terminal, otherwise False.
+
+ """
+ _term = self._environ.get("TERM", "")
+ is_dumb = _term.lower() in ("dumb", "unknown")
+ return self.is_terminal and is_dumb
+
+ @property
+ def options(self) -> ConsoleOptions:
+ """Get default console options."""
+ return ConsoleOptions(
+ max_height=self.size.height,
+ size=self.size,
+ legacy_windows=self.legacy_windows,
+ min_width=1,
+ max_width=self.width,
+ encoding=self.encoding,
+ is_terminal=self.is_terminal,
+ )
+
+ @property
+ def size(self) -> ConsoleDimensions:
+ """Get the size of the console.
+
+ Returns:
+ ConsoleDimensions: A named tuple containing the dimensions.
+ """
+
+ if self._width is not None and self._height is not None:
+ return ConsoleDimensions(self._width - self.legacy_windows, self._height)
+
+ if self.is_dumb_terminal:
+ return ConsoleDimensions(80, 25)
+
+ width: Optional[int] = None
+ height: Optional[int] = None
+
+ if WINDOWS: # pragma: no cover
+ try:
+ width, height = os.get_terminal_size()
+ except (AttributeError, ValueError, OSError): # Probably not a terminal
+ pass
+ else:
+ for file_descriptor in _STD_STREAMS:
+ try:
+ width, height = os.get_terminal_size(file_descriptor)
+ except (AttributeError, ValueError, OSError):
+ pass
+ else:
+ break
+
+ columns = self._environ.get("COLUMNS")
+ if columns is not None and columns.isdigit():
+ width = int(columns)
+ lines = self._environ.get("LINES")
+ if lines is not None and lines.isdigit():
+ height = int(lines)
+
+ # get_terminal_size can report 0, 0 if run from pseudo-terminal
+ width = width or 80
+ height = height or 25
+ return ConsoleDimensions(
+ width - self.legacy_windows if self._width is None else self._width,
+ height if self._height is None else self._height,
+ )
+
+ @size.setter
+ def size(self, new_size: Tuple[int, int]) -> None:
+ """Set a new size for the terminal.
+
+ Args:
+ new_size (Tuple[int, int]): New width and height.
+ """
+ width, height = new_size
+ self._width = width
+ self._height = height
+
+ @property
+ def width(self) -> int:
+ """Get the width of the console.
+
+ Returns:
+ int: The width (in characters) of the console.
+ """
+ return self.size.width
+
+ @width.setter
+ def width(self, width: int) -> None:
+ """Set width.
+
+ Args:
+ width (int): New width.
+ """
+ self._width = width
+
+ @property
+ def height(self) -> int:
+ """Get the height of the console.
+
+ Returns:
+ int: The height (in lines) of the console.
+ """
+ return self.size.height
+
+ @height.setter
+ def height(self, height: int) -> None:
+ """Set height.
+
+ Args:
+ height (int): new height.
+ """
+ self._height = height
+
+ def bell(self) -> None:
+ """Play a 'bell' sound (if supported by the terminal)."""
+ self.control(Control.bell())
+
+ def capture(self) -> Capture:
+ """A context manager to *capture* the result of print() or log() in a string,
+ rather than writing it to the console.
+
+ Example:
+ >>> from rich.console import Console
+ >>> console = Console()
+ >>> with console.capture() as capture:
+ ... console.print("[bold magenta]Hello World[/]")
+ >>> print(capture.get())
+
+ Returns:
+ Capture: Context manager with disables writing to the terminal.
+ """
+ capture = Capture(self)
+ return capture
+
+ def pager(
+ self, pager: Optional[Pager] = None, styles: bool = False, links: bool = False
+ ) -> PagerContext:
+ """A context manager to display anything printed within a "pager". The pager application
+ is defined by the system and will typically support at least pressing a key to scroll.
+
+ Args:
+ pager (Pager, optional): A pager object, or None to use :class:`~rich.pager.SystemPager`. Defaults to None.
+ styles (bool, optional): Show styles in pager. Defaults to False.
+ links (bool, optional): Show links in pager. Defaults to False.
+
+ Example:
+ >>> from rich.console import Console
+ >>> from rich.__main__ import make_test_card
+ >>> console = Console()
+ >>> with console.pager():
+ console.print(make_test_card())
+
+ Returns:
+ PagerContext: A context manager.
+ """
+ return PagerContext(self, pager=pager, styles=styles, links=links)
+
+ def line(self, count: int = 1) -> None:
+ """Write new line(s).
+
+ Args:
+ count (int, optional): Number of new lines. Defaults to 1.
+ """
+
+ assert count >= 0, "count must be >= 0"
+ self.print(NewLine(count))
+
+ def clear(self, home: bool = True) -> None:
+ """Clear the screen.
+
+ Args:
+ home (bool, optional): Also move the cursor to 'home' position. Defaults to True.
+ """
+ if home:
+ self.control(Control.clear(), Control.home())
+ else:
+ self.control(Control.clear())
+
+ def status(
+ self,
+ status: RenderableType,
+ *,
+ spinner: str = "dots",
+ spinner_style: str = "status.spinner",
+ speed: float = 1.0,
+ refresh_per_second: float = 12.5,
+ ) -> "Status":
+ """Display a status and spinner.
+
+ Args:
+ status (RenderableType): A status renderable (str or Text typically).
+ spinner (str, optional): Name of spinner animation (see python -m rich.spinner). Defaults to "dots".
+ spinner_style (StyleType, optional): Style of spinner. Defaults to "status.spinner".
+ speed (float, optional): Speed factor for spinner animation. Defaults to 1.0.
+ refresh_per_second (float, optional): Number of refreshes per second. Defaults to 12.5.
+
+ Returns:
+ Status: A Status object that may be used as a context manager.
+ """
+ from .status import Status
+
+ status_renderable = Status(
+ status,
+ console=self,
+ spinner=spinner,
+ spinner_style=spinner_style,
+ speed=speed,
+ refresh_per_second=refresh_per_second,
+ )
+ return status_renderable
+
+ def show_cursor(self, show: bool = True) -> bool:
+ """Show or hide the cursor.
+
+ Args:
+ show (bool, optional): Set visibility of the cursor.
+ """
+ if self.is_terminal:
+ self.control(Control.show_cursor(show))
+ return True
+ return False
+
+ def set_alt_screen(self, enable: bool = True) -> bool:
+ """Enables alternative screen mode.
+
+ Note, if you enable this mode, you should ensure that is disabled before
+ the application exits. See :meth:`~rich.Console.screen` for a context manager
+ that handles this for you.
+
+ Args:
+ enable (bool, optional): Enable (True) or disable (False) alternate screen. Defaults to True.
+
+ Returns:
+ bool: True if the control codes were written.
+
+ """
+ changed = False
+ if self.is_terminal and not self.legacy_windows:
+ self.control(Control.alt_screen(enable))
+ changed = True
+ self._is_alt_screen = enable
+ return changed
+
+ @property
+ def is_alt_screen(self) -> bool:
+ """Check if the alt screen was enabled.
+
+ Returns:
+ bool: True if the alt screen was enabled, otherwise False.
+ """
+ return self._is_alt_screen
+
+ def set_window_title(self, title: str) -> bool:
+ """Set the title of the console terminal window.
+
+ Warning: There is no means within Rich of "resetting" the window title to its
+ previous value, meaning the title you set will persist even after your application
+ exits.
+
+ ``fish`` shell resets the window title before and after each command by default,
+ negating this issue. Windows Terminal and command prompt will also reset the title for you.
+ Most other shells and terminals, however, do not do this.
+
+ Some terminals may require configuration changes before you can set the title.
+ Some terminals may not support setting the title at all.
+
+ Other software (including the terminal itself, the shell, custom prompts, plugins, etc.)
+ may also set the terminal window title. This could result in whatever value you write
+ using this method being overwritten.
+
+ Args:
+ title (str): The new title of the terminal window.
+
+ Returns:
+ bool: True if the control code to change the terminal title was
+ written, otherwise False. Note that a return value of True
+ does not guarantee that the window title has actually changed,
+ since the feature may be unsupported/disabled in some terminals.
+ """
+ if self.is_terminal:
+ self.control(Control.title(title))
+ return True
+ return False
+
+ def screen(
+ self, hide_cursor: bool = True, style: Optional[StyleType] = None
+ ) -> "ScreenContext":
+ """Context manager to enable and disable 'alternative screen' mode.
+
+ Args:
+ hide_cursor (bool, optional): Also hide the cursor. Defaults to False.
+ style (Style, optional): Optional style for screen. Defaults to None.
+
+ Returns:
+ ~ScreenContext: Context which enables alternate screen on enter, and disables it on exit.
+ """
+ return ScreenContext(self, hide_cursor=hide_cursor, style=style or "")
+
+ def measure(
+ self, renderable: RenderableType, *, options: Optional[ConsoleOptions] = None
+ ) -> Measurement:
+ """Measure a renderable. Returns a :class:`~rich.measure.Measurement` object which contains
+ information regarding the number of characters required to print the renderable.
+
+ Args:
+ renderable (RenderableType): Any renderable or string.
+ options (Optional[ConsoleOptions], optional): Options to use when measuring, or None
+ to use default options. Defaults to None.
+
+ Returns:
+ Measurement: A measurement of the renderable.
+ """
+ measurement = Measurement.get(self, options or self.options, renderable)
+ return measurement
+
+ def render(
+ self, renderable: RenderableType, options: Optional[ConsoleOptions] = None
+ ) -> Iterable[Segment]:
+ """Render an object in to an iterable of `Segment` instances.
+
+ This method contains the logic for rendering objects with the console protocol.
+ You are unlikely to need to use it directly, unless you are extending the library.
+
+ Args:
+ renderable (RenderableType): An object supporting the console protocol, or
+ an object that may be converted to a string.
+ options (ConsoleOptions, optional): An options object, or None to use self.options. Defaults to None.
+
+ Returns:
+ Iterable[Segment]: An iterable of segments that may be rendered.
+ """
+
+ _options = options or self.options
+ if _options.max_width < 1:
+ # No space to render anything. This prevents potential recursion errors.
+ return
+ render_iterable: RenderResult
+
+ renderable = rich_cast(renderable)
+ if hasattr(renderable, "__rich_console__") and not isclass(renderable):
+ render_iterable = renderable.__rich_console__(self, _options) # type: ignore[union-attr]
+ elif isinstance(renderable, str):
+ text_renderable = self.render_str(
+ renderable, highlight=_options.highlight, markup=_options.markup
+ )
+ render_iterable = text_renderable.__rich_console__(self, _options)
+ else:
+ raise errors.NotRenderableError(
+ f"Unable to render {renderable!r}; "
+ "A str, Segment or object with __rich_console__ method is required"
+ )
+
+ try:
+ iter_render = iter(render_iterable)
+ except TypeError:
+ raise errors.NotRenderableError(
+ f"object {render_iterable!r} is not renderable"
+ )
+ _Segment = Segment
+ _options = _options.reset_height()
+ for render_output in iter_render:
+ if isinstance(render_output, _Segment):
+ yield render_output
+ else:
+ yield from self.render(render_output, _options)
+
+ def render_lines(
+ self,
+ renderable: RenderableType,
+ options: Optional[ConsoleOptions] = None,
+ *,
+ style: Optional[Style] = None,
+ pad: bool = True,
+ new_lines: bool = False,
+ ) -> List[List[Segment]]:
+ """Render objects in to a list of lines.
+
+ The output of render_lines is useful when further formatting of rendered console text
+ is required, such as the Panel class which draws a border around any renderable object.
+
+ Args:
+ renderable (RenderableType): Any object renderable in the console.
+ options (Optional[ConsoleOptions], optional): Console options, or None to use self.options. Default to ``None``.
+ style (Style, optional): Optional style to apply to renderables. Defaults to ``None``.
+ pad (bool, optional): Pad lines shorter than render width. Defaults to ``True``.
+ new_lines (bool, optional): Include "\n" characters at end of lines.
+
+ Returns:
+ List[List[Segment]]: A list of lines, where a line is a list of Segment objects.
+ """
+ with self._lock:
+ render_options = options or self.options
+ _rendered = self.render(renderable, render_options)
+ if style:
+ _rendered = Segment.apply_style(_rendered, style)
+
+ render_height = render_options.height
+ if render_height is not None:
+ render_height = max(0, render_height)
+
+ lines = list(
+ islice(
+ Segment.split_and_crop_lines(
+ _rendered,
+ render_options.max_width,
+ include_new_lines=new_lines,
+ pad=pad,
+ style=style,
+ ),
+ None,
+ render_height,
+ )
+ )
+ if render_options.height is not None:
+ extra_lines = render_options.height - len(lines)
+ if extra_lines > 0:
+ pad_line = [
+ [Segment(" " * render_options.max_width, style), Segment("\n")]
+ if new_lines
+ else [Segment(" " * render_options.max_width, style)]
+ ]
+ lines.extend(pad_line * extra_lines)
+
+ return lines
+
+ def render_str(
+ self,
+ text: str,
+ *,
+ style: Union[str, Style] = "",
+ justify: Optional[JustifyMethod] = None,
+ overflow: Optional[OverflowMethod] = None,
+ emoji: Optional[bool] = None,
+ markup: Optional[bool] = None,
+ highlight: Optional[bool] = None,
+ highlighter: Optional[HighlighterType] = None,
+ ) -> "Text":
+ """Convert a string to a Text instance. This is called automatically if
+ you print or log a string.
+
+ Args:
+ text (str): Text to render.
+ style (Union[str, Style], optional): Style to apply to rendered text.
+ justify (str, optional): Justify method: "default", "left", "center", "full", or "right". Defaults to ``None``.
+ overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to ``None``.
+ emoji (Optional[bool], optional): Enable emoji, or ``None`` to use Console default.
+ markup (Optional[bool], optional): Enable markup, or ``None`` to use Console default.
+ highlight (Optional[bool], optional): Enable highlighting, or ``None`` to use Console default.
+ highlighter (HighlighterType, optional): Optional highlighter to apply.
+ Returns:
+ ConsoleRenderable: Renderable object.
+
+ """
+ emoji_enabled = emoji or (emoji is None and self._emoji)
+ markup_enabled = markup or (markup is None and self._markup)
+ highlight_enabled = highlight or (highlight is None and self._highlight)
+
+ if markup_enabled:
+ rich_text = render_markup(
+ text,
+ style=style,
+ emoji=emoji_enabled,
+ emoji_variant=self._emoji_variant,
+ )
+ rich_text.justify = justify
+ rich_text.overflow = overflow
+ else:
+ rich_text = Text(
+ _emoji_replace(text, default_variant=self._emoji_variant)
+ if emoji_enabled
+ else text,
+ justify=justify,
+ overflow=overflow,
+ style=style,
+ )
+
+ _highlighter = (highlighter or self.highlighter) if highlight_enabled else None
+ if _highlighter is not None:
+ highlight_text = _highlighter(str(rich_text))
+ highlight_text.copy_styles(rich_text)
+ return highlight_text
+
+ return rich_text
+
+ def get_style(
+ self, name: Union[str, Style], *, default: Optional[Union[Style, str]] = None
+ ) -> Style:
+ """Get a Style instance by its theme name or parse a definition.
+
+ Args:
+ name (str): The name of a style or a style definition.
+
+ Returns:
+ Style: A Style object.
+
+ Raises:
+ MissingStyle: If no style could be parsed from name.
+
+ """
+ if isinstance(name, Style):
+ return name
+
+ try:
+ style = self._theme_stack.get(name)
+ if style is None:
+ style = Style.parse(name)
+ return style.copy() if style.link else style
+ except errors.StyleSyntaxError as error:
+ if default is not None:
+ return self.get_style(default)
+ raise errors.MissingStyle(
+ f"Failed to get style {name!r}; {error}"
+ ) from None
+
+ def _collect_renderables(
+ self,
+ objects: Iterable[Any],
+ sep: str,
+ end: str,
+ *,
+ justify: Optional[JustifyMethod] = None,
+ emoji: Optional[bool] = None,
+ markup: Optional[bool] = None,
+ highlight: Optional[bool] = None,
+ ) -> List[ConsoleRenderable]:
+ """Combine a number of renderables and text into one renderable.
+
+ Args:
+ objects (Iterable[Any]): Anything that Rich can render.
+ sep (str): String to write between print data.
+ end (str): String to write at end of print data.
+ justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
+ emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default.
+ markup (Optional[bool], optional): Enable markup, or ``None`` to use console default.
+ highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default.
+
+ Returns:
+ List[ConsoleRenderable]: A list of things to render.
+ """
+ renderables: List[ConsoleRenderable] = []
+ _append = renderables.append
+ text: List[Text] = []
+ append_text = text.append
+
+ append = _append
+ if justify in ("left", "center", "right"):
+
+ def align_append(renderable: RenderableType) -> None:
+ _append(Align(renderable, cast(AlignMethod, justify)))
+
+ append = align_append
+
+ _highlighter: HighlighterType = _null_highlighter
+ if highlight or (highlight is None and self._highlight):
+ _highlighter = self.highlighter
+
+ def check_text() -> None:
+ if text:
+ sep_text = Text(sep, justify=justify, end=end)
+ append(sep_text.join(text))
+ del text[:]
+
+ for renderable in objects:
+ renderable = rich_cast(renderable)
+ if isinstance(renderable, str):
+ append_text(
+ self.render_str(
+ renderable, emoji=emoji, markup=markup, highlighter=_highlighter
+ )
+ )
+ elif isinstance(renderable, Text):
+ append_text(renderable)
+ elif isinstance(renderable, ConsoleRenderable):
+ check_text()
+ append(renderable)
+ elif is_expandable(renderable):
+ check_text()
+ append(Pretty(renderable, highlighter=_highlighter))
+ else:
+ append_text(_highlighter(str(renderable)))
+
+ check_text()
+
+ if self.style is not None:
+ style = self.get_style(self.style)
+ renderables = [Styled(renderable, style) for renderable in renderables]
+
+ return renderables
+
+ def rule(
+ self,
+ title: TextType = "",
+ *,
+ characters: str = "─",
+ style: Union[str, Style] = "rule.line",
+ align: AlignMethod = "center",
+ ) -> None:
+ """Draw a line with optional centered title.
+
+ Args:
+ title (str, optional): Text to render over the rule. Defaults to "".
+ characters (str, optional): Character(s) to form the line. Defaults to "─".
+ style (str, optional): Style of line. Defaults to "rule.line".
+ align (str, optional): How to align the title, one of "left", "center", or "right". Defaults to "center".
+ """
+ from .rule import Rule
+
+ rule = Rule(title=title, characters=characters, style=style, align=align)
+ self.print(rule)
+
+ def control(self, *control: Control) -> None:
+ """Insert non-printing control codes.
+
+ Args:
+ control_codes (str): Control codes, such as those that may move the cursor.
+ """
+ if not self.is_dumb_terminal:
+ with self:
+ self._buffer.extend(_control.segment for _control in control)
+
+ def out(
+ self,
+ *objects: Any,
+ sep: str = " ",
+ end: str = "\n",
+ style: Optional[Union[str, Style]] = None,
+ highlight: Optional[bool] = None,
+ ) -> None:
+ """Output to the terminal. This is a low-level way of writing to the terminal which unlike
+ :meth:`~rich.console.Console.print` won't pretty print, wrap text, or apply markup, but will
+ optionally apply highlighting and a basic style.
+
+ Args:
+ sep (str, optional): String to write between print data. Defaults to " ".
+ end (str, optional): String to write at end of print data. Defaults to "\\\\n".
+ style (Union[str, Style], optional): A style to apply to output. Defaults to None.
+ highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use
+ console default. Defaults to ``None``.
+ """
+ raw_output: str = sep.join(str(_object) for _object in objects)
+ self.print(
+ raw_output,
+ style=style,
+ highlight=highlight,
+ emoji=False,
+ markup=False,
+ no_wrap=True,
+ overflow="ignore",
+ crop=False,
+ end=end,
+ )
+
+ def print(
+ self,
+ *objects: Any,
+ sep: str = " ",
+ end: str = "\n",
+ style: Optional[Union[str, Style]] = None,
+ justify: Optional[JustifyMethod] = None,
+ overflow: Optional[OverflowMethod] = None,
+ no_wrap: Optional[bool] = None,
+ emoji: Optional[bool] = None,
+ markup: Optional[bool] = None,
+ highlight: Optional[bool] = None,
+ width: Optional[int] = None,
+ height: Optional[int] = None,
+ crop: bool = True,
+ soft_wrap: Optional[bool] = None,
+ new_line_start: bool = False,
+ ) -> None:
+ """Print to the console.
+
+ Args:
+ objects (positional args): Objects to log to the terminal.
+ sep (str, optional): String to write between print data. Defaults to " ".
+ end (str, optional): String to write at end of print data. Defaults to "\\\\n".
+ style (Union[str, Style], optional): A style to apply to output. Defaults to None.
+ justify (str, optional): Justify method: "default", "left", "right", "center", or "full". Defaults to ``None``.
+ overflow (str, optional): Overflow method: "ignore", "crop", "fold", or "ellipsis". Defaults to None.
+ no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to None.
+ emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to ``None``.
+ markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to ``None``.
+ highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to ``None``.
+ width (Optional[int], optional): Width of output, or ``None`` to auto-detect. Defaults to ``None``.
+ crop (Optional[bool], optional): Crop output to width of terminal. Defaults to True.
+ soft_wrap (bool, optional): Enable soft wrap mode which disables word wrapping and cropping of text or ``None`` for
+ Console default. Defaults to ``None``.
+ new_line_start (bool, False): Insert a new line at the start if the output contains more than one line. Defaults to ``False``.
+ """
+ if not objects:
+ objects = (NewLine(),)
+
+ if soft_wrap is None:
+ soft_wrap = self.soft_wrap
+ if soft_wrap:
+ if no_wrap is None:
+ no_wrap = True
+ if overflow is None:
+ overflow = "ignore"
+ crop = False
+ render_hooks = self._render_hooks[:]
+ with self:
+ renderables = self._collect_renderables(
+ objects,
+ sep,
+ end,
+ justify=justify,
+ emoji=emoji,
+ markup=markup,
+ highlight=highlight,
+ )
+ for hook in render_hooks:
+ renderables = hook.process_renderables(renderables)
+ render_options = self.options.update(
+ justify=justify,
+ overflow=overflow,
+ width=min(width, self.width) if width is not None else NO_CHANGE,
+ height=height,
+ no_wrap=no_wrap,
+ markup=markup,
+ highlight=highlight,
+ )
+
+ new_segments: List[Segment] = []
+ extend = new_segments.extend
+ render = self.render
+ if style is None:
+ for renderable in renderables:
+ extend(render(renderable, render_options))
+ else:
+ for renderable in renderables:
+ extend(
+ Segment.apply_style(
+ render(renderable, render_options), self.get_style(style)
+ )
+ )
+ if new_line_start:
+ if (
+ len("".join(segment.text for segment in new_segments).splitlines())
+ > 1
+ ):
+ new_segments.insert(0, Segment.line())
+ if crop:
+ buffer_extend = self._buffer.extend
+ for line in Segment.split_and_crop_lines(
+ new_segments, self.width, pad=False
+ ):
+ buffer_extend(line)
+ else:
+ self._buffer.extend(new_segments)
+
+ def print_json(
+ self,
+ json: Optional[str] = None,
+ *,
+ data: Any = None,
+ indent: Union[None, int, str] = 2,
+ highlight: bool = True,
+ skip_keys: bool = False,
+ ensure_ascii: bool = True,
+ check_circular: bool = True,
+ allow_nan: bool = True,
+ default: Optional[Callable[[Any], Any]] = None,
+ sort_keys: bool = False,
+ ) -> None:
+ """Pretty prints JSON. Output will be valid JSON.
+
+ Args:
+ json (Optional[str]): A string containing JSON.
+ data (Any): If json is not supplied, then encode this data.
+ indent (Union[None, int, str], optional): Number of spaces to indent. Defaults to 2.
+ highlight (bool, optional): Enable highlighting of output: Defaults to True.
+ skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False.
+ ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False.
+ check_circular (bool, optional): Check for circular references. Defaults to True.
+ allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True.
+ default (Callable, optional): A callable that converts values that can not be encoded
+ in to something that can be JSON encoded. Defaults to None.
+ sort_keys (bool, optional): Sort dictionary keys. Defaults to False.
+ """
+ from pip._vendor.rich.json import JSON
+
+ if json is None:
+ json_renderable = JSON.from_data(
+ data,
+ indent=indent,
+ highlight=highlight,
+ skip_keys=skip_keys,
+ ensure_ascii=ensure_ascii,
+ check_circular=check_circular,
+ allow_nan=allow_nan,
+ default=default,
+ sort_keys=sort_keys,
+ )
+ else:
+ if not isinstance(json, str):
+ raise TypeError(
+ f"json must be str. Did you mean print_json(data={json!r}) ?"
+ )
+ json_renderable = JSON(
+ json,
+ indent=indent,
+ highlight=highlight,
+ skip_keys=skip_keys,
+ ensure_ascii=ensure_ascii,
+ check_circular=check_circular,
+ allow_nan=allow_nan,
+ default=default,
+ sort_keys=sort_keys,
+ )
+ self.print(json_renderable, soft_wrap=True)
+
+ def update_screen(
+ self,
+ renderable: RenderableType,
+ *,
+ region: Optional[Region] = None,
+ options: Optional[ConsoleOptions] = None,
+ ) -> None:
+ """Update the screen at a given offset.
+
+ Args:
+ renderable (RenderableType): A Rich renderable.
+ region (Region, optional): Region of screen to update, or None for entire screen. Defaults to None.
+ x (int, optional): x offset. Defaults to 0.
+ y (int, optional): y offset. Defaults to 0.
+
+ Raises:
+ errors.NoAltScreen: If the Console isn't in alt screen mode.
+
+ """
+ if not self.is_alt_screen:
+ raise errors.NoAltScreen("Alt screen must be enabled to call update_screen")
+ render_options = options or self.options
+ if region is None:
+ x = y = 0
+ render_options = render_options.update_dimensions(
+ render_options.max_width, render_options.height or self.height
+ )
+ else:
+ x, y, width, height = region
+ render_options = render_options.update_dimensions(width, height)
+
+ lines = self.render_lines(renderable, options=render_options)
+ self.update_screen_lines(lines, x, y)
+
+ def update_screen_lines(
+ self, lines: List[List[Segment]], x: int = 0, y: int = 0
+ ) -> None:
+ """Update lines of the screen at a given offset.
+
+ Args:
+ lines (List[List[Segment]]): Rendered lines (as produced by :meth:`~rich.Console.render_lines`).
+ x (int, optional): x offset (column no). Defaults to 0.
+ y (int, optional): y offset (column no). Defaults to 0.
+
+ Raises:
+ errors.NoAltScreen: If the Console isn't in alt screen mode.
+ """
+ if not self.is_alt_screen:
+ raise errors.NoAltScreen("Alt screen must be enabled to call update_screen")
+ screen_update = ScreenUpdate(lines, x, y)
+ segments = self.render(screen_update)
+ self._buffer.extend(segments)
+ self._check_buffer()
+
+ def print_exception(
+ self,
+ *,
+ width: Optional[int] = 100,
+ extra_lines: int = 3,
+ theme: Optional[str] = None,
+ word_wrap: bool = False,
+ show_locals: bool = False,
+ suppress: Iterable[Union[str, ModuleType]] = (),
+ max_frames: int = 100,
+ ) -> None:
+ """Prints a rich render of the last exception and traceback.
+
+ Args:
+ width (Optional[int], optional): Number of characters used to render code. Defaults to 100.
+ extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
+ theme (str, optional): Override pygments theme used in traceback
+ word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
+ show_locals (bool, optional): Enable display of local variables. Defaults to False.
+ suppress (Iterable[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
+ max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
+ """
+ from .traceback import Traceback
+
+ traceback = Traceback(
+ width=width,
+ extra_lines=extra_lines,
+ theme=theme,
+ word_wrap=word_wrap,
+ show_locals=show_locals,
+ suppress=suppress,
+ max_frames=max_frames,
+ )
+ self.print(traceback)
+
+ @staticmethod
+ def _caller_frame_info(
+ offset: int,
+ currentframe: Callable[[], Optional[FrameType]] = inspect.currentframe,
+ ) -> Tuple[str, int, Dict[str, Any]]:
+ """Get caller frame information.
+
+ Args:
+ offset (int): the caller offset within the current frame stack.
+ currentframe (Callable[[], Optional[FrameType]], optional): the callable to use to
+ retrieve the current frame. Defaults to ``inspect.currentframe``.
+
+ Returns:
+ Tuple[str, int, Dict[str, Any]]: A tuple containing the filename, the line number and
+ the dictionary of local variables associated with the caller frame.
+
+ Raises:
+ RuntimeError: If the stack offset is invalid.
+ """
+ # Ignore the frame of this local helper
+ offset += 1
+
+ frame = currentframe()
+ if frame is not None:
+ # Use the faster currentframe where implemented
+ while offset and frame is not None:
+ frame = frame.f_back
+ offset -= 1
+ assert frame is not None
+ return frame.f_code.co_filename, frame.f_lineno, frame.f_locals
+ else:
+ # Fallback to the slower stack
+ frame_info = inspect.stack()[offset]
+ return frame_info.filename, frame_info.lineno, frame_info.frame.f_locals
+
+ def log(
+ self,
+ *objects: Any,
+ sep: str = " ",
+ end: str = "\n",
+ style: Optional[Union[str, Style]] = None,
+ justify: Optional[JustifyMethod] = None,
+ emoji: Optional[bool] = None,
+ markup: Optional[bool] = None,
+ highlight: Optional[bool] = None,
+ log_locals: bool = False,
+ _stack_offset: int = 1,
+ ) -> None:
+ """Log rich content to the terminal.
+
+ Args:
+ objects (positional args): Objects to log to the terminal.
+ sep (str, optional): String to write between print data. Defaults to " ".
+ end (str, optional): String to write at end of print data. Defaults to "\\\\n".
+ style (Union[str, Style], optional): A style to apply to output. Defaults to None.
+ justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
+ overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None.
+ emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None.
+ markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to None.
+ highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to None.
+ log_locals (bool, optional): Boolean to enable logging of locals where ``log()``
+ was called. Defaults to False.
+ _stack_offset (int, optional): Offset of caller from end of call stack. Defaults to 1.
+ """
+ if not objects:
+ objects = (NewLine(),)
+
+ render_hooks = self._render_hooks[:]
+
+ with self:
+ renderables = self._collect_renderables(
+ objects,
+ sep,
+ end,
+ justify=justify,
+ emoji=emoji,
+ markup=markup,
+ highlight=highlight,
+ )
+ if style is not None:
+ renderables = [Styled(renderable, style) for renderable in renderables]
+
+ filename, line_no, locals = self._caller_frame_info(_stack_offset)
+ link_path = None if filename.startswith("<") else os.path.abspath(filename)
+ path = filename.rpartition(os.sep)[-1]
+ if log_locals:
+ locals_map = {
+ key: value
+ for key, value in locals.items()
+ if not key.startswith("__")
+ }
+ renderables.append(render_scope(locals_map, title="[i]locals"))
+
+ renderables = [
+ self._log_render(
+ self,
+ renderables,
+ log_time=self.get_datetime(),
+ path=path,
+ line_no=line_no,
+ link_path=link_path,
+ )
+ ]
+ for hook in render_hooks:
+ renderables = hook.process_renderables(renderables)
+ new_segments: List[Segment] = []
+ extend = new_segments.extend
+ render = self.render
+ render_options = self.options
+ for renderable in renderables:
+ extend(render(renderable, render_options))
+ buffer_extend = self._buffer.extend
+ for line in Segment.split_and_crop_lines(
+ new_segments, self.width, pad=False
+ ):
+ buffer_extend(line)
+
+ def _check_buffer(self) -> None:
+ """Check if the buffer may be rendered. Render it if it can (e.g. Console.quiet is False)
+ Rendering is supported on Windows, Unix and Jupyter environments. For
+ legacy Windows consoles, the win32 API is called directly.
+ This method will also record what it renders if recording is enabled via Console.record.
+ """
+ if self.quiet:
+ del self._buffer[:]
+ return
+ with self._lock:
+ if self.record:
+ with self._record_buffer_lock:
+ self._record_buffer.extend(self._buffer[:])
+
+ if self._buffer_index == 0:
+
+ if self.is_jupyter: # pragma: no cover
+ from .jupyter import display
+
+ display(self._buffer, self._render_buffer(self._buffer[:]))
+ del self._buffer[:]
+ else:
+ if WINDOWS:
+ use_legacy_windows_render = False
+ if self.legacy_windows:
+ try:
+ use_legacy_windows_render = (
+ self.file.fileno() in _STD_STREAMS_OUTPUT
+ )
+ except (ValueError, io.UnsupportedOperation):
+ pass
+
+ if use_legacy_windows_render:
+ from pip._vendor.rich._win32_console import LegacyWindowsTerm
+ from pip._vendor.rich._windows_renderer import legacy_windows_render
+
+ legacy_windows_render(
+ self._buffer[:], LegacyWindowsTerm(self.file)
+ )
+ else:
+ # Either a non-std stream on legacy Windows, or modern Windows.
+ text = self._render_buffer(self._buffer[:])
+ # https://bugs.python.org/issue37871
+ write = self.file.write
+ for line in text.splitlines(True):
+ try:
+ write(line)
+ except UnicodeEncodeError as error:
+ error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***"
+ raise
+ else:
+ text = self._render_buffer(self._buffer[:])
+ try:
+ self.file.write(text)
+ except UnicodeEncodeError as error:
+ error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***"
+ raise
+
+ self.file.flush()
+ del self._buffer[:]
+
+ def _render_buffer(self, buffer: Iterable[Segment]) -> str:
+ """Render buffered output, and clear buffer."""
+ output: List[str] = []
+ append = output.append
+ color_system = self._color_system
+ legacy_windows = self.legacy_windows
+ not_terminal = not self.is_terminal
+ if self.no_color and color_system:
+ buffer = Segment.remove_color(buffer)
+ for text, style, control in buffer:
+ if style:
+ append(
+ style.render(
+ text,
+ color_system=color_system,
+ legacy_windows=legacy_windows,
+ )
+ )
+ elif not (not_terminal and control):
+ append(text)
+
+ rendered = "".join(output)
+ return rendered
+
+ def input(
+ self,
+ prompt: TextType = "",
+ *,
+ markup: bool = True,
+ emoji: bool = True,
+ password: bool = False,
+ stream: Optional[TextIO] = None,
+ ) -> str:
+ """Displays a prompt and waits for input from the user. The prompt may contain color / style.
+
+ It works in the same way as Python's builtin :func:`input` function and provides elaborate line editing and history features if Python's builtin :mod:`readline` module is previously loaded.
+
+ Args:
+ prompt (Union[str, Text]): Text to render in the prompt.
+ markup (bool, optional): Enable console markup (requires a str prompt). Defaults to True.
+ emoji (bool, optional): Enable emoji (requires a str prompt). Defaults to True.
+ password: (bool, optional): Hide typed text. Defaults to False.
+ stream: (TextIO, optional): Optional file to read input from (rather than stdin). Defaults to None.
+
+ Returns:
+ str: Text read from stdin.
+ """
+ if prompt:
+ self.print(prompt, markup=markup, emoji=emoji, end="")
+ if password:
+ result = getpass("", stream=stream)
+ else:
+ if stream:
+ result = stream.readline()
+ else:
+ result = input()
+ return result
+
+ def export_text(self, *, clear: bool = True, styles: bool = False) -> str:
+ """Generate text from console contents (requires record=True argument in constructor).
+
+ Args:
+ clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
+ styles (bool, optional): If ``True``, ansi escape codes will be included. ``False`` for plain text.
+ Defaults to ``False``.
+
+ Returns:
+ str: String containing console contents.
+
+ """
+ assert (
+ self.record
+ ), "To export console contents set record=True in the constructor or instance"
+
+ with self._record_buffer_lock:
+ if styles:
+ text = "".join(
+ (style.render(text) if style else text)
+ for text, style, _ in self._record_buffer
+ )
+ else:
+ text = "".join(
+ segment.text
+ for segment in self._record_buffer
+ if not segment.control
+ )
+ if clear:
+ del self._record_buffer[:]
+ return text
+
+ def save_text(self, path: str, *, clear: bool = True, styles: bool = False) -> None:
+ """Generate text from console and save to a given location (requires record=True argument in constructor).
+
+ Args:
+ path (str): Path to write text files.
+ clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
+ styles (bool, optional): If ``True``, ansi style codes will be included. ``False`` for plain text.
+ Defaults to ``False``.
+
+ """
+ text = self.export_text(clear=clear, styles=styles)
+ with open(path, "wt", encoding="utf-8") as write_file:
+ write_file.write(text)
+
+ def export_html(
+ self,
+ *,
+ theme: Optional[TerminalTheme] = None,
+ clear: bool = True,
+ code_format: Optional[str] = None,
+ inline_styles: bool = False,
+ ) -> str:
+ """Generate HTML from console contents (requires record=True argument in constructor).
+
+ Args:
+ theme (TerminalTheme, optional): TerminalTheme object containing console colors.
+ clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
+ code_format (str, optional): Format string to render HTML. In addition to '{foreground}',
+ '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``.
+ inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files
+ larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag.
+ Defaults to False.
+
+ Returns:
+ str: String containing console contents as HTML.
+ """
+ assert (
+ self.record
+ ), "To export console contents set record=True in the constructor or instance"
+ fragments: List[str] = []
+ append = fragments.append
+ _theme = theme or DEFAULT_TERMINAL_THEME
+ stylesheet = ""
+
+ render_code_format = CONSOLE_HTML_FORMAT if code_format is None else code_format
+
+ with self._record_buffer_lock:
+ if inline_styles:
+ for text, style, _ in Segment.filter_control(
+ Segment.simplify(self._record_buffer)
+ ):
+ text = escape(text)
+ if style:
+ rule = style.get_html_style(_theme)
+ if style.link:
+ text = f'<a href="{style.link}">{text}</a>'
+ text = f'<span style="{rule}">{text}</span>' if rule else text
+ append(text)
+ else:
+ styles: Dict[str, int] = {}
+ for text, style, _ in Segment.filter_control(
+ Segment.simplify(self._record_buffer)
+ ):
+ text = escape(text)
+ if style:
+ rule = style.get_html_style(_theme)
+ style_number = styles.setdefault(rule, len(styles) + 1)
+ if style.link:
+ text = f'<a class="r{style_number}" href="{style.link}">{text}</a>'
+ else:
+ text = f'<span class="r{style_number}">{text}</span>'
+ append(text)
+ stylesheet_rules: List[str] = []
+ stylesheet_append = stylesheet_rules.append
+ for style_rule, style_number in styles.items():
+ if style_rule:
+ stylesheet_append(f".r{style_number} {{{style_rule}}}")
+ stylesheet = "\n".join(stylesheet_rules)
+
+ rendered_code = render_code_format.format(
+ code="".join(fragments),
+ stylesheet=stylesheet,
+ foreground=_theme.foreground_color.hex,
+ background=_theme.background_color.hex,
+ )
+ if clear:
+ del self._record_buffer[:]
+ return rendered_code
+
+ def save_html(
+ self,
+ path: str,
+ *,
+ theme: Optional[TerminalTheme] = None,
+ clear: bool = True,
+ code_format: str = CONSOLE_HTML_FORMAT,
+ inline_styles: bool = False,
+ ) -> None:
+ """Generate HTML from console contents and write to a file (requires record=True argument in constructor).
+
+ Args:
+ path (str): Path to write html file.
+ theme (TerminalTheme, optional): TerminalTheme object containing console colors.
+ clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
+ code_format (str, optional): Format string to render HTML. In addition to '{foreground}',
+ '{background}', and '{code}', should contain '{stylesheet}' if inline_styles is ``False``.
+ inline_styles (bool, optional): If ``True`` styles will be inlined in to spans, which makes files
+ larger but easier to cut and paste markup. If ``False``, styles will be embedded in a style tag.
+ Defaults to False.
+
+ """
+ html = self.export_html(
+ theme=theme,
+ clear=clear,
+ code_format=code_format,
+ inline_styles=inline_styles,
+ )
+ with open(path, "wt", encoding="utf-8") as write_file:
+ write_file.write(html)
+
+ def export_svg(
+ self,
+ *,
+ title: str = "Rich",
+ theme: Optional[TerminalTheme] = None,
+ clear: bool = True,
+ code_format: str = CONSOLE_SVG_FORMAT,
+ ) -> str:
+ """
+ Generate an SVG from the console contents (requires record=True in Console constructor).
+
+ Args:
+ path (str): The path to write the SVG to.
+ title (str): The title of the tab in the output image
+ theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal
+ clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``
+ code_format (str): Format string used to generate the SVG. Rich will inject a number of variables
+ into the string in order to form the final SVG output. The default template used and the variables
+ injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable.
+ """
+
+ from pip._vendor.rich.cells import cell_len
+
+ style_cache: Dict[Style, str] = {}
+
+ def get_svg_style(style: Style) -> str:
+ """Convert a Style to CSS rules for SVG."""
+ if style in style_cache:
+ return style_cache[style]
+ css_rules = []
+ color = (
+ _theme.foreground_color
+ if (style.color is None or style.color.is_default)
+ else style.color.get_truecolor(_theme)
+ )
+ bgcolor = (
+ _theme.background_color
+ if (style.bgcolor is None or style.bgcolor.is_default)
+ else style.bgcolor.get_truecolor(_theme)
+ )
+ if style.reverse:
+ color, bgcolor = bgcolor, color
+ if style.dim:
+ color = blend_rgb(color, bgcolor, 0.4)
+ css_rules.append(f"fill: {color.hex}")
+ if style.bold:
+ css_rules.append("font-weight: bold")
+ if style.italic:
+ css_rules.append("font-style: italic;")
+ if style.underline:
+ css_rules.append("text-decoration: underline;")
+ if style.strike:
+ css_rules.append("text-decoration: line-through;")
+
+ css = ";".join(css_rules)
+ style_cache[style] = css
+ return css
+
+ _theme = theme or SVG_EXPORT_THEME
+
+ width = self.width
+ char_height = 20
+ char_width = char_height * 0.61
+ line_height = char_height * 1.22
+
+ margin_top = 1
+ margin_right = 1
+ margin_bottom = 1
+ margin_left = 1
+
+ padding_top = 40
+ padding_right = 8
+ padding_bottom = 8
+ padding_left = 8
+
+ padding_width = padding_left + padding_right
+ padding_height = padding_top + padding_bottom
+ margin_width = margin_left + margin_right
+ margin_height = margin_top + margin_bottom
+
+ text_backgrounds: List[str] = []
+ text_group: List[str] = []
+ classes: Dict[str, int] = {}
+ style_no = 1
+
+ def escape_text(text: str) -> str:
+ """HTML escape text and replace spaces with nbsp."""
+ return escape(text).replace(" ", "&#160;")
+
+ def make_tag(
+ name: str, content: Optional[str] = None, **attribs: object
+ ) -> str:
+ """Make a tag from name, content, and attributes."""
+
+ def stringify(value: object) -> str:
+ if isinstance(value, (float)):
+ return format(value, "g")
+ return str(value)
+
+ tag_attribs = " ".join(
+ f'{k.lstrip("_").replace("_", "-")}="{stringify(v)}"'
+ for k, v in attribs.items()
+ )
+ return (
+ f"<{name} {tag_attribs}>{content}</{name}>"
+ if content
+ else f"<{name} {tag_attribs}/>"
+ )
+
+ with self._record_buffer_lock:
+ segments = list(Segment.filter_control(self._record_buffer))
+ if clear:
+ self._record_buffer.clear()
+
+ unique_id = "terminal-" + str(
+ zlib.adler32(
+ ("".join(segment.text for segment in segments)).encode(
+ "utf-8", "ignore"
+ )
+ + title.encode("utf-8", "ignore")
+ )
+ )
+ y = 0
+ for y, line in enumerate(Segment.split_and_crop_lines(segments, length=width)):
+ x = 0
+ for text, style, _control in line:
+ style = style or Style()
+ rules = get_svg_style(style)
+ if rules not in classes:
+ classes[rules] = style_no
+ style_no += 1
+ class_name = f"r{classes[rules]}"
+
+ if style.reverse:
+ has_background = True
+ background = (
+ _theme.foreground_color.hex
+ if style.color is None
+ else style.color.get_truecolor(_theme).hex
+ )
+ else:
+ bgcolor = style.bgcolor
+ has_background = bgcolor is not None and not bgcolor.is_default
+ background = (
+ _theme.background_color.hex
+ if style.bgcolor is None
+ else style.bgcolor.get_truecolor(_theme).hex
+ )
+
+ text_length = cell_len(text)
+ if has_background:
+ text_backgrounds.append(
+ make_tag(
+ "rect",
+ fill=background,
+ x=x * char_width,
+ y=y * line_height + 1.5,
+ width=char_width * text_length,
+ height=line_height + 0.25,
+ shape_rendering="crispEdges",
+ )
+ )
+
+ if text != " " * len(text):
+ text_group.append(
+ make_tag(
+ "text",
+ escape_text(text),
+ _class=f"{unique_id}-{class_name}",
+ x=x * char_width,
+ y=y * line_height + char_height,
+ textLength=char_width * len(text),
+ clip_path=f"url(#{unique_id}-line-{y})",
+ )
+ )
+ x += cell_len(text)
+
+ line_offsets = [line_no * line_height + 1.5 for line_no in range(y)]
+ lines = "\n".join(
+ f"""<clipPath id="{unique_id}-line-{line_no}">
+ {make_tag("rect", x=0, y=offset, width=char_width * width, height=line_height + 0.25)}
+ </clipPath>"""
+ for line_no, offset in enumerate(line_offsets)
+ )
+
+ styles = "\n".join(
+ f".{unique_id}-r{rule_no} {{ {css} }}" for css, rule_no in classes.items()
+ )
+ backgrounds = "".join(text_backgrounds)
+ matrix = "".join(text_group)
+
+ terminal_width = ceil(width * char_width + padding_width)
+ terminal_height = (y + 1) * line_height + padding_height
+ chrome = make_tag(
+ "rect",
+ fill=_theme.background_color.hex,
+ stroke="rgba(255,255,255,0.35)",
+ stroke_width="1",
+ x=margin_left,
+ y=margin_top,
+ width=terminal_width,
+ height=terminal_height,
+ rx=8,
+ )
+
+ title_color = _theme.foreground_color.hex
+ if title:
+ chrome += make_tag(
+ "text",
+ escape_text(title),
+ _class=f"{unique_id}-title",
+ fill=title_color,
+ text_anchor="middle",
+ x=terminal_width // 2,
+ y=margin_top + char_height + 6,
+ )
+ chrome += f"""
+ <g transform="translate(26,22)">
+ <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+ <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+ <circle cx="44" cy="0" r="7" fill="#28c840"/>
+ </g>
+ """
+
+ svg = code_format.format(
+ unique_id=unique_id,
+ char_width=char_width,
+ char_height=char_height,
+ line_height=line_height,
+ terminal_width=char_width * width - 1,
+ terminal_height=(y + 1) * line_height - 1,
+ width=terminal_width + margin_width,
+ height=terminal_height + margin_height,
+ terminal_x=margin_left + padding_left,
+ terminal_y=margin_top + padding_top,
+ styles=styles,
+ chrome=chrome,
+ backgrounds=backgrounds,
+ matrix=matrix,
+ lines=lines,
+ )
+ return svg
+
+ def save_svg(
+ self,
+ path: str,
+ *,
+ title: str = "Rich",
+ theme: Optional[TerminalTheme] = None,
+ clear: bool = True,
+ code_format: str = CONSOLE_SVG_FORMAT,
+ ) -> None:
+ """Generate an SVG file from the console contents (requires record=True in Console constructor).
+
+ Args:
+ path (str): The path to write the SVG to.
+ title (str): The title of the tab in the output image
+ theme (TerminalTheme, optional): The ``TerminalTheme`` object to use to style the terminal
+ clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``
+ code_format (str): Format string used to generate the SVG. Rich will inject a number of variables
+ into the string in order to form the final SVG output. The default template used and the variables
+ injected by Rich can be found by inspecting the ``console.CONSOLE_SVG_FORMAT`` variable.
+ """
+ svg = self.export_svg(
+ title=title,
+ theme=theme,
+ clear=clear,
+ code_format=code_format,
+ )
+ with open(path, "wt", encoding="utf-8") as write_file:
+ write_file.write(svg)
+
+
+def _svg_hash(svg_main_code: str) -> str:
+ """Returns a unique hash for the given SVG main code.
+
+ Args:
+ svg_main_code (str): The content we're going to inject in the SVG envelope.
+
+ Returns:
+ str: a hash of the given content
+ """
+ return str(zlib.adler32(svg_main_code.encode()))
+
+
+if __name__ == "__main__": # pragma: no cover
+ console = Console(record=True)
+
+ console.log(
+ "JSONRPC [i]request[/i]",
+ 5,
+ 1.3,
+ True,
+ False,
+ None,
+ {
+ "jsonrpc": "2.0",
+ "method": "subtract",
+ "params": {"minuend": 42, "subtrahend": 23},
+ "id": 3,
+ },
+ )
+
+ console.log("Hello, World!", "{'a': 1}", repr(console))
+
+ console.print(
+ {
+ "name": None,
+ "empty": [],
+ "quiz": {
+ "sport": {
+ "answered": True,
+ "q1": {
+ "question": "Which one is correct team name in NBA?",
+ "options": [
+ "New York Bulls",
+ "Los Angeles Kings",
+ "Golden State Warriors",
+ "Huston Rocket",
+ ],
+ "answer": "Huston Rocket",
+ },
+ },
+ "maths": {
+ "answered": False,
+ "q1": {
+ "question": "5 + 7 = ?",
+ "options": [10, 11, 12, 13],
+ "answer": 12,
+ },
+ "q2": {
+ "question": "12 - 8 = ?",
+ "options": [1, 2, 3, 4],
+ "answer": 4,
+ },
+ },
+ },
+ }
+ )
diff --git a/src/pip/_vendor/rich/constrain.py b/src/pip/_vendor/rich/constrain.py
new file mode 100644
index 000000000..65fdf5634
--- /dev/null
+++ b/src/pip/_vendor/rich/constrain.py
@@ -0,0 +1,37 @@
+from typing import Optional, TYPE_CHECKING
+
+from .jupyter import JupyterMixin
+from .measure import Measurement
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderableType, RenderResult
+
+
+class Constrain(JupyterMixin):
+ """Constrain the width of a renderable to a given number of characters.
+
+ Args:
+ renderable (RenderableType): A renderable object.
+ width (int, optional): The maximum width (in characters) to render. Defaults to 80.
+ """
+
+ def __init__(self, renderable: "RenderableType", width: Optional[int] = 80) -> None:
+ self.renderable = renderable
+ self.width = width
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ if self.width is None:
+ yield self.renderable
+ else:
+ child_options = options.update_width(min(self.width, options.max_width))
+ yield from console.render(self.renderable, child_options)
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "Measurement":
+ if self.width is not None:
+ options = options.update_width(self.width)
+ measurement = Measurement.get(console, options, self.renderable)
+ return measurement
diff --git a/src/pip/_vendor/rich/containers.py b/src/pip/_vendor/rich/containers.py
new file mode 100644
index 000000000..e29cf3689
--- /dev/null
+++ b/src/pip/_vendor/rich/containers.py
@@ -0,0 +1,167 @@
+from itertools import zip_longest
+from typing import (
+ Iterator,
+ Iterable,
+ List,
+ Optional,
+ Union,
+ overload,
+ TypeVar,
+ TYPE_CHECKING,
+)
+
+if TYPE_CHECKING:
+ from .console import (
+ Console,
+ ConsoleOptions,
+ JustifyMethod,
+ OverflowMethod,
+ RenderResult,
+ RenderableType,
+ )
+ from .text import Text
+
+from .cells import cell_len
+from .measure import Measurement
+
+T = TypeVar("T")
+
+
+class Renderables:
+ """A list subclass which renders its contents to the console."""
+
+ def __init__(
+ self, renderables: Optional[Iterable["RenderableType"]] = None
+ ) -> None:
+ self._renderables: List["RenderableType"] = (
+ list(renderables) if renderables is not None else []
+ )
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ """Console render method to insert line-breaks."""
+ yield from self._renderables
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "Measurement":
+ dimensions = [
+ Measurement.get(console, options, renderable)
+ for renderable in self._renderables
+ ]
+ if not dimensions:
+ return Measurement(1, 1)
+ _min = max(dimension.minimum for dimension in dimensions)
+ _max = max(dimension.maximum for dimension in dimensions)
+ return Measurement(_min, _max)
+
+ def append(self, renderable: "RenderableType") -> None:
+ self._renderables.append(renderable)
+
+ def __iter__(self) -> Iterable["RenderableType"]:
+ return iter(self._renderables)
+
+
+class Lines:
+ """A list subclass which can render to the console."""
+
+ def __init__(self, lines: Iterable["Text"] = ()) -> None:
+ self._lines: List["Text"] = list(lines)
+
+ def __repr__(self) -> str:
+ return f"Lines({self._lines!r})"
+
+ def __iter__(self) -> Iterator["Text"]:
+ return iter(self._lines)
+
+ @overload
+ def __getitem__(self, index: int) -> "Text":
+ ...
+
+ @overload
+ def __getitem__(self, index: slice) -> List["Text"]:
+ ...
+
+ def __getitem__(self, index: Union[slice, int]) -> Union["Text", List["Text"]]:
+ return self._lines[index]
+
+ def __setitem__(self, index: int, value: "Text") -> "Lines":
+ self._lines[index] = value
+ return self
+
+ def __len__(self) -> int:
+ return self._lines.__len__()
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ """Console render method to insert line-breaks."""
+ yield from self._lines
+
+ def append(self, line: "Text") -> None:
+ self._lines.append(line)
+
+ def extend(self, lines: Iterable["Text"]) -> None:
+ self._lines.extend(lines)
+
+ def pop(self, index: int = -1) -> "Text":
+ return self._lines.pop(index)
+
+ def justify(
+ self,
+ console: "Console",
+ width: int,
+ justify: "JustifyMethod" = "left",
+ overflow: "OverflowMethod" = "fold",
+ ) -> None:
+ """Justify and overflow text to a given width.
+
+ Args:
+ console (Console): Console instance.
+ width (int): Number of characters per line.
+ justify (str, optional): Default justify method for text: "left", "center", "full" or "right". Defaults to "left".
+ overflow (str, optional): Default overflow for text: "crop", "fold", or "ellipsis". Defaults to "fold".
+
+ """
+ from .text import Text
+
+ if justify == "left":
+ for line in self._lines:
+ line.truncate(width, overflow=overflow, pad=True)
+ elif justify == "center":
+ for line in self._lines:
+ line.rstrip()
+ line.truncate(width, overflow=overflow)
+ line.pad_left((width - cell_len(line.plain)) // 2)
+ line.pad_right(width - cell_len(line.plain))
+ elif justify == "right":
+ for line in self._lines:
+ line.rstrip()
+ line.truncate(width, overflow=overflow)
+ line.pad_left(width - cell_len(line.plain))
+ elif justify == "full":
+ for line_index, line in enumerate(self._lines):
+ if line_index == len(self._lines) - 1:
+ break
+ words = line.split(" ")
+ words_size = sum(cell_len(word.plain) for word in words)
+ num_spaces = len(words) - 1
+ spaces = [1 for _ in range(num_spaces)]
+ index = 0
+ if spaces:
+ while words_size + num_spaces < width:
+ spaces[len(spaces) - index - 1] += 1
+ num_spaces += 1
+ index = (index + 1) % len(spaces)
+ tokens: List[Text] = []
+ for index, (word, next_word) in enumerate(
+ zip_longest(words, words[1:])
+ ):
+ tokens.append(word)
+ if index < len(spaces):
+ style = word.get_style_at_offset(console, -1)
+ next_style = next_word.get_style_at_offset(console, 0)
+ space_style = style if style == next_style else line.style
+ tokens.append(Text(" " * spaces[index], style=space_style))
+ self[line_index] = Text("").join(tokens)
diff --git a/src/pip/_vendor/rich/control.py b/src/pip/_vendor/rich/control.py
new file mode 100644
index 000000000..88fcb9295
--- /dev/null
+++ b/src/pip/_vendor/rich/control.py
@@ -0,0 +1,225 @@
+import sys
+import time
+from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Union
+
+if sys.version_info >= (3, 8):
+ from typing import Final
+else:
+ from pip._vendor.typing_extensions import Final # pragma: no cover
+
+from .segment import ControlCode, ControlType, Segment
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderResult
+
+STRIP_CONTROL_CODES: Final = [
+ 7, # Bell
+ 8, # Backspace
+ 11, # Vertical tab
+ 12, # Form feed
+ 13, # Carriage return
+]
+_CONTROL_STRIP_TRANSLATE: Final = {
+ _codepoint: None for _codepoint in STRIP_CONTROL_CODES
+}
+
+CONTROL_ESCAPE: Final = {
+ 7: "\\a",
+ 8: "\\b",
+ 11: "\\v",
+ 12: "\\f",
+ 13: "\\r",
+}
+
+CONTROL_CODES_FORMAT: Dict[int, Callable[..., str]] = {
+ ControlType.BELL: lambda: "\x07",
+ ControlType.CARRIAGE_RETURN: lambda: "\r",
+ ControlType.HOME: lambda: "\x1b[H",
+ ControlType.CLEAR: lambda: "\x1b[2J",
+ ControlType.ENABLE_ALT_SCREEN: lambda: "\x1b[?1049h",
+ ControlType.DISABLE_ALT_SCREEN: lambda: "\x1b[?1049l",
+ ControlType.SHOW_CURSOR: lambda: "\x1b[?25h",
+ ControlType.HIDE_CURSOR: lambda: "\x1b[?25l",
+ ControlType.CURSOR_UP: lambda param: f"\x1b[{param}A",
+ ControlType.CURSOR_DOWN: lambda param: f"\x1b[{param}B",
+ ControlType.CURSOR_FORWARD: lambda param: f"\x1b[{param}C",
+ ControlType.CURSOR_BACKWARD: lambda param: f"\x1b[{param}D",
+ ControlType.CURSOR_MOVE_TO_COLUMN: lambda param: f"\x1b[{param+1}G",
+ ControlType.ERASE_IN_LINE: lambda param: f"\x1b[{param}K",
+ ControlType.CURSOR_MOVE_TO: lambda x, y: f"\x1b[{y+1};{x+1}H",
+ ControlType.SET_WINDOW_TITLE: lambda title: f"\x1b]0;{title}\x07",
+}
+
+
+class Control:
+ """A renderable that inserts a control code (non printable but may move cursor).
+
+ Args:
+ *codes (str): Positional arguments are either a :class:`~rich.segment.ControlType` enum or a
+ tuple of ControlType and an integer parameter
+ """
+
+ __slots__ = ["segment"]
+
+ def __init__(self, *codes: Union[ControlType, ControlCode]) -> None:
+ control_codes: List[ControlCode] = [
+ (code,) if isinstance(code, ControlType) else code for code in codes
+ ]
+ _format_map = CONTROL_CODES_FORMAT
+ rendered_codes = "".join(
+ _format_map[code](*parameters) for code, *parameters in control_codes
+ )
+ self.segment = Segment(rendered_codes, None, control_codes)
+
+ @classmethod
+ def bell(cls) -> "Control":
+ """Ring the 'bell'."""
+ return cls(ControlType.BELL)
+
+ @classmethod
+ def home(cls) -> "Control":
+ """Move cursor to 'home' position."""
+ return cls(ControlType.HOME)
+
+ @classmethod
+ def move(cls, x: int = 0, y: int = 0) -> "Control":
+ """Move cursor relative to current position.
+
+ Args:
+ x (int): X offset.
+ y (int): Y offset.
+
+ Returns:
+ ~Control: Control object.
+
+ """
+
+ def get_codes() -> Iterable[ControlCode]:
+ control = ControlType
+ if x:
+ yield (
+ control.CURSOR_FORWARD if x > 0 else control.CURSOR_BACKWARD,
+ abs(x),
+ )
+ if y:
+ yield (
+ control.CURSOR_DOWN if y > 0 else control.CURSOR_UP,
+ abs(y),
+ )
+
+ control = cls(*get_codes())
+ return control
+
+ @classmethod
+ def move_to_column(cls, x: int, y: int = 0) -> "Control":
+ """Move to the given column, optionally add offset to row.
+
+ Returns:
+ x (int): absolute x (column)
+ y (int): optional y offset (row)
+
+ Returns:
+ ~Control: Control object.
+ """
+
+ return (
+ cls(
+ (ControlType.CURSOR_MOVE_TO_COLUMN, x),
+ (
+ ControlType.CURSOR_DOWN if y > 0 else ControlType.CURSOR_UP,
+ abs(y),
+ ),
+ )
+ if y
+ else cls((ControlType.CURSOR_MOVE_TO_COLUMN, x))
+ )
+
+ @classmethod
+ def move_to(cls, x: int, y: int) -> "Control":
+ """Move cursor to absolute position.
+
+ Args:
+ x (int): x offset (column)
+ y (int): y offset (row)
+
+ Returns:
+ ~Control: Control object.
+ """
+ return cls((ControlType.CURSOR_MOVE_TO, x, y))
+
+ @classmethod
+ def clear(cls) -> "Control":
+ """Clear the screen."""
+ return cls(ControlType.CLEAR)
+
+ @classmethod
+ def show_cursor(cls, show: bool) -> "Control":
+ """Show or hide the cursor."""
+ return cls(ControlType.SHOW_CURSOR if show else ControlType.HIDE_CURSOR)
+
+ @classmethod
+ def alt_screen(cls, enable: bool) -> "Control":
+ """Enable or disable alt screen."""
+ if enable:
+ return cls(ControlType.ENABLE_ALT_SCREEN, ControlType.HOME)
+ else:
+ return cls(ControlType.DISABLE_ALT_SCREEN)
+
+ @classmethod
+ def title(cls, title: str) -> "Control":
+ """Set the terminal window title
+
+ Args:
+ title (str): The new terminal window title
+ """
+ return cls((ControlType.SET_WINDOW_TITLE, title))
+
+ def __str__(self) -> str:
+ return self.segment.text
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ if self.segment.text:
+ yield self.segment
+
+
+def strip_control_codes(
+ text: str, _translate_table: Dict[int, None] = _CONTROL_STRIP_TRANSLATE
+) -> str:
+ """Remove control codes from text.
+
+ Args:
+ text (str): A string possibly contain control codes.
+
+ Returns:
+ str: String with control codes removed.
+ """
+ return text.translate(_translate_table)
+
+
+def escape_control_codes(
+ text: str,
+ _translate_table: Dict[int, str] = CONTROL_ESCAPE,
+) -> str:
+ """Replace control codes with their "escaped" equivalent in the given text.
+ (e.g. "\b" becomes "\\b")
+
+ Args:
+ text (str): A string possibly containing control codes.
+
+ Returns:
+ str: String with control codes replaced with their escaped version.
+ """
+ return text.translate(_translate_table)
+
+
+if __name__ == "__main__": # pragma: no cover
+ from pip._vendor.rich.console import Console
+
+ console = Console()
+ console.print("Look at the title of your terminal window ^")
+ # console.print(Control((ControlType.SET_WINDOW_TITLE, "Hello, world!")))
+ for i in range(10):
+ console.set_window_title("🚀 Loading" + "." * i)
+ time.sleep(0.5)
diff --git a/src/pip/_vendor/rich/default_styles.py b/src/pip/_vendor/rich/default_styles.py
new file mode 100644
index 000000000..46e9ea52c
--- /dev/null
+++ b/src/pip/_vendor/rich/default_styles.py
@@ -0,0 +1,188 @@
+from typing import Dict
+
+from .style import Style
+
+DEFAULT_STYLES: Dict[str, Style] = {
+ "none": Style.null(),
+ "reset": Style(
+ color="default",
+ bgcolor="default",
+ dim=False,
+ bold=False,
+ italic=False,
+ underline=False,
+ blink=False,
+ blink2=False,
+ reverse=False,
+ conceal=False,
+ strike=False,
+ ),
+ "dim": Style(dim=True),
+ "bright": Style(dim=False),
+ "bold": Style(bold=True),
+ "strong": Style(bold=True),
+ "code": Style(reverse=True, bold=True),
+ "italic": Style(italic=True),
+ "emphasize": Style(italic=True),
+ "underline": Style(underline=True),
+ "blink": Style(blink=True),
+ "blink2": Style(blink2=True),
+ "reverse": Style(reverse=True),
+ "strike": Style(strike=True),
+ "black": Style(color="black"),
+ "red": Style(color="red"),
+ "green": Style(color="green"),
+ "yellow": Style(color="yellow"),
+ "magenta": Style(color="magenta"),
+ "cyan": Style(color="cyan"),
+ "white": Style(color="white"),
+ "inspect.attr": Style(color="yellow", italic=True),
+ "inspect.attr.dunder": Style(color="yellow", italic=True, dim=True),
+ "inspect.callable": Style(bold=True, color="red"),
+ "inspect.async_def": Style(italic=True, color="bright_cyan"),
+ "inspect.def": Style(italic=True, color="bright_cyan"),
+ "inspect.class": Style(italic=True, color="bright_cyan"),
+ "inspect.error": Style(bold=True, color="red"),
+ "inspect.equals": Style(),
+ "inspect.help": Style(color="cyan"),
+ "inspect.doc": Style(dim=True),
+ "inspect.value.border": Style(color="green"),
+ "live.ellipsis": Style(bold=True, color="red"),
+ "layout.tree.row": Style(dim=False, color="red"),
+ "layout.tree.column": Style(dim=False, color="blue"),
+ "logging.keyword": Style(bold=True, color="yellow"),
+ "logging.level.notset": Style(dim=True),
+ "logging.level.debug": Style(color="green"),
+ "logging.level.info": Style(color="blue"),
+ "logging.level.warning": Style(color="red"),
+ "logging.level.error": Style(color="red", bold=True),
+ "logging.level.critical": Style(color="red", bold=True, reverse=True),
+ "log.level": Style.null(),
+ "log.time": Style(color="cyan", dim=True),
+ "log.message": Style.null(),
+ "log.path": Style(dim=True),
+ "repr.ellipsis": Style(color="yellow"),
+ "repr.indent": Style(color="green", dim=True),
+ "repr.error": Style(color="red", bold=True),
+ "repr.str": Style(color="green", italic=False, bold=False),
+ "repr.brace": Style(bold=True),
+ "repr.comma": Style(bold=True),
+ "repr.ipv4": Style(bold=True, color="bright_green"),
+ "repr.ipv6": Style(bold=True, color="bright_green"),
+ "repr.eui48": Style(bold=True, color="bright_green"),
+ "repr.eui64": Style(bold=True, color="bright_green"),
+ "repr.tag_start": Style(bold=True),
+ "repr.tag_name": Style(color="bright_magenta", bold=True),
+ "repr.tag_contents": Style(color="default"),
+ "repr.tag_end": Style(bold=True),
+ "repr.attrib_name": Style(color="yellow", italic=False),
+ "repr.attrib_equal": Style(bold=True),
+ "repr.attrib_value": Style(color="magenta", italic=False),
+ "repr.number": Style(color="cyan", bold=True, italic=False),
+ "repr.number_complex": Style(color="cyan", bold=True, italic=False), # same
+ "repr.bool_true": Style(color="bright_green", italic=True),
+ "repr.bool_false": Style(color="bright_red", italic=True),
+ "repr.none": Style(color="magenta", italic=True),
+ "repr.url": Style(underline=True, color="bright_blue", italic=False, bold=False),
+ "repr.uuid": Style(color="bright_yellow", bold=False),
+ "repr.call": Style(color="magenta", bold=True),
+ "repr.path": Style(color="magenta"),
+ "repr.filename": Style(color="bright_magenta"),
+ "rule.line": Style(color="bright_green"),
+ "rule.text": Style.null(),
+ "json.brace": Style(bold=True),
+ "json.bool_true": Style(color="bright_green", italic=True),
+ "json.bool_false": Style(color="bright_red", italic=True),
+ "json.null": Style(color="magenta", italic=True),
+ "json.number": Style(color="cyan", bold=True, italic=False),
+ "json.str": Style(color="green", italic=False, bold=False),
+ "json.key": Style(color="blue", bold=True),
+ "prompt": Style.null(),
+ "prompt.choices": Style(color="magenta", bold=True),
+ "prompt.default": Style(color="cyan", bold=True),
+ "prompt.invalid": Style(color="red"),
+ "prompt.invalid.choice": Style(color="red"),
+ "pretty": Style.null(),
+ "scope.border": Style(color="blue"),
+ "scope.key": Style(color="yellow", italic=True),
+ "scope.key.special": Style(color="yellow", italic=True, dim=True),
+ "scope.equals": Style(color="red"),
+ "table.header": Style(bold=True),
+ "table.footer": Style(bold=True),
+ "table.cell": Style.null(),
+ "table.title": Style(italic=True),
+ "table.caption": Style(italic=True, dim=True),
+ "traceback.error": Style(color="red", italic=True),
+ "traceback.border.syntax_error": Style(color="bright_red"),
+ "traceback.border": Style(color="red"),
+ "traceback.text": Style.null(),
+ "traceback.title": Style(color="red", bold=True),
+ "traceback.exc_type": Style(color="bright_red", bold=True),
+ "traceback.exc_value": Style.null(),
+ "traceback.offset": Style(color="bright_red", bold=True),
+ "bar.back": Style(color="grey23"),
+ "bar.complete": Style(color="rgb(249,38,114)"),
+ "bar.finished": Style(color="rgb(114,156,31)"),
+ "bar.pulse": Style(color="rgb(249,38,114)"),
+ "progress.description": Style.null(),
+ "progress.filesize": Style(color="green"),
+ "progress.filesize.total": Style(color="green"),
+ "progress.download": Style(color="green"),
+ "progress.elapsed": Style(color="yellow"),
+ "progress.percentage": Style(color="magenta"),
+ "progress.remaining": Style(color="cyan"),
+ "progress.data.speed": Style(color="red"),
+ "progress.spinner": Style(color="green"),
+ "status.spinner": Style(color="green"),
+ "tree": Style(),
+ "tree.line": Style(),
+ "markdown.paragraph": Style(),
+ "markdown.text": Style(),
+ "markdown.emph": Style(italic=True),
+ "markdown.strong": Style(bold=True),
+ "markdown.code": Style(bgcolor="black", color="bright_white"),
+ "markdown.code_block": Style(dim=True, color="cyan", bgcolor="black"),
+ "markdown.block_quote": Style(color="magenta"),
+ "markdown.list": Style(color="cyan"),
+ "markdown.item": Style(),
+ "markdown.item.bullet": Style(color="yellow", bold=True),
+ "markdown.item.number": Style(color="yellow", bold=True),
+ "markdown.hr": Style(color="yellow"),
+ "markdown.h1.border": Style(),
+ "markdown.h1": Style(bold=True),
+ "markdown.h2": Style(bold=True, underline=True),
+ "markdown.h3": Style(bold=True),
+ "markdown.h4": Style(bold=True, dim=True),
+ "markdown.h5": Style(underline=True),
+ "markdown.h6": Style(italic=True),
+ "markdown.h7": Style(italic=True, dim=True),
+ "markdown.link": Style(color="bright_blue"),
+ "markdown.link_url": Style(color="blue"),
+ "iso8601.date": Style(color="blue"),
+ "iso8601.time": Style(color="magenta"),
+ "iso8601.timezone": Style(color="yellow"),
+}
+
+
+if __name__ == "__main__": # pragma: no cover
+ import argparse
+ import io
+
+ from pip._vendor.rich.console import Console
+ from pip._vendor.rich.table import Table
+ from pip._vendor.rich.text import Text
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--html", action="store_true", help="Export as HTML table")
+ args = parser.parse_args()
+ html: bool = args.html
+ console = Console(record=True, width=70, file=io.StringIO()) if html else Console()
+
+ table = Table("Name", "Styling")
+
+ for style_name, style in DEFAULT_STYLES.items():
+ table.add_row(Text(style_name, style=style), str(style))
+
+ console.print(table)
+ if html:
+ print(console.export_html(inline_styles=True))
diff --git a/src/pip/_vendor/rich/diagnose.py b/src/pip/_vendor/rich/diagnose.py
new file mode 100644
index 000000000..ad3618389
--- /dev/null
+++ b/src/pip/_vendor/rich/diagnose.py
@@ -0,0 +1,37 @@
+import os
+import platform
+
+from pip._vendor.rich import inspect
+from pip._vendor.rich.console import Console, get_windows_console_features
+from pip._vendor.rich.panel import Panel
+from pip._vendor.rich.pretty import Pretty
+
+
+def report() -> None: # pragma: no cover
+ """Print a report to the terminal with debugging information"""
+ console = Console()
+ inspect(console)
+ features = get_windows_console_features()
+ inspect(features)
+
+ env_names = (
+ "TERM",
+ "COLORTERM",
+ "CLICOLOR",
+ "NO_COLOR",
+ "TERM_PROGRAM",
+ "COLUMNS",
+ "LINES",
+ "JUPYTER_COLUMNS",
+ "JUPYTER_LINES",
+ "JPY_PARENT_PID",
+ "VSCODE_VERBOSE_LOGGING",
+ )
+ env = {name: os.getenv(name) for name in env_names}
+ console.print(Panel.fit((Pretty(env)), title="[b]Environment Variables"))
+
+ console.print(f'platform="{platform.system()}"')
+
+
+if __name__ == "__main__": # pragma: no cover
+ report()
diff --git a/src/pip/_vendor/rich/emoji.py b/src/pip/_vendor/rich/emoji.py
new file mode 100644
index 000000000..791f0465d
--- /dev/null
+++ b/src/pip/_vendor/rich/emoji.py
@@ -0,0 +1,96 @@
+import sys
+from typing import TYPE_CHECKING, Optional, Union
+
+from .jupyter import JupyterMixin
+from .segment import Segment
+from .style import Style
+from ._emoji_codes import EMOJI
+from ._emoji_replace import _emoji_replace
+
+if sys.version_info >= (3, 8):
+ from typing import Literal
+else:
+ from pip._vendor.typing_extensions import Literal # pragma: no cover
+
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderResult
+
+
+EmojiVariant = Literal["emoji", "text"]
+
+
+class NoEmoji(Exception):
+ """No emoji by that name."""
+
+
+class Emoji(JupyterMixin):
+ __slots__ = ["name", "style", "_char", "variant"]
+
+ VARIANTS = {"text": "\uFE0E", "emoji": "\uFE0F"}
+
+ def __init__(
+ self,
+ name: str,
+ style: Union[str, Style] = "none",
+ variant: Optional[EmojiVariant] = None,
+ ) -> None:
+ """A single emoji character.
+
+ Args:
+ name (str): Name of emoji.
+ style (Union[str, Style], optional): Optional style. Defaults to None.
+
+ Raises:
+ NoEmoji: If the emoji doesn't exist.
+ """
+ self.name = name
+ self.style = style
+ self.variant = variant
+ try:
+ self._char = EMOJI[name]
+ except KeyError:
+ raise NoEmoji(f"No emoji called {name!r}")
+ if variant is not None:
+ self._char += self.VARIANTS.get(variant, "")
+
+ @classmethod
+ def replace(cls, text: str) -> str:
+ """Replace emoji markup with corresponding unicode characters.
+
+ Args:
+ text (str): A string with emojis codes, e.g. "Hello :smiley:!"
+
+ Returns:
+ str: A string with emoji codes replaces with actual emoji.
+ """
+ return _emoji_replace(text)
+
+ def __repr__(self) -> str:
+ return f"<emoji {self.name!r}>"
+
+ def __str__(self) -> str:
+ return self._char
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ yield Segment(self._char, console.get_style(self.style))
+
+
+if __name__ == "__main__": # pragma: no cover
+ import sys
+
+ from pip._vendor.rich.columns import Columns
+ from pip._vendor.rich.console import Console
+
+ console = Console(record=True)
+
+ columns = Columns(
+ (f":{name}: {name}" for name in sorted(EMOJI.keys()) if "\u200D" not in name),
+ column_first=True,
+ )
+
+ console.print(columns)
+ if len(sys.argv) > 1:
+ console.save_html(sys.argv[1])
diff --git a/src/pip/_vendor/rich/errors.py b/src/pip/_vendor/rich/errors.py
new file mode 100644
index 000000000..0bcbe53ef
--- /dev/null
+++ b/src/pip/_vendor/rich/errors.py
@@ -0,0 +1,34 @@
+class ConsoleError(Exception):
+ """An error in console operation."""
+
+
+class StyleError(Exception):
+ """An error in styles."""
+
+
+class StyleSyntaxError(ConsoleError):
+ """Style was badly formatted."""
+
+
+class MissingStyle(StyleError):
+ """No such style."""
+
+
+class StyleStackError(ConsoleError):
+ """Style stack is invalid."""
+
+
+class NotRenderableError(ConsoleError):
+ """Object is not renderable."""
+
+
+class MarkupError(ConsoleError):
+ """Markup was badly formatted."""
+
+
+class LiveError(ConsoleError):
+ """Error related to Live display."""
+
+
+class NoAltScreen(ConsoleError):
+ """Alt screen mode was required."""
diff --git a/src/pip/_vendor/rich/file_proxy.py b/src/pip/_vendor/rich/file_proxy.py
new file mode 100644
index 000000000..cc69f22f3
--- /dev/null
+++ b/src/pip/_vendor/rich/file_proxy.py
@@ -0,0 +1,54 @@
+import io
+from typing import IO, TYPE_CHECKING, Any, List
+
+from .ansi import AnsiDecoder
+from .text import Text
+
+if TYPE_CHECKING:
+ from .console import Console
+
+
+class FileProxy(io.TextIOBase):
+ """Wraps a file (e.g. sys.stdout) and redirects writes to a console."""
+
+ def __init__(self, console: "Console", file: IO[str]) -> None:
+ self.__console = console
+ self.__file = file
+ self.__buffer: List[str] = []
+ self.__ansi_decoder = AnsiDecoder()
+
+ @property
+ def rich_proxied_file(self) -> IO[str]:
+ """Get proxied file."""
+ return self.__file
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(self.__file, name)
+
+ def write(self, text: str) -> int:
+ if not isinstance(text, str):
+ raise TypeError(f"write() argument must be str, not {type(text).__name__}")
+ buffer = self.__buffer
+ lines: List[str] = []
+ while text:
+ line, new_line, text = text.partition("\n")
+ if new_line:
+ lines.append("".join(buffer) + line)
+ del buffer[:]
+ else:
+ buffer.append(line)
+ break
+ if lines:
+ console = self.__console
+ with console:
+ output = Text("\n").join(
+ self.__ansi_decoder.decode_line(line) for line in lines
+ )
+ console.print(output)
+ return len(text)
+
+ def flush(self) -> None:
+ output = "".join(self.__buffer)
+ if output:
+ self.__console.print(output)
+ del self.__buffer[:]
diff --git a/src/pip/_vendor/rich/filesize.py b/src/pip/_vendor/rich/filesize.py
new file mode 100644
index 000000000..61be47510
--- /dev/null
+++ b/src/pip/_vendor/rich/filesize.py
@@ -0,0 +1,89 @@
+# coding: utf-8
+"""Functions for reporting filesizes. Borrowed from https://github.com/PyFilesystem/pyfilesystem2
+
+The functions declared in this module should cover the different
+usecases needed to generate a string representation of a file size
+using several different units. Since there are many standards regarding
+file size units, three different functions have been implemented.
+
+See Also:
+ * `Wikipedia: Binary prefix <https://en.wikipedia.org/wiki/Binary_prefix>`_
+
+"""
+
+__all__ = ["decimal"]
+
+from typing import Iterable, List, Optional, Tuple
+
+
+def _to_str(
+ size: int,
+ suffixes: Iterable[str],
+ base: int,
+ *,
+ precision: Optional[int] = 1,
+ separator: Optional[str] = " ",
+) -> str:
+ if size == 1:
+ return "1 byte"
+ elif size < base:
+ return "{:,} bytes".format(size)
+
+ for i, suffix in enumerate(suffixes, 2): # noqa: B007
+ unit = base**i
+ if size < unit:
+ break
+ return "{:,.{precision}f}{separator}{}".format(
+ (base * size / unit),
+ suffix,
+ precision=precision,
+ separator=separator,
+ )
+
+
+def pick_unit_and_suffix(size: int, suffixes: List[str], base: int) -> Tuple[int, str]:
+ """Pick a suffix and base for the given size."""
+ for i, suffix in enumerate(suffixes):
+ unit = base**i
+ if size < unit * base:
+ break
+ return unit, suffix
+
+
+def decimal(
+ size: int,
+ *,
+ precision: Optional[int] = 1,
+ separator: Optional[str] = " ",
+) -> str:
+ """Convert a filesize in to a string (powers of 1000, SI prefixes).
+
+ In this convention, ``1000 B = 1 kB``.
+
+ This is typically the format used to advertise the storage
+ capacity of USB flash drives and the like (*256 MB* meaning
+ actually a storage capacity of more than *256 000 000 B*),
+ or used by **Mac OS X** since v10.6 to report file sizes.
+
+ Arguments:
+ int (size): A file size.
+ int (precision): The number of decimal places to include (default = 1).
+ str (separator): The string to separate the value from the units (default = " ").
+
+ Returns:
+ `str`: A string containing a abbreviated file size and units.
+
+ Example:
+ >>> filesize.decimal(30000)
+ '30.0 kB'
+ >>> filesize.decimal(30000, precision=2, separator="")
+ '30.00kB'
+
+ """
+ return _to_str(
+ size,
+ ("kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"),
+ 1000,
+ precision=precision,
+ separator=separator,
+ )
diff --git a/src/pip/_vendor/rich/highlighter.py b/src/pip/_vendor/rich/highlighter.py
new file mode 100644
index 000000000..82293dffc
--- /dev/null
+++ b/src/pip/_vendor/rich/highlighter.py
@@ -0,0 +1,232 @@
+import re
+from abc import ABC, abstractmethod
+from typing import List, Union
+
+from .text import Span, Text
+
+
+def _combine_regex(*regexes: str) -> str:
+ """Combine a number of regexes in to a single regex.
+
+ Returns:
+ str: New regex with all regexes ORed together.
+ """
+ return "|".join(regexes)
+
+
+class Highlighter(ABC):
+ """Abstract base class for highlighters."""
+
+ def __call__(self, text: Union[str, Text]) -> Text:
+ """Highlight a str or Text instance.
+
+ Args:
+ text (Union[str, ~Text]): Text to highlight.
+
+ Raises:
+ TypeError: If not called with text or str.
+
+ Returns:
+ Text: A test instance with highlighting applied.
+ """
+ if isinstance(text, str):
+ highlight_text = Text(text)
+ elif isinstance(text, Text):
+ highlight_text = text.copy()
+ else:
+ raise TypeError(f"str or Text instance required, not {text!r}")
+ self.highlight(highlight_text)
+ return highlight_text
+
+ @abstractmethod
+ def highlight(self, text: Text) -> None:
+ """Apply highlighting in place to text.
+
+ Args:
+ text (~Text): A text object highlight.
+ """
+
+
+class NullHighlighter(Highlighter):
+ """A highlighter object that doesn't highlight.
+
+ May be used to disable highlighting entirely.
+
+ """
+
+ def highlight(self, text: Text) -> None:
+ """Nothing to do"""
+
+
+class RegexHighlighter(Highlighter):
+ """Applies highlighting from a list of regular expressions."""
+
+ highlights: List[str] = []
+ base_style: str = ""
+
+ def highlight(self, text: Text) -> None:
+ """Highlight :class:`rich.text.Text` using regular expressions.
+
+ Args:
+ text (~Text): Text to highlighted.
+
+ """
+
+ highlight_regex = text.highlight_regex
+ for re_highlight in self.highlights:
+ highlight_regex(re_highlight, style_prefix=self.base_style)
+
+
+class ReprHighlighter(RegexHighlighter):
+ """Highlights the text typically produced from ``__repr__`` methods."""
+
+ base_style = "repr."
+ highlights = [
+ r"(?P<tag_start><)(?P<tag_name>[-\w.:|]*)(?P<tag_contents>[\w\W]*?)(?P<tag_end>>)",
+ r'(?P<attrib_name>[\w_]{1,50})=(?P<attrib_value>"?[\w_]+"?)?',
+ r"(?P<brace>[][{}()])",
+ _combine_regex(
+ r"(?P<ipv4>[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})",
+ r"(?P<ipv6>([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})",
+ r"(?P<eui64>(?:[0-9A-Fa-f]{1,2}-){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\.){3}[0-9A-Fa-f]{4})",
+ r"(?P<eui48>(?:[0-9A-Fa-f]{1,2}-){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4})",
+ r"(?P<uuid>[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})",
+ r"(?P<call>[\w.]*?)\(",
+ r"\b(?P<bool_true>True)\b|\b(?P<bool_false>False)\b|\b(?P<none>None)\b",
+ r"(?P<ellipsis>\.\.\.)",
+ r"(?P<number_complex>(?<!\w)(?:\-?[0-9]+\.?[0-9]*(?:e[-+]?\d+?)?)(?:[-+](?:[0-9]+\.?[0-9]*(?:e[-+]?\d+)?))?j)",
+ r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[-+]?\d+?)?\b|0x[0-9a-fA-F]*)",
+ r"(?P<path>\B(/[-\w._+]+)*\/)(?P<filename>[-\w._+]*)?",
+ r"(?<![\\\w])(?P<str>b?'''.*?(?<!\\)'''|b?'.*?(?<!\\)'|b?\"\"\".*?(?<!\\)\"\"\"|b?\".*?(?<!\\)\")",
+ r"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#]*)",
+ ),
+ ]
+
+
+class JSONHighlighter(RegexHighlighter):
+ """Highlights JSON"""
+
+ # Captures the start and end of JSON strings, handling escaped quotes
+ JSON_STR = r"(?<![\\\w])(?P<str>b?\".*?(?<!\\)\")"
+ JSON_WHITESPACE = {" ", "\n", "\r", "\t"}
+
+ base_style = "json."
+ highlights = [
+ _combine_regex(
+ r"(?P<brace>[\{\[\(\)\]\}])",
+ r"\b(?P<bool_true>true)\b|\b(?P<bool_false>false)\b|\b(?P<null>null)\b",
+ r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[\-\+]?\d+?)?\b|0x[0-9a-fA-F]*)",
+ JSON_STR,
+ ),
+ ]
+
+ def highlight(self, text: Text) -> None:
+ super().highlight(text)
+
+ # Additional work to handle highlighting JSON keys
+ plain = text.plain
+ append = text.spans.append
+ whitespace = self.JSON_WHITESPACE
+ for match in re.finditer(self.JSON_STR, plain):
+ start, end = match.span()
+ cursor = end
+ while cursor < len(plain):
+ char = plain[cursor]
+ cursor += 1
+ if char == ":":
+ append(Span(start, end, "json.key"))
+ elif char in whitespace:
+ continue
+ break
+
+
+class ISO8601Highlighter(RegexHighlighter):
+ """Highlights the ISO8601 date time strings.
+ Regex reference: https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s07.html
+ """
+
+ base_style = "iso8601."
+ highlights = [
+ #
+ # Dates
+ #
+ # Calendar month (e.g. 2008-08). The hyphen is required
+ r"^(?P<year>[0-9]{4})-(?P<month>1[0-2]|0[1-9])$",
+ # Calendar date w/o hyphens (e.g. 20080830)
+ r"^(?P<date>(?P<year>[0-9]{4})(?P<month>1[0-2]|0[1-9])(?P<day>3[01]|0[1-9]|[12][0-9]))$",
+ # Ordinal date (e.g. 2008-243). The hyphen is optional
+ r"^(?P<date>(?P<year>[0-9]{4})-?(?P<day>36[0-6]|3[0-5][0-9]|[12][0-9]{2}|0[1-9][0-9]|00[1-9]))$",
+ #
+ # Weeks
+ #
+ # Week of the year (e.g., 2008-W35). The hyphen is optional
+ r"^(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9]))$",
+ # Week date (e.g., 2008-W35-6). The hyphens are optional
+ r"^(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9])-?(?P<day>[1-7]))$",
+ #
+ # Times
+ #
+ # Hours and minutes (e.g., 17:21). The colon is optional
+ r"^(?P<time>(?P<hour>2[0-3]|[01][0-9]):?(?P<minute>[0-5][0-9]))$",
+ # Hours, minutes, and seconds w/o colons (e.g., 172159)
+ r"^(?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))$",
+ # Time zone designator (e.g., Z, +07 or +07:00). The colons and the minutes are optional
+ r"^(?P<timezone>(Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?))$",
+ # Hours, minutes, and seconds with time zone designator (e.g., 17:21:59+07:00).
+ # All the colons are optional. The minutes in the time zone designator are also optional
+ r"^(?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?)$",
+ #
+ # Date and Time
+ #
+ # Calendar date with hours, minutes, and seconds (e.g., 2008-08-30 17:21:59 or 20080830 172159).
+ # A space is required between the date and the time. The hyphens and colons are optional.
+ # This regex matches dates and times that specify some hyphens or colons but omit others.
+ # This does not follow ISO 8601
+ r"^(?P<date>(?P<year>[0-9]{4})(?P<hyphen>-)?(?P<month>1[0-2]|0[1-9])(?(hyphen)-)(?P<day>3[01]|0[1-9]|[12][0-9])) (?P<time>(?P<hour>2[0-3]|[01][0-9])(?(hyphen):)(?P<minute>[0-5][0-9])(?(hyphen):)(?P<second>[0-5][0-9]))$",
+ #
+ # XML Schema dates and times
+ #
+ # Date, with optional time zone (e.g., 2008-08-30 or 2008-08-30+07:00).
+ # Hyphens are required. This is the XML Schema 'date' type
+ r"^(?P<date>(?P<year>-?(?:[1-9][0-9]*)?[0-9]{4})-(?P<month>1[0-2]|0[1-9])-(?P<day>3[01]|0[1-9]|[12][0-9]))(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
+ # Time, with optional fractional seconds and time zone (e.g., 01:45:36 or 01:45:36.123+07:00).
+ # There is no limit on the number of digits for the fractional seconds. This is the XML Schema 'time' type
+ r"^(?P<time>(?P<hour>2[0-3]|[01][0-9]):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9])(?P<frac>\.[0-9]+)?)(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
+ # Date and time, with optional fractional seconds and time zone (e.g., 2008-08-30T01:45:36 or 2008-08-30T01:45:36.123Z).
+ # This is the XML Schema 'dateTime' type
+ r"^(?P<date>(?P<year>-?(?:[1-9][0-9]*)?[0-9]{4})-(?P<month>1[0-2]|0[1-9])-(?P<day>3[01]|0[1-9]|[12][0-9]))T(?P<time>(?P<hour>2[0-3]|[01][0-9]):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9])(?P<ms>\.[0-9]+)?)(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
+ ]
+
+
+if __name__ == "__main__": # pragma: no cover
+ from .console import Console
+
+ console = Console()
+ console.print("[bold green]hello world![/bold green]")
+ console.print("'[bold green]hello world![/bold green]'")
+
+ console.print(" /foo")
+ console.print("/foo/")
+ console.print("/foo/bar")
+ console.print("foo/bar/baz")
+
+ console.print("/foo/bar/baz?foo=bar+egg&egg=baz")
+ console.print("/foo/bar/baz/")
+ console.print("/foo/bar/baz/egg")
+ console.print("/foo/bar/baz/egg.py")
+ console.print("/foo/bar/baz/egg.py word")
+ console.print(" /foo/bar/baz/egg.py word")
+ console.print("foo /foo/bar/baz/egg.py word")
+ console.print("foo /foo/bar/ba._++z/egg+.py word")
+ console.print("https://example.org?foo=bar#header")
+
+ console.print(1234567.34)
+ console.print(1 / 2)
+ console.print(-1 / 123123123123)
+
+ console.print(
+ "127.0.1.1 bar 192.168.1.4 2001:0db8:85a3:0000:0000:8a2e:0370:7334 foo"
+ )
+ import json
+
+ console.print_json(json.dumps(obj={"name": "apple", "count": 1}), indent=None)
diff --git a/src/pip/_vendor/rich/json.py b/src/pip/_vendor/rich/json.py
new file mode 100644
index 000000000..23583871e
--- /dev/null
+++ b/src/pip/_vendor/rich/json.py
@@ -0,0 +1,140 @@
+from json import loads, dumps
+from typing import Any, Callable, Optional, Union
+
+from .text import Text
+from .highlighter import JSONHighlighter, NullHighlighter
+
+
+class JSON:
+ """A renderable which pretty prints JSON.
+
+ Args:
+ json (str): JSON encoded data.
+ indent (Union[None, int, str], optional): Number of characters to indent by. Defaults to 2.
+ highlight (bool, optional): Enable highlighting. Defaults to True.
+ skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False.
+ ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False.
+ check_circular (bool, optional): Check for circular references. Defaults to True.
+ allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True.
+ default (Callable, optional): A callable that converts values that can not be encoded
+ in to something that can be JSON encoded. Defaults to None.
+ sort_keys (bool, optional): Sort dictionary keys. Defaults to False.
+ """
+
+ def __init__(
+ self,
+ json: str,
+ indent: Union[None, int, str] = 2,
+ highlight: bool = True,
+ skip_keys: bool = False,
+ ensure_ascii: bool = True,
+ check_circular: bool = True,
+ allow_nan: bool = True,
+ default: Optional[Callable[[Any], Any]] = None,
+ sort_keys: bool = False,
+ ) -> None:
+ data = loads(json)
+ json = dumps(
+ data,
+ indent=indent,
+ skipkeys=skip_keys,
+ ensure_ascii=ensure_ascii,
+ check_circular=check_circular,
+ allow_nan=allow_nan,
+ default=default,
+ sort_keys=sort_keys,
+ )
+ highlighter = JSONHighlighter() if highlight else NullHighlighter()
+ self.text = highlighter(json)
+ self.text.no_wrap = True
+ self.text.overflow = None
+
+ @classmethod
+ def from_data(
+ cls,
+ data: Any,
+ indent: Union[None, int, str] = 2,
+ highlight: bool = True,
+ skip_keys: bool = False,
+ ensure_ascii: bool = True,
+ check_circular: bool = True,
+ allow_nan: bool = True,
+ default: Optional[Callable[[Any], Any]] = None,
+ sort_keys: bool = False,
+ ) -> "JSON":
+ """Encodes a JSON object from arbitrary data.
+
+ Args:
+ data (Any): An object that may be encoded in to JSON
+ indent (Union[None, int, str], optional): Number of characters to indent by. Defaults to 2.
+ highlight (bool, optional): Enable highlighting. Defaults to True.
+ default (Callable, optional): Optional callable which will be called for objects that cannot be serialized. Defaults to None.
+ skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False.
+ ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False.
+ check_circular (bool, optional): Check for circular references. Defaults to True.
+ allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True.
+ default (Callable, optional): A callable that converts values that can not be encoded
+ in to something that can be JSON encoded. Defaults to None.
+ sort_keys (bool, optional): Sort dictionary keys. Defaults to False.
+
+ Returns:
+ JSON: New JSON object from the given data.
+ """
+ json_instance: "JSON" = cls.__new__(cls)
+ json = dumps(
+ data,
+ indent=indent,
+ skipkeys=skip_keys,
+ ensure_ascii=ensure_ascii,
+ check_circular=check_circular,
+ allow_nan=allow_nan,
+ default=default,
+ sort_keys=sort_keys,
+ )
+ highlighter = JSONHighlighter() if highlight else NullHighlighter()
+ json_instance.text = highlighter(json)
+ json_instance.text.no_wrap = True
+ json_instance.text.overflow = None
+ return json_instance
+
+ def __rich__(self) -> Text:
+ return self.text
+
+
+if __name__ == "__main__":
+
+ import argparse
+ import sys
+
+ parser = argparse.ArgumentParser(description="Pretty print json")
+ parser.add_argument(
+ "path",
+ metavar="PATH",
+ help="path to file, or - for stdin",
+ )
+ parser.add_argument(
+ "-i",
+ "--indent",
+ metavar="SPACES",
+ type=int,
+ help="Number of spaces in an indent",
+ default=2,
+ )
+ args = parser.parse_args()
+
+ from pip._vendor.rich.console import Console
+
+ console = Console()
+ error_console = Console(stderr=True)
+
+ try:
+ if args.path == "-":
+ json_data = sys.stdin.read()
+ else:
+ with open(args.path, "rt") as json_file:
+ json_data = json_file.read()
+ except Exception as error:
+ error_console.print(f"Unable to read {args.path!r}; {error}")
+ sys.exit(-1)
+
+ console.print(JSON(json_data, indent=args.indent), soft_wrap=True)
diff --git a/src/pip/_vendor/rich/jupyter.py b/src/pip/_vendor/rich/jupyter.py
new file mode 100644
index 000000000..22f4d716a
--- /dev/null
+++ b/src/pip/_vendor/rich/jupyter.py
@@ -0,0 +1,101 @@
+from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Sequence
+
+if TYPE_CHECKING:
+ from pip._vendor.rich.console import ConsoleRenderable
+
+from . import get_console
+from .segment import Segment
+from .terminal_theme import DEFAULT_TERMINAL_THEME
+
+if TYPE_CHECKING:
+ from pip._vendor.rich.console import ConsoleRenderable
+
+JUPYTER_HTML_FORMAT = """\
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace">{code}</pre>
+"""
+
+
+class JupyterRenderable:
+ """A shim to write html to Jupyter notebook."""
+
+ def __init__(self, html: str, text: str) -> None:
+ self.html = html
+ self.text = text
+
+ def _repr_mimebundle_(
+ self, include: Sequence[str], exclude: Sequence[str], **kwargs: Any
+ ) -> Dict[str, str]:
+ data = {"text/plain": self.text, "text/html": self.html}
+ if include:
+ data = {k: v for (k, v) in data.items() if k in include}
+ if exclude:
+ data = {k: v for (k, v) in data.items() if k not in exclude}
+ return data
+
+
+class JupyterMixin:
+ """Add to an Rich renderable to make it render in Jupyter notebook."""
+
+ __slots__ = ()
+
+ def _repr_mimebundle_(
+ self: "ConsoleRenderable",
+ include: Sequence[str],
+ exclude: Sequence[str],
+ **kwargs: Any,
+ ) -> Dict[str, str]:
+ console = get_console()
+ segments = list(console.render(self, console.options))
+ html = _render_segments(segments)
+ text = console._render_buffer(segments)
+ data = {"text/plain": text, "text/html": html}
+ if include:
+ data = {k: v for (k, v) in data.items() if k in include}
+ if exclude:
+ data = {k: v for (k, v) in data.items() if k not in exclude}
+ return data
+
+
+def _render_segments(segments: Iterable[Segment]) -> str:
+ def escape(text: str) -> str:
+ """Escape html."""
+ return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
+
+ fragments: List[str] = []
+ append_fragment = fragments.append
+ theme = DEFAULT_TERMINAL_THEME
+ for text, style, control in Segment.simplify(segments):
+ if control:
+ continue
+ text = escape(text)
+ if style:
+ rule = style.get_html_style(theme)
+ text = f'<span style="{rule}">{text}</span>' if rule else text
+ if style.link:
+ text = f'<a href="{style.link}" target="_blank">{text}</a>'
+ append_fragment(text)
+
+ code = "".join(fragments)
+ html = JUPYTER_HTML_FORMAT.format(code=code)
+
+ return html
+
+
+def display(segments: Iterable[Segment], text: str) -> None:
+ """Render segments to Jupyter."""
+ html = _render_segments(segments)
+ jupyter_renderable = JupyterRenderable(html, text)
+ try:
+ from IPython.display import display as ipython_display
+
+ ipython_display(jupyter_renderable)
+ except ModuleNotFoundError:
+ # Handle the case where the Console has force_jupyter=True,
+ # but IPython is not installed.
+ pass
+
+
+def print(*args: Any, **kwargs: Any) -> None:
+ """Proxy for Console print."""
+ console = get_console()
+ return console.print(*args, **kwargs)
diff --git a/src/pip/_vendor/rich/layout.py b/src/pip/_vendor/rich/layout.py
new file mode 100644
index 000000000..1d704652e
--- /dev/null
+++ b/src/pip/_vendor/rich/layout.py
@@ -0,0 +1,445 @@
+from abc import ABC, abstractmethod
+from itertools import islice
+from operator import itemgetter
+from threading import RLock
+from typing import (
+ TYPE_CHECKING,
+ Dict,
+ Iterable,
+ List,
+ NamedTuple,
+ Optional,
+ Sequence,
+ Tuple,
+ Union,
+)
+
+from ._ratio import ratio_resolve
+from .align import Align
+from .console import Console, ConsoleOptions, RenderableType, RenderResult
+from .highlighter import ReprHighlighter
+from .panel import Panel
+from .pretty import Pretty
+from .repr import rich_repr, Result
+from .region import Region
+from .segment import Segment
+from .style import StyleType
+
+if TYPE_CHECKING:
+ from pip._vendor.rich.tree import Tree
+
+
+class LayoutRender(NamedTuple):
+ """An individual layout render."""
+
+ region: Region
+ render: List[List[Segment]]
+
+
+RegionMap = Dict["Layout", Region]
+RenderMap = Dict["Layout", LayoutRender]
+
+
+class LayoutError(Exception):
+ """Layout related error."""
+
+
+class NoSplitter(LayoutError):
+ """Requested splitter does not exist."""
+
+
+class _Placeholder:
+ """An internal renderable used as a Layout placeholder."""
+
+ highlighter = ReprHighlighter()
+
+ def __init__(self, layout: "Layout", style: StyleType = "") -> None:
+ self.layout = layout
+ self.style = style
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ width = options.max_width
+ height = options.height or options.size.height
+ layout = self.layout
+ title = (
+ f"{layout.name!r} ({width} x {height})"
+ if layout.name
+ else f"({width} x {height})"
+ )
+ yield Panel(
+ Align.center(Pretty(layout), vertical="middle"),
+ style=self.style,
+ title=self.highlighter(title),
+ border_style="blue",
+ height=height,
+ )
+
+
+class Splitter(ABC):
+ """Base class for a splitter."""
+
+ name: str = ""
+
+ @abstractmethod
+ def get_tree_icon(self) -> str:
+ """Get the icon (emoji) used in layout.tree"""
+
+ @abstractmethod
+ def divide(
+ self, children: Sequence["Layout"], region: Region
+ ) -> Iterable[Tuple["Layout", Region]]:
+ """Divide a region amongst several child layouts.
+
+ Args:
+ children (Sequence(Layout)): A number of child layouts.
+ region (Region): A rectangular region to divide.
+ """
+
+
+class RowSplitter(Splitter):
+ """Split a layout region in to rows."""
+
+ name = "row"
+
+ def get_tree_icon(self) -> str:
+ return "[layout.tree.row]⬌"
+
+ def divide(
+ self, children: Sequence["Layout"], region: Region
+ ) -> Iterable[Tuple["Layout", Region]]:
+ x, y, width, height = region
+ render_widths = ratio_resolve(width, children)
+ offset = 0
+ _Region = Region
+ for child, child_width in zip(children, render_widths):
+ yield child, _Region(x + offset, y, child_width, height)
+ offset += child_width
+
+
+class ColumnSplitter(Splitter):
+ """Split a layout region in to columns."""
+
+ name = "column"
+
+ def get_tree_icon(self) -> str:
+ return "[layout.tree.column]â¬"
+
+ def divide(
+ self, children: Sequence["Layout"], region: Region
+ ) -> Iterable[Tuple["Layout", Region]]:
+ x, y, width, height = region
+ render_heights = ratio_resolve(height, children)
+ offset = 0
+ _Region = Region
+ for child, child_height in zip(children, render_heights):
+ yield child, _Region(x, y + offset, width, child_height)
+ offset += child_height
+
+
+@rich_repr
+class Layout:
+ """A renderable to divide a fixed height in to rows or columns.
+
+ Args:
+ renderable (RenderableType, optional): Renderable content, or None for placeholder. Defaults to None.
+ name (str, optional): Optional identifier for Layout. Defaults to None.
+ size (int, optional): Optional fixed size of layout. Defaults to None.
+ minimum_size (int, optional): Minimum size of layout. Defaults to 1.
+ ratio (int, optional): Optional ratio for flexible layout. Defaults to 1.
+ visible (bool, optional): Visibility of layout. Defaults to True.
+ """
+
+ splitters = {"row": RowSplitter, "column": ColumnSplitter}
+
+ def __init__(
+ self,
+ renderable: Optional[RenderableType] = None,
+ *,
+ name: Optional[str] = None,
+ size: Optional[int] = None,
+ minimum_size: int = 1,
+ ratio: int = 1,
+ visible: bool = True,
+ height: Optional[int] = None,
+ ) -> None:
+ self._renderable = renderable or _Placeholder(self)
+ self.size = size
+ self.minimum_size = minimum_size
+ self.ratio = ratio
+ self.name = name
+ self.visible = visible
+ self.height = height
+ self.splitter: Splitter = self.splitters["column"]()
+ self._children: List[Layout] = []
+ self._render_map: RenderMap = {}
+ self._lock = RLock()
+
+ def __rich_repr__(self) -> Result:
+ yield "name", self.name, None
+ yield "size", self.size, None
+ yield "minimum_size", self.minimum_size, 1
+ yield "ratio", self.ratio, 1
+
+ @property
+ def renderable(self) -> RenderableType:
+ """Layout renderable."""
+ return self if self._children else self._renderable
+
+ @property
+ def children(self) -> List["Layout"]:
+ """Gets (visible) layout children."""
+ return [child for child in self._children if child.visible]
+
+ @property
+ def map(self) -> RenderMap:
+ """Get a map of the last render."""
+ return self._render_map
+
+ def get(self, name: str) -> Optional["Layout"]:
+ """Get a named layout, or None if it doesn't exist.
+
+ Args:
+ name (str): Name of layout.
+
+ Returns:
+ Optional[Layout]: Layout instance or None if no layout was found.
+ """
+ if self.name == name:
+ return self
+ else:
+ for child in self._children:
+ named_layout = child.get(name)
+ if named_layout is not None:
+ return named_layout
+ return None
+
+ def __getitem__(self, name: str) -> "Layout":
+ layout = self.get(name)
+ if layout is None:
+ raise KeyError(f"No layout with name {name!r}")
+ return layout
+
+ @property
+ def tree(self) -> "Tree":
+ """Get a tree renderable to show layout structure."""
+ from pip._vendor.rich.styled import Styled
+ from pip._vendor.rich.table import Table
+ from pip._vendor.rich.tree import Tree
+
+ def summary(layout: "Layout") -> Table:
+
+ icon = layout.splitter.get_tree_icon()
+
+ table = Table.grid(padding=(0, 1, 0, 0))
+
+ text: RenderableType = (
+ Pretty(layout) if layout.visible else Styled(Pretty(layout), "dim")
+ )
+ table.add_row(icon, text)
+ _summary = table
+ return _summary
+
+ layout = self
+ tree = Tree(
+ summary(layout),
+ guide_style=f"layout.tree.{layout.splitter.name}",
+ highlight=True,
+ )
+
+ def recurse(tree: "Tree", layout: "Layout") -> None:
+ for child in layout._children:
+ recurse(
+ tree.add(
+ summary(child),
+ guide_style=f"layout.tree.{child.splitter.name}",
+ ),
+ child,
+ )
+
+ recurse(tree, self)
+ return tree
+
+ def split(
+ self,
+ *layouts: Union["Layout", RenderableType],
+ splitter: Union[Splitter, str] = "column",
+ ) -> None:
+ """Split the layout in to multiple sub-layouts.
+
+ Args:
+ *layouts (Layout): Positional arguments should be (sub) Layout instances.
+ splitter (Union[Splitter, str]): Splitter instance or name of splitter.
+ """
+ _layouts = [
+ layout if isinstance(layout, Layout) else Layout(layout)
+ for layout in layouts
+ ]
+ try:
+ self.splitter = (
+ splitter
+ if isinstance(splitter, Splitter)
+ else self.splitters[splitter]()
+ )
+ except KeyError:
+ raise NoSplitter(f"No splitter called {splitter!r}")
+ self._children[:] = _layouts
+
+ def add_split(self, *layouts: Union["Layout", RenderableType]) -> None:
+ """Add a new layout(s) to existing split.
+
+ Args:
+ *layouts (Union[Layout, RenderableType]): Positional arguments should be renderables or (sub) Layout instances.
+
+ """
+ _layouts = (
+ layout if isinstance(layout, Layout) else Layout(layout)
+ for layout in layouts
+ )
+ self._children.extend(_layouts)
+
+ def split_row(self, *layouts: Union["Layout", RenderableType]) -> None:
+ """Split the layout in to a row (layouts side by side).
+
+ Args:
+ *layouts (Layout): Positional arguments should be (sub) Layout instances.
+ """
+ self.split(*layouts, splitter="row")
+
+ def split_column(self, *layouts: Union["Layout", RenderableType]) -> None:
+ """Split the layout in to a column (layouts stacked on top of each other).
+
+ Args:
+ *layouts (Layout): Positional arguments should be (sub) Layout instances.
+ """
+ self.split(*layouts, splitter="column")
+
+ def unsplit(self) -> None:
+ """Reset splits to initial state."""
+ del self._children[:]
+
+ def update(self, renderable: RenderableType) -> None:
+ """Update renderable.
+
+ Args:
+ renderable (RenderableType): New renderable object.
+ """
+ with self._lock:
+ self._renderable = renderable
+
+ def refresh_screen(self, console: "Console", layout_name: str) -> None:
+ """Refresh a sub-layout.
+
+ Args:
+ console (Console): Console instance where Layout is to be rendered.
+ layout_name (str): Name of layout.
+ """
+ with self._lock:
+ layout = self[layout_name]
+ region, _lines = self._render_map[layout]
+ (x, y, width, height) = region
+ lines = console.render_lines(
+ layout, console.options.update_dimensions(width, height)
+ )
+ self._render_map[layout] = LayoutRender(region, lines)
+ console.update_screen_lines(lines, x, y)
+
+ def _make_region_map(self, width: int, height: int) -> RegionMap:
+ """Create a dict that maps layout on to Region."""
+ stack: List[Tuple[Layout, Region]] = [(self, Region(0, 0, width, height))]
+ push = stack.append
+ pop = stack.pop
+ layout_regions: List[Tuple[Layout, Region]] = []
+ append_layout_region = layout_regions.append
+ while stack:
+ append_layout_region(pop())
+ layout, region = layout_regions[-1]
+ children = layout.children
+ if children:
+ for child_and_region in layout.splitter.divide(children, region):
+ push(child_and_region)
+
+ region_map = {
+ layout: region
+ for layout, region in sorted(layout_regions, key=itemgetter(1))
+ }
+ return region_map
+
+ def render(self, console: Console, options: ConsoleOptions) -> RenderMap:
+ """Render the sub_layouts.
+
+ Args:
+ console (Console): Console instance.
+ options (ConsoleOptions): Console options.
+
+ Returns:
+ RenderMap: A dict that maps Layout on to a tuple of Region, lines
+ """
+ render_width = options.max_width
+ render_height = options.height or console.height
+ region_map = self._make_region_map(render_width, render_height)
+ layout_regions = [
+ (layout, region)
+ for layout, region in region_map.items()
+ if not layout.children
+ ]
+ render_map: Dict["Layout", "LayoutRender"] = {}
+ render_lines = console.render_lines
+ update_dimensions = options.update_dimensions
+
+ for layout, region in layout_regions:
+ lines = render_lines(
+ layout.renderable, update_dimensions(region.width, region.height)
+ )
+ render_map[layout] = LayoutRender(region, lines)
+ return render_map
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ with self._lock:
+ width = options.max_width or console.width
+ height = options.height or console.height
+ render_map = self.render(console, options.update_dimensions(width, height))
+ self._render_map = render_map
+ layout_lines: List[List[Segment]] = [[] for _ in range(height)]
+ _islice = islice
+ for (region, lines) in render_map.values():
+ _x, y, _layout_width, layout_height = region
+ for row, line in zip(
+ _islice(layout_lines, y, y + layout_height), lines
+ ):
+ row.extend(line)
+
+ new_line = Segment.line()
+ for layout_row in layout_lines:
+ yield from layout_row
+ yield new_line
+
+
+if __name__ == "__main__":
+ from pip._vendor.rich.console import Console
+
+ console = Console()
+ layout = Layout()
+
+ layout.split_column(
+ Layout(name="header", size=3),
+ Layout(ratio=1, name="main"),
+ Layout(size=10, name="footer"),
+ )
+
+ layout["main"].split_row(Layout(name="side"), Layout(name="body", ratio=2))
+
+ layout["body"].split_row(Layout(name="content", ratio=2), Layout(name="s2"))
+
+ layout["s2"].split_column(
+ Layout(name="top"), Layout(name="middle"), Layout(name="bottom")
+ )
+
+ layout["side"].split_column(Layout(layout.tree, name="left1"), Layout(name="left2"))
+
+ layout["content"].update("foo")
+
+ console.print(layout)
diff --git a/src/pip/_vendor/rich/live.py b/src/pip/_vendor/rich/live.py
new file mode 100644
index 000000000..e635fe5c9
--- /dev/null
+++ b/src/pip/_vendor/rich/live.py
@@ -0,0 +1,373 @@
+import sys
+from threading import Event, RLock, Thread
+from types import TracebackType
+from typing import IO, Any, Callable, List, Optional, TextIO, Type, cast
+
+from . import get_console
+from .console import Console, ConsoleRenderable, RenderableType, RenderHook
+from .control import Control
+from .file_proxy import FileProxy
+from .jupyter import JupyterMixin
+from .live_render import LiveRender, VerticalOverflowMethod
+from .screen import Screen
+from .text import Text
+
+
+class _RefreshThread(Thread):
+ """A thread that calls refresh() at regular intervals."""
+
+ def __init__(self, live: "Live", refresh_per_second: float) -> None:
+ self.live = live
+ self.refresh_per_second = refresh_per_second
+ self.done = Event()
+ super().__init__(daemon=True)
+
+ def stop(self) -> None:
+ self.done.set()
+
+ def run(self) -> None:
+ while not self.done.wait(1 / self.refresh_per_second):
+ with self.live._lock:
+ if not self.done.is_set():
+ self.live.refresh()
+
+
+class Live(JupyterMixin, RenderHook):
+ """Renders an auto-updating live display of any given renderable.
+
+ Args:
+ renderable (RenderableType, optional): The renderable to live display. Defaults to displaying nothing.
+ console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout.
+ screen (bool, optional): Enable alternate screen mode. Defaults to False.
+ auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()` or `update()` with refresh flag. Defaults to True
+ refresh_per_second (float, optional): Number of times per second to refresh the live display. Defaults to 4.
+ transient (bool, optional): Clear the renderable on exit (has no effect when screen=True). Defaults to False.
+ redirect_stdout (bool, optional): Enable redirection of stdout, so ``print`` may be used. Defaults to True.
+ redirect_stderr (bool, optional): Enable redirection of stderr. Defaults to True.
+ vertical_overflow (VerticalOverflowMethod, optional): How to handle renderable when it is too tall for the console. Defaults to "ellipsis".
+ get_renderable (Callable[[], RenderableType], optional): Optional callable to get renderable. Defaults to None.
+ """
+
+ def __init__(
+ self,
+ renderable: Optional[RenderableType] = None,
+ *,
+ console: Optional[Console] = None,
+ screen: bool = False,
+ auto_refresh: bool = True,
+ refresh_per_second: float = 4,
+ transient: bool = False,
+ redirect_stdout: bool = True,
+ redirect_stderr: bool = True,
+ vertical_overflow: VerticalOverflowMethod = "ellipsis",
+ get_renderable: Optional[Callable[[], RenderableType]] = None,
+ ) -> None:
+ assert refresh_per_second > 0, "refresh_per_second must be > 0"
+ self._renderable = renderable
+ self.console = console if console is not None else get_console()
+ self._screen = screen
+ self._alt_screen = False
+
+ self._redirect_stdout = redirect_stdout
+ self._redirect_stderr = redirect_stderr
+ self._restore_stdout: Optional[IO[str]] = None
+ self._restore_stderr: Optional[IO[str]] = None
+
+ self._lock = RLock()
+ self.ipy_widget: Optional[Any] = None
+ self.auto_refresh = auto_refresh
+ self._started: bool = False
+ self.transient = True if screen else transient
+
+ self._refresh_thread: Optional[_RefreshThread] = None
+ self.refresh_per_second = refresh_per_second
+
+ self.vertical_overflow = vertical_overflow
+ self._get_renderable = get_renderable
+ self._live_render = LiveRender(
+ self.get_renderable(), vertical_overflow=vertical_overflow
+ )
+
+ @property
+ def is_started(self) -> bool:
+ """Check if live display has been started."""
+ return self._started
+
+ def get_renderable(self) -> RenderableType:
+ renderable = (
+ self._get_renderable()
+ if self._get_renderable is not None
+ else self._renderable
+ )
+ return renderable or ""
+
+ def start(self, refresh: bool = False) -> None:
+ """Start live rendering display.
+
+ Args:
+ refresh (bool, optional): Also refresh. Defaults to False.
+ """
+ with self._lock:
+ if self._started:
+ return
+ self.console.set_live(self)
+ self._started = True
+ if self._screen:
+ self._alt_screen = self.console.set_alt_screen(True)
+ self.console.show_cursor(False)
+ self._enable_redirect_io()
+ self.console.push_render_hook(self)
+ if refresh:
+ try:
+ self.refresh()
+ except Exception:
+ # If refresh fails, we want to stop the redirection of sys.stderr,
+ # so the error stacktrace is properly displayed in the terminal.
+ # (or, if the code that calls Rich captures the exception and wants to display something,
+ # let this be displayed in the terminal).
+ self.stop()
+ raise
+ if self.auto_refresh:
+ self._refresh_thread = _RefreshThread(self, self.refresh_per_second)
+ self._refresh_thread.start()
+
+ def stop(self) -> None:
+ """Stop live rendering display."""
+ with self._lock:
+ if not self._started:
+ return
+ self.console.clear_live()
+ self._started = False
+
+ if self.auto_refresh and self._refresh_thread is not None:
+ self._refresh_thread.stop()
+ self._refresh_thread = None
+ # allow it to fully render on the last even if overflow
+ self.vertical_overflow = "visible"
+ with self.console:
+ try:
+ if not self._alt_screen and not self.console.is_jupyter:
+ self.refresh()
+ finally:
+ self._disable_redirect_io()
+ self.console.pop_render_hook()
+ if not self._alt_screen and self.console.is_terminal:
+ self.console.line()
+ self.console.show_cursor(True)
+ if self._alt_screen:
+ self.console.set_alt_screen(False)
+
+ if self.transient and not self._alt_screen:
+ self.console.control(self._live_render.restore_cursor())
+ if self.ipy_widget is not None and self.transient:
+ self.ipy_widget.close() # pragma: no cover
+
+ def __enter__(self) -> "Live":
+ self.start(refresh=self._renderable is not None)
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ self.stop()
+
+ def _enable_redirect_io(self) -> None:
+ """Enable redirecting of stdout / stderr."""
+ if self.console.is_terminal or self.console.is_jupyter:
+ if self._redirect_stdout and not isinstance(sys.stdout, FileProxy):
+ self._restore_stdout = sys.stdout
+ sys.stdout = cast("TextIO", FileProxy(self.console, sys.stdout))
+ if self._redirect_stderr and not isinstance(sys.stderr, FileProxy):
+ self._restore_stderr = sys.stderr
+ sys.stderr = cast("TextIO", FileProxy(self.console, sys.stderr))
+
+ def _disable_redirect_io(self) -> None:
+ """Disable redirecting of stdout / stderr."""
+ if self._restore_stdout:
+ sys.stdout = cast("TextIO", self._restore_stdout)
+ self._restore_stdout = None
+ if self._restore_stderr:
+ sys.stderr = cast("TextIO", self._restore_stderr)
+ self._restore_stderr = None
+
+ @property
+ def renderable(self) -> RenderableType:
+ """Get the renderable that is being displayed
+
+ Returns:
+ RenderableType: Displayed renderable.
+ """
+ renderable = self.get_renderable()
+ return Screen(renderable) if self._alt_screen else renderable
+
+ def update(self, renderable: RenderableType, *, refresh: bool = False) -> None:
+ """Update the renderable that is being displayed
+
+ Args:
+ renderable (RenderableType): New renderable to use.
+ refresh (bool, optional): Refresh the display. Defaults to False.
+ """
+ with self._lock:
+ self._renderable = renderable
+ if refresh:
+ self.refresh()
+
+ def refresh(self) -> None:
+ """Update the display of the Live Render."""
+ with self._lock:
+ self._live_render.set_renderable(self.renderable)
+ if self.console.is_jupyter: # pragma: no cover
+ try:
+ from IPython.display import display
+ from ipywidgets import Output
+ except ImportError:
+ import warnings
+
+ warnings.warn('install "ipywidgets" for Jupyter support')
+ else:
+ if self.ipy_widget is None:
+ self.ipy_widget = Output()
+ display(self.ipy_widget)
+
+ with self.ipy_widget:
+ self.ipy_widget.clear_output(wait=True)
+ self.console.print(self._live_render.renderable)
+ elif self.console.is_terminal and not self.console.is_dumb_terminal:
+ with self.console:
+ self.console.print(Control())
+ elif (
+ not self._started and not self.transient
+ ): # if it is finished allow files or dumb-terminals to see final result
+ with self.console:
+ self.console.print(Control())
+
+ def process_renderables(
+ self, renderables: List[ConsoleRenderable]
+ ) -> List[ConsoleRenderable]:
+ """Process renderables to restore cursor and display progress."""
+ self._live_render.vertical_overflow = self.vertical_overflow
+ if self.console.is_interactive:
+ # lock needs acquiring as user can modify live_render renderable at any time unlike in Progress.
+ with self._lock:
+ reset = (
+ Control.home()
+ if self._alt_screen
+ else self._live_render.position_cursor()
+ )
+ renderables = [reset, *renderables, self._live_render]
+ elif (
+ not self._started and not self.transient
+ ): # if it is finished render the final output for files or dumb_terminals
+ renderables = [*renderables, self._live_render]
+
+ return renderables
+
+
+if __name__ == "__main__": # pragma: no cover
+ import random
+ import time
+ from itertools import cycle
+ from typing import Dict, List, Tuple
+
+ from .align import Align
+ from .console import Console
+ from .live import Live as Live
+ from .panel import Panel
+ from .rule import Rule
+ from .syntax import Syntax
+ from .table import Table
+
+ console = Console()
+
+ syntax = Syntax(
+ '''def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
+ """Iterate and generate a tuple with a flag for last value."""
+ iter_values = iter(values)
+ try:
+ previous_value = next(iter_values)
+ except StopIteration:
+ return
+ for value in iter_values:
+ yield False, previous_value
+ previous_value = value
+ yield True, previous_value''',
+ "python",
+ line_numbers=True,
+ )
+
+ table = Table("foo", "bar", "baz")
+ table.add_row("1", "2", "3")
+
+ progress_renderables = [
+ "You can make the terminal shorter and taller to see the live table hide"
+ "Text may be printed while the progress bars are rendering.",
+ Panel("In fact, [i]any[/i] renderable will work"),
+ "Such as [magenta]tables[/]...",
+ table,
+ "Pretty printed structures...",
+ {"type": "example", "text": "Pretty printed"},
+ "Syntax...",
+ syntax,
+ Rule("Give it a try!"),
+ ]
+
+ examples = cycle(progress_renderables)
+
+ exchanges = [
+ "SGD",
+ "MYR",
+ "EUR",
+ "USD",
+ "AUD",
+ "JPY",
+ "CNH",
+ "HKD",
+ "CAD",
+ "INR",
+ "DKK",
+ "GBP",
+ "RUB",
+ "NZD",
+ "MXN",
+ "IDR",
+ "TWD",
+ "THB",
+ "VND",
+ ]
+ with Live(console=console) as live_table:
+ exchange_rate_dict: Dict[Tuple[str, str], float] = {}
+
+ for index in range(100):
+ select_exchange = exchanges[index % len(exchanges)]
+
+ for exchange in exchanges:
+ if exchange == select_exchange:
+ continue
+ time.sleep(0.4)
+ if random.randint(0, 10) < 1:
+ console.log(next(examples))
+ exchange_rate_dict[(select_exchange, exchange)] = 200 / (
+ (random.random() * 320) + 1
+ )
+ if len(exchange_rate_dict) > len(exchanges) - 1:
+ exchange_rate_dict.pop(list(exchange_rate_dict.keys())[0])
+ table = Table(title="Exchange Rates")
+
+ table.add_column("Source Currency")
+ table.add_column("Destination Currency")
+ table.add_column("Exchange Rate")
+
+ for ((source, dest), exchange_rate) in exchange_rate_dict.items():
+ table.add_row(
+ source,
+ dest,
+ Text(
+ f"{exchange_rate:.4f}",
+ style="red" if exchange_rate < 1.0 else "green",
+ ),
+ )
+
+ live_table.update(Align.center(table))
diff --git a/src/pip/_vendor/rich/live_render.py b/src/pip/_vendor/rich/live_render.py
new file mode 100644
index 000000000..b90fbf7f3
--- /dev/null
+++ b/src/pip/_vendor/rich/live_render.py
@@ -0,0 +1,113 @@
+import sys
+from typing import Optional, Tuple
+
+if sys.version_info >= (3, 8):
+ from typing import Literal
+else:
+ from pip._vendor.typing_extensions import Literal # pragma: no cover
+
+
+from ._loop import loop_last
+from .console import Console, ConsoleOptions, RenderableType, RenderResult
+from .control import Control
+from .segment import ControlType, Segment
+from .style import StyleType
+from .text import Text
+
+VerticalOverflowMethod = Literal["crop", "ellipsis", "visible"]
+
+
+class LiveRender:
+ """Creates a renderable that may be updated.
+
+ Args:
+ renderable (RenderableType): Any renderable object.
+ style (StyleType, optional): An optional style to apply to the renderable. Defaults to "".
+ """
+
+ def __init__(
+ self,
+ renderable: RenderableType,
+ style: StyleType = "",
+ vertical_overflow: VerticalOverflowMethod = "ellipsis",
+ ) -> None:
+ self.renderable = renderable
+ self.style = style
+ self.vertical_overflow = vertical_overflow
+ self._shape: Optional[Tuple[int, int]] = None
+
+ def set_renderable(self, renderable: RenderableType) -> None:
+ """Set a new renderable.
+
+ Args:
+ renderable (RenderableType): Any renderable object, including str.
+ """
+ self.renderable = renderable
+
+ def position_cursor(self) -> Control:
+ """Get control codes to move cursor to beginning of live render.
+
+ Returns:
+ Control: A control instance that may be printed.
+ """
+ if self._shape is not None:
+ _, height = self._shape
+ return Control(
+ ControlType.CARRIAGE_RETURN,
+ (ControlType.ERASE_IN_LINE, 2),
+ *(
+ (
+ (ControlType.CURSOR_UP, 1),
+ (ControlType.ERASE_IN_LINE, 2),
+ )
+ * (height - 1)
+ )
+ )
+ return Control()
+
+ def restore_cursor(self) -> Control:
+ """Get control codes to clear the render and restore the cursor to its previous position.
+
+ Returns:
+ Control: A Control instance that may be printed.
+ """
+ if self._shape is not None:
+ _, height = self._shape
+ return Control(
+ ControlType.CARRIAGE_RETURN,
+ *((ControlType.CURSOR_UP, 1), (ControlType.ERASE_IN_LINE, 2)) * height
+ )
+ return Control()
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+
+ renderable = self.renderable
+ style = console.get_style(self.style)
+ lines = console.render_lines(renderable, options, style=style, pad=False)
+ shape = Segment.get_shape(lines)
+
+ _, height = shape
+ if height > options.size.height:
+ if self.vertical_overflow == "crop":
+ lines = lines[: options.size.height]
+ shape = Segment.get_shape(lines)
+ elif self.vertical_overflow == "ellipsis":
+ lines = lines[: (options.size.height - 1)]
+ overflow_text = Text(
+ "...",
+ overflow="crop",
+ justify="center",
+ end="",
+ style="live.ellipsis",
+ )
+ lines.append(list(console.render(overflow_text)))
+ shape = Segment.get_shape(lines)
+ self._shape = shape
+
+ new_line = Segment.line()
+ for last, line in loop_last(lines):
+ yield from line
+ if not last:
+ yield new_line
diff --git a/src/pip/_vendor/rich/logging.py b/src/pip/_vendor/rich/logging.py
new file mode 100644
index 000000000..58188fd8a
--- /dev/null
+++ b/src/pip/_vendor/rich/logging.py
@@ -0,0 +1,280 @@
+import logging
+from datetime import datetime
+from logging import Handler, LogRecord
+from pathlib import Path
+from types import ModuleType
+from typing import ClassVar, List, Optional, Iterable, Type, Union
+
+from . import get_console
+from ._log_render import LogRender, FormatTimeCallable
+from .console import Console, ConsoleRenderable
+from .highlighter import Highlighter, ReprHighlighter
+from .text import Text
+from .traceback import Traceback
+
+
+class RichHandler(Handler):
+ """A logging handler that renders output with Rich. The time / level / message and file are displayed in columns.
+ The level is color coded, and the message is syntax highlighted.
+
+ Note:
+ Be careful when enabling console markup in log messages if you have configured logging for libraries not
+ under your control. If a dependency writes messages containing square brackets, it may not produce the intended output.
+
+ Args:
+ level (Union[int, str], optional): Log level. Defaults to logging.NOTSET.
+ console (:class:`~rich.console.Console`, optional): Optional console instance to write logs.
+ Default will use a global console instance writing to stdout.
+ show_time (bool, optional): Show a column for the time. Defaults to True.
+ omit_repeated_times (bool, optional): Omit repetition of the same time. Defaults to True.
+ show_level (bool, optional): Show a column for the level. Defaults to True.
+ show_path (bool, optional): Show the path to the original log call. Defaults to True.
+ enable_link_path (bool, optional): Enable terminal link of path column to file. Defaults to True.
+ highlighter (Highlighter, optional): Highlighter to style log messages, or None to use ReprHighlighter. Defaults to None.
+ markup (bool, optional): Enable console markup in log messages. Defaults to False.
+ rich_tracebacks (bool, optional): Enable rich tracebacks with syntax highlighting and formatting. Defaults to False.
+ tracebacks_width (Optional[int], optional): Number of characters used to render tracebacks, or None for full width. Defaults to None.
+ tracebacks_extra_lines (int, optional): Additional lines of code to render tracebacks, or None for full width. Defaults to None.
+ tracebacks_theme (str, optional): Override pygments theme used in traceback.
+ tracebacks_word_wrap (bool, optional): Enable word wrapping of long tracebacks lines. Defaults to True.
+ tracebacks_show_locals (bool, optional): Enable display of locals in tracebacks. Defaults to False.
+ tracebacks_suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
+ locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to 10.
+ locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
+ log_time_format (Union[str, TimeFormatterCallable], optional): If ``log_time`` is enabled, either string for strftime or callable that formats the time. Defaults to "[%x %X] ".
+ keywords (List[str], optional): List of words to highlight instead of ``RichHandler.KEYWORDS``.
+ """
+
+ KEYWORDS: ClassVar[Optional[List[str]]] = [
+ "GET",
+ "POST",
+ "HEAD",
+ "PUT",
+ "DELETE",
+ "OPTIONS",
+ "TRACE",
+ "PATCH",
+ ]
+ HIGHLIGHTER_CLASS: ClassVar[Type[Highlighter]] = ReprHighlighter
+
+ def __init__(
+ self,
+ level: Union[int, str] = logging.NOTSET,
+ console: Optional[Console] = None,
+ *,
+ show_time: bool = True,
+ omit_repeated_times: bool = True,
+ show_level: bool = True,
+ show_path: bool = True,
+ enable_link_path: bool = True,
+ highlighter: Optional[Highlighter] = None,
+ markup: bool = False,
+ rich_tracebacks: bool = False,
+ tracebacks_width: Optional[int] = None,
+ tracebacks_extra_lines: int = 3,
+ tracebacks_theme: Optional[str] = None,
+ tracebacks_word_wrap: bool = True,
+ tracebacks_show_locals: bool = False,
+ tracebacks_suppress: Iterable[Union[str, ModuleType]] = (),
+ locals_max_length: int = 10,
+ locals_max_string: int = 80,
+ log_time_format: Union[str, FormatTimeCallable] = "[%x %X]",
+ keywords: Optional[List[str]] = None,
+ ) -> None:
+ super().__init__(level=level)
+ self.console = console or get_console()
+ self.highlighter = highlighter or self.HIGHLIGHTER_CLASS()
+ self._log_render = LogRender(
+ show_time=show_time,
+ show_level=show_level,
+ show_path=show_path,
+ time_format=log_time_format,
+ omit_repeated_times=omit_repeated_times,
+ level_width=None,
+ )
+ self.enable_link_path = enable_link_path
+ self.markup = markup
+ self.rich_tracebacks = rich_tracebacks
+ self.tracebacks_width = tracebacks_width
+ self.tracebacks_extra_lines = tracebacks_extra_lines
+ self.tracebacks_theme = tracebacks_theme
+ self.tracebacks_word_wrap = tracebacks_word_wrap
+ self.tracebacks_show_locals = tracebacks_show_locals
+ self.tracebacks_suppress = tracebacks_suppress
+ self.locals_max_length = locals_max_length
+ self.locals_max_string = locals_max_string
+ self.keywords = keywords
+
+ def get_level_text(self, record: LogRecord) -> Text:
+ """Get the level name from the record.
+
+ Args:
+ record (LogRecord): LogRecord instance.
+
+ Returns:
+ Text: A tuple of the style and level name.
+ """
+ level_name = record.levelname
+ level_text = Text.styled(
+ level_name.ljust(8), f"logging.level.{level_name.lower()}"
+ )
+ return level_text
+
+ def emit(self, record: LogRecord) -> None:
+ """Invoked by logging."""
+ message = self.format(record)
+ traceback = None
+ if (
+ self.rich_tracebacks
+ and record.exc_info
+ and record.exc_info != (None, None, None)
+ ):
+ exc_type, exc_value, exc_traceback = record.exc_info
+ assert exc_type is not None
+ assert exc_value is not None
+ traceback = Traceback.from_exception(
+ exc_type,
+ exc_value,
+ exc_traceback,
+ width=self.tracebacks_width,
+ extra_lines=self.tracebacks_extra_lines,
+ theme=self.tracebacks_theme,
+ word_wrap=self.tracebacks_word_wrap,
+ show_locals=self.tracebacks_show_locals,
+ locals_max_length=self.locals_max_length,
+ locals_max_string=self.locals_max_string,
+ suppress=self.tracebacks_suppress,
+ )
+ message = record.getMessage()
+ if self.formatter:
+ record.message = record.getMessage()
+ formatter = self.formatter
+ if hasattr(formatter, "usesTime") and formatter.usesTime():
+ record.asctime = formatter.formatTime(record, formatter.datefmt)
+ message = formatter.formatMessage(record)
+
+ message_renderable = self.render_message(record, message)
+ log_renderable = self.render(
+ record=record, traceback=traceback, message_renderable=message_renderable
+ )
+ try:
+ self.console.print(log_renderable)
+ except Exception:
+ self.handleError(record)
+
+ def render_message(self, record: LogRecord, message: str) -> "ConsoleRenderable":
+ """Render message text in to Text.
+
+ record (LogRecord): logging Record.
+ message (str): String containing log message.
+
+ Returns:
+ ConsoleRenderable: Renderable to display log message.
+ """
+ use_markup = getattr(record, "markup", self.markup)
+ message_text = Text.from_markup(message) if use_markup else Text(message)
+
+ highlighter = getattr(record, "highlighter", self.highlighter)
+ if highlighter:
+ message_text = highlighter(message_text)
+
+ if self.keywords is None:
+ self.keywords = self.KEYWORDS
+
+ if self.keywords:
+ message_text.highlight_words(self.keywords, "logging.keyword")
+
+ return message_text
+
+ def render(
+ self,
+ *,
+ record: LogRecord,
+ traceback: Optional[Traceback],
+ message_renderable: "ConsoleRenderable",
+ ) -> "ConsoleRenderable":
+ """Render log for display.
+
+ Args:
+ record (LogRecord): logging Record.
+ traceback (Optional[Traceback]): Traceback instance or None for no Traceback.
+ message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents.
+
+ Returns:
+ ConsoleRenderable: Renderable to display log.
+ """
+ path = Path(record.pathname).name
+ level = self.get_level_text(record)
+ time_format = None if self.formatter is None else self.formatter.datefmt
+ log_time = datetime.fromtimestamp(record.created)
+
+ log_renderable = self._log_render(
+ self.console,
+ [message_renderable] if not traceback else [message_renderable, traceback],
+ log_time=log_time,
+ time_format=time_format,
+ level=level,
+ path=path,
+ line_no=record.lineno,
+ link_path=record.pathname if self.enable_link_path else None,
+ )
+ return log_renderable
+
+
+if __name__ == "__main__": # pragma: no cover
+ from time import sleep
+
+ FORMAT = "%(message)s"
+ # FORMAT = "%(asctime)-15s - %(levelname)s - %(message)s"
+ logging.basicConfig(
+ level="NOTSET",
+ format=FORMAT,
+ datefmt="[%X]",
+ handlers=[RichHandler(rich_tracebacks=True, tracebacks_show_locals=True)],
+ )
+ log = logging.getLogger("rich")
+
+ log.info("Server starting...")
+ log.info("Listening on http://127.0.0.1:8080")
+ sleep(1)
+
+ log.info("GET /index.html 200 1298")
+ log.info("GET /imgs/backgrounds/back1.jpg 200 54386")
+ log.info("GET /css/styles.css 200 54386")
+ log.warning("GET /favicon.ico 404 242")
+ sleep(1)
+
+ log.debug(
+ "JSONRPC request\n--> %r\n<-- %r",
+ {
+ "version": "1.1",
+ "method": "confirmFruitPurchase",
+ "params": [["apple", "orange", "mangoes", "pomelo"], 1.123],
+ "id": "194521489",
+ },
+ {"version": "1.1", "result": True, "error": None, "id": "194521489"},
+ )
+ log.debug(
+ "Loading configuration file /adasd/asdasd/qeqwe/qwrqwrqwr/sdgsdgsdg/werwerwer/dfgerert/ertertert/ertetert/werwerwer"
+ )
+ log.error("Unable to find 'pomelo' in database!")
+ log.info("POST /jsonrpc/ 200 65532")
+ log.info("POST /admin/ 401 42234")
+ log.warning("password was rejected for admin site.")
+
+ def divide() -> None:
+ number = 1
+ divisor = 0
+ foos = ["foo"] * 100
+ log.debug("in divide")
+ try:
+ number / divisor
+ except:
+ log.exception("An error of some kind occurred!")
+
+ divide()
+ sleep(1)
+ log.critical("Out of memory!")
+ log.info("Server exited with code=-1")
+ log.info("[bold]EXITING...[/bold]", extra=dict(markup=True))
diff --git a/src/pip/_vendor/rich/markup.py b/src/pip/_vendor/rich/markup.py
new file mode 100644
index 000000000..fd80d8c11
--- /dev/null
+++ b/src/pip/_vendor/rich/markup.py
@@ -0,0 +1,246 @@
+import re
+from ast import literal_eval
+from operator import attrgetter
+from typing import Callable, Iterable, List, Match, NamedTuple, Optional, Tuple, Union
+
+from ._emoji_replace import _emoji_replace
+from .emoji import EmojiVariant
+from .errors import MarkupError
+from .style import Style
+from .text import Span, Text
+
+RE_TAGS = re.compile(
+ r"""((\\*)\[([a-z#/@][^[]*?)])""",
+ re.VERBOSE,
+)
+
+RE_HANDLER = re.compile(r"^([\w.]*?)(\(.*?\))?$")
+
+
+class Tag(NamedTuple):
+ """A tag in console markup."""
+
+ name: str
+ """The tag name. e.g. 'bold'."""
+ parameters: Optional[str]
+ """Any additional parameters after the name."""
+
+ def __str__(self) -> str:
+ return (
+ self.name if self.parameters is None else f"{self.name} {self.parameters}"
+ )
+
+ @property
+ def markup(self) -> str:
+ """Get the string representation of this tag."""
+ return (
+ f"[{self.name}]"
+ if self.parameters is None
+ else f"[{self.name}={self.parameters}]"
+ )
+
+
+_ReStringMatch = Match[str] # regex match object
+_ReSubCallable = Callable[[_ReStringMatch], str] # Callable invoked by re.sub
+_EscapeSubMethod = Callable[[_ReSubCallable, str], str] # Sub method of a compiled re
+
+
+def escape(
+ markup: str,
+ _escape: _EscapeSubMethod = re.compile(r"(\\*)(\[[a-z#/@][^[]*?])").sub,
+) -> str:
+ """Escapes text so that it won't be interpreted as markup.
+
+ Args:
+ markup (str): Content to be inserted in to markup.
+
+ Returns:
+ str: Markup with square brackets escaped.
+ """
+
+ def escape_backslashes(match: Match[str]) -> str:
+ """Called by re.sub replace matches."""
+ backslashes, text = match.groups()
+ return f"{backslashes}{backslashes}\\{text}"
+
+ markup = _escape(escape_backslashes, markup)
+ return markup
+
+
+def _parse(markup: str) -> Iterable[Tuple[int, Optional[str], Optional[Tag]]]:
+ """Parse markup in to an iterable of tuples of (position, text, tag).
+
+ Args:
+ markup (str): A string containing console markup
+
+ """
+ position = 0
+ _divmod = divmod
+ _Tag = Tag
+ for match in RE_TAGS.finditer(markup):
+ full_text, escapes, tag_text = match.groups()
+ start, end = match.span()
+ if start > position:
+ yield start, markup[position:start], None
+ if escapes:
+ backslashes, escaped = _divmod(len(escapes), 2)
+ if backslashes:
+ # Literal backslashes
+ yield start, "\\" * backslashes, None
+ start += backslashes * 2
+ if escaped:
+ # Escape of tag
+ yield start, full_text[len(escapes) :], None
+ position = end
+ continue
+ text, equals, parameters = tag_text.partition("=")
+ yield start, None, _Tag(text, parameters if equals else None)
+ position = end
+ if position < len(markup):
+ yield position, markup[position:], None
+
+
+def render(
+ markup: str,
+ style: Union[str, Style] = "",
+ emoji: bool = True,
+ emoji_variant: Optional[EmojiVariant] = None,
+) -> Text:
+ """Render console markup in to a Text instance.
+
+ Args:
+ markup (str): A string containing console markup.
+ emoji (bool, optional): Also render emoji code. Defaults to True.
+
+ Raises:
+ MarkupError: If there is a syntax error in the markup.
+
+ Returns:
+ Text: A test instance.
+ """
+ emoji_replace = _emoji_replace
+ if "[" not in markup:
+ return Text(
+ emoji_replace(markup, default_variant=emoji_variant) if emoji else markup,
+ style=style,
+ )
+ text = Text(style=style)
+ append = text.append
+ normalize = Style.normalize
+
+ style_stack: List[Tuple[int, Tag]] = []
+ pop = style_stack.pop
+
+ spans: List[Span] = []
+ append_span = spans.append
+
+ _Span = Span
+ _Tag = Tag
+
+ def pop_style(style_name: str) -> Tuple[int, Tag]:
+ """Pop tag matching given style name."""
+ for index, (_, tag) in enumerate(reversed(style_stack), 1):
+ if tag.name == style_name:
+ return pop(-index)
+ raise KeyError(style_name)
+
+ for position, plain_text, tag in _parse(markup):
+ if plain_text is not None:
+ # Handle open brace escapes, where the brace is not part of a tag.
+ plain_text = plain_text.replace("\\[", "[")
+ append(emoji_replace(plain_text) if emoji else plain_text)
+ elif tag is not None:
+ if tag.name.startswith("/"): # Closing tag
+ style_name = tag.name[1:].strip()
+
+ if style_name: # explicit close
+ style_name = normalize(style_name)
+ try:
+ start, open_tag = pop_style(style_name)
+ except KeyError:
+ raise MarkupError(
+ f"closing tag '{tag.markup}' at position {position} doesn't match any open tag"
+ ) from None
+ else: # implicit close
+ try:
+ start, open_tag = pop()
+ except IndexError:
+ raise MarkupError(
+ f"closing tag '[/]' at position {position} has nothing to close"
+ ) from None
+
+ if open_tag.name.startswith("@"):
+ if open_tag.parameters:
+ handler_name = ""
+ parameters = open_tag.parameters.strip()
+ handler_match = RE_HANDLER.match(parameters)
+ if handler_match is not None:
+ handler_name, match_parameters = handler_match.groups()
+ parameters = (
+ "()" if match_parameters is None else match_parameters
+ )
+
+ try:
+ meta_params = literal_eval(parameters)
+ except SyntaxError as error:
+ raise MarkupError(
+ f"error parsing {parameters!r} in {open_tag.parameters!r}; {error.msg}"
+ )
+ except Exception as error:
+ raise MarkupError(
+ f"error parsing {open_tag.parameters!r}; {error}"
+ ) from None
+
+ if handler_name:
+ meta_params = (
+ handler_name,
+ meta_params
+ if isinstance(meta_params, tuple)
+ else (meta_params,),
+ )
+
+ else:
+ meta_params = ()
+
+ append_span(
+ _Span(
+ start, len(text), Style(meta={open_tag.name: meta_params})
+ )
+ )
+ else:
+ append_span(_Span(start, len(text), str(open_tag)))
+
+ else: # Opening tag
+ normalized_tag = _Tag(normalize(tag.name), tag.parameters)
+ style_stack.append((len(text), normalized_tag))
+
+ text_length = len(text)
+ while style_stack:
+ start, tag = style_stack.pop()
+ style = str(tag)
+ if style:
+ append_span(_Span(start, text_length, style))
+
+ text.spans = sorted(spans[::-1], key=attrgetter("start"))
+ return text
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ MARKUP = [
+ "[red]Hello World[/red]",
+ "[magenta]Hello [b]World[/b]",
+ "[bold]Bold[italic] bold and italic [/bold]italic[/italic]",
+ "Click [link=https://www.willmcgugan.com]here[/link] to visit my Blog",
+ ":warning-emoji: [bold red blink] DANGER![/]",
+ ]
+
+ from pip._vendor.rich import print
+ from pip._vendor.rich.table import Table
+
+ grid = Table("Markup", "Result", padding=(0, 1))
+
+ for markup in MARKUP:
+ grid.add_row(Text(markup), markup)
+
+ print(grid)
diff --git a/src/pip/_vendor/rich/measure.py b/src/pip/_vendor/rich/measure.py
new file mode 100644
index 000000000..a508ffa80
--- /dev/null
+++ b/src/pip/_vendor/rich/measure.py
@@ -0,0 +1,151 @@
+from operator import itemgetter
+from typing import TYPE_CHECKING, Callable, NamedTuple, Optional, Sequence
+
+from . import errors
+from .protocol import is_renderable, rich_cast
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderableType
+
+
+class Measurement(NamedTuple):
+ """Stores the minimum and maximum widths (in characters) required to render an object."""
+
+ minimum: int
+ """Minimum number of cells required to render."""
+ maximum: int
+ """Maximum number of cells required to render."""
+
+ @property
+ def span(self) -> int:
+ """Get difference between maximum and minimum."""
+ return self.maximum - self.minimum
+
+ def normalize(self) -> "Measurement":
+ """Get measurement that ensures that minimum <= maximum and minimum >= 0
+
+ Returns:
+ Measurement: A normalized measurement.
+ """
+ minimum, maximum = self
+ minimum = min(max(0, minimum), maximum)
+ return Measurement(max(0, minimum), max(0, max(minimum, maximum)))
+
+ def with_maximum(self, width: int) -> "Measurement":
+ """Get a RenderableWith where the widths are <= width.
+
+ Args:
+ width (int): Maximum desired width.
+
+ Returns:
+ Measurement: New Measurement object.
+ """
+ minimum, maximum = self
+ return Measurement(min(minimum, width), min(maximum, width))
+
+ def with_minimum(self, width: int) -> "Measurement":
+ """Get a RenderableWith where the widths are >= width.
+
+ Args:
+ width (int): Minimum desired width.
+
+ Returns:
+ Measurement: New Measurement object.
+ """
+ minimum, maximum = self
+ width = max(0, width)
+ return Measurement(max(minimum, width), max(maximum, width))
+
+ def clamp(
+ self, min_width: Optional[int] = None, max_width: Optional[int] = None
+ ) -> "Measurement":
+ """Clamp a measurement within the specified range.
+
+ Args:
+ min_width (int): Minimum desired width, or ``None`` for no minimum. Defaults to None.
+ max_width (int): Maximum desired width, or ``None`` for no maximum. Defaults to None.
+
+ Returns:
+ Measurement: New Measurement object.
+ """
+ measurement = self
+ if min_width is not None:
+ measurement = measurement.with_minimum(min_width)
+ if max_width is not None:
+ measurement = measurement.with_maximum(max_width)
+ return measurement
+
+ @classmethod
+ def get(
+ cls, console: "Console", options: "ConsoleOptions", renderable: "RenderableType"
+ ) -> "Measurement":
+ """Get a measurement for a renderable.
+
+ Args:
+ console (~rich.console.Console): Console instance.
+ options (~rich.console.ConsoleOptions): Console options.
+ renderable (RenderableType): An object that may be rendered with Rich.
+
+ Raises:
+ errors.NotRenderableError: If the object is not renderable.
+
+ Returns:
+ Measurement: Measurement object containing range of character widths required to render the object.
+ """
+ _max_width = options.max_width
+ if _max_width < 1:
+ return Measurement(0, 0)
+ if isinstance(renderable, str):
+ renderable = console.render_str(
+ renderable, markup=options.markup, highlight=False
+ )
+ renderable = rich_cast(renderable)
+ if is_renderable(renderable):
+ get_console_width: Optional[
+ Callable[["Console", "ConsoleOptions"], "Measurement"]
+ ] = getattr(renderable, "__rich_measure__", None)
+ if get_console_width is not None:
+ render_width = (
+ get_console_width(console, options)
+ .normalize()
+ .with_maximum(_max_width)
+ )
+ if render_width.maximum < 1:
+ return Measurement(0, 0)
+ return render_width.normalize()
+ else:
+ return Measurement(0, _max_width)
+ else:
+ raise errors.NotRenderableError(
+ f"Unable to get render width for {renderable!r}; "
+ "a str, Segment, or object with __rich_console__ method is required"
+ )
+
+
+def measure_renderables(
+ console: "Console",
+ options: "ConsoleOptions",
+ renderables: Sequence["RenderableType"],
+) -> "Measurement":
+ """Get a measurement that would fit a number of renderables.
+
+ Args:
+ console (~rich.console.Console): Console instance.
+ options (~rich.console.ConsoleOptions): Console options.
+ renderables (Iterable[RenderableType]): One or more renderable objects.
+
+ Returns:
+ Measurement: Measurement object containing range of character widths required to
+ contain all given renderables.
+ """
+ if not renderables:
+ return Measurement(0, 0)
+ get_measurement = Measurement.get
+ measurements = [
+ get_measurement(console, options, renderable) for renderable in renderables
+ ]
+ measured_width = Measurement(
+ max(measurements, key=itemgetter(0)).minimum,
+ max(measurements, key=itemgetter(1)).maximum,
+ )
+ return measured_width
diff --git a/src/pip/_vendor/rich/padding.py b/src/pip/_vendor/rich/padding.py
new file mode 100644
index 000000000..1b2204f59
--- /dev/null
+++ b/src/pip/_vendor/rich/padding.py
@@ -0,0 +1,141 @@
+from typing import cast, List, Optional, Tuple, TYPE_CHECKING, Union
+
+if TYPE_CHECKING:
+ from .console import (
+ Console,
+ ConsoleOptions,
+ RenderableType,
+ RenderResult,
+ )
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .style import Style
+from .segment import Segment
+
+
+PaddingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]]
+
+
+class Padding(JupyterMixin):
+ """Draw space around content.
+
+ Example:
+ >>> print(Padding("Hello", (2, 4), style="on blue"))
+
+ Args:
+ renderable (RenderableType): String or other renderable.
+ pad (Union[int, Tuple[int]]): Padding for top, right, bottom, and left borders.
+ May be specified with 1, 2, or 4 integers (CSS style).
+ style (Union[str, Style], optional): Style for padding characters. Defaults to "none".
+ expand (bool, optional): Expand padding to fit available width. Defaults to True.
+ """
+
+ def __init__(
+ self,
+ renderable: "RenderableType",
+ pad: "PaddingDimensions" = (0, 0, 0, 0),
+ *,
+ style: Union[str, Style] = "none",
+ expand: bool = True,
+ ):
+ self.renderable = renderable
+ self.top, self.right, self.bottom, self.left = self.unpack(pad)
+ self.style = style
+ self.expand = expand
+
+ @classmethod
+ def indent(cls, renderable: "RenderableType", level: int) -> "Padding":
+ """Make padding instance to render an indent.
+
+ Args:
+ renderable (RenderableType): String or other renderable.
+ level (int): Number of characters to indent.
+
+ Returns:
+ Padding: A Padding instance.
+ """
+
+ return Padding(renderable, pad=(0, 0, 0, level), expand=False)
+
+ @staticmethod
+ def unpack(pad: "PaddingDimensions") -> Tuple[int, int, int, int]:
+ """Unpack padding specified in CSS style."""
+ if isinstance(pad, int):
+ return (pad, pad, pad, pad)
+ if len(pad) == 1:
+ _pad = pad[0]
+ return (_pad, _pad, _pad, _pad)
+ if len(pad) == 2:
+ pad_top, pad_right = cast(Tuple[int, int], pad)
+ return (pad_top, pad_right, pad_top, pad_right)
+ if len(pad) == 4:
+ top, right, bottom, left = cast(Tuple[int, int, int, int], pad)
+ return (top, right, bottom, left)
+ raise ValueError(f"1, 2 or 4 integers required for padding; {len(pad)} given")
+
+ def __repr__(self) -> str:
+ return f"Padding({self.renderable!r}, ({self.top},{self.right},{self.bottom},{self.left}))"
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ style = console.get_style(self.style)
+ if self.expand:
+ width = options.max_width
+ else:
+ width = min(
+ Measurement.get(console, options, self.renderable).maximum
+ + self.left
+ + self.right,
+ options.max_width,
+ )
+ render_options = options.update_width(width - self.left - self.right)
+ if render_options.height is not None:
+ render_options = render_options.update_height(
+ height=render_options.height - self.top - self.bottom
+ )
+ lines = console.render_lines(
+ self.renderable, render_options, style=style, pad=True
+ )
+ _Segment = Segment
+
+ left = _Segment(" " * self.left, style) if self.left else None
+ right = (
+ [_Segment(f'{" " * self.right}', style), _Segment.line()]
+ if self.right
+ else [_Segment.line()]
+ )
+ blank_line: Optional[List[Segment]] = None
+ if self.top:
+ blank_line = [_Segment(f'{" " * width}\n', style)]
+ yield from blank_line * self.top
+ if left:
+ for line in lines:
+ yield left
+ yield from line
+ yield from right
+ else:
+ for line in lines:
+ yield from line
+ yield from right
+ if self.bottom:
+ blank_line = blank_line or [_Segment(f'{" " * width}\n', style)]
+ yield from blank_line * self.bottom
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "Measurement":
+ max_width = options.max_width
+ extra_width = self.left + self.right
+ if max_width - extra_width < 1:
+ return Measurement(max_width, max_width)
+ measure_min, measure_max = Measurement.get(console, options, self.renderable)
+ measurement = Measurement(measure_min + extra_width, measure_max + extra_width)
+ measurement = measurement.with_maximum(max_width)
+ return measurement
+
+
+if __name__ == "__main__": # pragma: no cover
+ from pip._vendor.rich import print
+
+ print(Padding("Hello, World", (2, 4), style="on blue"))
diff --git a/src/pip/_vendor/rich/pager.py b/src/pip/_vendor/rich/pager.py
new file mode 100644
index 000000000..a3f7aa62a
--- /dev/null
+++ b/src/pip/_vendor/rich/pager.py
@@ -0,0 +1,34 @@
+from abc import ABC, abstractmethod
+from typing import Any
+
+
+class Pager(ABC):
+ """Base class for a pager."""
+
+ @abstractmethod
+ def show(self, content: str) -> None:
+ """Show content in pager.
+
+ Args:
+ content (str): Content to be displayed.
+ """
+
+
+class SystemPager(Pager):
+ """Uses the pager installed on the system."""
+
+ def _pager(self, content: str) -> Any: #  pragma: no cover
+ return __import__("pydoc").pager(content)
+
+ def show(self, content: str) -> None:
+ """Use the same pager used by pydoc."""
+ self._pager(content)
+
+
+if __name__ == "__main__": # pragma: no cover
+ from .__main__ import make_test_card
+ from .console import Console
+
+ console = Console()
+ with console.pager(styles=True):
+ console.print(make_test_card())
diff --git a/src/pip/_vendor/rich/palette.py b/src/pip/_vendor/rich/palette.py
new file mode 100644
index 000000000..fa0c4dd40
--- /dev/null
+++ b/src/pip/_vendor/rich/palette.py
@@ -0,0 +1,100 @@
+from math import sqrt
+from functools import lru_cache
+from typing import Sequence, Tuple, TYPE_CHECKING
+
+from .color_triplet import ColorTriplet
+
+if TYPE_CHECKING:
+ from pip._vendor.rich.table import Table
+
+
+class Palette:
+ """A palette of available colors."""
+
+ def __init__(self, colors: Sequence[Tuple[int, int, int]]):
+ self._colors = colors
+
+ def __getitem__(self, number: int) -> ColorTriplet:
+ return ColorTriplet(*self._colors[number])
+
+ def __rich__(self) -> "Table":
+ from pip._vendor.rich.color import Color
+ from pip._vendor.rich.style import Style
+ from pip._vendor.rich.text import Text
+ from pip._vendor.rich.table import Table
+
+ table = Table(
+ "index",
+ "RGB",
+ "Color",
+ title="Palette",
+ caption=f"{len(self._colors)} colors",
+ highlight=True,
+ caption_justify="right",
+ )
+ for index, color in enumerate(self._colors):
+ table.add_row(
+ str(index),
+ repr(color),
+ Text(" " * 16, style=Style(bgcolor=Color.from_rgb(*color))),
+ )
+ return table
+
+ # This is somewhat inefficient and needs caching
+ @lru_cache(maxsize=1024)
+ def match(self, color: Tuple[int, int, int]) -> int:
+ """Find a color from a palette that most closely matches a given color.
+
+ Args:
+ color (Tuple[int, int, int]): RGB components in range 0 > 255.
+
+ Returns:
+ int: Index of closes matching color.
+ """
+ red1, green1, blue1 = color
+ _sqrt = sqrt
+ get_color = self._colors.__getitem__
+
+ def get_color_distance(index: int) -> float:
+ """Get the distance to a color."""
+ red2, green2, blue2 = get_color(index)
+ red_mean = (red1 + red2) // 2
+ red = red1 - red2
+ green = green1 - green2
+ blue = blue1 - blue2
+ return _sqrt(
+ (((512 + red_mean) * red * red) >> 8)
+ + 4 * green * green
+ + (((767 - red_mean) * blue * blue) >> 8)
+ )
+
+ min_index = min(range(len(self._colors)), key=get_color_distance)
+ return min_index
+
+
+if __name__ == "__main__": # pragma: no cover
+ import colorsys
+ from typing import Iterable
+ from pip._vendor.rich.color import Color
+ from pip._vendor.rich.console import Console, ConsoleOptions
+ from pip._vendor.rich.segment import Segment
+ from pip._vendor.rich.style import Style
+
+ class ColorBox:
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> Iterable[Segment]:
+ height = console.size.height - 3
+ for y in range(0, height):
+ for x in range(options.max_width):
+ h = x / options.max_width
+ l = y / (height + 1)
+ r1, g1, b1 = colorsys.hls_to_rgb(h, l, 1.0)
+ r2, g2, b2 = colorsys.hls_to_rgb(h, l + (1 / height / 2), 1.0)
+ bgcolor = Color.from_rgb(r1 * 255, g1 * 255, b1 * 255)
+ color = Color.from_rgb(r2 * 255, g2 * 255, b2 * 255)
+ yield Segment("â–„", Style(color=color, bgcolor=bgcolor))
+ yield Segment.line()
+
+ console = Console()
+ console.print(ColorBox())
diff --git a/src/pip/_vendor/rich/panel.py b/src/pip/_vendor/rich/panel.py
new file mode 100644
index 000000000..fc2807c31
--- /dev/null
+++ b/src/pip/_vendor/rich/panel.py
@@ -0,0 +1,251 @@
+from typing import TYPE_CHECKING, Optional
+
+from .align import AlignMethod
+from .box import ROUNDED, Box
+from .jupyter import JupyterMixin
+from .measure import Measurement, measure_renderables
+from .padding import Padding, PaddingDimensions
+from .segment import Segment
+from .style import StyleType
+from .text import Text, TextType
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderableType, RenderResult
+
+
+class Panel(JupyterMixin):
+ """A console renderable that draws a border around its contents.
+
+ Example:
+ >>> console.print(Panel("Hello, World!"))
+
+ Args:
+ renderable (RenderableType): A console renderable object.
+ box (Box, optional): A Box instance that defines the look of the border (see :ref:`appendix_box`.
+ Defaults to box.ROUNDED.
+ safe_box (bool, optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
+ expand (bool, optional): If True the panel will stretch to fill the console
+ width, otherwise it will be sized to fit the contents. Defaults to True.
+ style (str, optional): The style of the panel (border and contents). Defaults to "none".
+ border_style (str, optional): The style of the border. Defaults to "none".
+ width (Optional[int], optional): Optional width of panel. Defaults to None to auto-detect.
+ height (Optional[int], optional): Optional height of panel. Defaults to None to auto-detect.
+ padding (Optional[PaddingDimensions]): Optional padding around renderable. Defaults to 0.
+ highlight (bool, optional): Enable automatic highlighting of panel title (if str). Defaults to False.
+ """
+
+ def __init__(
+ self,
+ renderable: "RenderableType",
+ box: Box = ROUNDED,
+ *,
+ title: Optional[TextType] = None,
+ title_align: AlignMethod = "center",
+ subtitle: Optional[TextType] = None,
+ subtitle_align: AlignMethod = "center",
+ safe_box: Optional[bool] = None,
+ expand: bool = True,
+ style: StyleType = "none",
+ border_style: StyleType = "none",
+ width: Optional[int] = None,
+ height: Optional[int] = None,
+ padding: PaddingDimensions = (0, 1),
+ highlight: bool = False,
+ ) -> None:
+ self.renderable = renderable
+ self.box = box
+ self.title = title
+ self.title_align: AlignMethod = title_align
+ self.subtitle = subtitle
+ self.subtitle_align = subtitle_align
+ self.safe_box = safe_box
+ self.expand = expand
+ self.style = style
+ self.border_style = border_style
+ self.width = width
+ self.height = height
+ self.padding = padding
+ self.highlight = highlight
+
+ @classmethod
+ def fit(
+ cls,
+ renderable: "RenderableType",
+ box: Box = ROUNDED,
+ *,
+ title: Optional[TextType] = None,
+ title_align: AlignMethod = "center",
+ subtitle: Optional[TextType] = None,
+ subtitle_align: AlignMethod = "center",
+ safe_box: Optional[bool] = None,
+ style: StyleType = "none",
+ border_style: StyleType = "none",
+ width: Optional[int] = None,
+ padding: PaddingDimensions = (0, 1),
+ ) -> "Panel":
+ """An alternative constructor that sets expand=False."""
+ return cls(
+ renderable,
+ box,
+ title=title,
+ title_align=title_align,
+ subtitle=subtitle,
+ subtitle_align=subtitle_align,
+ safe_box=safe_box,
+ style=style,
+ border_style=border_style,
+ width=width,
+ padding=padding,
+ expand=False,
+ )
+
+ @property
+ def _title(self) -> Optional[Text]:
+ if self.title:
+ title_text = (
+ Text.from_markup(self.title)
+ if isinstance(self.title, str)
+ else self.title.copy()
+ )
+ title_text.end = ""
+ title_text.plain = title_text.plain.replace("\n", " ")
+ title_text.no_wrap = True
+ title_text.expand_tabs()
+ title_text.pad(1)
+ return title_text
+ return None
+
+ @property
+ def _subtitle(self) -> Optional[Text]:
+ if self.subtitle:
+ subtitle_text = (
+ Text.from_markup(self.subtitle)
+ if isinstance(self.subtitle, str)
+ else self.subtitle.copy()
+ )
+ subtitle_text.end = ""
+ subtitle_text.plain = subtitle_text.plain.replace("\n", " ")
+ subtitle_text.no_wrap = True
+ subtitle_text.expand_tabs()
+ subtitle_text.pad(1)
+ return subtitle_text
+ return None
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ _padding = Padding.unpack(self.padding)
+ renderable = (
+ Padding(self.renderable, _padding) if any(_padding) else self.renderable
+ )
+ style = console.get_style(self.style)
+ border_style = style + console.get_style(self.border_style)
+ width = (
+ options.max_width
+ if self.width is None
+ else min(options.max_width, self.width)
+ )
+
+ safe_box: bool = console.safe_box if self.safe_box is None else self.safe_box
+ box = self.box.substitute(options, safe=safe_box)
+
+ title_text = self._title
+ if title_text is not None:
+ title_text.style = border_style
+
+ child_width = (
+ width - 2
+ if self.expand
+ else console.measure(
+ renderable, options=options.update_width(width - 2)
+ ).maximum
+ )
+ child_height = self.height or options.height or None
+ if child_height:
+ child_height -= 2
+ if title_text is not None:
+ child_width = min(
+ options.max_width - 2, max(child_width, title_text.cell_len + 2)
+ )
+
+ width = child_width + 2
+ child_options = options.update(
+ width=child_width, height=child_height, highlight=self.highlight
+ )
+ lines = console.render_lines(renderable, child_options, style=style)
+
+ line_start = Segment(box.mid_left, border_style)
+ line_end = Segment(f"{box.mid_right}", border_style)
+ new_line = Segment.line()
+ if title_text is None or width <= 4:
+ yield Segment(box.get_top([width - 2]), border_style)
+ else:
+ title_text.align(self.title_align, width - 4, character=box.top)
+ yield Segment(box.top_left + box.top, border_style)
+ yield from console.render(title_text, child_options.update_width(width - 4))
+ yield Segment(box.top + box.top_right, border_style)
+
+ yield new_line
+ for line in lines:
+ yield line_start
+ yield from line
+ yield line_end
+ yield new_line
+
+ subtitle_text = self._subtitle
+ if subtitle_text is not None:
+ subtitle_text.style = border_style
+
+ if subtitle_text is None or width <= 4:
+ yield Segment(box.get_bottom([width - 2]), border_style)
+ else:
+ subtitle_text.align(self.subtitle_align, width - 4, character=box.bottom)
+ yield Segment(box.bottom_left + box.bottom, border_style)
+ yield from console.render(
+ subtitle_text, child_options.update_width(width - 4)
+ )
+ yield Segment(box.bottom + box.bottom_right, border_style)
+
+ yield new_line
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "Measurement":
+ _title = self._title
+ _, right, _, left = Padding.unpack(self.padding)
+ padding = left + right
+ renderables = [self.renderable, _title] if _title else [self.renderable]
+
+ if self.width is None:
+ width = (
+ measure_renderables(
+ console,
+ options.update_width(options.max_width - padding - 2),
+ renderables,
+ ).maximum
+ + padding
+ + 2
+ )
+ else:
+ width = self.width
+ return Measurement(width, width)
+
+
+if __name__ == "__main__": # pragma: no cover
+ from .console import Console
+
+ c = Console()
+
+ from .box import DOUBLE, ROUNDED
+ from .padding import Padding
+
+ p = Panel(
+ "Hello, World!",
+ title="rich.Panel",
+ style="white on blue",
+ box=DOUBLE,
+ padding=1,
+ )
+
+ c.print()
+ c.print(p)
diff --git a/src/pip/_vendor/rich/pretty.py b/src/pip/_vendor/rich/pretty.py
new file mode 100644
index 000000000..4a5ddaaf7
--- /dev/null
+++ b/src/pip/_vendor/rich/pretty.py
@@ -0,0 +1,1010 @@
+import builtins
+import collections
+import dataclasses
+import inspect
+import os
+import sys
+from array import array
+from collections import Counter, UserDict, UserList, defaultdict, deque
+from dataclasses import dataclass, fields, is_dataclass
+from inspect import isclass
+from itertools import islice
+from types import MappingProxyType
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ DefaultDict,
+ Dict,
+ Iterable,
+ List,
+ Optional,
+ Sequence,
+ Set,
+ Tuple,
+ Union,
+)
+
+from pip._vendor.rich.repr import RichReprResult
+
+try:
+ import attr as _attr_module
+
+ _has_attrs = True
+except ImportError: # pragma: no cover
+ _has_attrs = False
+
+from . import get_console
+from ._loop import loop_last
+from ._pick import pick_bool
+from .abc import RichRenderable
+from .cells import cell_len
+from .highlighter import ReprHighlighter
+from .jupyter import JupyterMixin, JupyterRenderable
+from .measure import Measurement
+from .text import Text
+
+if TYPE_CHECKING:
+ from .console import (
+ Console,
+ ConsoleOptions,
+ HighlighterType,
+ JustifyMethod,
+ OverflowMethod,
+ RenderResult,
+ )
+
+
+JUPYTER_CLASSES_TO_NOT_RENDER = {
+ # Matplotlib "Artists" manage their own rendering in a Jupyter notebook, and we should not try to render them too.
+ # "Typically, all [Matplotlib] visible elements in a figure are subclasses of Artist."
+ "matplotlib.artist.Artist",
+}
+
+
+def _is_attr_object(obj: Any) -> bool:
+ """Check if an object was created with attrs module."""
+ return _has_attrs and _attr_module.has(type(obj))
+
+
+def _get_attr_fields(obj: Any) -> Sequence["_attr_module.Attribute[Any]"]:
+ """Get fields for an attrs object."""
+ return _attr_module.fields(type(obj)) if _has_attrs else []
+
+
+def _is_dataclass_repr(obj: object) -> bool:
+ """Check if an instance of a dataclass contains the default repr.
+
+ Args:
+ obj (object): A dataclass instance.
+
+ Returns:
+ bool: True if the default repr is used, False if there is a custom repr.
+ """
+ # Digging in to a lot of internals here
+ # Catching all exceptions in case something is missing on a non CPython implementation
+ try:
+ return obj.__repr__.__code__.co_filename == dataclasses.__file__
+ except Exception: # pragma: no coverage
+ return False
+
+
+_dummy_namedtuple = collections.namedtuple("_dummy_namedtuple", [])
+
+
+def _has_default_namedtuple_repr(obj: object) -> bool:
+ """Check if an instance of namedtuple contains the default repr
+
+ Args:
+ obj (object): A namedtuple
+
+ Returns:
+ bool: True if the default repr is used, False if there's a custom repr.
+ """
+ obj_file = None
+ try:
+ obj_file = inspect.getfile(obj.__repr__)
+ except (OSError, TypeError):
+ # OSError handles case where object is defined in __main__ scope, e.g. REPL - no filename available.
+ # TypeError trapped defensively, in case of object without filename slips through.
+ pass
+ default_repr_file = inspect.getfile(_dummy_namedtuple.__repr__)
+ return obj_file == default_repr_file
+
+
+def _ipy_display_hook(
+ value: Any,
+ console: Optional["Console"] = None,
+ overflow: "OverflowMethod" = "ignore",
+ crop: bool = False,
+ indent_guides: bool = False,
+ max_length: Optional[int] = None,
+ max_string: Optional[int] = None,
+ expand_all: bool = False,
+) -> None:
+ # needed here to prevent circular import:
+ from ._inspect import is_object_one_of_types
+ from .console import ConsoleRenderable
+
+ # always skip rich generated jupyter renderables or None values
+ if _safe_isinstance(value, JupyterRenderable) or value is None:
+ return
+
+ console = console or get_console()
+ if console.is_jupyter:
+ # Delegate rendering to IPython if the object (and IPython) supports it
+ # https://ipython.readthedocs.io/en/stable/config/integrating.html#rich-display
+ ipython_repr_methods = [
+ "_repr_html_",
+ "_repr_markdown_",
+ "_repr_json_",
+ "_repr_latex_",
+ "_repr_jpeg_",
+ "_repr_png_",
+ "_repr_svg_",
+ "_repr_mimebundle_",
+ ]
+ for repr_method in ipython_repr_methods:
+ method = getattr(value, repr_method, None)
+ if inspect.ismethod(method):
+ # Calling the method ourselves isn't ideal. The interface for the `_repr_*_` methods
+ # specifies that if they return None, then they should not be rendered
+ # by the notebook.
+ try:
+ repr_result = method()
+ except Exception:
+ continue # If the method raises, treat it as if it doesn't exist, try any others
+ if repr_result is not None:
+ return # Delegate rendering to IPython
+
+ # When in a Jupyter notebook let's avoid the display of some specific classes,
+ # as they result in the rendering of useless and noisy lines such as `<Figure size 432x288 with 1 Axes>`.
+ # What does this do?
+ # --> if the class has "matplotlib.artist.Artist" in its hierarchy for example, we don't render it.
+ if is_object_one_of_types(value, JUPYTER_CLASSES_TO_NOT_RENDER):
+ return
+
+ # certain renderables should start on a new line
+ if _safe_isinstance(value, ConsoleRenderable):
+ console.line()
+
+ console.print(
+ value
+ if _safe_isinstance(value, RichRenderable)
+ else Pretty(
+ value,
+ overflow=overflow,
+ indent_guides=indent_guides,
+ max_length=max_length,
+ max_string=max_string,
+ expand_all=expand_all,
+ margin=12,
+ ),
+ crop=crop,
+ new_line_start=True,
+ )
+
+
+def _safe_isinstance(
+ obj: object, class_or_tuple: Union[type, Tuple[type, ...]]
+) -> bool:
+ """isinstance can fail in rare cases, for example types with no __class__"""
+ try:
+ return isinstance(obj, class_or_tuple)
+ except Exception:
+ return False
+
+
+def install(
+ console: Optional["Console"] = None,
+ overflow: "OverflowMethod" = "ignore",
+ crop: bool = False,
+ indent_guides: bool = False,
+ max_length: Optional[int] = None,
+ max_string: Optional[int] = None,
+ expand_all: bool = False,
+) -> None:
+ """Install automatic pretty printing in the Python REPL.
+
+ Args:
+ console (Console, optional): Console instance or ``None`` to use global console. Defaults to None.
+ overflow (Optional[OverflowMethod], optional): Overflow method. Defaults to "ignore".
+ crop (Optional[bool], optional): Enable cropping of long lines. Defaults to False.
+ indent_guides (bool, optional): Enable indentation guides. Defaults to False.
+ max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to None.
+ max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None.
+ expand_all (bool, optional): Expand all containers. Defaults to False.
+ max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
+ """
+ from pip._vendor.rich import get_console
+
+ console = console or get_console()
+ assert console is not None
+
+ def display_hook(value: Any) -> None:
+ """Replacement sys.displayhook which prettifies objects with Rich."""
+ if value is not None:
+ assert console is not None
+ builtins._ = None # type: ignore[attr-defined]
+ console.print(
+ value
+ if _safe_isinstance(value, RichRenderable)
+ else Pretty(
+ value,
+ overflow=overflow,
+ indent_guides=indent_guides,
+ max_length=max_length,
+ max_string=max_string,
+ expand_all=expand_all,
+ ),
+ crop=crop,
+ )
+ builtins._ = value # type: ignore[attr-defined]
+
+ try: # pragma: no cover
+ ip = get_ipython() # type: ignore[name-defined]
+ from IPython.core.formatters import BaseFormatter
+
+ class RichFormatter(BaseFormatter): # type: ignore[misc]
+ pprint: bool = True
+
+ def __call__(self, value: Any) -> Any:
+ if self.pprint:
+ return _ipy_display_hook(
+ value,
+ console=get_console(),
+ overflow=overflow,
+ indent_guides=indent_guides,
+ max_length=max_length,
+ max_string=max_string,
+ expand_all=expand_all,
+ )
+ else:
+ return repr(value)
+
+ # replace plain text formatter with rich formatter
+ rich_formatter = RichFormatter()
+ ip.display_formatter.formatters["text/plain"] = rich_formatter
+ except Exception:
+ sys.displayhook = display_hook
+
+
+class Pretty(JupyterMixin):
+ """A rich renderable that pretty prints an object.
+
+ Args:
+ _object (Any): An object to pretty print.
+ highlighter (HighlighterType, optional): Highlighter object to apply to result, or None for ReprHighlighter. Defaults to None.
+ indent_size (int, optional): Number of spaces in indent. Defaults to 4.
+ justify (JustifyMethod, optional): Justify method, or None for default. Defaults to None.
+ overflow (OverflowMethod, optional): Overflow method, or None for default. Defaults to None.
+ no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to False.
+ indent_guides (bool, optional): Enable indentation guides. Defaults to False.
+ max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to None.
+ max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None.
+ max_depth (int, optional): Maximum depth of nested data structures, or None for no maximum. Defaults to None.
+ expand_all (bool, optional): Expand all containers. Defaults to False.
+ margin (int, optional): Subtrace a margin from width to force containers to expand earlier. Defaults to 0.
+ insert_line (bool, optional): Insert a new line if the output has multiple new lines. Defaults to False.
+ """
+
+ def __init__(
+ self,
+ _object: Any,
+ highlighter: Optional["HighlighterType"] = None,
+ *,
+ indent_size: int = 4,
+ justify: Optional["JustifyMethod"] = None,
+ overflow: Optional["OverflowMethod"] = None,
+ no_wrap: Optional[bool] = False,
+ indent_guides: bool = False,
+ max_length: Optional[int] = None,
+ max_string: Optional[int] = None,
+ max_depth: Optional[int] = None,
+ expand_all: bool = False,
+ margin: int = 0,
+ insert_line: bool = False,
+ ) -> None:
+ self._object = _object
+ self.highlighter = highlighter or ReprHighlighter()
+ self.indent_size = indent_size
+ self.justify: Optional["JustifyMethod"] = justify
+ self.overflow: Optional["OverflowMethod"] = overflow
+ self.no_wrap = no_wrap
+ self.indent_guides = indent_guides
+ self.max_length = max_length
+ self.max_string = max_string
+ self.max_depth = max_depth
+ self.expand_all = expand_all
+ self.margin = margin
+ self.insert_line = insert_line
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ pretty_str = pretty_repr(
+ self._object,
+ max_width=options.max_width - self.margin,
+ indent_size=self.indent_size,
+ max_length=self.max_length,
+ max_string=self.max_string,
+ max_depth=self.max_depth,
+ expand_all=self.expand_all,
+ )
+ pretty_text = Text(
+ pretty_str,
+ justify=self.justify or options.justify,
+ overflow=self.overflow or options.overflow,
+ no_wrap=pick_bool(self.no_wrap, options.no_wrap),
+ style="pretty",
+ )
+ pretty_text = (
+ self.highlighter(pretty_text)
+ if pretty_text
+ else Text(
+ f"{type(self._object)}.__repr__ returned empty string",
+ style="dim italic",
+ )
+ )
+ if self.indent_guides and not options.ascii_only:
+ pretty_text = pretty_text.with_indent_guides(
+ self.indent_size, style="repr.indent"
+ )
+ if self.insert_line and "\n" in pretty_text:
+ yield ""
+ yield pretty_text
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "Measurement":
+ pretty_str = pretty_repr(
+ self._object,
+ max_width=options.max_width,
+ indent_size=self.indent_size,
+ max_length=self.max_length,
+ max_string=self.max_string,
+ expand_all=self.expand_all,
+ )
+ text_width = (
+ max(cell_len(line) for line in pretty_str.splitlines()) if pretty_str else 0
+ )
+ return Measurement(text_width, text_width)
+
+
+def _get_braces_for_defaultdict(_object: DefaultDict[Any, Any]) -> Tuple[str, str, str]:
+ return (
+ f"defaultdict({_object.default_factory!r}, {{",
+ "})",
+ f"defaultdict({_object.default_factory!r}, {{}})",
+ )
+
+
+def _get_braces_for_array(_object: "array[Any]") -> Tuple[str, str, str]:
+ return (f"array({_object.typecode!r}, [", "])", f"array({_object.typecode!r})")
+
+
+_BRACES: Dict[type, Callable[[Any], Tuple[str, str, str]]] = {
+ os._Environ: lambda _object: ("environ({", "})", "environ({})"),
+ array: _get_braces_for_array,
+ defaultdict: _get_braces_for_defaultdict,
+ Counter: lambda _object: ("Counter({", "})", "Counter()"),
+ deque: lambda _object: ("deque([", "])", "deque()"),
+ dict: lambda _object: ("{", "}", "{}"),
+ UserDict: lambda _object: ("{", "}", "{}"),
+ frozenset: lambda _object: ("frozenset({", "})", "frozenset()"),
+ list: lambda _object: ("[", "]", "[]"),
+ UserList: lambda _object: ("[", "]", "[]"),
+ set: lambda _object: ("{", "}", "set()"),
+ tuple: lambda _object: ("(", ")", "()"),
+ MappingProxyType: lambda _object: ("mappingproxy({", "})", "mappingproxy({})"),
+}
+_CONTAINERS = tuple(_BRACES.keys())
+_MAPPING_CONTAINERS = (dict, os._Environ, MappingProxyType, UserDict)
+
+
+def is_expandable(obj: Any) -> bool:
+ """Check if an object may be expanded by pretty print."""
+ return (
+ _safe_isinstance(obj, _CONTAINERS)
+ or (is_dataclass(obj))
+ or (hasattr(obj, "__rich_repr__"))
+ or _is_attr_object(obj)
+ ) and not isclass(obj)
+
+
+@dataclass
+class Node:
+ """A node in a repr tree. May be atomic or a container."""
+
+ key_repr: str = ""
+ value_repr: str = ""
+ open_brace: str = ""
+ close_brace: str = ""
+ empty: str = ""
+ last: bool = False
+ is_tuple: bool = False
+ is_namedtuple: bool = False
+ children: Optional[List["Node"]] = None
+ key_separator = ": "
+ separator: str = ", "
+
+ def iter_tokens(self) -> Iterable[str]:
+ """Generate tokens for this node."""
+ if self.key_repr:
+ yield self.key_repr
+ yield self.key_separator
+ if self.value_repr:
+ yield self.value_repr
+ elif self.children is not None:
+ if self.children:
+ yield self.open_brace
+ if self.is_tuple and not self.is_namedtuple and len(self.children) == 1:
+ yield from self.children[0].iter_tokens()
+ yield ","
+ else:
+ for child in self.children:
+ yield from child.iter_tokens()
+ if not child.last:
+ yield self.separator
+ yield self.close_brace
+ else:
+ yield self.empty
+
+ def check_length(self, start_length: int, max_length: int) -> bool:
+ """Check the length fits within a limit.
+
+ Args:
+ start_length (int): Starting length of the line (indent, prefix, suffix).
+ max_length (int): Maximum length.
+
+ Returns:
+ bool: True if the node can be rendered within max length, otherwise False.
+ """
+ total_length = start_length
+ for token in self.iter_tokens():
+ total_length += cell_len(token)
+ if total_length > max_length:
+ return False
+ return True
+
+ def __str__(self) -> str:
+ repr_text = "".join(self.iter_tokens())
+ return repr_text
+
+ def render(
+ self, max_width: int = 80, indent_size: int = 4, expand_all: bool = False
+ ) -> str:
+ """Render the node to a pretty repr.
+
+ Args:
+ max_width (int, optional): Maximum width of the repr. Defaults to 80.
+ indent_size (int, optional): Size of indents. Defaults to 4.
+ expand_all (bool, optional): Expand all levels. Defaults to False.
+
+ Returns:
+ str: A repr string of the original object.
+ """
+ lines = [_Line(node=self, is_root=True)]
+ line_no = 0
+ while line_no < len(lines):
+ line = lines[line_no]
+ if line.expandable and not line.expanded:
+ if expand_all or not line.check_length(max_width):
+ lines[line_no : line_no + 1] = line.expand(indent_size)
+ line_no += 1
+
+ repr_str = "\n".join(str(line) for line in lines)
+ return repr_str
+
+
+@dataclass
+class _Line:
+ """A line in repr output."""
+
+ parent: Optional["_Line"] = None
+ is_root: bool = False
+ node: Optional[Node] = None
+ text: str = ""
+ suffix: str = ""
+ whitespace: str = ""
+ expanded: bool = False
+ last: bool = False
+
+ @property
+ def expandable(self) -> bool:
+ """Check if the line may be expanded."""
+ return bool(self.node is not None and self.node.children)
+
+ def check_length(self, max_length: int) -> bool:
+ """Check this line fits within a given number of cells."""
+ start_length = (
+ len(self.whitespace) + cell_len(self.text) + cell_len(self.suffix)
+ )
+ assert self.node is not None
+ return self.node.check_length(start_length, max_length)
+
+ def expand(self, indent_size: int) -> Iterable["_Line"]:
+ """Expand this line by adding children on their own line."""
+ node = self.node
+ assert node is not None
+ whitespace = self.whitespace
+ assert node.children
+ if node.key_repr:
+ new_line = yield _Line(
+ text=f"{node.key_repr}{node.key_separator}{node.open_brace}",
+ whitespace=whitespace,
+ )
+ else:
+ new_line = yield _Line(text=node.open_brace, whitespace=whitespace)
+ child_whitespace = self.whitespace + " " * indent_size
+ tuple_of_one = node.is_tuple and len(node.children) == 1
+ for last, child in loop_last(node.children):
+ separator = "," if tuple_of_one else node.separator
+ line = _Line(
+ parent=new_line,
+ node=child,
+ whitespace=child_whitespace,
+ suffix=separator,
+ last=last and not tuple_of_one,
+ )
+ yield line
+
+ yield _Line(
+ text=node.close_brace,
+ whitespace=whitespace,
+ suffix=self.suffix,
+ last=self.last,
+ )
+
+ def __str__(self) -> str:
+ if self.last:
+ return f"{self.whitespace}{self.text}{self.node or ''}"
+ else:
+ return (
+ f"{self.whitespace}{self.text}{self.node or ''}{self.suffix.rstrip()}"
+ )
+
+
+def _is_namedtuple(obj: Any) -> bool:
+ """Checks if an object is most likely a namedtuple. It is possible
+ to craft an object that passes this check and isn't a namedtuple, but
+ there is only a minuscule chance of this happening unintentionally.
+
+ Args:
+ obj (Any): The object to test
+
+ Returns:
+ bool: True if the object is a namedtuple. False otherwise.
+ """
+ try:
+ fields = getattr(obj, "_fields", None)
+ except Exception:
+ # Being very defensive - if we cannot get the attr then its not a namedtuple
+ return False
+ return isinstance(obj, tuple) and isinstance(fields, tuple)
+
+
+def traverse(
+ _object: Any,
+ max_length: Optional[int] = None,
+ max_string: Optional[int] = None,
+ max_depth: Optional[int] = None,
+) -> Node:
+ """Traverse object and generate a tree.
+
+ Args:
+ _object (Any): Object to be traversed.
+ max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to None.
+ max_string (int, optional): Maximum length of string before truncating, or None to disable truncating.
+ Defaults to None.
+ max_depth (int, optional): Maximum depth of data structures, or None for no maximum.
+ Defaults to None.
+
+ Returns:
+ Node: The root of a tree structure which can be used to render a pretty repr.
+ """
+
+ def to_repr(obj: Any) -> str:
+ """Get repr string for an object, but catch errors."""
+ if (
+ max_string is not None
+ and _safe_isinstance(obj, (bytes, str))
+ and len(obj) > max_string
+ ):
+ truncated = len(obj) - max_string
+ obj_repr = f"{obj[:max_string]!r}+{truncated}"
+ else:
+ try:
+ obj_repr = repr(obj)
+ except Exception as error:
+ obj_repr = f"<repr-error {str(error)!r}>"
+ return obj_repr
+
+ visited_ids: Set[int] = set()
+ push_visited = visited_ids.add
+ pop_visited = visited_ids.remove
+
+ def _traverse(obj: Any, root: bool = False, depth: int = 0) -> Node:
+ """Walk the object depth first."""
+
+ obj_type = type(obj)
+ py_version = (sys.version_info.major, sys.version_info.minor)
+ children: List[Node]
+ reached_max_depth = max_depth is not None and depth >= max_depth
+
+ def iter_rich_args(rich_args: Any) -> Iterable[Union[Any, Tuple[str, Any]]]:
+ for arg in rich_args:
+ if _safe_isinstance(arg, tuple):
+ if len(arg) == 3:
+ key, child, default = arg
+ if default == child:
+ continue
+ yield key, child
+ elif len(arg) == 2:
+ key, child = arg
+ yield key, child
+ elif len(arg) == 1:
+ yield arg[0]
+ else:
+ yield arg
+
+ try:
+ fake_attributes = hasattr(
+ obj, "awehoi234_wdfjwljet234_234wdfoijsdfmmnxpi492"
+ )
+ except Exception:
+ fake_attributes = False
+
+ rich_repr_result: Optional[RichReprResult] = None
+ if not fake_attributes:
+ try:
+ if hasattr(obj, "__rich_repr__") and not isclass(obj):
+ rich_repr_result = obj.__rich_repr__()
+ except Exception:
+ pass
+
+ if rich_repr_result is not None:
+ angular = getattr(obj.__rich_repr__, "angular", False)
+ args = list(iter_rich_args(rich_repr_result))
+ class_name = obj.__class__.__name__
+
+ if args:
+ children = []
+ append = children.append
+
+ if reached_max_depth:
+ node = Node(value_repr=f"...")
+ else:
+ if angular:
+ node = Node(
+ open_brace=f"<{class_name} ",
+ close_brace=">",
+ children=children,
+ last=root,
+ separator=" ",
+ )
+ else:
+ node = Node(
+ open_brace=f"{class_name}(",
+ close_brace=")",
+ children=children,
+ last=root,
+ )
+ for last, arg in loop_last(args):
+ if _safe_isinstance(arg, tuple):
+ key, child = arg
+ child_node = _traverse(child, depth=depth + 1)
+ child_node.last = last
+ child_node.key_repr = key
+ child_node.key_separator = "="
+ append(child_node)
+ else:
+ child_node = _traverse(arg, depth=depth + 1)
+ child_node.last = last
+ append(child_node)
+ else:
+ node = Node(
+ value_repr=f"<{class_name}>" if angular else f"{class_name}()",
+ children=[],
+ last=root,
+ )
+ elif _is_attr_object(obj) and not fake_attributes:
+ children = []
+ append = children.append
+
+ attr_fields = _get_attr_fields(obj)
+ if attr_fields:
+ if reached_max_depth:
+ node = Node(value_repr=f"...")
+ else:
+ node = Node(
+ open_brace=f"{obj.__class__.__name__}(",
+ close_brace=")",
+ children=children,
+ last=root,
+ )
+
+ def iter_attrs() -> Iterable[
+ Tuple[str, Any, Optional[Callable[[Any], str]]]
+ ]:
+ """Iterate over attr fields and values."""
+ for attr in attr_fields:
+ if attr.repr:
+ try:
+ value = getattr(obj, attr.name)
+ except Exception as error:
+ # Can happen, albeit rarely
+ yield (attr.name, error, None)
+ else:
+ yield (
+ attr.name,
+ value,
+ attr.repr if callable(attr.repr) else None,
+ )
+
+ for last, (name, value, repr_callable) in loop_last(iter_attrs()):
+ if repr_callable:
+ child_node = Node(value_repr=str(repr_callable(value)))
+ else:
+ child_node = _traverse(value, depth=depth + 1)
+ child_node.last = last
+ child_node.key_repr = name
+ child_node.key_separator = "="
+ append(child_node)
+ else:
+ node = Node(
+ value_repr=f"{obj.__class__.__name__}()", children=[], last=root
+ )
+
+ elif (
+ is_dataclass(obj)
+ and not _safe_isinstance(obj, type)
+ and not fake_attributes
+ and (_is_dataclass_repr(obj) or py_version == (3, 6))
+ ):
+ obj_id = id(obj)
+ if obj_id in visited_ids:
+ # Recursion detected
+ return Node(value_repr="...")
+ push_visited(obj_id)
+
+ children = []
+ append = children.append
+ if reached_max_depth:
+ node = Node(value_repr=f"...")
+ else:
+ node = Node(
+ open_brace=f"{obj.__class__.__name__}(",
+ close_brace=")",
+ children=children,
+ last=root,
+ )
+
+ for last, field in loop_last(
+ field for field in fields(obj) if field.repr
+ ):
+ child_node = _traverse(getattr(obj, field.name), depth=depth + 1)
+ child_node.key_repr = field.name
+ child_node.last = last
+ child_node.key_separator = "="
+ append(child_node)
+
+ pop_visited(obj_id)
+ elif _is_namedtuple(obj) and _has_default_namedtuple_repr(obj):
+ if reached_max_depth:
+ node = Node(value_repr="...")
+ else:
+ children = []
+ class_name = obj.__class__.__name__
+ node = Node(
+ open_brace=f"{class_name}(",
+ close_brace=")",
+ children=children,
+ empty=f"{class_name}()",
+ )
+ append = children.append
+ for last, (key, value) in loop_last(obj._asdict().items()):
+ child_node = _traverse(value, depth=depth + 1)
+ child_node.key_repr = key
+ child_node.last = last
+ child_node.key_separator = "="
+ append(child_node)
+ elif _safe_isinstance(obj, _CONTAINERS):
+ for container_type in _CONTAINERS:
+ if _safe_isinstance(obj, container_type):
+ obj_type = container_type
+ break
+
+ obj_id = id(obj)
+ if obj_id in visited_ids:
+ # Recursion detected
+ return Node(value_repr="...")
+ push_visited(obj_id)
+
+ open_brace, close_brace, empty = _BRACES[obj_type](obj)
+
+ if reached_max_depth:
+ node = Node(value_repr=f"...", last=root)
+ elif obj_type.__repr__ != type(obj).__repr__:
+ node = Node(value_repr=to_repr(obj), last=root)
+ elif obj:
+ children = []
+ node = Node(
+ open_brace=open_brace,
+ close_brace=close_brace,
+ children=children,
+ last=root,
+ )
+ append = children.append
+ num_items = len(obj)
+ last_item_index = num_items - 1
+
+ if _safe_isinstance(obj, _MAPPING_CONTAINERS):
+ iter_items = iter(obj.items())
+ if max_length is not None:
+ iter_items = islice(iter_items, max_length)
+ for index, (key, child) in enumerate(iter_items):
+ child_node = _traverse(child, depth=depth + 1)
+ child_node.key_repr = to_repr(key)
+ child_node.last = index == last_item_index
+ append(child_node)
+ else:
+ iter_values = iter(obj)
+ if max_length is not None:
+ iter_values = islice(iter_values, max_length)
+ for index, child in enumerate(iter_values):
+ child_node = _traverse(child, depth=depth + 1)
+ child_node.last = index == last_item_index
+ append(child_node)
+ if max_length is not None and num_items > max_length:
+ append(Node(value_repr=f"... +{num_items - max_length}", last=True))
+ else:
+ node = Node(empty=empty, children=[], last=root)
+
+ pop_visited(obj_id)
+ else:
+ node = Node(value_repr=to_repr(obj), last=root)
+ node.is_tuple = _safe_isinstance(obj, tuple)
+ node.is_namedtuple = _is_namedtuple(obj)
+ return node
+
+ node = _traverse(_object, root=True)
+ return node
+
+
+def pretty_repr(
+ _object: Any,
+ *,
+ max_width: int = 80,
+ indent_size: int = 4,
+ max_length: Optional[int] = None,
+ max_string: Optional[int] = None,
+ max_depth: Optional[int] = None,
+ expand_all: bool = False,
+) -> str:
+ """Prettify repr string by expanding on to new lines to fit within a given width.
+
+ Args:
+ _object (Any): Object to repr.
+ max_width (int, optional): Desired maximum width of repr string. Defaults to 80.
+ indent_size (int, optional): Number of spaces to indent. Defaults to 4.
+ max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to None.
+ max_string (int, optional): Maximum length of string before truncating, or None to disable truncating.
+ Defaults to None.
+ max_depth (int, optional): Maximum depth of nested data structure, or None for no depth.
+ Defaults to None.
+ expand_all (bool, optional): Expand all containers regardless of available width. Defaults to False.
+
+ Returns:
+ str: A possibly multi-line representation of the object.
+ """
+
+ if _safe_isinstance(_object, Node):
+ node = _object
+ else:
+ node = traverse(
+ _object, max_length=max_length, max_string=max_string, max_depth=max_depth
+ )
+ repr_str: str = node.render(
+ max_width=max_width, indent_size=indent_size, expand_all=expand_all
+ )
+ return repr_str
+
+
+def pprint(
+ _object: Any,
+ *,
+ console: Optional["Console"] = None,
+ indent_guides: bool = True,
+ max_length: Optional[int] = None,
+ max_string: Optional[int] = None,
+ max_depth: Optional[int] = None,
+ expand_all: bool = False,
+) -> None:
+ """A convenience function for pretty printing.
+
+ Args:
+ _object (Any): Object to pretty print.
+ console (Console, optional): Console instance, or None to use default. Defaults to None.
+ max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to None.
+ max_string (int, optional): Maximum length of strings before truncating, or None to disable. Defaults to None.
+ max_depth (int, optional): Maximum depth for nested data structures, or None for unlimited depth. Defaults to None.
+ indent_guides (bool, optional): Enable indentation guides. Defaults to True.
+ expand_all (bool, optional): Expand all containers. Defaults to False.
+ """
+ _console = get_console() if console is None else console
+ _console.print(
+ Pretty(
+ _object,
+ max_length=max_length,
+ max_string=max_string,
+ max_depth=max_depth,
+ indent_guides=indent_guides,
+ expand_all=expand_all,
+ overflow="ignore",
+ ),
+ soft_wrap=True,
+ )
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ class BrokenRepr:
+ def __repr__(self) -> str:
+ 1 / 0
+ return "this will fail"
+
+ from typing import NamedTuple
+
+ class StockKeepingUnit(NamedTuple):
+ name: str
+ description: str
+ price: float
+ category: str
+ reviews: List[str]
+
+ d = defaultdict(int)
+ d["foo"] = 5
+ data = {
+ "foo": [
+ 1,
+ "Hello World!",
+ 100.123,
+ 323.232,
+ 432324.0,
+ {5, 6, 7, (1, 2, 3, 4), 8},
+ ],
+ "bar": frozenset({1, 2, 3}),
+ "defaultdict": defaultdict(
+ list, {"crumble": ["apple", "rhubarb", "butter", "sugar", "flour"]}
+ ),
+ "counter": Counter(
+ [
+ "apple",
+ "orange",
+ "pear",
+ "kumquat",
+ "kumquat",
+ "durian" * 100,
+ ]
+ ),
+ "atomic": (False, True, None),
+ "namedtuple": StockKeepingUnit(
+ "Sparkling British Spring Water",
+ "Carbonated spring water",
+ 0.9,
+ "water",
+ ["its amazing!", "its terrible!"],
+ ),
+ "Broken": BrokenRepr(),
+ }
+ data["foo"].append(data) # type: ignore[attr-defined]
+
+ from pip._vendor.rich import print
+
+ print(Pretty(data, indent_guides=True, max_string=20))
diff --git a/src/pip/_vendor/rich/progress.py b/src/pip/_vendor/rich/progress.py
new file mode 100644
index 000000000..92cfa8023
--- /dev/null
+++ b/src/pip/_vendor/rich/progress.py
@@ -0,0 +1,1703 @@
+import io
+import sys
+import typing
+import warnings
+from abc import ABC, abstractmethod
+from collections import deque
+from collections.abc import Sized
+from dataclasses import dataclass, field
+from datetime import timedelta
+from io import RawIOBase, UnsupportedOperation
+from math import ceil
+from mmap import mmap
+from os import PathLike, stat
+from threading import Event, RLock, Thread
+from types import TracebackType
+from typing import (
+ Any,
+ BinaryIO,
+ Callable,
+ ContextManager,
+ Deque,
+ Dict,
+ Generic,
+ Iterable,
+ List,
+ NamedTuple,
+ NewType,
+ Optional,
+ Sequence,
+ TextIO,
+ Tuple,
+ Type,
+ TypeVar,
+ Union,
+)
+
+if sys.version_info >= (3, 8):
+ from typing import Literal
+else:
+ from pip._vendor.typing_extensions import Literal # pragma: no cover
+
+from . import filesize, get_console
+from .console import Console, Group, JustifyMethod, RenderableType
+from .highlighter import Highlighter
+from .jupyter import JupyterMixin
+from .live import Live
+from .progress_bar import ProgressBar
+from .spinner import Spinner
+from .style import StyleType
+from .table import Column, Table
+from .text import Text, TextType
+
+TaskID = NewType("TaskID", int)
+
+ProgressType = TypeVar("ProgressType")
+
+GetTimeCallable = Callable[[], float]
+
+
+_I = typing.TypeVar("_I", TextIO, BinaryIO)
+
+
+class _TrackThread(Thread):
+ """A thread to periodically update progress."""
+
+ def __init__(self, progress: "Progress", task_id: "TaskID", update_period: float):
+ self.progress = progress
+ self.task_id = task_id
+ self.update_period = update_period
+ self.done = Event()
+
+ self.completed = 0
+ super().__init__()
+
+ def run(self) -> None:
+ task_id = self.task_id
+ advance = self.progress.advance
+ update_period = self.update_period
+ last_completed = 0
+ wait = self.done.wait
+ while not wait(update_period):
+ completed = self.completed
+ if last_completed != completed:
+ advance(task_id, completed - last_completed)
+ last_completed = completed
+
+ self.progress.update(self.task_id, completed=self.completed, refresh=True)
+
+ def __enter__(self) -> "_TrackThread":
+ self.start()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ self.done.set()
+ self.join()
+
+
+def track(
+ sequence: Union[Sequence[ProgressType], Iterable[ProgressType]],
+ description: str = "Working...",
+ total: Optional[float] = None,
+ auto_refresh: bool = True,
+ console: Optional[Console] = None,
+ transient: bool = False,
+ get_time: Optional[Callable[[], float]] = None,
+ refresh_per_second: float = 10,
+ style: StyleType = "bar.back",
+ complete_style: StyleType = "bar.complete",
+ finished_style: StyleType = "bar.finished",
+ pulse_style: StyleType = "bar.pulse",
+ update_period: float = 0.1,
+ disable: bool = False,
+ show_speed: bool = True,
+) -> Iterable[ProgressType]:
+ """Track progress by iterating over a sequence.
+
+ Args:
+ sequence (Iterable[ProgressType]): A sequence (must support "len") you wish to iterate over.
+ description (str, optional): Description of task show next to progress bar. Defaults to "Working".
+ total: (float, optional): Total number of steps. Default is len(sequence).
+ auto_refresh (bool, optional): Automatic refresh, disable to force a refresh after each iteration. Default is True.
+ transient: (bool, optional): Clear the progress on exit. Defaults to False.
+ console (Console, optional): Console to write to. Default creates internal Console instance.
+ refresh_per_second (float): Number of times per second to refresh the progress information. Defaults to 10.
+ style (StyleType, optional): Style for the bar background. Defaults to "bar.back".
+ complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete".
+ finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done".
+ pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
+ update_period (float, optional): Minimum time (in seconds) between calls to update(). Defaults to 0.1.
+ disable (bool, optional): Disable display of progress.
+ show_speed (bool, optional): Show speed if total isn't known. Defaults to True.
+ Returns:
+ Iterable[ProgressType]: An iterable of the values in the sequence.
+
+ """
+
+ columns: List["ProgressColumn"] = (
+ [TextColumn("[progress.description]{task.description}")] if description else []
+ )
+ columns.extend(
+ (
+ BarColumn(
+ style=style,
+ complete_style=complete_style,
+ finished_style=finished_style,
+ pulse_style=pulse_style,
+ ),
+ TaskProgressColumn(show_speed=show_speed),
+ TimeRemainingColumn(),
+ )
+ )
+ progress = Progress(
+ *columns,
+ auto_refresh=auto_refresh,
+ console=console,
+ transient=transient,
+ get_time=get_time,
+ refresh_per_second=refresh_per_second or 10,
+ disable=disable,
+ )
+
+ with progress:
+ yield from progress.track(
+ sequence, total=total, description=description, update_period=update_period
+ )
+
+
+class _Reader(RawIOBase, BinaryIO):
+ """A reader that tracks progress while it's being read from."""
+
+ def __init__(
+ self,
+ handle: BinaryIO,
+ progress: "Progress",
+ task: TaskID,
+ close_handle: bool = True,
+ ) -> None:
+ self.handle = handle
+ self.progress = progress
+ self.task = task
+ self.close_handle = close_handle
+ self._closed = False
+
+ def __enter__(self) -> "_Reader":
+ self.handle.__enter__()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ self.close()
+
+ def __iter__(self) -> BinaryIO:
+ return self
+
+ def __next__(self) -> bytes:
+ line = next(self.handle)
+ self.progress.advance(self.task, advance=len(line))
+ return line
+
+ @property
+ def closed(self) -> bool:
+ return self._closed
+
+ def fileno(self) -> int:
+ return self.handle.fileno()
+
+ def isatty(self) -> bool:
+ return self.handle.isatty()
+
+ @property
+ def name(self) -> str:
+ return self.handle.name
+
+ def readable(self) -> bool:
+ return self.handle.readable()
+
+ def seekable(self) -> bool:
+ return self.handle.seekable()
+
+ def writable(self) -> bool:
+ return False
+
+ def read(self, size: int = -1) -> bytes:
+ block = self.handle.read(size)
+ self.progress.advance(self.task, advance=len(block))
+ return block
+
+ def readinto(self, b: Union[bytearray, memoryview, mmap]): # type: ignore[no-untyped-def, override]
+ n = self.handle.readinto(b) # type: ignore[attr-defined]
+ self.progress.advance(self.task, advance=n)
+ return n
+
+ def readline(self, size: int = -1) -> bytes: # type: ignore[override]
+ line = self.handle.readline(size)
+ self.progress.advance(self.task, advance=len(line))
+ return line
+
+ def readlines(self, hint: int = -1) -> List[bytes]:
+ lines = self.handle.readlines(hint)
+ self.progress.advance(self.task, advance=sum(map(len, lines)))
+ return lines
+
+ def close(self) -> None:
+ if self.close_handle:
+ self.handle.close()
+ self._closed = True
+
+ def seek(self, offset: int, whence: int = 0) -> int:
+ pos = self.handle.seek(offset, whence)
+ self.progress.update(self.task, completed=pos)
+ return pos
+
+ def tell(self) -> int:
+ return self.handle.tell()
+
+ def write(self, s: Any) -> int:
+ raise UnsupportedOperation("write")
+
+
+class _ReadContext(ContextManager[_I], Generic[_I]):
+ """A utility class to handle a context for both a reader and a progress."""
+
+ def __init__(self, progress: "Progress", reader: _I) -> None:
+ self.progress = progress
+ self.reader: _I = reader
+
+ def __enter__(self) -> _I:
+ self.progress.start()
+ return self.reader.__enter__()
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ self.progress.stop()
+ self.reader.__exit__(exc_type, exc_val, exc_tb)
+
+
+def wrap_file(
+ file: BinaryIO,
+ total: int,
+ *,
+ description: str = "Reading...",
+ auto_refresh: bool = True,
+ console: Optional[Console] = None,
+ transient: bool = False,
+ get_time: Optional[Callable[[], float]] = None,
+ refresh_per_second: float = 10,
+ style: StyleType = "bar.back",
+ complete_style: StyleType = "bar.complete",
+ finished_style: StyleType = "bar.finished",
+ pulse_style: StyleType = "bar.pulse",
+ disable: bool = False,
+) -> ContextManager[BinaryIO]:
+ """Read bytes from a file while tracking progress.
+
+ Args:
+ file (Union[str, PathLike[str], BinaryIO]): The path to the file to read, or a file-like object in binary mode.
+ total (int): Total number of bytes to read.
+ description (str, optional): Description of task show next to progress bar. Defaults to "Reading".
+ auto_refresh (bool, optional): Automatic refresh, disable to force a refresh after each iteration. Default is True.
+ transient: (bool, optional): Clear the progress on exit. Defaults to False.
+ console (Console, optional): Console to write to. Default creates internal Console instance.
+ refresh_per_second (float): Number of times per second to refresh the progress information. Defaults to 10.
+ style (StyleType, optional): Style for the bar background. Defaults to "bar.back".
+ complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete".
+ finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done".
+ pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
+ disable (bool, optional): Disable display of progress.
+ Returns:
+ ContextManager[BinaryIO]: A context manager yielding a progress reader.
+
+ """
+
+ columns: List["ProgressColumn"] = (
+ [TextColumn("[progress.description]{task.description}")] if description else []
+ )
+ columns.extend(
+ (
+ BarColumn(
+ style=style,
+ complete_style=complete_style,
+ finished_style=finished_style,
+ pulse_style=pulse_style,
+ ),
+ DownloadColumn(),
+ TimeRemainingColumn(),
+ )
+ )
+ progress = Progress(
+ *columns,
+ auto_refresh=auto_refresh,
+ console=console,
+ transient=transient,
+ get_time=get_time,
+ refresh_per_second=refresh_per_second or 10,
+ disable=disable,
+ )
+
+ reader = progress.wrap_file(file, total=total, description=description)
+ return _ReadContext(progress, reader)
+
+
+@typing.overload
+def open(
+ file: Union[str, "PathLike[str]", bytes],
+ mode: Union[Literal["rt"], Literal["r"]],
+ buffering: int = -1,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = None,
+ newline: Optional[str] = None,
+ *,
+ total: Optional[int] = None,
+ description: str = "Reading...",
+ auto_refresh: bool = True,
+ console: Optional[Console] = None,
+ transient: bool = False,
+ get_time: Optional[Callable[[], float]] = None,
+ refresh_per_second: float = 10,
+ style: StyleType = "bar.back",
+ complete_style: StyleType = "bar.complete",
+ finished_style: StyleType = "bar.finished",
+ pulse_style: StyleType = "bar.pulse",
+ disable: bool = False,
+) -> ContextManager[TextIO]:
+ pass
+
+
+@typing.overload
+def open(
+ file: Union[str, "PathLike[str]", bytes],
+ mode: Literal["rb"],
+ buffering: int = -1,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = None,
+ newline: Optional[str] = None,
+ *,
+ total: Optional[int] = None,
+ description: str = "Reading...",
+ auto_refresh: bool = True,
+ console: Optional[Console] = None,
+ transient: bool = False,
+ get_time: Optional[Callable[[], float]] = None,
+ refresh_per_second: float = 10,
+ style: StyleType = "bar.back",
+ complete_style: StyleType = "bar.complete",
+ finished_style: StyleType = "bar.finished",
+ pulse_style: StyleType = "bar.pulse",
+ disable: bool = False,
+) -> ContextManager[BinaryIO]:
+ pass
+
+
+def open(
+ file: Union[str, "PathLike[str]", bytes],
+ mode: Union[Literal["rb"], Literal["rt"], Literal["r"]] = "r",
+ buffering: int = -1,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = None,
+ newline: Optional[str] = None,
+ *,
+ total: Optional[int] = None,
+ description: str = "Reading...",
+ auto_refresh: bool = True,
+ console: Optional[Console] = None,
+ transient: bool = False,
+ get_time: Optional[Callable[[], float]] = None,
+ refresh_per_second: float = 10,
+ style: StyleType = "bar.back",
+ complete_style: StyleType = "bar.complete",
+ finished_style: StyleType = "bar.finished",
+ pulse_style: StyleType = "bar.pulse",
+ disable: bool = False,
+) -> Union[ContextManager[BinaryIO], ContextManager[TextIO]]:
+ """Read bytes from a file while tracking progress.
+
+ Args:
+ path (Union[str, PathLike[str], BinaryIO]): The path to the file to read, or a file-like object in binary mode.
+ mode (str): The mode to use to open the file. Only supports "r", "rb" or "rt".
+ buffering (int): The buffering strategy to use, see :func:`io.open`.
+ encoding (str, optional): The encoding to use when reading in text mode, see :func:`io.open`.
+ errors (str, optional): The error handling strategy for decoding errors, see :func:`io.open`.
+ newline (str, optional): The strategy for handling newlines in text mode, see :func:`io.open`
+ total: (int, optional): Total number of bytes to read. Must be provided if reading from a file handle. Default for a path is os.stat(file).st_size.
+ description (str, optional): Description of task show next to progress bar. Defaults to "Reading".
+ auto_refresh (bool, optional): Automatic refresh, disable to force a refresh after each iteration. Default is True.
+ transient: (bool, optional): Clear the progress on exit. Defaults to False.
+ console (Console, optional): Console to write to. Default creates internal Console instance.
+ refresh_per_second (float): Number of times per second to refresh the progress information. Defaults to 10.
+ style (StyleType, optional): Style for the bar background. Defaults to "bar.back".
+ complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete".
+ finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done".
+ pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
+ disable (bool, optional): Disable display of progress.
+ encoding (str, optional): The encoding to use when reading in text mode.
+
+ Returns:
+ ContextManager[BinaryIO]: A context manager yielding a progress reader.
+
+ """
+
+ columns: List["ProgressColumn"] = (
+ [TextColumn("[progress.description]{task.description}")] if description else []
+ )
+ columns.extend(
+ (
+ BarColumn(
+ style=style,
+ complete_style=complete_style,
+ finished_style=finished_style,
+ pulse_style=pulse_style,
+ ),
+ DownloadColumn(),
+ TimeRemainingColumn(),
+ )
+ )
+ progress = Progress(
+ *columns,
+ auto_refresh=auto_refresh,
+ console=console,
+ transient=transient,
+ get_time=get_time,
+ refresh_per_second=refresh_per_second or 10,
+ disable=disable,
+ )
+
+ reader = progress.open(
+ file,
+ mode=mode,
+ buffering=buffering,
+ encoding=encoding,
+ errors=errors,
+ newline=newline,
+ total=total,
+ description=description,
+ )
+ return _ReadContext(progress, reader) # type: ignore[return-value, type-var]
+
+
+class ProgressColumn(ABC):
+ """Base class for a widget to use in progress display."""
+
+ max_refresh: Optional[float] = None
+
+ def __init__(self, table_column: Optional[Column] = None) -> None:
+ self._table_column = table_column
+ self._renderable_cache: Dict[TaskID, Tuple[float, RenderableType]] = {}
+ self._update_time: Optional[float] = None
+
+ def get_table_column(self) -> Column:
+ """Get a table column, used to build tasks table."""
+ return self._table_column or Column()
+
+ def __call__(self, task: "Task") -> RenderableType:
+ """Called by the Progress object to return a renderable for the given task.
+
+ Args:
+ task (Task): An object containing information regarding the task.
+
+ Returns:
+ RenderableType: Anything renderable (including str).
+ """
+ current_time = task.get_time()
+ if self.max_refresh is not None and not task.completed:
+ try:
+ timestamp, renderable = self._renderable_cache[task.id]
+ except KeyError:
+ pass
+ else:
+ if timestamp + self.max_refresh > current_time:
+ return renderable
+
+ renderable = self.render(task)
+ self._renderable_cache[task.id] = (current_time, renderable)
+ return renderable
+
+ @abstractmethod
+ def render(self, task: "Task") -> RenderableType:
+ """Should return a renderable object."""
+
+
+class RenderableColumn(ProgressColumn):
+ """A column to insert an arbitrary column.
+
+ Args:
+ renderable (RenderableType, optional): Any renderable. Defaults to empty string.
+ """
+
+ def __init__(
+ self, renderable: RenderableType = "", *, table_column: Optional[Column] = None
+ ):
+ self.renderable = renderable
+ super().__init__(table_column=table_column)
+
+ def render(self, task: "Task") -> RenderableType:
+ return self.renderable
+
+
+class SpinnerColumn(ProgressColumn):
+ """A column with a 'spinner' animation.
+
+ Args:
+ spinner_name (str, optional): Name of spinner animation. Defaults to "dots".
+ style (StyleType, optional): Style of spinner. Defaults to "progress.spinner".
+ speed (float, optional): Speed factor of spinner. Defaults to 1.0.
+ finished_text (TextType, optional): Text used when task is finished. Defaults to " ".
+ """
+
+ def __init__(
+ self,
+ spinner_name: str = "dots",
+ style: Optional[StyleType] = "progress.spinner",
+ speed: float = 1.0,
+ finished_text: TextType = " ",
+ table_column: Optional[Column] = None,
+ ):
+ self.spinner = Spinner(spinner_name, style=style, speed=speed)
+ self.finished_text = (
+ Text.from_markup(finished_text)
+ if isinstance(finished_text, str)
+ else finished_text
+ )
+ super().__init__(table_column=table_column)
+
+ def set_spinner(
+ self,
+ spinner_name: str,
+ spinner_style: Optional[StyleType] = "progress.spinner",
+ speed: float = 1.0,
+ ) -> None:
+ """Set a new spinner.
+
+ Args:
+ spinner_name (str): Spinner name, see python -m rich.spinner.
+ spinner_style (Optional[StyleType], optional): Spinner style. Defaults to "progress.spinner".
+ speed (float, optional): Speed factor of spinner. Defaults to 1.0.
+ """
+ self.spinner = Spinner(spinner_name, style=spinner_style, speed=speed)
+
+ def render(self, task: "Task") -> RenderableType:
+ text = (
+ self.finished_text
+ if task.finished
+ else self.spinner.render(task.get_time())
+ )
+ return text
+
+
+class TextColumn(ProgressColumn):
+ """A column containing text."""
+
+ def __init__(
+ self,
+ text_format: str,
+ style: StyleType = "none",
+ justify: JustifyMethod = "left",
+ markup: bool = True,
+ highlighter: Optional[Highlighter] = None,
+ table_column: Optional[Column] = None,
+ ) -> None:
+ self.text_format = text_format
+ self.justify: JustifyMethod = justify
+ self.style = style
+ self.markup = markup
+ self.highlighter = highlighter
+ super().__init__(table_column=table_column or Column(no_wrap=True))
+
+ def render(self, task: "Task") -> Text:
+ _text = self.text_format.format(task=task)
+ if self.markup:
+ text = Text.from_markup(_text, style=self.style, justify=self.justify)
+ else:
+ text = Text(_text, style=self.style, justify=self.justify)
+ if self.highlighter:
+ self.highlighter.highlight(text)
+ return text
+
+
+class BarColumn(ProgressColumn):
+ """Renders a visual progress bar.
+
+ Args:
+ bar_width (Optional[int], optional): Width of bar or None for full width. Defaults to 40.
+ style (StyleType, optional): Style for the bar background. Defaults to "bar.back".
+ complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete".
+ finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done".
+ pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
+ """
+
+ def __init__(
+ self,
+ bar_width: Optional[int] = 40,
+ style: StyleType = "bar.back",
+ complete_style: StyleType = "bar.complete",
+ finished_style: StyleType = "bar.finished",
+ pulse_style: StyleType = "bar.pulse",
+ table_column: Optional[Column] = None,
+ ) -> None:
+ self.bar_width = bar_width
+ self.style = style
+ self.complete_style = complete_style
+ self.finished_style = finished_style
+ self.pulse_style = pulse_style
+ super().__init__(table_column=table_column)
+
+ def render(self, task: "Task") -> ProgressBar:
+ """Gets a progress bar widget for a task."""
+ return ProgressBar(
+ total=max(0, task.total) if task.total is not None else None,
+ completed=max(0, task.completed),
+ width=None if self.bar_width is None else max(1, self.bar_width),
+ pulse=not task.started,
+ animation_time=task.get_time(),
+ style=self.style,
+ complete_style=self.complete_style,
+ finished_style=self.finished_style,
+ pulse_style=self.pulse_style,
+ )
+
+
+class TimeElapsedColumn(ProgressColumn):
+ """Renders time elapsed."""
+
+ def render(self, task: "Task") -> Text:
+ """Show time remaining."""
+ elapsed = task.finished_time if task.finished else task.elapsed
+ if elapsed is None:
+ return Text("-:--:--", style="progress.elapsed")
+ delta = timedelta(seconds=int(elapsed))
+ return Text(str(delta), style="progress.elapsed")
+
+
+class TaskProgressColumn(TextColumn):
+ """Show task progress as a percentage.
+
+ Args:
+ text_format (str, optional): Format for percentage display. Defaults to "[progress.percentage]{task.percentage:>3.0f}%".
+ text_format_no_percentage (str, optional): Format if percentage is unknown. Defaults to "".
+ style (StyleType, optional): Style of output. Defaults to "none".
+ justify (JustifyMethod, optional): Text justification. Defaults to "left".
+ markup (bool, optional): Enable markup. Defaults to True.
+ highlighter (Optional[Highlighter], optional): Highlighter to apply to output. Defaults to None.
+ table_column (Optional[Column], optional): Table Column to use. Defaults to None.
+ show_speed (bool, optional): Show speed if total is unknown. Defaults to False.
+ """
+
+ def __init__(
+ self,
+ text_format: str = "[progress.percentage]{task.percentage:>3.0f}%",
+ text_format_no_percentage: str = "",
+ style: StyleType = "none",
+ justify: JustifyMethod = "left",
+ markup: bool = True,
+ highlighter: Optional[Highlighter] = None,
+ table_column: Optional[Column] = None,
+ show_speed: bool = False,
+ ) -> None:
+
+ self.text_format_no_percentage = text_format_no_percentage
+ self.show_speed = show_speed
+ super().__init__(
+ text_format=text_format,
+ style=style,
+ justify=justify,
+ markup=markup,
+ highlighter=highlighter,
+ table_column=table_column,
+ )
+
+ @classmethod
+ def render_speed(cls, speed: Optional[float]) -> Text:
+ """Render the speed in iterations per second.
+
+ Args:
+ task (Task): A Task object.
+
+ Returns:
+ Text: Text object containing the task speed.
+ """
+ if speed is None:
+ return Text("", style="progress.percentage")
+ unit, suffix = filesize.pick_unit_and_suffix(
+ int(speed),
+ ["", "×10³", "×10â¶", "×10â¹", "×10¹²"],
+ 1000,
+ )
+ data_speed = speed / unit
+ return Text(f"{data_speed:.1f}{suffix} it/s", style="progress.percentage")
+
+ def render(self, task: "Task") -> Text:
+ if task.total is None and self.show_speed:
+ return self.render_speed(task.finished_speed or task.speed)
+ text_format = (
+ self.text_format_no_percentage if task.total is None else self.text_format
+ )
+ _text = text_format.format(task=task)
+ if self.markup:
+ text = Text.from_markup(_text, style=self.style, justify=self.justify)
+ else:
+ text = Text(_text, style=self.style, justify=self.justify)
+ if self.highlighter:
+ self.highlighter.highlight(text)
+ return text
+
+
+class TimeRemainingColumn(ProgressColumn):
+ """Renders estimated time remaining.
+
+ Args:
+ compact (bool, optional): Render MM:SS when time remaining is less than an hour. Defaults to False.
+ elapsed_when_finished (bool, optional): Render time elapsed when the task is finished. Defaults to False.
+ """
+
+ # Only refresh twice a second to prevent jitter
+ max_refresh = 0.5
+
+ def __init__(
+ self,
+ compact: bool = False,
+ elapsed_when_finished: bool = False,
+ table_column: Optional[Column] = None,
+ ):
+ self.compact = compact
+ self.elapsed_when_finished = elapsed_when_finished
+ super().__init__(table_column=table_column)
+
+ def render(self, task: "Task") -> Text:
+ """Show time remaining."""
+ if self.elapsed_when_finished and task.finished:
+ task_time = task.finished_time
+ style = "progress.elapsed"
+ else:
+ task_time = task.time_remaining
+ style = "progress.remaining"
+
+ if task.total is None:
+ return Text("", style=style)
+
+ if task_time is None:
+ return Text("--:--" if self.compact else "-:--:--", style=style)
+
+ # Based on https://github.com/tqdm/tqdm/blob/master/tqdm/std.py
+ minutes, seconds = divmod(int(task_time), 60)
+ hours, minutes = divmod(minutes, 60)
+
+ if self.compact and not hours:
+ formatted = f"{minutes:02d}:{seconds:02d}"
+ else:
+ formatted = f"{hours:d}:{minutes:02d}:{seconds:02d}"
+
+ return Text(formatted, style=style)
+
+
+class FileSizeColumn(ProgressColumn):
+ """Renders completed filesize."""
+
+ def render(self, task: "Task") -> Text:
+ """Show data completed."""
+ data_size = filesize.decimal(int(task.completed))
+ return Text(data_size, style="progress.filesize")
+
+
+class TotalFileSizeColumn(ProgressColumn):
+ """Renders total filesize."""
+
+ def render(self, task: "Task") -> Text:
+ """Show data completed."""
+ data_size = filesize.decimal(int(task.total)) if task.total is not None else ""
+ return Text(data_size, style="progress.filesize.total")
+
+
+class MofNCompleteColumn(ProgressColumn):
+ """Renders completed count/total, e.g. ' 10/1000'.
+
+ Best for bounded tasks with int quantities.
+
+ Space pads the completed count so that progress length does not change as task progresses
+ past powers of 10.
+
+ Args:
+ separator (str, optional): Text to separate completed and total values. Defaults to "/".
+ """
+
+ def __init__(self, separator: str = "/", table_column: Optional[Column] = None):
+ self.separator = separator
+ super().__init__(table_column=table_column)
+
+ def render(self, task: "Task") -> Text:
+ """Show completed/total."""
+ completed = int(task.completed)
+ total = int(task.total) if task.total is not None else "?"
+ total_width = len(str(total))
+ return Text(
+ f"{completed:{total_width}d}{self.separator}{total}",
+ style="progress.download",
+ )
+
+
+class DownloadColumn(ProgressColumn):
+ """Renders file size downloaded and total, e.g. '0.5/2.3 GB'.
+
+ Args:
+ binary_units (bool, optional): Use binary units, KiB, MiB etc. Defaults to False.
+ """
+
+ def __init__(
+ self, binary_units: bool = False, table_column: Optional[Column] = None
+ ) -> None:
+ self.binary_units = binary_units
+ super().__init__(table_column=table_column)
+
+ def render(self, task: "Task") -> Text:
+ """Calculate common unit for completed and total."""
+ completed = int(task.completed)
+
+ unit_and_suffix_calculation_base = (
+ int(task.total) if task.total is not None else completed
+ )
+ if self.binary_units:
+ unit, suffix = filesize.pick_unit_and_suffix(
+ unit_and_suffix_calculation_base,
+ ["bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"],
+ 1024,
+ )
+ else:
+ unit, suffix = filesize.pick_unit_and_suffix(
+ unit_and_suffix_calculation_base,
+ ["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
+ 1000,
+ )
+ precision = 0 if unit == 1 else 1
+
+ completed_ratio = completed / unit
+ completed_str = f"{completed_ratio:,.{precision}f}"
+
+ if task.total is not None:
+ total = int(task.total)
+ total_ratio = total / unit
+ total_str = f"{total_ratio:,.{precision}f}"
+ else:
+ total_str = "?"
+
+ download_status = f"{completed_str}/{total_str} {suffix}"
+ download_text = Text(download_status, style="progress.download")
+ return download_text
+
+
+class TransferSpeedColumn(ProgressColumn):
+ """Renders human readable transfer speed."""
+
+ def render(self, task: "Task") -> Text:
+ """Show data transfer speed."""
+ speed = task.finished_speed or task.speed
+ if speed is None:
+ return Text("?", style="progress.data.speed")
+ data_speed = filesize.decimal(int(speed))
+ return Text(f"{data_speed}/s", style="progress.data.speed")
+
+
+class ProgressSample(NamedTuple):
+ """Sample of progress for a given time."""
+
+ timestamp: float
+ """Timestamp of sample."""
+ completed: float
+ """Number of steps completed."""
+
+
+@dataclass
+class Task:
+ """Information regarding a progress task.
+
+ This object should be considered read-only outside of the :class:`~Progress` class.
+
+ """
+
+ id: TaskID
+ """Task ID associated with this task (used in Progress methods)."""
+
+ description: str
+ """str: Description of the task."""
+
+ total: Optional[float]
+ """Optional[float]: Total number of steps in this task."""
+
+ completed: float
+ """float: Number of steps completed"""
+
+ _get_time: GetTimeCallable
+ """Callable to get the current time."""
+
+ finished_time: Optional[float] = None
+ """float: Time task was finished."""
+
+ visible: bool = True
+ """bool: Indicates if this task is visible in the progress display."""
+
+ fields: Dict[str, Any] = field(default_factory=dict)
+ """dict: Arbitrary fields passed in via Progress.update."""
+
+ start_time: Optional[float] = field(default=None, init=False, repr=False)
+ """Optional[float]: Time this task was started, or None if not started."""
+
+ stop_time: Optional[float] = field(default=None, init=False, repr=False)
+ """Optional[float]: Time this task was stopped, or None if not stopped."""
+
+ finished_speed: Optional[float] = None
+ """Optional[float]: The last speed for a finished task."""
+
+ _progress: Deque[ProgressSample] = field(
+ default_factory=lambda: deque(maxlen=1000), init=False, repr=False
+ )
+
+ _lock: RLock = field(repr=False, default_factory=RLock)
+ """Thread lock."""
+
+ def get_time(self) -> float:
+ """float: Get the current time, in seconds."""
+ return self._get_time()
+
+ @property
+ def started(self) -> bool:
+ """bool: Check if the task as started."""
+ return self.start_time is not None
+
+ @property
+ def remaining(self) -> Optional[float]:
+ """Optional[float]: Get the number of steps remaining, if a non-None total was set."""
+ if self.total is None:
+ return None
+ return self.total - self.completed
+
+ @property
+ def elapsed(self) -> Optional[float]:
+ """Optional[float]: Time elapsed since task was started, or ``None`` if the task hasn't started."""
+ if self.start_time is None:
+ return None
+ if self.stop_time is not None:
+ return self.stop_time - self.start_time
+ return self.get_time() - self.start_time
+
+ @property
+ def finished(self) -> bool:
+ """Check if the task has finished."""
+ return self.finished_time is not None
+
+ @property
+ def percentage(self) -> float:
+ """float: Get progress of task as a percentage. If a None total was set, returns 0"""
+ if not self.total:
+ return 0.0
+ completed = (self.completed / self.total) * 100.0
+ completed = min(100.0, max(0.0, completed))
+ return completed
+
+ @property
+ def speed(self) -> Optional[float]:
+ """Optional[float]: Get the estimated speed in steps per second."""
+ if self.start_time is None:
+ return None
+ with self._lock:
+ progress = self._progress
+ if not progress:
+ return None
+ total_time = progress[-1].timestamp - progress[0].timestamp
+ if total_time == 0:
+ return None
+ iter_progress = iter(progress)
+ next(iter_progress)
+ total_completed = sum(sample.completed for sample in iter_progress)
+ speed = total_completed / total_time
+ return speed
+
+ @property
+ def time_remaining(self) -> Optional[float]:
+ """Optional[float]: Get estimated time to completion, or ``None`` if no data."""
+ if self.finished:
+ return 0.0
+ speed = self.speed
+ if not speed:
+ return None
+ remaining = self.remaining
+ if remaining is None:
+ return None
+ estimate = ceil(remaining / speed)
+ return estimate
+
+ def _reset(self) -> None:
+ """Reset progress."""
+ self._progress.clear()
+ self.finished_time = None
+ self.finished_speed = None
+
+
+class Progress(JupyterMixin):
+ """Renders an auto-updating progress bar(s).
+
+ Args:
+ console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout.
+ auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()`.
+ refresh_per_second (Optional[float], optional): Number of times per second to refresh the progress information or None to use default (10). Defaults to None.
+ speed_estimate_period: (float, optional): Period (in seconds) used to calculate the speed estimate. Defaults to 30.
+ transient: (bool, optional): Clear the progress on exit. Defaults to False.
+ redirect_stdout: (bool, optional): Enable redirection of stdout, so ``print`` may be used. Defaults to True.
+ redirect_stderr: (bool, optional): Enable redirection of stderr. Defaults to True.
+ get_time: (Callable, optional): A callable that gets the current time, or None to use Console.get_time. Defaults to None.
+ disable (bool, optional): Disable progress display. Defaults to False
+ expand (bool, optional): Expand tasks table to fit width. Defaults to False.
+ """
+
+ def __init__(
+ self,
+ *columns: Union[str, ProgressColumn],
+ console: Optional[Console] = None,
+ auto_refresh: bool = True,
+ refresh_per_second: float = 10,
+ speed_estimate_period: float = 30.0,
+ transient: bool = False,
+ redirect_stdout: bool = True,
+ redirect_stderr: bool = True,
+ get_time: Optional[GetTimeCallable] = None,
+ disable: bool = False,
+ expand: bool = False,
+ ) -> None:
+ assert refresh_per_second > 0, "refresh_per_second must be > 0"
+ self._lock = RLock()
+ self.columns = columns or self.get_default_columns()
+ self.speed_estimate_period = speed_estimate_period
+
+ self.disable = disable
+ self.expand = expand
+ self._tasks: Dict[TaskID, Task] = {}
+ self._task_index: TaskID = TaskID(0)
+ self.live = Live(
+ console=console or get_console(),
+ auto_refresh=auto_refresh,
+ refresh_per_second=refresh_per_second,
+ transient=transient,
+ redirect_stdout=redirect_stdout,
+ redirect_stderr=redirect_stderr,
+ get_renderable=self.get_renderable,
+ )
+ self.get_time = get_time or self.console.get_time
+ self.print = self.console.print
+ self.log = self.console.log
+
+ @classmethod
+ def get_default_columns(cls) -> Tuple[ProgressColumn, ...]:
+ """Get the default columns used for a new Progress instance:
+ - a text column for the description (TextColumn)
+ - the bar itself (BarColumn)
+ - a text column showing completion percentage (TextColumn)
+ - an estimated-time-remaining column (TimeRemainingColumn)
+ If the Progress instance is created without passing a columns argument,
+ the default columns defined here will be used.
+
+ You can also create a Progress instance using custom columns before
+ and/or after the defaults, as in this example:
+
+ progress = Progress(
+ SpinnerColumn(),
+ *Progress.default_columns(),
+ "Elapsed:",
+ TimeElapsedColumn(),
+ )
+
+ This code shows the creation of a Progress display, containing
+ a spinner to the left, the default columns, and a labeled elapsed
+ time column.
+ """
+ return (
+ TextColumn("[progress.description]{task.description}"),
+ BarColumn(),
+ TaskProgressColumn(),
+ TimeRemainingColumn(),
+ )
+
+ @property
+ def console(self) -> Console:
+ return self.live.console
+
+ @property
+ def tasks(self) -> List[Task]:
+ """Get a list of Task instances."""
+ with self._lock:
+ return list(self._tasks.values())
+
+ @property
+ def task_ids(self) -> List[TaskID]:
+ """A list of task IDs."""
+ with self._lock:
+ return list(self._tasks.keys())
+
+ @property
+ def finished(self) -> bool:
+ """Check if all tasks have been completed."""
+ with self._lock:
+ if not self._tasks:
+ return True
+ return all(task.finished for task in self._tasks.values())
+
+ def start(self) -> None:
+ """Start the progress display."""
+ if not self.disable:
+ self.live.start(refresh=True)
+
+ def stop(self) -> None:
+ """Stop the progress display."""
+ self.live.stop()
+ if not self.console.is_interactive:
+ self.console.print()
+
+ def __enter__(self) -> "Progress":
+ self.start()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ self.stop()
+
+ def track(
+ self,
+ sequence: Union[Iterable[ProgressType], Sequence[ProgressType]],
+ total: Optional[float] = None,
+ task_id: Optional[TaskID] = None,
+ description: str = "Working...",
+ update_period: float = 0.1,
+ ) -> Iterable[ProgressType]:
+ """Track progress by iterating over a sequence.
+
+ Args:
+ sequence (Sequence[ProgressType]): A sequence of values you want to iterate over and track progress.
+ total: (float, optional): Total number of steps. Default is len(sequence).
+ task_id: (TaskID): Task to track. Default is new task.
+ description: (str, optional): Description of task, if new task is created.
+ update_period (float, optional): Minimum time (in seconds) between calls to update(). Defaults to 0.1.
+
+ Returns:
+ Iterable[ProgressType]: An iterable of values taken from the provided sequence.
+ """
+
+ task_total: Optional[float] = None
+ if total is None:
+ if isinstance(sequence, Sized):
+ task_total = float(len(sequence))
+ else:
+ task_total = total
+
+ if task_id is None:
+ task_id = self.add_task(description, total=task_total)
+ else:
+ self.update(task_id, total=task_total)
+
+ if self.live.auto_refresh:
+ with _TrackThread(self, task_id, update_period) as track_thread:
+ for value in sequence:
+ yield value
+ track_thread.completed += 1
+ else:
+ advance = self.advance
+ refresh = self.refresh
+ for value in sequence:
+ yield value
+ advance(task_id, 1)
+ refresh()
+
+ def wrap_file(
+ self,
+ file: BinaryIO,
+ total: Optional[int] = None,
+ *,
+ task_id: Optional[TaskID] = None,
+ description: str = "Reading...",
+ ) -> BinaryIO:
+ """Track progress file reading from a binary file.
+
+ Args:
+ file (BinaryIO): A file-like object opened in binary mode.
+ total (int, optional): Total number of bytes to read. This must be provided unless a task with a total is also given.
+ task_id (TaskID): Task to track. Default is new task.
+ description (str, optional): Description of task, if new task is created.
+
+ Returns:
+ BinaryIO: A readable file-like object in binary mode.
+
+ Raises:
+ ValueError: When no total value can be extracted from the arguments or the task.
+ """
+ # attempt to recover the total from the task
+ total_bytes: Optional[float] = None
+ if total is not None:
+ total_bytes = total
+ elif task_id is not None:
+ with self._lock:
+ total_bytes = self._tasks[task_id].total
+ if total_bytes is None:
+ raise ValueError(
+ f"unable to get the total number of bytes, please specify 'total'"
+ )
+
+ # update total of task or create new task
+ if task_id is None:
+ task_id = self.add_task(description, total=total_bytes)
+ else:
+ self.update(task_id, total=total_bytes)
+
+ return _Reader(file, self, task_id, close_handle=False)
+
+ @typing.overload
+ def open(
+ self,
+ file: Union[str, "PathLike[str]", bytes],
+ mode: Literal["rb"],
+ buffering: int = -1,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = None,
+ newline: Optional[str] = None,
+ *,
+ total: Optional[int] = None,
+ task_id: Optional[TaskID] = None,
+ description: str = "Reading...",
+ ) -> BinaryIO:
+ pass
+
+ @typing.overload
+ def open(
+ self,
+ file: Union[str, "PathLike[str]", bytes],
+ mode: Union[Literal["r"], Literal["rt"]],
+ buffering: int = -1,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = None,
+ newline: Optional[str] = None,
+ *,
+ total: Optional[int] = None,
+ task_id: Optional[TaskID] = None,
+ description: str = "Reading...",
+ ) -> TextIO:
+ pass
+
+ def open(
+ self,
+ file: Union[str, "PathLike[str]", bytes],
+ mode: Union[Literal["rb"], Literal["rt"], Literal["r"]] = "r",
+ buffering: int = -1,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = None,
+ newline: Optional[str] = None,
+ *,
+ total: Optional[int] = None,
+ task_id: Optional[TaskID] = None,
+ description: str = "Reading...",
+ ) -> Union[BinaryIO, TextIO]:
+ """Track progress while reading from a binary file.
+
+ Args:
+ path (Union[str, PathLike[str]]): The path to the file to read.
+ mode (str): The mode to use to open the file. Only supports "r", "rb" or "rt".
+ buffering (int): The buffering strategy to use, see :func:`io.open`.
+ encoding (str, optional): The encoding to use when reading in text mode, see :func:`io.open`.
+ errors (str, optional): The error handling strategy for decoding errors, see :func:`io.open`.
+ newline (str, optional): The strategy for handling newlines in text mode, see :func:`io.open`.
+ total (int, optional): Total number of bytes to read. If none given, os.stat(path).st_size is used.
+ task_id (TaskID): Task to track. Default is new task.
+ description (str, optional): Description of task, if new task is created.
+
+ Returns:
+ BinaryIO: A readable file-like object in binary mode.
+
+ Raises:
+ ValueError: When an invalid mode is given.
+ """
+ # normalize the mode (always rb, rt)
+ _mode = "".join(sorted(mode, reverse=False))
+ if _mode not in ("br", "rt", "r"):
+ raise ValueError("invalid mode {!r}".format(mode))
+
+ # patch buffering to provide the same behaviour as the builtin `open`
+ line_buffering = buffering == 1
+ if _mode == "br" and buffering == 1:
+ warnings.warn(
+ "line buffering (buffering=1) isn't supported in binary mode, the default buffer size will be used",
+ RuntimeWarning,
+ )
+ buffering = -1
+ elif _mode == "rt" or _mode == "r":
+ if buffering == 0:
+ raise ValueError("can't have unbuffered text I/O")
+ elif buffering == 1:
+ buffering = -1
+
+ # attempt to get the total with `os.stat`
+ if total is None:
+ total = stat(file).st_size
+
+ # update total of task or create new task
+ if task_id is None:
+ task_id = self.add_task(description, total=total)
+ else:
+ self.update(task_id, total=total)
+
+ # open the file in binary mode,
+ handle = io.open(file, "rb", buffering=buffering)
+ reader = _Reader(handle, self, task_id, close_handle=True)
+
+ # wrap the reader in a `TextIOWrapper` if text mode
+ if mode == "r" or mode == "rt":
+ return io.TextIOWrapper(
+ reader,
+ encoding=encoding,
+ errors=errors,
+ newline=newline,
+ line_buffering=line_buffering,
+ )
+
+ return reader
+
+ def start_task(self, task_id: TaskID) -> None:
+ """Start a task.
+
+ Starts a task (used when calculating elapsed time). You may need to call this manually,
+ if you called ``add_task`` with ``start=False``.
+
+ Args:
+ task_id (TaskID): ID of task.
+ """
+ with self._lock:
+ task = self._tasks[task_id]
+ if task.start_time is None:
+ task.start_time = self.get_time()
+
+ def stop_task(self, task_id: TaskID) -> None:
+ """Stop a task.
+
+ This will freeze the elapsed time on the task.
+
+ Args:
+ task_id (TaskID): ID of task.
+ """
+ with self._lock:
+ task = self._tasks[task_id]
+ current_time = self.get_time()
+ if task.start_time is None:
+ task.start_time = current_time
+ task.stop_time = current_time
+
+ def update(
+ self,
+ task_id: TaskID,
+ *,
+ total: Optional[float] = None,
+ completed: Optional[float] = None,
+ advance: Optional[float] = None,
+ description: Optional[str] = None,
+ visible: Optional[bool] = None,
+ refresh: bool = False,
+ **fields: Any,
+ ) -> None:
+ """Update information associated with a task.
+
+ Args:
+ task_id (TaskID): Task id (returned by add_task).
+ total (float, optional): Updates task.total if not None.
+ completed (float, optional): Updates task.completed if not None.
+ advance (float, optional): Add a value to task.completed if not None.
+ description (str, optional): Change task description if not None.
+ visible (bool, optional): Set visible flag if not None.
+ refresh (bool): Force a refresh of progress information. Default is False.
+ **fields (Any): Additional data fields required for rendering.
+ """
+ with self._lock:
+ task = self._tasks[task_id]
+ completed_start = task.completed
+
+ if total is not None and total != task.total:
+ task.total = total
+ task._reset()
+ if advance is not None:
+ task.completed += advance
+ if completed is not None:
+ task.completed = completed
+ if description is not None:
+ task.description = description
+ if visible is not None:
+ task.visible = visible
+ task.fields.update(fields)
+ update_completed = task.completed - completed_start
+
+ current_time = self.get_time()
+ old_sample_time = current_time - self.speed_estimate_period
+ _progress = task._progress
+
+ popleft = _progress.popleft
+ while _progress and _progress[0].timestamp < old_sample_time:
+ popleft()
+ if update_completed > 0:
+ _progress.append(ProgressSample(current_time, update_completed))
+ if (
+ task.total is not None
+ and task.completed >= task.total
+ and task.finished_time is None
+ ):
+ task.finished_time = task.elapsed
+
+ if refresh:
+ self.refresh()
+
+ def reset(
+ self,
+ task_id: TaskID,
+ *,
+ start: bool = True,
+ total: Optional[float] = None,
+ completed: int = 0,
+ visible: Optional[bool] = None,
+ description: Optional[str] = None,
+ **fields: Any,
+ ) -> None:
+ """Reset a task so completed is 0 and the clock is reset.
+
+ Args:
+ task_id (TaskID): ID of task.
+ start (bool, optional): Start the task after reset. Defaults to True.
+ total (float, optional): New total steps in task, or None to use current total. Defaults to None.
+ completed (int, optional): Number of steps completed. Defaults to 0.
+ visible (bool, optional): Enable display of the task. Defaults to True.
+ description (str, optional): Change task description if not None. Defaults to None.
+ **fields (str): Additional data fields required for rendering.
+ """
+ current_time = self.get_time()
+ with self._lock:
+ task = self._tasks[task_id]
+ task._reset()
+ task.start_time = current_time if start else None
+ if total is not None:
+ task.total = total
+ task.completed = completed
+ if visible is not None:
+ task.visible = visible
+ if fields:
+ task.fields = fields
+ if description is not None:
+ task.description = description
+ task.finished_time = None
+ self.refresh()
+
+ def advance(self, task_id: TaskID, advance: float = 1) -> None:
+ """Advance task by a number of steps.
+
+ Args:
+ task_id (TaskID): ID of task.
+ advance (float): Number of steps to advance. Default is 1.
+ """
+ current_time = self.get_time()
+ with self._lock:
+ task = self._tasks[task_id]
+ completed_start = task.completed
+ task.completed += advance
+ update_completed = task.completed - completed_start
+ old_sample_time = current_time - self.speed_estimate_period
+ _progress = task._progress
+
+ popleft = _progress.popleft
+ while _progress and _progress[0].timestamp < old_sample_time:
+ popleft()
+ while len(_progress) > 1000:
+ popleft()
+ _progress.append(ProgressSample(current_time, update_completed))
+ if (
+ task.total is not None
+ and task.completed >= task.total
+ and task.finished_time is None
+ ):
+ task.finished_time = task.elapsed
+ task.finished_speed = task.speed
+
+ def refresh(self) -> None:
+ """Refresh (render) the progress information."""
+ if not self.disable and self.live.is_started:
+ self.live.refresh()
+
+ def get_renderable(self) -> RenderableType:
+ """Get a renderable for the progress display."""
+ renderable = Group(*self.get_renderables())
+ return renderable
+
+ def get_renderables(self) -> Iterable[RenderableType]:
+ """Get a number of renderables for the progress display."""
+ table = self.make_tasks_table(self.tasks)
+ yield table
+
+ def make_tasks_table(self, tasks: Iterable[Task]) -> Table:
+ """Get a table to render the Progress display.
+
+ Args:
+ tasks (Iterable[Task]): An iterable of Task instances, one per row of the table.
+
+ Returns:
+ Table: A table instance.
+ """
+ table_columns = (
+ (
+ Column(no_wrap=True)
+ if isinstance(_column, str)
+ else _column.get_table_column().copy()
+ )
+ for _column in self.columns
+ )
+ table = Table.grid(*table_columns, padding=(0, 1), expand=self.expand)
+
+ for task in tasks:
+ if task.visible:
+ table.add_row(
+ *(
+ (
+ column.format(task=task)
+ if isinstance(column, str)
+ else column(task)
+ )
+ for column in self.columns
+ )
+ )
+ return table
+
+ def __rich__(self) -> RenderableType:
+ """Makes the Progress class itself renderable."""
+ with self._lock:
+ return self.get_renderable()
+
+ def add_task(
+ self,
+ description: str,
+ start: bool = True,
+ total: Optional[float] = 100.0,
+ completed: int = 0,
+ visible: bool = True,
+ **fields: Any,
+ ) -> TaskID:
+ """Add a new 'task' to the Progress display.
+
+ Args:
+ description (str): A description of the task.
+ start (bool, optional): Start the task immediately (to calculate elapsed time). If set to False,
+ you will need to call `start` manually. Defaults to True.
+ total (float, optional): Number of total steps in the progress if known.
+ Set to None to render a pulsing animation. Defaults to 100.
+ completed (int, optional): Number of steps completed so far. Defaults to 0.
+ visible (bool, optional): Enable display of the task. Defaults to True.
+ **fields (str): Additional data fields required for rendering.
+
+ Returns:
+ TaskID: An ID you can use when calling `update`.
+ """
+ with self._lock:
+ task = Task(
+ self._task_index,
+ description,
+ total,
+ completed,
+ visible=visible,
+ fields=fields,
+ _get_time=self.get_time,
+ _lock=self._lock,
+ )
+ self._tasks[self._task_index] = task
+ if start:
+ self.start_task(self._task_index)
+ new_task_index = self._task_index
+ self._task_index = TaskID(int(self._task_index) + 1)
+ self.refresh()
+ return new_task_index
+
+ def remove_task(self, task_id: TaskID) -> None:
+ """Delete a task if it exists.
+
+ Args:
+ task_id (TaskID): A task ID.
+
+ """
+ with self._lock:
+ del self._tasks[task_id]
+
+
+if __name__ == "__main__": # pragma: no coverage
+
+ import random
+ import time
+
+ from .panel import Panel
+ from .rule import Rule
+ from .syntax import Syntax
+ from .table import Table
+
+ syntax = Syntax(
+ '''def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
+ """Iterate and generate a tuple with a flag for last value."""
+ iter_values = iter(values)
+ try:
+ previous_value = next(iter_values)
+ except StopIteration:
+ return
+ for value in iter_values:
+ yield False, previous_value
+ previous_value = value
+ yield True, previous_value''',
+ "python",
+ line_numbers=True,
+ )
+
+ table = Table("foo", "bar", "baz")
+ table.add_row("1", "2", "3")
+
+ progress_renderables = [
+ "Text may be printed while the progress bars are rendering.",
+ Panel("In fact, [i]any[/i] renderable will work"),
+ "Such as [magenta]tables[/]...",
+ table,
+ "Pretty printed structures...",
+ {"type": "example", "text": "Pretty printed"},
+ "Syntax...",
+ syntax,
+ Rule("Give it a try!"),
+ ]
+
+ from itertools import cycle
+
+ examples = cycle(progress_renderables)
+
+ console = Console(record=True)
+
+ with Progress(
+ SpinnerColumn(),
+ *Progress.get_default_columns(),
+ TimeElapsedColumn(),
+ console=console,
+ transient=False,
+ ) as progress:
+
+ task1 = progress.add_task("[red]Downloading", total=1000)
+ task2 = progress.add_task("[green]Processing", total=1000)
+ task3 = progress.add_task("[yellow]Thinking", total=None)
+
+ while not progress.finished:
+ progress.update(task1, advance=0.5)
+ progress.update(task2, advance=0.3)
+ time.sleep(0.01)
+ if random.randint(0, 100) < 1:
+ progress.log(next(examples))
diff --git a/src/pip/_vendor/rich/progress_bar.py b/src/pip/_vendor/rich/progress_bar.py
new file mode 100644
index 000000000..9c3a4f25a
--- /dev/null
+++ b/src/pip/_vendor/rich/progress_bar.py
@@ -0,0 +1,224 @@
+import math
+from functools import lru_cache
+from time import monotonic
+from typing import Iterable, List, Optional
+
+from .color import Color, blend_rgb
+from .color_triplet import ColorTriplet
+from .console import Console, ConsoleOptions, RenderResult
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .segment import Segment
+from .style import Style, StyleType
+
+# Number of characters before 'pulse' animation repeats
+PULSE_SIZE = 20
+
+
+class ProgressBar(JupyterMixin):
+ """Renders a (progress) bar. Used by rich.progress.
+
+ Args:
+ total (float, optional): Number of steps in the bar. Defaults to 100. Set to None to render a pulsing animation.
+ completed (float, optional): Number of steps completed. Defaults to 0.
+ width (int, optional): Width of the bar, or ``None`` for maximum width. Defaults to None.
+ pulse (bool, optional): Enable pulse effect. Defaults to False. Will pulse if a None total was passed.
+ style (StyleType, optional): Style for the bar background. Defaults to "bar.back".
+ complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete".
+ finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done".
+ pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
+ animation_time (Optional[float], optional): Time in seconds to use for animation, or None to use system time.
+ """
+
+ def __init__(
+ self,
+ total: Optional[float] = 100.0,
+ completed: float = 0,
+ width: Optional[int] = None,
+ pulse: bool = False,
+ style: StyleType = "bar.back",
+ complete_style: StyleType = "bar.complete",
+ finished_style: StyleType = "bar.finished",
+ pulse_style: StyleType = "bar.pulse",
+ animation_time: Optional[float] = None,
+ ):
+ self.total = total
+ self.completed = completed
+ self.width = width
+ self.pulse = pulse
+ self.style = style
+ self.complete_style = complete_style
+ self.finished_style = finished_style
+ self.pulse_style = pulse_style
+ self.animation_time = animation_time
+
+ self._pulse_segments: Optional[List[Segment]] = None
+
+ def __repr__(self) -> str:
+ return f"<Bar {self.completed!r} of {self.total!r}>"
+
+ @property
+ def percentage_completed(self) -> Optional[float]:
+ """Calculate percentage complete."""
+ if self.total is None:
+ return None
+ completed = (self.completed / self.total) * 100.0
+ completed = min(100, max(0.0, completed))
+ return completed
+
+ @lru_cache(maxsize=16)
+ def _get_pulse_segments(
+ self,
+ fore_style: Style,
+ back_style: Style,
+ color_system: str,
+ no_color: bool,
+ ascii: bool = False,
+ ) -> List[Segment]:
+ """Get a list of segments to render a pulse animation.
+
+ Returns:
+ List[Segment]: A list of segments, one segment per character.
+ """
+ bar = "-" if ascii else "â”"
+ segments: List[Segment] = []
+ if color_system not in ("standard", "eight_bit", "truecolor") or no_color:
+ segments += [Segment(bar, fore_style)] * (PULSE_SIZE // 2)
+ segments += [Segment(" " if no_color else bar, back_style)] * (
+ PULSE_SIZE - (PULSE_SIZE // 2)
+ )
+ return segments
+
+ append = segments.append
+ fore_color = (
+ fore_style.color.get_truecolor()
+ if fore_style.color
+ else ColorTriplet(255, 0, 255)
+ )
+ back_color = (
+ back_style.color.get_truecolor()
+ if back_style.color
+ else ColorTriplet(0, 0, 0)
+ )
+ cos = math.cos
+ pi = math.pi
+ _Segment = Segment
+ _Style = Style
+ from_triplet = Color.from_triplet
+
+ for index in range(PULSE_SIZE):
+ position = index / PULSE_SIZE
+ fade = 0.5 + cos((position * pi * 2)) / 2.0
+ color = blend_rgb(fore_color, back_color, cross_fade=fade)
+ append(_Segment(bar, _Style(color=from_triplet(color))))
+ return segments
+
+ def update(self, completed: float, total: Optional[float] = None) -> None:
+ """Update progress with new values.
+
+ Args:
+ completed (float): Number of steps completed.
+ total (float, optional): Total number of steps, or ``None`` to not change. Defaults to None.
+ """
+ self.completed = completed
+ self.total = total if total is not None else self.total
+
+ def _render_pulse(
+ self, console: Console, width: int, ascii: bool = False
+ ) -> Iterable[Segment]:
+ """Renders the pulse animation.
+
+ Args:
+ console (Console): Console instance.
+ width (int): Width in characters of pulse animation.
+
+ Returns:
+ RenderResult: [description]
+
+ Yields:
+ Iterator[Segment]: Segments to render pulse
+ """
+ fore_style = console.get_style(self.pulse_style, default="white")
+ back_style = console.get_style(self.style, default="black")
+
+ pulse_segments = self._get_pulse_segments(
+ fore_style, back_style, console.color_system, console.no_color, ascii=ascii
+ )
+ segment_count = len(pulse_segments)
+ current_time = (
+ monotonic() if self.animation_time is None else self.animation_time
+ )
+ segments = pulse_segments * (int(width / segment_count) + 2)
+ offset = int(-current_time * 15) % segment_count
+ segments = segments[offset : offset + width]
+ yield from segments
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+
+ width = min(self.width or options.max_width, options.max_width)
+ ascii = options.legacy_windows or options.ascii_only
+ should_pulse = self.pulse or self.total is None
+ if should_pulse:
+ yield from self._render_pulse(console, width, ascii=ascii)
+ return
+
+ completed: Optional[float] = (
+ min(self.total, max(0, self.completed)) if self.total is not None else None
+ )
+
+ bar = "-" if ascii else "â”"
+ half_bar_right = " " if ascii else "╸"
+ half_bar_left = " " if ascii else "╺"
+ complete_halves = (
+ int(width * 2 * completed / self.total)
+ if self.total and completed is not None
+ else width * 2
+ )
+ bar_count = complete_halves // 2
+ half_bar_count = complete_halves % 2
+ style = console.get_style(self.style)
+ is_finished = self.total is None or self.completed >= self.total
+ complete_style = console.get_style(
+ self.finished_style if is_finished else self.complete_style
+ )
+ _Segment = Segment
+ if bar_count:
+ yield _Segment(bar * bar_count, complete_style)
+ if half_bar_count:
+ yield _Segment(half_bar_right * half_bar_count, complete_style)
+
+ if not console.no_color:
+ remaining_bars = width - bar_count - half_bar_count
+ if remaining_bars and console.color_system is not None:
+ if not half_bar_count and bar_count:
+ yield _Segment(half_bar_left, style)
+ remaining_bars -= 1
+ if remaining_bars:
+ yield _Segment(bar * remaining_bars, style)
+
+ def __rich_measure__(
+ self, console: Console, options: ConsoleOptions
+ ) -> Measurement:
+ return (
+ Measurement(self.width, self.width)
+ if self.width is not None
+ else Measurement(4, options.max_width)
+ )
+
+
+if __name__ == "__main__": # pragma: no cover
+ console = Console()
+ bar = ProgressBar(width=50, total=100)
+
+ import time
+
+ console.show_cursor(False)
+ for n in range(0, 101, 1):
+ bar.update(n)
+ console.print(bar)
+ console.file.write("\r")
+ time.sleep(0.05)
+ console.show_cursor(True)
+ console.print()
diff --git a/src/pip/_vendor/rich/prompt.py b/src/pip/_vendor/rich/prompt.py
new file mode 100644
index 000000000..2bd0a7724
--- /dev/null
+++ b/src/pip/_vendor/rich/prompt.py
@@ -0,0 +1,376 @@
+from typing import Any, Generic, List, Optional, TextIO, TypeVar, Union, overload
+
+from . import get_console
+from .console import Console
+from .text import Text, TextType
+
+PromptType = TypeVar("PromptType")
+DefaultType = TypeVar("DefaultType")
+
+
+class PromptError(Exception):
+ """Exception base class for prompt related errors."""
+
+
+class InvalidResponse(PromptError):
+ """Exception to indicate a response was invalid. Raise this within process_response() to indicate an error
+ and provide an error message.
+
+ Args:
+ message (Union[str, Text]): Error message.
+ """
+
+ def __init__(self, message: TextType) -> None:
+ self.message = message
+
+ def __rich__(self) -> TextType:
+ return self.message
+
+
+class PromptBase(Generic[PromptType]):
+ """Ask the user for input until a valid response is received. This is the base class, see one of
+ the concrete classes for examples.
+
+ Args:
+ prompt (TextType, optional): Prompt text. Defaults to "".
+ console (Console, optional): A Console instance or None to use global console. Defaults to None.
+ password (bool, optional): Enable password input. Defaults to False.
+ choices (List[str], optional): A list of valid choices. Defaults to None.
+ show_default (bool, optional): Show default in prompt. Defaults to True.
+ show_choices (bool, optional): Show choices in prompt. Defaults to True.
+ """
+
+ response_type: type = str
+
+ validate_error_message = "[prompt.invalid]Please enter a valid value"
+ illegal_choice_message = (
+ "[prompt.invalid.choice]Please select one of the available options"
+ )
+ prompt_suffix = ": "
+
+ choices: Optional[List[str]] = None
+
+ def __init__(
+ self,
+ prompt: TextType = "",
+ *,
+ console: Optional[Console] = None,
+ password: bool = False,
+ choices: Optional[List[str]] = None,
+ show_default: bool = True,
+ show_choices: bool = True,
+ ) -> None:
+ self.console = console or get_console()
+ self.prompt = (
+ Text.from_markup(prompt, style="prompt")
+ if isinstance(prompt, str)
+ else prompt
+ )
+ self.password = password
+ if choices is not None:
+ self.choices = choices
+ self.show_default = show_default
+ self.show_choices = show_choices
+
+ @classmethod
+ @overload
+ def ask(
+ cls,
+ prompt: TextType = "",
+ *,
+ console: Optional[Console] = None,
+ password: bool = False,
+ choices: Optional[List[str]] = None,
+ show_default: bool = True,
+ show_choices: bool = True,
+ default: DefaultType,
+ stream: Optional[TextIO] = None,
+ ) -> Union[DefaultType, PromptType]:
+ ...
+
+ @classmethod
+ @overload
+ def ask(
+ cls,
+ prompt: TextType = "",
+ *,
+ console: Optional[Console] = None,
+ password: bool = False,
+ choices: Optional[List[str]] = None,
+ show_default: bool = True,
+ show_choices: bool = True,
+ stream: Optional[TextIO] = None,
+ ) -> PromptType:
+ ...
+
+ @classmethod
+ def ask(
+ cls,
+ prompt: TextType = "",
+ *,
+ console: Optional[Console] = None,
+ password: bool = False,
+ choices: Optional[List[str]] = None,
+ show_default: bool = True,
+ show_choices: bool = True,
+ default: Any = ...,
+ stream: Optional[TextIO] = None,
+ ) -> Any:
+ """Shortcut to construct and run a prompt loop and return the result.
+
+ Example:
+ >>> filename = Prompt.ask("Enter a filename")
+
+ Args:
+ prompt (TextType, optional): Prompt text. Defaults to "".
+ console (Console, optional): A Console instance or None to use global console. Defaults to None.
+ password (bool, optional): Enable password input. Defaults to False.
+ choices (List[str], optional): A list of valid choices. Defaults to None.
+ show_default (bool, optional): Show default in prompt. Defaults to True.
+ show_choices (bool, optional): Show choices in prompt. Defaults to True.
+ stream (TextIO, optional): Optional text file open for reading to get input. Defaults to None.
+ """
+ _prompt = cls(
+ prompt,
+ console=console,
+ password=password,
+ choices=choices,
+ show_default=show_default,
+ show_choices=show_choices,
+ )
+ return _prompt(default=default, stream=stream)
+
+ def render_default(self, default: DefaultType) -> Text:
+ """Turn the supplied default in to a Text instance.
+
+ Args:
+ default (DefaultType): Default value.
+
+ Returns:
+ Text: Text containing rendering of default value.
+ """
+ return Text(f"({default})", "prompt.default")
+
+ def make_prompt(self, default: DefaultType) -> Text:
+ """Make prompt text.
+
+ Args:
+ default (DefaultType): Default value.
+
+ Returns:
+ Text: Text to display in prompt.
+ """
+ prompt = self.prompt.copy()
+ prompt.end = ""
+
+ if self.show_choices and self.choices:
+ _choices = "/".join(self.choices)
+ choices = f"[{_choices}]"
+ prompt.append(" ")
+ prompt.append(choices, "prompt.choices")
+
+ if (
+ default != ...
+ and self.show_default
+ and isinstance(default, (str, self.response_type))
+ ):
+ prompt.append(" ")
+ _default = self.render_default(default)
+ prompt.append(_default)
+
+ prompt.append(self.prompt_suffix)
+
+ return prompt
+
+ @classmethod
+ def get_input(
+ cls,
+ console: Console,
+ prompt: TextType,
+ password: bool,
+ stream: Optional[TextIO] = None,
+ ) -> str:
+ """Get input from user.
+
+ Args:
+ console (Console): Console instance.
+ prompt (TextType): Prompt text.
+ password (bool): Enable password entry.
+
+ Returns:
+ str: String from user.
+ """
+ return console.input(prompt, password=password, stream=stream)
+
+ def check_choice(self, value: str) -> bool:
+ """Check value is in the list of valid choices.
+
+ Args:
+ value (str): Value entered by user.
+
+ Returns:
+ bool: True if choice was valid, otherwise False.
+ """
+ assert self.choices is not None
+ return value.strip() in self.choices
+
+ def process_response(self, value: str) -> PromptType:
+ """Process response from user, convert to prompt type.
+
+ Args:
+ value (str): String typed by user.
+
+ Raises:
+ InvalidResponse: If ``value`` is invalid.
+
+ Returns:
+ PromptType: The value to be returned from ask method.
+ """
+ value = value.strip()
+ try:
+ return_value: PromptType = self.response_type(value)
+ except ValueError:
+ raise InvalidResponse(self.validate_error_message)
+
+ if self.choices is not None and not self.check_choice(value):
+ raise InvalidResponse(self.illegal_choice_message)
+
+ return return_value
+
+ def on_validate_error(self, value: str, error: InvalidResponse) -> None:
+ """Called to handle validation error.
+
+ Args:
+ value (str): String entered by user.
+ error (InvalidResponse): Exception instance the initiated the error.
+ """
+ self.console.print(error)
+
+ def pre_prompt(self) -> None:
+ """Hook to display something before the prompt."""
+
+ @overload
+ def __call__(self, *, stream: Optional[TextIO] = None) -> PromptType:
+ ...
+
+ @overload
+ def __call__(
+ self, *, default: DefaultType, stream: Optional[TextIO] = None
+ ) -> Union[PromptType, DefaultType]:
+ ...
+
+ def __call__(self, *, default: Any = ..., stream: Optional[TextIO] = None) -> Any:
+ """Run the prompt loop.
+
+ Args:
+ default (Any, optional): Optional default value.
+
+ Returns:
+ PromptType: Processed value.
+ """
+ while True:
+ self.pre_prompt()
+ prompt = self.make_prompt(default)
+ value = self.get_input(self.console, prompt, self.password, stream=stream)
+ if value == "" and default != ...:
+ return default
+ try:
+ return_value = self.process_response(value)
+ except InvalidResponse as error:
+ self.on_validate_error(value, error)
+ continue
+ else:
+ return return_value
+
+
+class Prompt(PromptBase[str]):
+ """A prompt that returns a str.
+
+ Example:
+ >>> name = Prompt.ask("Enter your name")
+
+
+ """
+
+ response_type = str
+
+
+class IntPrompt(PromptBase[int]):
+ """A prompt that returns an integer.
+
+ Example:
+ >>> burrito_count = IntPrompt.ask("How many burritos do you want to order")
+
+ """
+
+ response_type = int
+ validate_error_message = "[prompt.invalid]Please enter a valid integer number"
+
+
+class FloatPrompt(PromptBase[int]):
+ """A prompt that returns a float.
+
+ Example:
+ >>> temperature = FloatPrompt.ask("Enter desired temperature")
+
+ """
+
+ response_type = float
+ validate_error_message = "[prompt.invalid]Please enter a number"
+
+
+class Confirm(PromptBase[bool]):
+ """A yes / no confirmation prompt.
+
+ Example:
+ >>> if Confirm.ask("Continue"):
+ run_job()
+
+ """
+
+ response_type = bool
+ validate_error_message = "[prompt.invalid]Please enter Y or N"
+ choices: List[str] = ["y", "n"]
+
+ def render_default(self, default: DefaultType) -> Text:
+ """Render the default as (y) or (n) rather than True/False."""
+ yes, no = self.choices
+ return Text(f"({yes})" if default else f"({no})", style="prompt.default")
+
+ def process_response(self, value: str) -> bool:
+ """Convert choices to a bool."""
+ value = value.strip().lower()
+ if value not in self.choices:
+ raise InvalidResponse(self.validate_error_message)
+ return value == self.choices[0]
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from pip._vendor.rich import print
+
+ if Confirm.ask("Run [i]prompt[/i] tests?", default=True):
+ while True:
+ result = IntPrompt.ask(
+ ":rocket: Enter a number between [b]1[/b] and [b]10[/b]", default=5
+ )
+ if result >= 1 and result <= 10:
+ break
+ print(":pile_of_poo: [prompt.invalid]Number must be between 1 and 10")
+ print(f"number={result}")
+
+ while True:
+ password = Prompt.ask(
+ "Please enter a password [cyan](must be at least 5 characters)",
+ password=True,
+ )
+ if len(password) >= 5:
+ break
+ print("[prompt.invalid]password too short")
+ print(f"password={password!r}")
+
+ fruit = Prompt.ask("Enter a fruit", choices=["apple", "orange", "pear"])
+ print(f"fruit={fruit!r}")
+
+ else:
+ print("[b]OK :loudly_crying_face:")
diff --git a/src/pip/_vendor/rich/protocol.py b/src/pip/_vendor/rich/protocol.py
new file mode 100644
index 000000000..12ab23713
--- /dev/null
+++ b/src/pip/_vendor/rich/protocol.py
@@ -0,0 +1,42 @@
+from typing import Any, cast, Set, TYPE_CHECKING
+from inspect import isclass
+
+if TYPE_CHECKING:
+ from pip._vendor.rich.console import RenderableType
+
+_GIBBERISH = """aihwerij235234ljsdnp34ksodfipwoe234234jlskjdf"""
+
+
+def is_renderable(check_object: Any) -> bool:
+ """Check if an object may be rendered by Rich."""
+ return (
+ isinstance(check_object, str)
+ or hasattr(check_object, "__rich__")
+ or hasattr(check_object, "__rich_console__")
+ )
+
+
+def rich_cast(renderable: object) -> "RenderableType":
+ """Cast an object to a renderable by calling __rich__ if present.
+
+ Args:
+ renderable (object): A potentially renderable object
+
+ Returns:
+ object: The result of recursively calling __rich__.
+ """
+ from pip._vendor.rich.console import RenderableType
+
+ rich_visited_set: Set[type] = set() # Prevent potential infinite loop
+ while hasattr(renderable, "__rich__") and not isclass(renderable):
+ # Detect object which claim to have all the attributes
+ if hasattr(renderable, _GIBBERISH):
+ return repr(renderable)
+ cast_method = getattr(renderable, "__rich__")
+ renderable = cast_method()
+ renderable_type = type(renderable)
+ if renderable_type in rich_visited_set:
+ break
+ rich_visited_set.add(renderable_type)
+
+ return cast(RenderableType, renderable)
diff --git a/src/pip/_vendor/rich/py.typed b/src/pip/_vendor/rich/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pip/_vendor/rich/py.typed
diff --git a/src/pip/_vendor/rich/region.py b/src/pip/_vendor/rich/region.py
new file mode 100644
index 000000000..75b3631c3
--- /dev/null
+++ b/src/pip/_vendor/rich/region.py
@@ -0,0 +1,10 @@
+from typing import NamedTuple
+
+
+class Region(NamedTuple):
+ """Defines a rectangular region of the screen."""
+
+ x: int
+ y: int
+ width: int
+ height: int
diff --git a/src/pip/_vendor/rich/repr.py b/src/pip/_vendor/rich/repr.py
new file mode 100644
index 000000000..36966e70f
--- /dev/null
+++ b/src/pip/_vendor/rich/repr.py
@@ -0,0 +1,152 @@
+from functools import partial
+import inspect
+import sys
+
+from typing import (
+ Any,
+ Callable,
+ Iterable,
+ List,
+ Optional,
+ overload,
+ Union,
+ Tuple,
+ Type,
+ TypeVar,
+)
+
+
+T = TypeVar("T")
+
+
+Result = Iterable[Union[Any, Tuple[Any], Tuple[str, Any], Tuple[str, Any, Any]]]
+RichReprResult = Result
+
+
+class ReprError(Exception):
+ """An error occurred when attempting to build a repr."""
+
+
+@overload
+def auto(cls: Optional[Type[T]]) -> Type[T]:
+ ...
+
+
+@overload
+def auto(*, angular: bool = False) -> Callable[[Type[T]], Type[T]]:
+ ...
+
+
+def auto(
+ cls: Optional[Type[T]] = None, *, angular: Optional[bool] = None
+) -> Union[Type[T], Callable[[Type[T]], Type[T]]]:
+ """Class decorator to create __repr__ from __rich_repr__"""
+
+ def do_replace(cls: Type[T], angular: Optional[bool] = None) -> Type[T]:
+ def auto_repr(self: T) -> str:
+ """Create repr string from __rich_repr__"""
+ repr_str: List[str] = []
+ append = repr_str.append
+
+ angular: bool = getattr(self.__rich_repr__, "angular", False) # type: ignore[attr-defined]
+ for arg in self.__rich_repr__(): # type: ignore[attr-defined]
+ if isinstance(arg, tuple):
+ if len(arg) == 1:
+ append(repr(arg[0]))
+ else:
+ key, value, *default = arg
+ if key is None:
+ append(repr(value))
+ else:
+ if len(default) and default[0] == value:
+ continue
+ append(f"{key}={value!r}")
+ else:
+ append(repr(arg))
+ if angular:
+ return f"<{self.__class__.__name__} {' '.join(repr_str)}>"
+ else:
+ return f"{self.__class__.__name__}({', '.join(repr_str)})"
+
+ def auto_rich_repr(self: Type[T]) -> Result:
+ """Auto generate __rich_rep__ from signature of __init__"""
+ try:
+ signature = inspect.signature(self.__init__)
+ for name, param in signature.parameters.items():
+ if param.kind == param.POSITIONAL_ONLY:
+ yield getattr(self, name)
+ elif param.kind in (
+ param.POSITIONAL_OR_KEYWORD,
+ param.KEYWORD_ONLY,
+ ):
+ if param.default == param.empty:
+ yield getattr(self, param.name)
+ else:
+ yield param.name, getattr(self, param.name), param.default
+ except Exception as error:
+ raise ReprError(
+ f"Failed to auto generate __rich_repr__; {error}"
+ ) from None
+
+ if not hasattr(cls, "__rich_repr__"):
+ auto_rich_repr.__doc__ = "Build a rich repr"
+ cls.__rich_repr__ = auto_rich_repr # type: ignore[attr-defined]
+
+ auto_repr.__doc__ = "Return repr(self)"
+ cls.__repr__ = auto_repr # type: ignore[assignment]
+ if angular is not None:
+ cls.__rich_repr__.angular = angular # type: ignore[attr-defined]
+ return cls
+
+ if cls is None:
+ return partial(do_replace, angular=angular)
+ else:
+ return do_replace(cls, angular=angular)
+
+
+@overload
+def rich_repr(cls: Optional[Type[T]]) -> Type[T]:
+ ...
+
+
+@overload
+def rich_repr(*, angular: bool = False) -> Callable[[Type[T]], Type[T]]:
+ ...
+
+
+def rich_repr(
+ cls: Optional[Type[T]] = None, *, angular: bool = False
+) -> Union[Type[T], Callable[[Type[T]], Type[T]]]:
+ if cls is None:
+ return auto(angular=angular)
+ else:
+ return auto(cls)
+
+
+if __name__ == "__main__":
+
+ @auto
+ class Foo:
+ def __rich_repr__(self) -> Result:
+ yield "foo"
+ yield "bar", {"shopping": ["eggs", "ham", "pineapple"]}
+ yield "buy", "hand sanitizer"
+
+ foo = Foo()
+ from pip._vendor.rich.console import Console
+
+ console = Console()
+
+ console.rule("Standard repr")
+ console.print(foo)
+
+ console.print(foo, width=60)
+ console.print(foo, width=30)
+
+ console.rule("Angular repr")
+ Foo.__rich_repr__.angular = True # type: ignore[attr-defined]
+
+ console.print(foo)
+
+ console.print(foo, width=60)
+ console.print(foo, width=30)
diff --git a/src/pip/_vendor/rich/rule.py b/src/pip/_vendor/rich/rule.py
new file mode 100644
index 000000000..0b78f7a4e
--- /dev/null
+++ b/src/pip/_vendor/rich/rule.py
@@ -0,0 +1,134 @@
+from typing import Union
+
+from .align import AlignMethod
+from .cells import cell_len, set_cell_size
+from .console import Console, ConsoleOptions, RenderResult
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .style import Style
+from .text import Text
+
+
+class Rule(JupyterMixin):
+ """A console renderable to draw a horizontal rule (line).
+
+ Args:
+ title (Union[str, Text], optional): Text to render in the rule. Defaults to "".
+ characters (str, optional): Character(s) used to draw the line. Defaults to "─".
+ style (StyleType, optional): Style of Rule. Defaults to "rule.line".
+ end (str, optional): Character at end of Rule. defaults to "\\\\n"
+ align (str, optional): How to align the title, one of "left", "center", or "right". Defaults to "center".
+ """
+
+ def __init__(
+ self,
+ title: Union[str, Text] = "",
+ *,
+ characters: str = "─",
+ style: Union[str, Style] = "rule.line",
+ end: str = "\n",
+ align: AlignMethod = "center",
+ ) -> None:
+ if cell_len(characters) < 1:
+ raise ValueError(
+ "'characters' argument must have a cell width of at least 1"
+ )
+ if align not in ("left", "center", "right"):
+ raise ValueError(
+ f'invalid value for align, expected "left", "center", "right" (not {align!r})'
+ )
+ self.title = title
+ self.characters = characters
+ self.style = style
+ self.end = end
+ self.align = align
+
+ def __repr__(self) -> str:
+ return f"Rule({self.title!r}, {self.characters!r})"
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ width = options.max_width
+
+ # Python3.6 doesn't have an isascii method on str
+ isascii = getattr(str, "isascii", None) or (
+ lambda s: all(ord(c) < 128 for c in s)
+ )
+ characters = (
+ "-"
+ if (options.ascii_only and not isascii(self.characters))
+ else self.characters
+ )
+
+ chars_len = cell_len(characters)
+ if not self.title:
+ yield self._rule_line(chars_len, width)
+ return
+
+ if isinstance(self.title, Text):
+ title_text = self.title
+ else:
+ title_text = console.render_str(self.title, style="rule.text")
+
+ title_text.plain = title_text.plain.replace("\n", " ")
+ title_text.expand_tabs()
+
+ required_space = 4 if self.align == "center" else 2
+ truncate_width = max(0, width - required_space)
+ if not truncate_width:
+ yield self._rule_line(chars_len, width)
+ return
+
+ rule_text = Text(end=self.end)
+ if self.align == "center":
+ title_text.truncate(truncate_width, overflow="ellipsis")
+ side_width = (width - cell_len(title_text.plain)) // 2
+ left = Text(characters * (side_width // chars_len + 1))
+ left.truncate(side_width - 1)
+ right_length = width - cell_len(left.plain) - cell_len(title_text.plain)
+ right = Text(characters * (side_width // chars_len + 1))
+ right.truncate(right_length)
+ rule_text.append(left.plain + " ", self.style)
+ rule_text.append(title_text)
+ rule_text.append(" " + right.plain, self.style)
+ elif self.align == "left":
+ title_text.truncate(truncate_width, overflow="ellipsis")
+ rule_text.append(title_text)
+ rule_text.append(" ")
+ rule_text.append(characters * (width - rule_text.cell_len), self.style)
+ elif self.align == "right":
+ title_text.truncate(truncate_width, overflow="ellipsis")
+ rule_text.append(characters * (width - title_text.cell_len - 1), self.style)
+ rule_text.append(" ")
+ rule_text.append(title_text)
+
+ rule_text.plain = set_cell_size(rule_text.plain, width)
+ yield rule_text
+
+ def _rule_line(self, chars_len: int, width: int) -> Text:
+ rule_text = Text(self.characters * ((width // chars_len) + 1), self.style)
+ rule_text.truncate(width)
+ rule_text.plain = set_cell_size(rule_text.plain, width)
+ return rule_text
+
+ def __rich_measure__(
+ self, console: Console, options: ConsoleOptions
+ ) -> Measurement:
+ return Measurement(1, 1)
+
+
+if __name__ == "__main__": # pragma: no cover
+ import sys
+
+ from pip._vendor.rich.console import Console
+
+ try:
+ text = sys.argv[1]
+ except IndexError:
+ text = "Hello, World"
+ console = Console()
+ console.print(Rule(title=text))
+
+ console = Console()
+ console.print(Rule("foo"), width=4)
diff --git a/src/pip/_vendor/rich/scope.py b/src/pip/_vendor/rich/scope.py
new file mode 100644
index 000000000..6822b8ca5
--- /dev/null
+++ b/src/pip/_vendor/rich/scope.py
@@ -0,0 +1,86 @@
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Optional, Tuple
+
+from .highlighter import ReprHighlighter
+from .panel import Panel
+from .pretty import Pretty
+from .table import Table
+from .text import Text, TextType
+
+if TYPE_CHECKING:
+ from .console import ConsoleRenderable
+
+
+def render_scope(
+ scope: "Mapping[str, Any]",
+ *,
+ title: Optional[TextType] = None,
+ sort_keys: bool = True,
+ indent_guides: bool = False,
+ max_length: Optional[int] = None,
+ max_string: Optional[int] = None,
+) -> "ConsoleRenderable":
+ """Render python variables in a given scope.
+
+ Args:
+ scope (Mapping): A mapping containing variable names and values.
+ title (str, optional): Optional title. Defaults to None.
+ sort_keys (bool, optional): Enable sorting of items. Defaults to True.
+ indent_guides (bool, optional): Enable indentaton guides. Defaults to False.
+ max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to None.
+ max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None.
+
+ Returns:
+ ConsoleRenderable: A renderable object.
+ """
+ highlighter = ReprHighlighter()
+ items_table = Table.grid(padding=(0, 1), expand=False)
+ items_table.add_column(justify="right")
+
+ def sort_items(item: Tuple[str, Any]) -> Tuple[bool, str]:
+ """Sort special variables first, then alphabetically."""
+ key, _ = item
+ return (not key.startswith("__"), key.lower())
+
+ items = sorted(scope.items(), key=sort_items) if sort_keys else scope.items()
+ for key, value in items:
+ key_text = Text.assemble(
+ (key, "scope.key.special" if key.startswith("__") else "scope.key"),
+ (" =", "scope.equals"),
+ )
+ items_table.add_row(
+ key_text,
+ Pretty(
+ value,
+ highlighter=highlighter,
+ indent_guides=indent_guides,
+ max_length=max_length,
+ max_string=max_string,
+ ),
+ )
+ return Panel.fit(
+ items_table,
+ title=title,
+ border_style="scope.border",
+ padding=(0, 1),
+ )
+
+
+if __name__ == "__main__": # pragma: no cover
+ from pip._vendor.rich import print
+
+ print()
+
+ def test(foo: float, bar: float) -> None:
+ list_of_things = [1, 2, 3, None, 4, True, False, "Hello World"]
+ dict_of_things = {
+ "version": "1.1",
+ "method": "confirmFruitPurchase",
+ "params": [["apple", "orange", "mangoes", "pomelo"], 1.123],
+ "id": "194521489",
+ }
+ print(render_scope(locals(), title="[i]locals", sort_keys=False))
+
+ test(20.3423, 3.1427)
+ print()
diff --git a/src/pip/_vendor/rich/screen.py b/src/pip/_vendor/rich/screen.py
new file mode 100644
index 000000000..7f416e1e7
--- /dev/null
+++ b/src/pip/_vendor/rich/screen.py
@@ -0,0 +1,54 @@
+from typing import Optional, TYPE_CHECKING
+
+from .segment import Segment
+from .style import StyleType
+from ._loop import loop_last
+
+
+if TYPE_CHECKING:
+ from .console import (
+ Console,
+ ConsoleOptions,
+ RenderResult,
+ RenderableType,
+ Group,
+ )
+
+
+class Screen:
+ """A renderable that fills the terminal screen and crops excess.
+
+ Args:
+ renderable (RenderableType): Child renderable.
+ style (StyleType, optional): Optional background style. Defaults to None.
+ """
+
+ renderable: "RenderableType"
+
+ def __init__(
+ self,
+ *renderables: "RenderableType",
+ style: Optional[StyleType] = None,
+ application_mode: bool = False,
+ ) -> None:
+ from pip._vendor.rich.console import Group
+
+ self.renderable = Group(*renderables)
+ self.style = style
+ self.application_mode = application_mode
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ width, height = options.size
+ style = console.get_style(self.style) if self.style else None
+ render_options = options.update(width=width, height=height)
+ lines = console.render_lines(
+ self.renderable or "", render_options, style=style, pad=True
+ )
+ lines = Segment.set_shape(lines, width, height, style=style)
+ new_line = Segment("\n\r") if self.application_mode else Segment.line()
+ for last, line in loop_last(lines):
+ yield from line
+ if not last:
+ yield new_line
diff --git a/src/pip/_vendor/rich/segment.py b/src/pip/_vendor/rich/segment.py
new file mode 100644
index 000000000..1ea5435ad
--- /dev/null
+++ b/src/pip/_vendor/rich/segment.py
@@ -0,0 +1,739 @@
+from enum import IntEnum
+from functools import lru_cache
+from itertools import filterfalse
+from logging import getLogger
+from operator import attrgetter
+from typing import (
+ TYPE_CHECKING,
+ Dict,
+ Iterable,
+ List,
+ NamedTuple,
+ Optional,
+ Sequence,
+ Tuple,
+ Type,
+ Union,
+)
+
+from .cells import (
+ _is_single_cell_widths,
+ cached_cell_len,
+ cell_len,
+ get_character_cell_size,
+ set_cell_size,
+)
+from .repr import Result, rich_repr
+from .style import Style
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderResult
+
+log = getLogger("rich")
+
+
+class ControlType(IntEnum):
+ """Non-printable control codes which typically translate to ANSI codes."""
+
+ BELL = 1
+ CARRIAGE_RETURN = 2
+ HOME = 3
+ CLEAR = 4
+ SHOW_CURSOR = 5
+ HIDE_CURSOR = 6
+ ENABLE_ALT_SCREEN = 7
+ DISABLE_ALT_SCREEN = 8
+ CURSOR_UP = 9
+ CURSOR_DOWN = 10
+ CURSOR_FORWARD = 11
+ CURSOR_BACKWARD = 12
+ CURSOR_MOVE_TO_COLUMN = 13
+ CURSOR_MOVE_TO = 14
+ ERASE_IN_LINE = 15
+ SET_WINDOW_TITLE = 16
+
+
+ControlCode = Union[
+ Tuple[ControlType],
+ Tuple[ControlType, Union[int, str]],
+ Tuple[ControlType, int, int],
+]
+
+
+@rich_repr()
+class Segment(NamedTuple):
+ """A piece of text with associated style. Segments are produced by the Console render process and
+ are ultimately converted in to strings to be written to the terminal.
+
+ Args:
+ text (str): A piece of text.
+ style (:class:`~rich.style.Style`, optional): An optional style to apply to the text.
+ control (Tuple[ControlCode], optional): Optional sequence of control codes.
+
+ Attributes:
+ cell_length (int): The cell length of this Segment.
+ """
+
+ text: str
+ style: Optional[Style] = None
+ control: Optional[Sequence[ControlCode]] = None
+
+ @property
+ def cell_length(self) -> int:
+ """The number of terminal cells required to display self.text.
+
+ Returns:
+ int: A number of cells.
+ """
+ text, _style, control = self
+ return 0 if control else cell_len(text)
+
+ def __rich_repr__(self) -> Result:
+ yield self.text
+ if self.control is None:
+ if self.style is not None:
+ yield self.style
+ else:
+ yield self.style
+ yield self.control
+
+ def __bool__(self) -> bool:
+ """Check if the segment contains text."""
+ return bool(self.text)
+
+ @property
+ def is_control(self) -> bool:
+ """Check if the segment contains control codes."""
+ return self.control is not None
+
+ @classmethod
+ @lru_cache(1024 * 16)
+ def _split_cells(cls, segment: "Segment", cut: int) -> Tuple["Segment", "Segment"]:
+
+ text, style, control = segment
+ _Segment = Segment
+
+ cell_length = segment.cell_length
+ if cut >= cell_length:
+ return segment, _Segment("", style, control)
+
+ cell_size = get_character_cell_size
+
+ pos = int((cut / cell_length) * len(text))
+
+ before = text[:pos]
+ cell_pos = cell_len(before)
+ if cell_pos == cut:
+ return (
+ _Segment(before, style, control),
+ _Segment(text[pos:], style, control),
+ )
+ while pos < len(text):
+ char = text[pos]
+ pos += 1
+ cell_pos += cell_size(char)
+ before = text[:pos]
+ if cell_pos == cut:
+ return (
+ _Segment(before, style, control),
+ _Segment(text[pos:], style, control),
+ )
+ if cell_pos > cut:
+ return (
+ _Segment(before[: pos - 1] + " ", style, control),
+ _Segment(" " + text[pos:], style, control),
+ )
+
+ raise AssertionError("Will never reach here")
+
+ def split_cells(self, cut: int) -> Tuple["Segment", "Segment"]:
+ """Split segment in to two segments at the specified column.
+
+ If the cut point falls in the middle of a 2-cell wide character then it is replaced
+ by two spaces, to preserve the display width of the parent segment.
+
+ Returns:
+ Tuple[Segment, Segment]: Two segments.
+ """
+ text, style, control = self
+
+ if _is_single_cell_widths(text):
+ # Fast path with all 1 cell characters
+ if cut >= len(text):
+ return self, Segment("", style, control)
+ return (
+ Segment(text[:cut], style, control),
+ Segment(text[cut:], style, control),
+ )
+
+ return self._split_cells(self, cut)
+
+ @classmethod
+ def line(cls) -> "Segment":
+ """Make a new line segment."""
+ return cls("\n")
+
+ @classmethod
+ def apply_style(
+ cls,
+ segments: Iterable["Segment"],
+ style: Optional[Style] = None,
+ post_style: Optional[Style] = None,
+ ) -> Iterable["Segment"]:
+ """Apply style(s) to an iterable of segments.
+
+ Returns an iterable of segments where the style is replaced by ``style + segment.style + post_style``.
+
+ Args:
+ segments (Iterable[Segment]): Segments to process.
+ style (Style, optional): Base style. Defaults to None.
+ post_style (Style, optional): Style to apply on top of segment style. Defaults to None.
+
+ Returns:
+ Iterable[Segments]: A new iterable of segments (possibly the same iterable).
+ """
+ result_segments = segments
+ if style:
+ apply = style.__add__
+ result_segments = (
+ cls(text, None if control else apply(_style), control)
+ for text, _style, control in result_segments
+ )
+ if post_style:
+ result_segments = (
+ cls(
+ text,
+ (
+ None
+ if control
+ else (_style + post_style if _style else post_style)
+ ),
+ control,
+ )
+ for text, _style, control in result_segments
+ )
+ return result_segments
+
+ @classmethod
+ def filter_control(
+ cls, segments: Iterable["Segment"], is_control: bool = False
+ ) -> Iterable["Segment"]:
+ """Filter segments by ``is_control`` attribute.
+
+ Args:
+ segments (Iterable[Segment]): An iterable of Segment instances.
+ is_control (bool, optional): is_control flag to match in search.
+
+ Returns:
+ Iterable[Segment]: And iterable of Segment instances.
+
+ """
+ if is_control:
+ return filter(attrgetter("control"), segments)
+ else:
+ return filterfalse(attrgetter("control"), segments)
+
+ @classmethod
+ def split_lines(cls, segments: Iterable["Segment"]) -> Iterable[List["Segment"]]:
+ """Split a sequence of segments in to a list of lines.
+
+ Args:
+ segments (Iterable[Segment]): Segments potentially containing line feeds.
+
+ Yields:
+ Iterable[List[Segment]]: Iterable of segment lists, one per line.
+ """
+ line: List[Segment] = []
+ append = line.append
+
+ for segment in segments:
+ if "\n" in segment.text and not segment.control:
+ text, style, _ = segment
+ while text:
+ _text, new_line, text = text.partition("\n")
+ if _text:
+ append(cls(_text, style))
+ if new_line:
+ yield line
+ line = []
+ append = line.append
+ else:
+ append(segment)
+ if line:
+ yield line
+
+ @classmethod
+ def split_and_crop_lines(
+ cls,
+ segments: Iterable["Segment"],
+ length: int,
+ style: Optional[Style] = None,
+ pad: bool = True,
+ include_new_lines: bool = True,
+ ) -> Iterable[List["Segment"]]:
+ """Split segments in to lines, and crop lines greater than a given length.
+
+ Args:
+ segments (Iterable[Segment]): An iterable of segments, probably
+ generated from console.render.
+ length (int): Desired line length.
+ style (Style, optional): Style to use for any padding.
+ pad (bool): Enable padding of lines that are less than `length`.
+
+ Returns:
+ Iterable[List[Segment]]: An iterable of lines of segments.
+ """
+ line: List[Segment] = []
+ append = line.append
+
+ adjust_line_length = cls.adjust_line_length
+ new_line_segment = cls("\n")
+
+ for segment in segments:
+ if "\n" in segment.text and not segment.control:
+ text, segment_style, _ = segment
+ while text:
+ _text, new_line, text = text.partition("\n")
+ if _text:
+ append(cls(_text, segment_style))
+ if new_line:
+ cropped_line = adjust_line_length(
+ line, length, style=style, pad=pad
+ )
+ if include_new_lines:
+ cropped_line.append(new_line_segment)
+ yield cropped_line
+ del line[:]
+ else:
+ append(segment)
+ if line:
+ yield adjust_line_length(line, length, style=style, pad=pad)
+
+ @classmethod
+ def adjust_line_length(
+ cls,
+ line: List["Segment"],
+ length: int,
+ style: Optional[Style] = None,
+ pad: bool = True,
+ ) -> List["Segment"]:
+ """Adjust a line to a given width (cropping or padding as required).
+
+ Args:
+ segments (Iterable[Segment]): A list of segments in a single line.
+ length (int): The desired width of the line.
+ style (Style, optional): The style of padding if used (space on the end). Defaults to None.
+ pad (bool, optional): Pad lines with spaces if they are shorter than `length`. Defaults to True.
+
+ Returns:
+ List[Segment]: A line of segments with the desired length.
+ """
+ line_length = sum(segment.cell_length for segment in line)
+ new_line: List[Segment]
+
+ if line_length < length:
+ if pad:
+ new_line = line + [cls(" " * (length - line_length), style)]
+ else:
+ new_line = line[:]
+ elif line_length > length:
+ new_line = []
+ append = new_line.append
+ line_length = 0
+ for segment in line:
+ segment_length = segment.cell_length
+ if line_length + segment_length < length or segment.control:
+ append(segment)
+ line_length += segment_length
+ else:
+ text, segment_style, _ = segment
+ text = set_cell_size(text, length - line_length)
+ append(cls(text, segment_style))
+ break
+ else:
+ new_line = line[:]
+ return new_line
+
+ @classmethod
+ def get_line_length(cls, line: List["Segment"]) -> int:
+ """Get the length of list of segments.
+
+ Args:
+ line (List[Segment]): A line encoded as a list of Segments (assumes no '\\\\n' characters),
+
+ Returns:
+ int: The length of the line.
+ """
+ _cell_len = cell_len
+ return sum(_cell_len(segment.text) for segment in line)
+
+ @classmethod
+ def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]:
+ """Get the shape (enclosing rectangle) of a list of lines.
+
+ Args:
+ lines (List[List[Segment]]): A list of lines (no '\\\\n' characters).
+
+ Returns:
+ Tuple[int, int]: Width and height in characters.
+ """
+ get_line_length = cls.get_line_length
+ max_width = max(get_line_length(line) for line in lines) if lines else 0
+ return (max_width, len(lines))
+
+ @classmethod
+ def set_shape(
+ cls,
+ lines: List[List["Segment"]],
+ width: int,
+ height: Optional[int] = None,
+ style: Optional[Style] = None,
+ new_lines: bool = False,
+ ) -> List[List["Segment"]]:
+ """Set the shape of a list of lines (enclosing rectangle).
+
+ Args:
+ lines (List[List[Segment]]): A list of lines.
+ width (int): Desired width.
+ height (int, optional): Desired height or None for no change.
+ style (Style, optional): Style of any padding added.
+ new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
+
+ Returns:
+ List[List[Segment]]: New list of lines.
+ """
+ _height = height or len(lines)
+
+ blank = (
+ [cls(" " * width + "\n", style)] if new_lines else [cls(" " * width, style)]
+ )
+
+ adjust_line_length = cls.adjust_line_length
+ shaped_lines = lines[:_height]
+ shaped_lines[:] = [
+ adjust_line_length(line, width, style=style) for line in lines
+ ]
+ if len(shaped_lines) < _height:
+ shaped_lines.extend([blank] * (_height - len(shaped_lines)))
+ return shaped_lines
+
+ @classmethod
+ def align_top(
+ cls: Type["Segment"],
+ lines: List[List["Segment"]],
+ width: int,
+ height: int,
+ style: Style,
+ new_lines: bool = False,
+ ) -> List[List["Segment"]]:
+ """Aligns lines to top (adds extra lines to bottom as required).
+
+ Args:
+ lines (List[List[Segment]]): A list of lines.
+ width (int): Desired width.
+ height (int, optional): Desired height or None for no change.
+ style (Style): Style of any padding added.
+ new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
+
+ Returns:
+ List[List[Segment]]: New list of lines.
+ """
+ extra_lines = height - len(lines)
+ if not extra_lines:
+ return lines[:]
+ lines = lines[:height]
+ blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style)
+ lines = lines + [[blank]] * extra_lines
+ return lines
+
+ @classmethod
+ def align_bottom(
+ cls: Type["Segment"],
+ lines: List[List["Segment"]],
+ width: int,
+ height: int,
+ style: Style,
+ new_lines: bool = False,
+ ) -> List[List["Segment"]]:
+ """Aligns render to bottom (adds extra lines above as required).
+
+ Args:
+ lines (List[List[Segment]]): A list of lines.
+ width (int): Desired width.
+ height (int, optional): Desired height or None for no change.
+ style (Style): Style of any padding added. Defaults to None.
+ new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
+
+ Returns:
+ List[List[Segment]]: New list of lines.
+ """
+ extra_lines = height - len(lines)
+ if not extra_lines:
+ return lines[:]
+ lines = lines[:height]
+ blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style)
+ lines = [[blank]] * extra_lines + lines
+ return lines
+
+ @classmethod
+ def align_middle(
+ cls: Type["Segment"],
+ lines: List[List["Segment"]],
+ width: int,
+ height: int,
+ style: Style,
+ new_lines: bool = False,
+ ) -> List[List["Segment"]]:
+ """Aligns lines to middle (adds extra lines to above and below as required).
+
+ Args:
+ lines (List[List[Segment]]): A list of lines.
+ width (int): Desired width.
+ height (int, optional): Desired height or None for no change.
+ style (Style): Style of any padding added.
+ new_lines (bool, optional): Padded lines should include "\n". Defaults to False.
+
+ Returns:
+ List[List[Segment]]: New list of lines.
+ """
+ extra_lines = height - len(lines)
+ if not extra_lines:
+ return lines[:]
+ lines = lines[:height]
+ blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style)
+ top_lines = extra_lines // 2
+ bottom_lines = extra_lines - top_lines
+ lines = [[blank]] * top_lines + lines + [[blank]] * bottom_lines
+ return lines
+
+ @classmethod
+ def simplify(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
+ """Simplify an iterable of segments by combining contiguous segments with the same style.
+
+ Args:
+ segments (Iterable[Segment]): An iterable of segments.
+
+ Returns:
+ Iterable[Segment]: A possibly smaller iterable of segments that will render the same way.
+ """
+ iter_segments = iter(segments)
+ try:
+ last_segment = next(iter_segments)
+ except StopIteration:
+ return
+
+ _Segment = Segment
+ for segment in iter_segments:
+ if last_segment.style == segment.style and not segment.control:
+ last_segment = _Segment(
+ last_segment.text + segment.text, last_segment.style
+ )
+ else:
+ yield last_segment
+ last_segment = segment
+ yield last_segment
+
+ @classmethod
+ def strip_links(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
+ """Remove all links from an iterable of styles.
+
+ Args:
+ segments (Iterable[Segment]): An iterable segments.
+
+ Yields:
+ Segment: Segments with link removed.
+ """
+ for segment in segments:
+ if segment.control or segment.style is None:
+ yield segment
+ else:
+ text, style, _control = segment
+ yield cls(text, style.update_link(None) if style else None)
+
+ @classmethod
+ def strip_styles(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
+ """Remove all styles from an iterable of segments.
+
+ Args:
+ segments (Iterable[Segment]): An iterable segments.
+
+ Yields:
+ Segment: Segments with styles replace with None
+ """
+ for text, _style, control in segments:
+ yield cls(text, None, control)
+
+ @classmethod
+ def remove_color(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
+ """Remove all color from an iterable of segments.
+
+ Args:
+ segments (Iterable[Segment]): An iterable segments.
+
+ Yields:
+ Segment: Segments with colorless style.
+ """
+
+ cache: Dict[Style, Style] = {}
+ for text, style, control in segments:
+ if style:
+ colorless_style = cache.get(style)
+ if colorless_style is None:
+ colorless_style = style.without_color
+ cache[style] = colorless_style
+ yield cls(text, colorless_style, control)
+ else:
+ yield cls(text, None, control)
+
+ @classmethod
+ def divide(
+ cls, segments: Iterable["Segment"], cuts: Iterable[int]
+ ) -> Iterable[List["Segment"]]:
+ """Divides an iterable of segments in to portions.
+
+ Args:
+ cuts (Iterable[int]): Cell positions where to divide.
+
+ Yields:
+ [Iterable[List[Segment]]]: An iterable of Segments in List.
+ """
+ split_segments: List["Segment"] = []
+ add_segment = split_segments.append
+
+ iter_cuts = iter(cuts)
+
+ while True:
+ cut = next(iter_cuts, -1)
+ if cut == -1:
+ return []
+ if cut != 0:
+ break
+ yield []
+ pos = 0
+
+ segments_clear = split_segments.clear
+ segments_copy = split_segments.copy
+
+ _cell_len = cached_cell_len
+ for segment in segments:
+ text, _style, control = segment
+ while text:
+ end_pos = pos if control else pos + _cell_len(text)
+ if end_pos < cut:
+ add_segment(segment)
+ pos = end_pos
+ break
+
+ if end_pos == cut:
+ add_segment(segment)
+ yield segments_copy()
+ segments_clear()
+ pos = end_pos
+
+ cut = next(iter_cuts, -1)
+ if cut == -1:
+ if split_segments:
+ yield segments_copy()
+ return
+
+ break
+
+ else:
+ before, segment = segment.split_cells(cut - pos)
+ text, _style, control = segment
+ add_segment(before)
+ yield segments_copy()
+ segments_clear()
+ pos = cut
+
+ cut = next(iter_cuts, -1)
+ if cut == -1:
+ if split_segments:
+ yield segments_copy()
+ return
+
+ yield segments_copy()
+
+
+class Segments:
+ """A simple renderable to render an iterable of segments. This class may be useful if
+ you want to print segments outside of a __rich_console__ method.
+
+ Args:
+ segments (Iterable[Segment]): An iterable of segments.
+ new_lines (bool, optional): Add new lines between segments. Defaults to False.
+ """
+
+ def __init__(self, segments: Iterable[Segment], new_lines: bool = False) -> None:
+ self.segments = list(segments)
+ self.new_lines = new_lines
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ if self.new_lines:
+ line = Segment.line()
+ for segment in self.segments:
+ yield segment
+ yield line
+ else:
+ yield from self.segments
+
+
+class SegmentLines:
+ def __init__(self, lines: Iterable[List[Segment]], new_lines: bool = False) -> None:
+ """A simple renderable containing a number of lines of segments. May be used as an intermediate
+ in rendering process.
+
+ Args:
+ lines (Iterable[List[Segment]]): Lists of segments forming lines.
+ new_lines (bool, optional): Insert new lines after each line. Defaults to False.
+ """
+ self.lines = list(lines)
+ self.new_lines = new_lines
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ if self.new_lines:
+ new_line = Segment.line()
+ for line in self.lines:
+ yield from line
+ yield new_line
+ else:
+ for line in self.lines:
+ yield from line
+
+
+if __name__ == "__main__": # pragma: no cover
+ from pip._vendor.rich.console import Console
+ from pip._vendor.rich.syntax import Syntax
+ from pip._vendor.rich.text import Text
+
+ code = """from rich.console import Console
+console = Console()
+text = Text.from_markup("Hello, [bold magenta]World[/]!")
+console.print(text)"""
+
+ text = Text.from_markup("Hello, [bold magenta]World[/]!")
+
+ console = Console()
+
+ console.rule("rich.Segment")
+ console.print(
+ "A Segment is the last step in the Rich render process before generating text with ANSI codes."
+ )
+ console.print("\nConsider the following code:\n")
+ console.print(Syntax(code, "python", line_numbers=True))
+ console.print()
+ console.print(
+ "When you call [b]print()[/b], Rich [i]renders[/i] the object in to the the following:\n"
+ )
+ fragments = list(console.render(text))
+ console.print(fragments)
+ console.print()
+ console.print("The Segments are then processed to produce the following output:\n")
+ console.print(text)
+ console.print(
+ "\nYou will only need to know this if you are implementing your own Rich renderables."
+ )
diff --git a/src/pip/_vendor/rich/spinner.py b/src/pip/_vendor/rich/spinner.py
new file mode 100644
index 000000000..0879088e1
--- /dev/null
+++ b/src/pip/_vendor/rich/spinner.py
@@ -0,0 +1,136 @@
+from typing import cast, List, Optional, TYPE_CHECKING, Union
+
+from ._spinners import SPINNERS
+from .measure import Measurement
+from .table import Table
+from .text import Text
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderResult, RenderableType
+ from .style import StyleType
+
+
+class Spinner:
+ def __init__(
+ self,
+ name: str,
+ text: "RenderableType" = "",
+ *,
+ style: Optional["StyleType"] = None,
+ speed: float = 1.0,
+ ) -> None:
+ """A spinner animation.
+
+ Args:
+ name (str): Name of spinner (run python -m rich.spinner).
+ text (RenderableType, optional): A renderable to display at the right of the spinner (str or Text typically). Defaults to "".
+ style (StyleType, optional): Style for spinner animation. Defaults to None.
+ speed (float, optional): Speed factor for animation. Defaults to 1.0.
+
+ Raises:
+ KeyError: If name isn't one of the supported spinner animations.
+ """
+ try:
+ spinner = SPINNERS[name]
+ except KeyError:
+ raise KeyError(f"no spinner called {name!r}")
+ self.text: "Union[RenderableType, Text]" = (
+ Text.from_markup(text) if isinstance(text, str) else text
+ )
+ self.frames = cast(List[str], spinner["frames"])[:]
+ self.interval = cast(float, spinner["interval"])
+ self.start_time: Optional[float] = None
+ self.style = style
+ self.speed = speed
+ self.frame_no_offset: float = 0.0
+ self._update_speed = 0.0
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ yield self.render(console.get_time())
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> Measurement:
+ text = self.render(0)
+ return Measurement.get(console, options, text)
+
+ def render(self, time: float) -> "RenderableType":
+ """Render the spinner for a given time.
+
+ Args:
+ time (float): Time in seconds.
+
+ Returns:
+ RenderableType: A renderable containing animation frame.
+ """
+ if self.start_time is None:
+ self.start_time = time
+
+ frame_no = ((time - self.start_time) * self.speed) / (
+ self.interval / 1000.0
+ ) + self.frame_no_offset
+ frame = Text(
+ self.frames[int(frame_no) % len(self.frames)], style=self.style or ""
+ )
+
+ if self._update_speed:
+ self.frame_no_offset = frame_no
+ self.start_time = time
+ self.speed = self._update_speed
+ self._update_speed = 0.0
+
+ if not self.text:
+ return frame
+ elif isinstance(self.text, (str, Text)):
+ return Text.assemble(frame, " ", self.text)
+ else:
+ table = Table.grid(padding=1)
+ table.add_row(frame, self.text)
+ return table
+
+ def update(
+ self,
+ *,
+ text: "RenderableType" = "",
+ style: Optional["StyleType"] = None,
+ speed: Optional[float] = None,
+ ) -> None:
+ """Updates attributes of a spinner after it has been started.
+
+ Args:
+ text (RenderableType, optional): A renderable to display at the right of the spinner (str or Text typically). Defaults to "".
+ style (StyleType, optional): Style for spinner animation. Defaults to None.
+ speed (float, optional): Speed factor for animation. Defaults to None.
+ """
+ if text:
+ self.text = Text.from_markup(text) if isinstance(text, str) else text
+ if style:
+ self.style = style
+ if speed:
+ self._update_speed = speed
+
+
+if __name__ == "__main__": # pragma: no cover
+ from time import sleep
+
+ from .columns import Columns
+ from .panel import Panel
+ from .live import Live
+
+ all_spinners = Columns(
+ [
+ Spinner(spinner_name, text=Text(repr(spinner_name), style="green"))
+ for spinner_name in sorted(SPINNERS.keys())
+ ],
+ column_first=True,
+ expand=True,
+ )
+
+ with Live(
+ Panel(all_spinners, title="Spinners", border_style="blue"),
+ refresh_per_second=20,
+ ) as live:
+ while True:
+ sleep(0.1)
diff --git a/src/pip/_vendor/rich/status.py b/src/pip/_vendor/rich/status.py
new file mode 100644
index 000000000..09eff405e
--- /dev/null
+++ b/src/pip/_vendor/rich/status.py
@@ -0,0 +1,132 @@
+from types import TracebackType
+from typing import Optional, Type
+
+from .console import Console, RenderableType
+from .jupyter import JupyterMixin
+from .live import Live
+from .spinner import Spinner
+from .style import StyleType
+
+
+class Status(JupyterMixin):
+ """Displays a status indicator with a 'spinner' animation.
+
+ Args:
+ status (RenderableType): A status renderable (str or Text typically).
+ console (Console, optional): Console instance to use, or None for global console. Defaults to None.
+ spinner (str, optional): Name of spinner animation (see python -m rich.spinner). Defaults to "dots".
+ spinner_style (StyleType, optional): Style of spinner. Defaults to "status.spinner".
+ speed (float, optional): Speed factor for spinner animation. Defaults to 1.0.
+ refresh_per_second (float, optional): Number of refreshes per second. Defaults to 12.5.
+ """
+
+ def __init__(
+ self,
+ status: RenderableType,
+ *,
+ console: Optional[Console] = None,
+ spinner: str = "dots",
+ spinner_style: StyleType = "status.spinner",
+ speed: float = 1.0,
+ refresh_per_second: float = 12.5,
+ ):
+ self.status = status
+ self.spinner_style = spinner_style
+ self.speed = speed
+ self._spinner = Spinner(spinner, text=status, style=spinner_style, speed=speed)
+ self._live = Live(
+ self.renderable,
+ console=console,
+ refresh_per_second=refresh_per_second,
+ transient=True,
+ )
+
+ @property
+ def renderable(self) -> Spinner:
+ return self._spinner
+
+ @property
+ def console(self) -> "Console":
+ """Get the Console used by the Status objects."""
+ return self._live.console
+
+ def update(
+ self,
+ status: Optional[RenderableType] = None,
+ *,
+ spinner: Optional[str] = None,
+ spinner_style: Optional[StyleType] = None,
+ speed: Optional[float] = None,
+ ) -> None:
+ """Update status.
+
+ Args:
+ status (Optional[RenderableType], optional): New status renderable or None for no change. Defaults to None.
+ spinner (Optional[str], optional): New spinner or None for no change. Defaults to None.
+ spinner_style (Optional[StyleType], optional): New spinner style or None for no change. Defaults to None.
+ speed (Optional[float], optional): Speed factor for spinner animation or None for no change. Defaults to None.
+ """
+ if status is not None:
+ self.status = status
+ if spinner_style is not None:
+ self.spinner_style = spinner_style
+ if speed is not None:
+ self.speed = speed
+ if spinner is not None:
+ self._spinner = Spinner(
+ spinner, text=self.status, style=self.spinner_style, speed=self.speed
+ )
+ self._live.update(self.renderable, refresh=True)
+ else:
+ self._spinner.update(
+ text=self.status, style=self.spinner_style, speed=self.speed
+ )
+
+ def start(self) -> None:
+ """Start the status animation."""
+ self._live.start()
+
+ def stop(self) -> None:
+ """Stop the spinner animation."""
+ self._live.stop()
+
+ def __rich__(self) -> RenderableType:
+ return self.renderable
+
+ def __enter__(self) -> "Status":
+ self.start()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ self.stop()
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from time import sleep
+
+ from .console import Console
+
+ console = Console()
+ with console.status("[magenta]Covid detector booting up") as status:
+ sleep(3)
+ console.log("Importing advanced AI")
+ sleep(3)
+ console.log("Advanced Covid AI Ready")
+ sleep(3)
+ status.update(status="[bold blue] Scanning for Covid", spinner="earth")
+ sleep(3)
+ console.log("Found 10,000,000,000 copies of Covid32.exe")
+ sleep(3)
+ status.update(
+ status="[bold red]Moving Covid32.exe to Trash",
+ spinner="bouncingBall",
+ spinner_style="yellow",
+ )
+ sleep(5)
+ console.print("[bold green]Covid deleted successfully")
diff --git a/src/pip/_vendor/rich/style.py b/src/pip/_vendor/rich/style.py
new file mode 100644
index 000000000..b2e8aff71
--- /dev/null
+++ b/src/pip/_vendor/rich/style.py
@@ -0,0 +1,771 @@
+import sys
+from functools import lru_cache
+from marshal import dumps, loads
+from random import randint
+from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast
+
+from . import errors
+from .color import Color, ColorParseError, ColorSystem, blend_rgb
+from .repr import Result, rich_repr
+from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme
+
+# Style instances and style definitions are often interchangeable
+StyleType = Union[str, "Style"]
+
+
+class _Bit:
+ """A descriptor to get/set a style attribute bit."""
+
+ __slots__ = ["bit"]
+
+ def __init__(self, bit_no: int) -> None:
+ self.bit = 1 << bit_no
+
+ def __get__(self, obj: "Style", objtype: Type["Style"]) -> Optional[bool]:
+ if obj._set_attributes & self.bit:
+ return obj._attributes & self.bit != 0
+ return None
+
+
+@rich_repr
+class Style:
+ """A terminal style.
+
+ A terminal style consists of a color (`color`), a background color (`bgcolor`), and a number of attributes, such
+ as bold, italic etc. The attributes have 3 states: they can either be on
+ (``True``), off (``False``), or not set (``None``).
+
+ Args:
+ color (Union[Color, str], optional): Color of terminal text. Defaults to None.
+ bgcolor (Union[Color, str], optional): Color of terminal background. Defaults to None.
+ bold (bool, optional): Enable bold text. Defaults to None.
+ dim (bool, optional): Enable dim text. Defaults to None.
+ italic (bool, optional): Enable italic text. Defaults to None.
+ underline (bool, optional): Enable underlined text. Defaults to None.
+ blink (bool, optional): Enabled blinking text. Defaults to None.
+ blink2 (bool, optional): Enable fast blinking text. Defaults to None.
+ reverse (bool, optional): Enabled reverse text. Defaults to None.
+ conceal (bool, optional): Enable concealed text. Defaults to None.
+ strike (bool, optional): Enable strikethrough text. Defaults to None.
+ underline2 (bool, optional): Enable doubly underlined text. Defaults to None.
+ frame (bool, optional): Enable framed text. Defaults to None.
+ encircle (bool, optional): Enable encircled text. Defaults to None.
+ overline (bool, optional): Enable overlined text. Defaults to None.
+ link (str, link): Link URL. Defaults to None.
+
+ """
+
+ _color: Optional[Color]
+ _bgcolor: Optional[Color]
+ _attributes: int
+ _set_attributes: int
+ _hash: Optional[int]
+ _null: bool
+ _meta: Optional[bytes]
+
+ __slots__ = [
+ "_color",
+ "_bgcolor",
+ "_attributes",
+ "_set_attributes",
+ "_link",
+ "_link_id",
+ "_ansi",
+ "_style_definition",
+ "_hash",
+ "_null",
+ "_meta",
+ ]
+
+ # maps bits on to SGR parameter
+ _style_map = {
+ 0: "1",
+ 1: "2",
+ 2: "3",
+ 3: "4",
+ 4: "5",
+ 5: "6",
+ 6: "7",
+ 7: "8",
+ 8: "9",
+ 9: "21",
+ 10: "51",
+ 11: "52",
+ 12: "53",
+ }
+
+ STYLE_ATTRIBUTES = {
+ "dim": "dim",
+ "d": "dim",
+ "bold": "bold",
+ "b": "bold",
+ "italic": "italic",
+ "i": "italic",
+ "underline": "underline",
+ "u": "underline",
+ "blink": "blink",
+ "blink2": "blink2",
+ "reverse": "reverse",
+ "r": "reverse",
+ "conceal": "conceal",
+ "c": "conceal",
+ "strike": "strike",
+ "s": "strike",
+ "underline2": "underline2",
+ "uu": "underline2",
+ "frame": "frame",
+ "encircle": "encircle",
+ "overline": "overline",
+ "o": "overline",
+ }
+
+ def __init__(
+ self,
+ *,
+ color: Optional[Union[Color, str]] = None,
+ bgcolor: Optional[Union[Color, str]] = None,
+ bold: Optional[bool] = None,
+ dim: Optional[bool] = None,
+ italic: Optional[bool] = None,
+ underline: Optional[bool] = None,
+ blink: Optional[bool] = None,
+ blink2: Optional[bool] = None,
+ reverse: Optional[bool] = None,
+ conceal: Optional[bool] = None,
+ strike: Optional[bool] = None,
+ underline2: Optional[bool] = None,
+ frame: Optional[bool] = None,
+ encircle: Optional[bool] = None,
+ overline: Optional[bool] = None,
+ link: Optional[str] = None,
+ meta: Optional[Dict[str, Any]] = None,
+ ):
+ self._ansi: Optional[str] = None
+ self._style_definition: Optional[str] = None
+
+ def _make_color(color: Union[Color, str]) -> Color:
+ return color if isinstance(color, Color) else Color.parse(color)
+
+ self._color = None if color is None else _make_color(color)
+ self._bgcolor = None if bgcolor is None else _make_color(bgcolor)
+ self._set_attributes = sum(
+ (
+ bold is not None,
+ dim is not None and 2,
+ italic is not None and 4,
+ underline is not None and 8,
+ blink is not None and 16,
+ blink2 is not None and 32,
+ reverse is not None and 64,
+ conceal is not None and 128,
+ strike is not None and 256,
+ underline2 is not None and 512,
+ frame is not None and 1024,
+ encircle is not None and 2048,
+ overline is not None and 4096,
+ )
+ )
+ self._attributes = (
+ sum(
+ (
+ bold and 1 or 0,
+ dim and 2 or 0,
+ italic and 4 or 0,
+ underline and 8 or 0,
+ blink and 16 or 0,
+ blink2 and 32 or 0,
+ reverse and 64 or 0,
+ conceal and 128 or 0,
+ strike and 256 or 0,
+ underline2 and 512 or 0,
+ frame and 1024 or 0,
+ encircle and 2048 or 0,
+ overline and 4096 or 0,
+ )
+ )
+ if self._set_attributes
+ else 0
+ )
+
+ self._link = link
+ self._link_id = f"{randint(0, 999999)}" if link else ""
+ self._meta = None if meta is None else dumps(meta)
+ self._hash: Optional[int] = None
+ self._null = not (self._set_attributes or color or bgcolor or link or meta)
+
+ @classmethod
+ def null(cls) -> "Style":
+ """Create an 'null' style, equivalent to Style(), but more performant."""
+ return NULL_STYLE
+
+ @classmethod
+ def from_color(
+ cls, color: Optional[Color] = None, bgcolor: Optional[Color] = None
+ ) -> "Style":
+ """Create a new style with colors and no attributes.
+
+ Returns:
+ color (Optional[Color]): A (foreground) color, or None for no color. Defaults to None.
+ bgcolor (Optional[Color]): A (background) color, or None for no color. Defaults to None.
+ """
+ style: Style = cls.__new__(Style)
+ style._ansi = None
+ style._style_definition = None
+ style._color = color
+ style._bgcolor = bgcolor
+ style._set_attributes = 0
+ style._attributes = 0
+ style._link = None
+ style._link_id = ""
+ style._meta = None
+ style._null = not (color or bgcolor)
+ style._hash = None
+ return style
+
+ @classmethod
+ def from_meta(cls, meta: Optional[Dict[str, Any]]) -> "Style":
+ """Create a new style with meta data.
+
+ Returns:
+ meta (Optional[Dict[str, Any]]): A dictionary of meta data. Defaults to None.
+ """
+ style: Style = cls.__new__(Style)
+ style._ansi = None
+ style._style_definition = None
+ style._color = None
+ style._bgcolor = None
+ style._set_attributes = 0
+ style._attributes = 0
+ style._link = None
+ style._link_id = ""
+ style._meta = dumps(meta)
+ style._hash = None
+ style._null = not (meta)
+ return style
+
+ @classmethod
+ def on(cls, meta: Optional[Dict[str, Any]] = None, **handlers: Any) -> "Style":
+ """Create a blank style with meta information.
+
+ Example:
+ style = Style.on(click=self.on_click)
+
+ Args:
+ meta (Optional[Dict[str, Any]], optional): An optional dict of meta information.
+ **handlers (Any): Keyword arguments are translated in to handlers.
+
+ Returns:
+ Style: A Style with meta information attached.
+ """
+ meta = {} if meta is None else meta
+ meta.update({f"@{key}": value for key, value in handlers.items()})
+ return cls.from_meta(meta)
+
+ bold = _Bit(0)
+ dim = _Bit(1)
+ italic = _Bit(2)
+ underline = _Bit(3)
+ blink = _Bit(4)
+ blink2 = _Bit(5)
+ reverse = _Bit(6)
+ conceal = _Bit(7)
+ strike = _Bit(8)
+ underline2 = _Bit(9)
+ frame = _Bit(10)
+ encircle = _Bit(11)
+ overline = _Bit(12)
+
+ @property
+ def link_id(self) -> str:
+ """Get a link id, used in ansi code for links."""
+ return self._link_id
+
+ def __str__(self) -> str:
+ """Re-generate style definition from attributes."""
+ if self._style_definition is None:
+ attributes: List[str] = []
+ append = attributes.append
+ bits = self._set_attributes
+ if bits & 0b0000000001111:
+ if bits & 1:
+ append("bold" if self.bold else "not bold")
+ if bits & (1 << 1):
+ append("dim" if self.dim else "not dim")
+ if bits & (1 << 2):
+ append("italic" if self.italic else "not italic")
+ if bits & (1 << 3):
+ append("underline" if self.underline else "not underline")
+ if bits & 0b0000111110000:
+ if bits & (1 << 4):
+ append("blink" if self.blink else "not blink")
+ if bits & (1 << 5):
+ append("blink2" if self.blink2 else "not blink2")
+ if bits & (1 << 6):
+ append("reverse" if self.reverse else "not reverse")
+ if bits & (1 << 7):
+ append("conceal" if self.conceal else "not conceal")
+ if bits & (1 << 8):
+ append("strike" if self.strike else "not strike")
+ if bits & 0b1111000000000:
+ if bits & (1 << 9):
+ append("underline2" if self.underline2 else "not underline2")
+ if bits & (1 << 10):
+ append("frame" if self.frame else "not frame")
+ if bits & (1 << 11):
+ append("encircle" if self.encircle else "not encircle")
+ if bits & (1 << 12):
+ append("overline" if self.overline else "not overline")
+ if self._color is not None:
+ append(self._color.name)
+ if self._bgcolor is not None:
+ append("on")
+ append(self._bgcolor.name)
+ if self._link:
+ append("link")
+ append(self._link)
+ self._style_definition = " ".join(attributes) or "none"
+ return self._style_definition
+
+ def __bool__(self) -> bool:
+ """A Style is false if it has no attributes, colors, or links."""
+ return not self._null
+
+ def _make_ansi_codes(self, color_system: ColorSystem) -> str:
+ """Generate ANSI codes for this style.
+
+ Args:
+ color_system (ColorSystem): Color system.
+
+ Returns:
+ str: String containing codes.
+ """
+
+ if self._ansi is None:
+ sgr: List[str] = []
+ append = sgr.append
+ _style_map = self._style_map
+ attributes = self._attributes & self._set_attributes
+ if attributes:
+ if attributes & 1:
+ append(_style_map[0])
+ if attributes & 2:
+ append(_style_map[1])
+ if attributes & 4:
+ append(_style_map[2])
+ if attributes & 8:
+ append(_style_map[3])
+ if attributes & 0b0000111110000:
+ for bit in range(4, 9):
+ if attributes & (1 << bit):
+ append(_style_map[bit])
+ if attributes & 0b1111000000000:
+ for bit in range(9, 13):
+ if attributes & (1 << bit):
+ append(_style_map[bit])
+ if self._color is not None:
+ sgr.extend(self._color.downgrade(color_system).get_ansi_codes())
+ if self._bgcolor is not None:
+ sgr.extend(
+ self._bgcolor.downgrade(color_system).get_ansi_codes(
+ foreground=False
+ )
+ )
+ self._ansi = ";".join(sgr)
+ return self._ansi
+
+ @classmethod
+ @lru_cache(maxsize=1024)
+ def normalize(cls, style: str) -> str:
+ """Normalize a style definition so that styles with the same effect have the same string
+ representation.
+
+ Args:
+ style (str): A style definition.
+
+ Returns:
+ str: Normal form of style definition.
+ """
+ try:
+ return str(cls.parse(style))
+ except errors.StyleSyntaxError:
+ return style.strip().lower()
+
+ @classmethod
+ def pick_first(cls, *values: Optional[StyleType]) -> StyleType:
+ """Pick first non-None style."""
+ for value in values:
+ if value is not None:
+ return value
+ raise ValueError("expected at least one non-None style")
+
+ def __rich_repr__(self) -> Result:
+ yield "color", self.color, None
+ yield "bgcolor", self.bgcolor, None
+ yield "bold", self.bold, None,
+ yield "dim", self.dim, None,
+ yield "italic", self.italic, None
+ yield "underline", self.underline, None,
+ yield "blink", self.blink, None
+ yield "blink2", self.blink2, None
+ yield "reverse", self.reverse, None
+ yield "conceal", self.conceal, None
+ yield "strike", self.strike, None
+ yield "underline2", self.underline2, None
+ yield "frame", self.frame, None
+ yield "encircle", self.encircle, None
+ yield "link", self.link, None
+ if self._meta:
+ yield "meta", self.meta
+
+ def __eq__(self, other: Any) -> bool:
+ if not isinstance(other, Style):
+ return NotImplemented
+ return self.__hash__() == other.__hash__()
+
+ def __ne__(self, other: Any) -> bool:
+ if not isinstance(other, Style):
+ return NotImplemented
+ return self.__hash__() != other.__hash__()
+
+ def __hash__(self) -> int:
+ if self._hash is not None:
+ return self._hash
+ self._hash = hash(
+ (
+ self._color,
+ self._bgcolor,
+ self._attributes,
+ self._set_attributes,
+ self._link,
+ self._meta,
+ )
+ )
+ return self._hash
+
+ @property
+ def color(self) -> Optional[Color]:
+ """The foreground color or None if it is not set."""
+ return self._color
+
+ @property
+ def bgcolor(self) -> Optional[Color]:
+ """The background color or None if it is not set."""
+ return self._bgcolor
+
+ @property
+ def link(self) -> Optional[str]:
+ """Link text, if set."""
+ return self._link
+
+ @property
+ def transparent_background(self) -> bool:
+ """Check if the style specified a transparent background."""
+ return self.bgcolor is None or self.bgcolor.is_default
+
+ @property
+ def background_style(self) -> "Style":
+ """A Style with background only."""
+ return Style(bgcolor=self.bgcolor)
+
+ @property
+ def meta(self) -> Dict[str, Any]:
+ """Get meta information (can not be changed after construction)."""
+ return {} if self._meta is None else cast(Dict[str, Any], loads(self._meta))
+
+ @property
+ def without_color(self) -> "Style":
+ """Get a copy of the style with color removed."""
+ if self._null:
+ return NULL_STYLE
+ style: Style = self.__new__(Style)
+ style._ansi = None
+ style._style_definition = None
+ style._color = None
+ style._bgcolor = None
+ style._attributes = self._attributes
+ style._set_attributes = self._set_attributes
+ style._link = self._link
+ style._link_id = f"{randint(0, 999999)}" if self._link else ""
+ style._null = False
+ style._meta = None
+ style._hash = None
+ return style
+
+ @classmethod
+ @lru_cache(maxsize=4096)
+ def parse(cls, style_definition: str) -> "Style":
+ """Parse a style definition.
+
+ Args:
+ style_definition (str): A string containing a style.
+
+ Raises:
+ errors.StyleSyntaxError: If the style definition syntax is invalid.
+
+ Returns:
+ `Style`: A Style instance.
+ """
+ if style_definition.strip() == "none" or not style_definition:
+ return cls.null()
+
+ STYLE_ATTRIBUTES = cls.STYLE_ATTRIBUTES
+ color: Optional[str] = None
+ bgcolor: Optional[str] = None
+ attributes: Dict[str, Optional[Any]] = {}
+ link: Optional[str] = None
+
+ words = iter(style_definition.split())
+ for original_word in words:
+ word = original_word.lower()
+ if word == "on":
+ word = next(words, "")
+ if not word:
+ raise errors.StyleSyntaxError("color expected after 'on'")
+ try:
+ Color.parse(word) is None
+ except ColorParseError as error:
+ raise errors.StyleSyntaxError(
+ f"unable to parse {word!r} as background color; {error}"
+ ) from None
+ bgcolor = word
+
+ elif word == "not":
+ word = next(words, "")
+ attribute = STYLE_ATTRIBUTES.get(word)
+ if attribute is None:
+ raise errors.StyleSyntaxError(
+ f"expected style attribute after 'not', found {word!r}"
+ )
+ attributes[attribute] = False
+
+ elif word == "link":
+ word = next(words, "")
+ if not word:
+ raise errors.StyleSyntaxError("URL expected after 'link'")
+ link = word
+
+ elif word in STYLE_ATTRIBUTES:
+ attributes[STYLE_ATTRIBUTES[word]] = True
+
+ else:
+ try:
+ Color.parse(word)
+ except ColorParseError as error:
+ raise errors.StyleSyntaxError(
+ f"unable to parse {word!r} as color; {error}"
+ ) from None
+ color = word
+ style = Style(color=color, bgcolor=bgcolor, link=link, **attributes)
+ return style
+
+ @lru_cache(maxsize=1024)
+ def get_html_style(self, theme: Optional[TerminalTheme] = None) -> str:
+ """Get a CSS style rule."""
+ theme = theme or DEFAULT_TERMINAL_THEME
+ css: List[str] = []
+ append = css.append
+
+ color = self.color
+ bgcolor = self.bgcolor
+ if self.reverse:
+ color, bgcolor = bgcolor, color
+ if self.dim:
+ foreground_color = (
+ theme.foreground_color if color is None else color.get_truecolor(theme)
+ )
+ color = Color.from_triplet(
+ blend_rgb(foreground_color, theme.background_color, 0.5)
+ )
+ if color is not None:
+ theme_color = color.get_truecolor(theme)
+ append(f"color: {theme_color.hex}")
+ append(f"text-decoration-color: {theme_color.hex}")
+ if bgcolor is not None:
+ theme_color = bgcolor.get_truecolor(theme, foreground=False)
+ append(f"background-color: {theme_color.hex}")
+ if self.bold:
+ append("font-weight: bold")
+ if self.italic:
+ append("font-style: italic")
+ if self.underline:
+ append("text-decoration: underline")
+ if self.strike:
+ append("text-decoration: line-through")
+ if self.overline:
+ append("text-decoration: overline")
+ return "; ".join(css)
+
+ @classmethod
+ def combine(cls, styles: Iterable["Style"]) -> "Style":
+ """Combine styles and get result.
+
+ Args:
+ styles (Iterable[Style]): Styles to combine.
+
+ Returns:
+ Style: A new style instance.
+ """
+ iter_styles = iter(styles)
+ return sum(iter_styles, next(iter_styles))
+
+ @classmethod
+ def chain(cls, *styles: "Style") -> "Style":
+ """Combine styles from positional argument in to a single style.
+
+ Args:
+ *styles (Iterable[Style]): Styles to combine.
+
+ Returns:
+ Style: A new style instance.
+ """
+ iter_styles = iter(styles)
+ return sum(iter_styles, next(iter_styles))
+
+ def copy(self) -> "Style":
+ """Get a copy of this style.
+
+ Returns:
+ Style: A new Style instance with identical attributes.
+ """
+ if self._null:
+ return NULL_STYLE
+ style: Style = self.__new__(Style)
+ style._ansi = self._ansi
+ style._style_definition = self._style_definition
+ style._color = self._color
+ style._bgcolor = self._bgcolor
+ style._attributes = self._attributes
+ style._set_attributes = self._set_attributes
+ style._link = self._link
+ style._link_id = f"{randint(0, 999999)}" if self._link else ""
+ style._hash = self._hash
+ style._null = False
+ style._meta = self._meta
+ return style
+
+ def update_link(self, link: Optional[str] = None) -> "Style":
+ """Get a copy with a different value for link.
+
+ Args:
+ link (str, optional): New value for link. Defaults to None.
+
+ Returns:
+ Style: A new Style instance.
+ """
+ style: Style = self.__new__(Style)
+ style._ansi = self._ansi
+ style._style_definition = self._style_definition
+ style._color = self._color
+ style._bgcolor = self._bgcolor
+ style._attributes = self._attributes
+ style._set_attributes = self._set_attributes
+ style._link = link
+ style._link_id = f"{randint(0, 999999)}" if link else ""
+ style._hash = None
+ style._null = False
+ style._meta = self._meta
+ return style
+
+ def render(
+ self,
+ text: str = "",
+ *,
+ color_system: Optional[ColorSystem] = ColorSystem.TRUECOLOR,
+ legacy_windows: bool = False,
+ ) -> str:
+ """Render the ANSI codes for the style.
+
+ Args:
+ text (str, optional): A string to style. Defaults to "".
+ color_system (Optional[ColorSystem], optional): Color system to render to. Defaults to ColorSystem.TRUECOLOR.
+
+ Returns:
+ str: A string containing ANSI style codes.
+ """
+ if not text or color_system is None:
+ return text
+ attrs = self._ansi or self._make_ansi_codes(color_system)
+ rendered = f"\x1b[{attrs}m{text}\x1b[0m" if attrs else text
+ if self._link and not legacy_windows:
+ rendered = (
+ f"\x1b]8;id={self._link_id};{self._link}\x1b\\{rendered}\x1b]8;;\x1b\\"
+ )
+ return rendered
+
+ def test(self, text: Optional[str] = None) -> None:
+ """Write text with style directly to terminal.
+
+ This method is for testing purposes only.
+
+ Args:
+ text (Optional[str], optional): Text to style or None for style name.
+
+ """
+ text = text or str(self)
+ sys.stdout.write(f"{self.render(text)}\n")
+
+ @lru_cache(maxsize=1024)
+ def _add(self, style: Optional["Style"]) -> "Style":
+ if style is None or style._null:
+ return self
+ if self._null:
+ return style
+ new_style: Style = self.__new__(Style)
+ new_style._ansi = None
+ new_style._style_definition = None
+ new_style._color = style._color or self._color
+ new_style._bgcolor = style._bgcolor or self._bgcolor
+ new_style._attributes = (self._attributes & ~style._set_attributes) | (
+ style._attributes & style._set_attributes
+ )
+ new_style._set_attributes = self._set_attributes | style._set_attributes
+ new_style._link = style._link or self._link
+ new_style._link_id = style._link_id or self._link_id
+ new_style._null = style._null
+ if self._meta and style._meta:
+ new_style._meta = dumps({**self.meta, **style.meta})
+ else:
+ new_style._meta = self._meta or style._meta
+ new_style._hash = None
+ return new_style
+
+ def __add__(self, style: Optional["Style"]) -> "Style":
+ combined_style = self._add(style)
+ return combined_style.copy() if combined_style.link else combined_style
+
+
+NULL_STYLE = Style()
+
+
+class StyleStack:
+ """A stack of styles."""
+
+ __slots__ = ["_stack"]
+
+ def __init__(self, default_style: "Style") -> None:
+ self._stack: List[Style] = [default_style]
+
+ def __repr__(self) -> str:
+ return f"<stylestack {self._stack!r}>"
+
+ @property
+ def current(self) -> Style:
+ """Get the Style at the top of the stack."""
+ return self._stack[-1]
+
+ def push(self, style: Style) -> None:
+ """Push a new style on to the stack.
+
+ Args:
+ style (Style): New style to combine with current style.
+ """
+ self._stack.append(self._stack[-1] + style)
+
+ def pop(self) -> Style:
+ """Pop last style and discard.
+
+ Returns:
+ Style: New current style (also available as stack.current)
+ """
+ self._stack.pop()
+ return self._stack[-1]
diff --git a/src/pip/_vendor/rich/styled.py b/src/pip/_vendor/rich/styled.py
new file mode 100644
index 000000000..91cd0db31
--- /dev/null
+++ b/src/pip/_vendor/rich/styled.py
@@ -0,0 +1,42 @@
+from typing import TYPE_CHECKING
+
+from .measure import Measurement
+from .segment import Segment
+from .style import StyleType
+
+if TYPE_CHECKING:
+ from .console import Console, ConsoleOptions, RenderResult, RenderableType
+
+
+class Styled:
+ """Apply a style to a renderable.
+
+ Args:
+ renderable (RenderableType): Any renderable.
+ style (StyleType): A style to apply across the entire renderable.
+ """
+
+ def __init__(self, renderable: "RenderableType", style: "StyleType") -> None:
+ self.renderable = renderable
+ self.style = style
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+ style = console.get_style(self.style)
+ rendered_segments = console.render(self.renderable, options)
+ segments = Segment.apply_style(rendered_segments, style)
+ return segments
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> Measurement:
+ return Measurement.get(console, options, self.renderable)
+
+
+if __name__ == "__main__": # pragma: no cover
+ from pip._vendor.rich import print
+ from pip._vendor.rich.panel import Panel
+
+ panel = Styled(Panel("hello"), "on blue")
+ print(panel)
diff --git a/src/pip/_vendor/rich/syntax.py b/src/pip/_vendor/rich/syntax.py
new file mode 100644
index 000000000..dace718c1
--- /dev/null
+++ b/src/pip/_vendor/rich/syntax.py
@@ -0,0 +1,934 @@
+import os.path
+import platform
+import re
+import sys
+import textwrap
+from abc import ABC, abstractmethod
+from typing import (
+ Any,
+ Dict,
+ Iterable,
+ List,
+ NamedTuple,
+ Optional,
+ Sequence,
+ Set,
+ Tuple,
+ Type,
+ Union,
+)
+
+from pip._vendor.pygments.lexer import Lexer
+from pip._vendor.pygments.lexers import get_lexer_by_name, guess_lexer_for_filename
+from pip._vendor.pygments.style import Style as PygmentsStyle
+from pip._vendor.pygments.styles import get_style_by_name
+from pip._vendor.pygments.token import (
+ Comment,
+ Error,
+ Generic,
+ Keyword,
+ Name,
+ Number,
+ Operator,
+ String,
+ Token,
+ Whitespace,
+)
+from pip._vendor.pygments.util import ClassNotFound
+
+from pip._vendor.rich.containers import Lines
+from pip._vendor.rich.padding import Padding, PaddingDimensions
+
+from ._loop import loop_first
+from .color import Color, blend_rgb
+from .console import Console, ConsoleOptions, JustifyMethod, RenderResult
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .segment import Segment, Segments
+from .style import Style, StyleType
+from .text import Text
+
+TokenType = Tuple[str, ...]
+
+WINDOWS = platform.system() == "Windows"
+DEFAULT_THEME = "monokai"
+
+# The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py
+# A few modifications were made
+
+ANSI_LIGHT: Dict[TokenType, Style] = {
+ Token: Style(),
+ Whitespace: Style(color="white"),
+ Comment: Style(dim=True),
+ Comment.Preproc: Style(color="cyan"),
+ Keyword: Style(color="blue"),
+ Keyword.Type: Style(color="cyan"),
+ Operator.Word: Style(color="magenta"),
+ Name.Builtin: Style(color="cyan"),
+ Name.Function: Style(color="green"),
+ Name.Namespace: Style(color="cyan", underline=True),
+ Name.Class: Style(color="green", underline=True),
+ Name.Exception: Style(color="cyan"),
+ Name.Decorator: Style(color="magenta", bold=True),
+ Name.Variable: Style(color="red"),
+ Name.Constant: Style(color="red"),
+ Name.Attribute: Style(color="cyan"),
+ Name.Tag: Style(color="bright_blue"),
+ String: Style(color="yellow"),
+ Number: Style(color="blue"),
+ Generic.Deleted: Style(color="bright_red"),
+ Generic.Inserted: Style(color="green"),
+ Generic.Heading: Style(bold=True),
+ Generic.Subheading: Style(color="magenta", bold=True),
+ Generic.Prompt: Style(bold=True),
+ Generic.Error: Style(color="bright_red"),
+ Error: Style(color="red", underline=True),
+}
+
+ANSI_DARK: Dict[TokenType, Style] = {
+ Token: Style(),
+ Whitespace: Style(color="bright_black"),
+ Comment: Style(dim=True),
+ Comment.Preproc: Style(color="bright_cyan"),
+ Keyword: Style(color="bright_blue"),
+ Keyword.Type: Style(color="bright_cyan"),
+ Operator.Word: Style(color="bright_magenta"),
+ Name.Builtin: Style(color="bright_cyan"),
+ Name.Function: Style(color="bright_green"),
+ Name.Namespace: Style(color="bright_cyan", underline=True),
+ Name.Class: Style(color="bright_green", underline=True),
+ Name.Exception: Style(color="bright_cyan"),
+ Name.Decorator: Style(color="bright_magenta", bold=True),
+ Name.Variable: Style(color="bright_red"),
+ Name.Constant: Style(color="bright_red"),
+ Name.Attribute: Style(color="bright_cyan"),
+ Name.Tag: Style(color="bright_blue"),
+ String: Style(color="yellow"),
+ Number: Style(color="bright_blue"),
+ Generic.Deleted: Style(color="bright_red"),
+ Generic.Inserted: Style(color="bright_green"),
+ Generic.Heading: Style(bold=True),
+ Generic.Subheading: Style(color="bright_magenta", bold=True),
+ Generic.Prompt: Style(bold=True),
+ Generic.Error: Style(color="bright_red"),
+ Error: Style(color="red", underline=True),
+}
+
+RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK}
+NUMBERS_COLUMN_DEFAULT_PADDING = 2
+
+
+class SyntaxTheme(ABC):
+ """Base class for a syntax theme."""
+
+ @abstractmethod
+ def get_style_for_token(self, token_type: TokenType) -> Style:
+ """Get a style for a given Pygments token."""
+ raise NotImplementedError # pragma: no cover
+
+ @abstractmethod
+ def get_background_style(self) -> Style:
+ """Get the background color."""
+ raise NotImplementedError # pragma: no cover
+
+
+class PygmentsSyntaxTheme(SyntaxTheme):
+ """Syntax theme that delegates to Pygments theme."""
+
+ def __init__(self, theme: Union[str, Type[PygmentsStyle]]) -> None:
+ self._style_cache: Dict[TokenType, Style] = {}
+ if isinstance(theme, str):
+ try:
+ self._pygments_style_class = get_style_by_name(theme)
+ except ClassNotFound:
+ self._pygments_style_class = get_style_by_name("default")
+ else:
+ self._pygments_style_class = theme
+
+ self._background_color = self._pygments_style_class.background_color
+ self._background_style = Style(bgcolor=self._background_color)
+
+ def get_style_for_token(self, token_type: TokenType) -> Style:
+ """Get a style from a Pygments class."""
+ try:
+ return self._style_cache[token_type]
+ except KeyError:
+ try:
+ pygments_style = self._pygments_style_class.style_for_token(token_type)
+ except KeyError:
+ style = Style.null()
+ else:
+ color = pygments_style["color"]
+ bgcolor = pygments_style["bgcolor"]
+ style = Style(
+ color="#" + color if color else "#000000",
+ bgcolor="#" + bgcolor if bgcolor else self._background_color,
+ bold=pygments_style["bold"],
+ italic=pygments_style["italic"],
+ underline=pygments_style["underline"],
+ )
+ self._style_cache[token_type] = style
+ return style
+
+ def get_background_style(self) -> Style:
+ return self._background_style
+
+
+class ANSISyntaxTheme(SyntaxTheme):
+ """Syntax theme to use standard colors."""
+
+ def __init__(self, style_map: Dict[TokenType, Style]) -> None:
+ self.style_map = style_map
+ self._missing_style = Style.null()
+ self._background_style = Style.null()
+ self._style_cache: Dict[TokenType, Style] = {}
+
+ def get_style_for_token(self, token_type: TokenType) -> Style:
+ """Look up style in the style map."""
+ try:
+ return self._style_cache[token_type]
+ except KeyError:
+ # Styles form a hierarchy
+ # We need to go from most to least specific
+ # e.g. ("foo", "bar", "baz") to ("foo", "bar") to ("foo",)
+ get_style = self.style_map.get
+ token = tuple(token_type)
+ style = self._missing_style
+ while token:
+ _style = get_style(token)
+ if _style is not None:
+ style = _style
+ break
+ token = token[:-1]
+ self._style_cache[token_type] = style
+ return style
+
+ def get_background_style(self) -> Style:
+ return self._background_style
+
+
+SyntaxPosition = Tuple[int, int]
+
+
+class _SyntaxHighlightRange(NamedTuple):
+ """
+ A range to highlight in a Syntax object.
+ `start` and `end` are 2-integers tuples, where the first integer is the line number
+ (starting from 1) and the second integer is the column index (starting from 0).
+ """
+
+ style: StyleType
+ start: SyntaxPosition
+ end: SyntaxPosition
+
+
+class Syntax(JupyterMixin):
+ """Construct a Syntax object to render syntax highlighted code.
+
+ Args:
+ code (str): Code to highlight.
+ lexer (Lexer | str): Lexer to use (see https://pygments.org/docs/lexers/)
+ theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "monokai".
+ dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False.
+ line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.
+ start_line (int, optional): Starting number for line numbers. Defaults to 1.
+ line_range (Tuple[int | None, int | None], optional): If given should be a tuple of the start and end line to render.
+ A value of None in the tuple indicates the range is open in that direction.
+ highlight_lines (Set[int]): A set of line numbers to highlight.
+ code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
+ tab_size (int, optional): Size of tabs. Defaults to 4.
+ word_wrap (bool, optional): Enable word wrapping.
+ background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
+ indent_guides (bool, optional): Show indent guides. Defaults to False.
+ padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).
+ """
+
+ _pygments_style_class: Type[PygmentsStyle]
+ _theme: SyntaxTheme
+
+ @classmethod
+ def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme:
+ """Get a syntax theme instance."""
+ if isinstance(name, SyntaxTheme):
+ return name
+ theme: SyntaxTheme
+ if name in RICH_SYNTAX_THEMES:
+ theme = ANSISyntaxTheme(RICH_SYNTAX_THEMES[name])
+ else:
+ theme = PygmentsSyntaxTheme(name)
+ return theme
+
+ def __init__(
+ self,
+ code: str,
+ lexer: Union[Lexer, str],
+ *,
+ theme: Union[str, SyntaxTheme] = DEFAULT_THEME,
+ dedent: bool = False,
+ line_numbers: bool = False,
+ start_line: int = 1,
+ line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,
+ highlight_lines: Optional[Set[int]] = None,
+ code_width: Optional[int] = None,
+ tab_size: int = 4,
+ word_wrap: bool = False,
+ background_color: Optional[str] = None,
+ indent_guides: bool = False,
+ padding: PaddingDimensions = 0,
+ ) -> None:
+ self.code = code
+ self._lexer = lexer
+ self.dedent = dedent
+ self.line_numbers = line_numbers
+ self.start_line = start_line
+ self.line_range = line_range
+ self.highlight_lines = highlight_lines or set()
+ self.code_width = code_width
+ self.tab_size = tab_size
+ self.word_wrap = word_wrap
+ self.background_color = background_color
+ self.background_style = (
+ Style(bgcolor=background_color) if background_color else Style()
+ )
+ self.indent_guides = indent_guides
+ self.padding = padding
+
+ self._theme = self.get_theme(theme)
+ self._stylized_ranges: List[_SyntaxHighlightRange] = []
+
+ @classmethod
+ def from_path(
+ cls,
+ path: str,
+ encoding: str = "utf-8",
+ lexer: Optional[Union[Lexer, str]] = None,
+ theme: Union[str, SyntaxTheme] = DEFAULT_THEME,
+ dedent: bool = False,
+ line_numbers: bool = False,
+ line_range: Optional[Tuple[int, int]] = None,
+ start_line: int = 1,
+ highlight_lines: Optional[Set[int]] = None,
+ code_width: Optional[int] = None,
+ tab_size: int = 4,
+ word_wrap: bool = False,
+ background_color: Optional[str] = None,
+ indent_guides: bool = False,
+ padding: PaddingDimensions = 0,
+ ) -> "Syntax":
+ """Construct a Syntax object from a file.
+
+ Args:
+ path (str): Path to file to highlight.
+ encoding (str): Encoding of file.
+ lexer (str | Lexer, optional): Lexer to use. If None, lexer will be auto-detected from path/file content.
+ theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs".
+ dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True.
+ line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.
+ start_line (int, optional): Starting number for line numbers. Defaults to 1.
+ line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render.
+ highlight_lines (Set[int]): A set of line numbers to highlight.
+ code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
+ tab_size (int, optional): Size of tabs. Defaults to 4.
+ word_wrap (bool, optional): Enable word wrapping of code.
+ background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
+ indent_guides (bool, optional): Show indent guides. Defaults to False.
+ padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).
+
+ Returns:
+ [Syntax]: A Syntax object that may be printed to the console
+ """
+ with open(path, "rt", encoding=encoding) as code_file:
+ code = code_file.read()
+
+ if not lexer:
+ lexer = cls.guess_lexer(path, code=code)
+
+ return cls(
+ code,
+ lexer,
+ theme=theme,
+ dedent=dedent,
+ line_numbers=line_numbers,
+ line_range=line_range,
+ start_line=start_line,
+ highlight_lines=highlight_lines,
+ code_width=code_width,
+ tab_size=tab_size,
+ word_wrap=word_wrap,
+ background_color=background_color,
+ indent_guides=indent_guides,
+ padding=padding,
+ )
+
+ @classmethod
+ def guess_lexer(cls, path: str, code: Optional[str] = None) -> str:
+ """Guess the alias of the Pygments lexer to use based on a path and an optional string of code.
+ If code is supplied, it will use a combination of the code and the filename to determine the
+ best lexer to use. For example, if the file is ``index.html`` and the file contains Django
+ templating syntax, then "html+django" will be returned. If the file is ``index.html``, and no
+ templating language is used, the "html" lexer will be used. If no string of code
+ is supplied, the lexer will be chosen based on the file extension..
+
+ Args:
+ path (AnyStr): The path to the file containing the code you wish to know the lexer for.
+ code (str, optional): Optional string of code that will be used as a fallback if no lexer
+ is found for the supplied path.
+
+ Returns:
+ str: The name of the Pygments lexer that best matches the supplied path/code.
+ """
+ lexer: Optional[Lexer] = None
+ lexer_name = "default"
+ if code:
+ try:
+ lexer = guess_lexer_for_filename(path, code)
+ except ClassNotFound:
+ pass
+
+ if not lexer:
+ try:
+ _, ext = os.path.splitext(path)
+ if ext:
+ extension = ext.lstrip(".").lower()
+ lexer = get_lexer_by_name(extension)
+ except ClassNotFound:
+ pass
+
+ if lexer:
+ if lexer.aliases:
+ lexer_name = lexer.aliases[0]
+ else:
+ lexer_name = lexer.name
+
+ return lexer_name
+
+ def _get_base_style(self) -> Style:
+ """Get the base style."""
+ default_style = self._theme.get_background_style() + self.background_style
+ return default_style
+
+ def _get_token_color(self, token_type: TokenType) -> Optional[Color]:
+ """Get a color (if any) for the given token.
+
+ Args:
+ token_type (TokenType): A token type tuple from Pygments.
+
+ Returns:
+ Optional[Color]: Color from theme, or None for no color.
+ """
+ style = self._theme.get_style_for_token(token_type)
+ return style.color
+
+ @property
+ def lexer(self) -> Optional[Lexer]:
+ """The lexer for this syntax, or None if no lexer was found.
+
+ Tries to find the lexer by name if a string was passed to the constructor.
+ """
+
+ if isinstance(self._lexer, Lexer):
+ return self._lexer
+ try:
+ return get_lexer_by_name(
+ self._lexer,
+ stripnl=False,
+ ensurenl=True,
+ tabsize=self.tab_size,
+ )
+ except ClassNotFound:
+ return None
+
+ def highlight(
+ self,
+ code: str,
+ line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,
+ ) -> Text:
+ """Highlight code and return a Text instance.
+
+ Args:
+ code (str): Code to highlight.
+ line_range(Tuple[int, int], optional): Optional line range to highlight.
+
+ Returns:
+ Text: A text instance containing highlighted syntax.
+ """
+
+ base_style = self._get_base_style()
+ justify: JustifyMethod = (
+ "default" if base_style.transparent_background else "left"
+ )
+
+ text = Text(
+ justify=justify,
+ style=base_style,
+ tab_size=self.tab_size,
+ no_wrap=not self.word_wrap,
+ )
+ _get_theme_style = self._theme.get_style_for_token
+
+ lexer = self.lexer
+
+ if lexer is None:
+ text.append(code)
+ else:
+ if line_range:
+ # More complicated path to only stylize a portion of the code
+ # This speeds up further operations as there are less spans to process
+ line_start, line_end = line_range
+
+ def line_tokenize() -> Iterable[Tuple[Any, str]]:
+ """Split tokens to one per line."""
+ assert lexer # required to make MyPy happy - we know lexer is not None at this point
+
+ for token_type, token in lexer.get_tokens(code):
+ while token:
+ line_token, new_line, token = token.partition("\n")
+ yield token_type, line_token + new_line
+
+ def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]:
+ """Convert tokens to spans."""
+ tokens = iter(line_tokenize())
+ line_no = 0
+ _line_start = line_start - 1 if line_start else 0
+
+ # Skip over tokens until line start
+ while line_no < _line_start:
+ _token_type, token = next(tokens)
+ yield (token, None)
+ if token.endswith("\n"):
+ line_no += 1
+ # Generate spans until line end
+ for token_type, token in tokens:
+ yield (token, _get_theme_style(token_type))
+ if token.endswith("\n"):
+ line_no += 1
+ if line_end and line_no >= line_end:
+ break
+
+ text.append_tokens(tokens_to_spans())
+
+ else:
+ text.append_tokens(
+ (token, _get_theme_style(token_type))
+ for token_type, token in lexer.get_tokens(code)
+ )
+ if self.background_color is not None:
+ text.stylize(f"on {self.background_color}")
+
+ if self._stylized_ranges:
+ self._apply_stylized_ranges(text)
+
+ return text
+
+ def stylize_range(
+ self, style: StyleType, start: SyntaxPosition, end: SyntaxPosition
+ ) -> None:
+ """
+ Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered.
+ Line numbers are 1-based, while column indexes are 0-based.
+
+ Args:
+ style (StyleType): The style to apply.
+ start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`.
+ end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`.
+ """
+ self._stylized_ranges.append(_SyntaxHighlightRange(style, start, end))
+
+ def _get_line_numbers_color(self, blend: float = 0.3) -> Color:
+ background_style = self._theme.get_background_style() + self.background_style
+ background_color = background_style.bgcolor
+ if background_color is None or background_color.is_system_defined:
+ return Color.default()
+ foreground_color = self._get_token_color(Token.Text)
+ if foreground_color is None or foreground_color.is_system_defined:
+ return foreground_color or Color.default()
+ new_color = blend_rgb(
+ background_color.get_truecolor(),
+ foreground_color.get_truecolor(),
+ cross_fade=blend,
+ )
+ return Color.from_triplet(new_color)
+
+ @property
+ def _numbers_column_width(self) -> int:
+ """Get the number of characters used to render the numbers column."""
+ column_width = 0
+ if self.line_numbers:
+ column_width = (
+ len(str(self.start_line + self.code.count("\n")))
+ + NUMBERS_COLUMN_DEFAULT_PADDING
+ )
+ return column_width
+
+ def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]:
+ """Get background, number, and highlight styles for line numbers."""
+ background_style = self._get_base_style()
+ if background_style.transparent_background:
+ return Style.null(), Style(dim=True), Style.null()
+ if console.color_system in ("256", "truecolor"):
+ number_style = Style.chain(
+ background_style,
+ self._theme.get_style_for_token(Token.Text),
+ Style(color=self._get_line_numbers_color()),
+ self.background_style,
+ )
+ highlight_number_style = Style.chain(
+ background_style,
+ self._theme.get_style_for_token(Token.Text),
+ Style(bold=True, color=self._get_line_numbers_color(0.9)),
+ self.background_style,
+ )
+ else:
+ number_style = background_style + Style(dim=True)
+ highlight_number_style = background_style + Style(dim=False)
+ return background_style, number_style, highlight_number_style
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "Measurement":
+ _, right, _, left = Padding.unpack(self.padding)
+ if self.code_width is not None:
+ width = self.code_width + self._numbers_column_width + right + left
+ return Measurement(self._numbers_column_width, width)
+ return Measurement(self._numbers_column_width, options.max_width)
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ segments = Segments(self._get_syntax(console, options))
+ if self.padding:
+ yield Padding(
+ segments, style=self._theme.get_background_style(), pad=self.padding
+ )
+ else:
+ yield segments
+
+ def _get_syntax(
+ self,
+ console: Console,
+ options: ConsoleOptions,
+ ) -> Iterable[Segment]:
+ """
+ Get the Segments for the Syntax object, excluding any vertical/horizontal padding
+ """
+ transparent_background = self._get_base_style().transparent_background
+ code_width = (
+ (
+ (options.max_width - self._numbers_column_width - 1)
+ if self.line_numbers
+ else options.max_width
+ )
+ if self.code_width is None
+ else self.code_width
+ )
+
+ ends_on_nl, processed_code = self._process_code(self.code)
+ text = self.highlight(processed_code, self.line_range)
+
+ if not self.line_numbers and not self.word_wrap and not self.line_range:
+ if not ends_on_nl:
+ text.remove_suffix("\n")
+ # Simple case of just rendering text
+ style = (
+ self._get_base_style()
+ + self._theme.get_style_for_token(Comment)
+ + Style(dim=True)
+ + self.background_style
+ )
+ if self.indent_guides and not options.ascii_only:
+ text = text.with_indent_guides(self.tab_size, style=style)
+ text.overflow = "crop"
+ if style.transparent_background:
+ yield from console.render(
+ text, options=options.update(width=code_width)
+ )
+ else:
+ syntax_lines = console.render_lines(
+ text,
+ options.update(width=code_width, height=None, justify="left"),
+ style=self.background_style,
+ pad=True,
+ new_lines=True,
+ )
+ for syntax_line in syntax_lines:
+ yield from syntax_line
+ return
+
+ start_line, end_line = self.line_range or (None, None)
+ line_offset = 0
+ if start_line:
+ line_offset = max(0, start_line - 1)
+ lines: Union[List[Text], Lines] = text.split("\n", allow_blank=ends_on_nl)
+ if self.line_range:
+ lines = lines[line_offset:end_line]
+
+ if self.indent_guides and not options.ascii_only:
+ style = (
+ self._get_base_style()
+ + self._theme.get_style_for_token(Comment)
+ + Style(dim=True)
+ + self.background_style
+ )
+ lines = (
+ Text("\n")
+ .join(lines)
+ .with_indent_guides(self.tab_size, style=style)
+ .split("\n", allow_blank=True)
+ )
+
+ numbers_column_width = self._numbers_column_width
+ render_options = options.update(width=code_width)
+
+ highlight_line = self.highlight_lines.__contains__
+ _Segment = Segment
+ new_line = _Segment("\n")
+
+ line_pointer = "> " if options.legacy_windows else "â± "
+
+ (
+ background_style,
+ number_style,
+ highlight_number_style,
+ ) = self._get_number_styles(console)
+
+ for line_no, line in enumerate(lines, self.start_line + line_offset):
+ if self.word_wrap:
+ wrapped_lines = console.render_lines(
+ line,
+ render_options.update(height=None, justify="left"),
+ style=background_style,
+ pad=not transparent_background,
+ )
+ else:
+ segments = list(line.render(console, end=""))
+ if options.no_wrap:
+ wrapped_lines = [segments]
+ else:
+ wrapped_lines = [
+ _Segment.adjust_line_length(
+ segments,
+ render_options.max_width,
+ style=background_style,
+ pad=not transparent_background,
+ )
+ ]
+
+ if self.line_numbers:
+ wrapped_line_left_pad = _Segment(
+ " " * numbers_column_width + " ", background_style
+ )
+ for first, wrapped_line in loop_first(wrapped_lines):
+ if first:
+ line_column = str(line_no).rjust(numbers_column_width - 2) + " "
+ if highlight_line(line_no):
+ yield _Segment(line_pointer, Style(color="red"))
+ yield _Segment(line_column, highlight_number_style)
+ else:
+ yield _Segment(" ", highlight_number_style)
+ yield _Segment(line_column, number_style)
+ else:
+ yield wrapped_line_left_pad
+ yield from wrapped_line
+ yield new_line
+ else:
+ for wrapped_line in wrapped_lines:
+ yield from wrapped_line
+ yield new_line
+
+ def _apply_stylized_ranges(self, text: Text) -> None:
+ """
+ Apply stylized ranges to a text instance,
+ using the given code to determine the right portion to apply the style to.
+
+ Args:
+ text (Text): Text instance to apply the style to.
+ """
+ code = text.plain
+ newlines_offsets = [
+ # Let's add outer boundaries at each side of the list:
+ 0,
+ # N.B. using "\n" here is much faster than using metacharacters such as "^" or "\Z":
+ *[
+ match.start() + 1
+ for match in re.finditer("\n", code, flags=re.MULTILINE)
+ ],
+ len(code) + 1,
+ ]
+
+ for stylized_range in self._stylized_ranges:
+ start = _get_code_index_for_syntax_position(
+ newlines_offsets, stylized_range.start
+ )
+ end = _get_code_index_for_syntax_position(
+ newlines_offsets, stylized_range.end
+ )
+ if start is not None and end is not None:
+ text.stylize(stylized_range.style, start, end)
+
+ def _process_code(self, code: str) -> Tuple[bool, str]:
+ """
+ Applies various processing to a raw code string
+ (normalises it so it always ends with a line return, dedents it if necessary, etc.)
+
+ Args:
+ code (str): The raw code string to process
+
+ Returns:
+ Tuple[bool, str]: the boolean indicates whether the raw code ends with a line return,
+ while the string is the processed code.
+ """
+ ends_on_nl = code.endswith("\n")
+ processed_code = code if ends_on_nl else code + "\n"
+ processed_code = (
+ textwrap.dedent(processed_code) if self.dedent else processed_code
+ )
+ processed_code = processed_code.expandtabs(self.tab_size)
+ return ends_on_nl, processed_code
+
+
+def _get_code_index_for_syntax_position(
+ newlines_offsets: Sequence[int], position: SyntaxPosition
+) -> Optional[int]:
+ """
+ Returns the index of the code string for the given positions.
+
+ Args:
+ newlines_offsets (Sequence[int]): The offset of each newline character found in the code snippet.
+ position (SyntaxPosition): The position to search for.
+
+ Returns:
+ Optional[int]: The index of the code string for this position, or `None`
+ if the given position's line number is out of range (if it's the column that is out of range
+ we silently clamp its value so that it reaches the end of the line)
+ """
+ lines_count = len(newlines_offsets)
+
+ line_number, column_index = position
+ if line_number > lines_count or len(newlines_offsets) < (line_number + 1):
+ return None # `line_number` is out of range
+ line_index = line_number - 1
+ line_length = newlines_offsets[line_index + 1] - newlines_offsets[line_index] - 1
+ # If `column_index` is out of range: let's silently clamp it:
+ column_index = min(line_length, column_index)
+ return newlines_offsets[line_index] + column_index
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ import argparse
+ import sys
+
+ parser = argparse.ArgumentParser(
+ description="Render syntax to the console with Rich"
+ )
+ parser.add_argument(
+ "path",
+ metavar="PATH",
+ help="path to file, or - for stdin",
+ )
+ parser.add_argument(
+ "-c",
+ "--force-color",
+ dest="force_color",
+ action="store_true",
+ default=None,
+ help="force color for non-terminals",
+ )
+ parser.add_argument(
+ "-i",
+ "--indent-guides",
+ dest="indent_guides",
+ action="store_true",
+ default=False,
+ help="display indent guides",
+ )
+ parser.add_argument(
+ "-l",
+ "--line-numbers",
+ dest="line_numbers",
+ action="store_true",
+ help="render line numbers",
+ )
+ parser.add_argument(
+ "-w",
+ "--width",
+ type=int,
+ dest="width",
+ default=None,
+ help="width of output (default will auto-detect)",
+ )
+ parser.add_argument(
+ "-r",
+ "--wrap",
+ dest="word_wrap",
+ action="store_true",
+ default=False,
+ help="word wrap long lines",
+ )
+ parser.add_argument(
+ "-s",
+ "--soft-wrap",
+ action="store_true",
+ dest="soft_wrap",
+ default=False,
+ help="enable soft wrapping mode",
+ )
+ parser.add_argument(
+ "-t", "--theme", dest="theme", default="monokai", help="pygments theme"
+ )
+ parser.add_argument(
+ "-b",
+ "--background-color",
+ dest="background_color",
+ default=None,
+ help="Override background color",
+ )
+ parser.add_argument(
+ "-x",
+ "--lexer",
+ default=None,
+ dest="lexer_name",
+ help="Lexer name",
+ )
+ parser.add_argument(
+ "-p", "--padding", type=int, default=0, dest="padding", help="Padding"
+ )
+ parser.add_argument(
+ "--highlight-line",
+ type=int,
+ default=None,
+ dest="highlight_line",
+ help="The line number (not index!) to highlight",
+ )
+ args = parser.parse_args()
+
+ from pip._vendor.rich.console import Console
+
+ console = Console(force_terminal=args.force_color, width=args.width)
+
+ if args.path == "-":
+ code = sys.stdin.read()
+ syntax = Syntax(
+ code=code,
+ lexer=args.lexer_name,
+ line_numbers=args.line_numbers,
+ word_wrap=args.word_wrap,
+ theme=args.theme,
+ background_color=args.background_color,
+ indent_guides=args.indent_guides,
+ padding=args.padding,
+ highlight_lines={args.highlight_line},
+ )
+ else:
+ syntax = Syntax.from_path(
+ args.path,
+ lexer=args.lexer_name,
+ line_numbers=args.line_numbers,
+ word_wrap=args.word_wrap,
+ theme=args.theme,
+ background_color=args.background_color,
+ indent_guides=args.indent_guides,
+ padding=args.padding,
+ highlight_lines={args.highlight_line},
+ )
+ console.print(syntax, soft_wrap=args.soft_wrap)
diff --git a/src/pip/_vendor/rich/table.py b/src/pip/_vendor/rich/table.py
new file mode 100644
index 000000000..8fc28ef2f
--- /dev/null
+++ b/src/pip/_vendor/rich/table.py
@@ -0,0 +1,996 @@
+from dataclasses import dataclass, field, replace
+from typing import (
+ TYPE_CHECKING,
+ Dict,
+ Iterable,
+ List,
+ NamedTuple,
+ Optional,
+ Sequence,
+ Tuple,
+ Union,
+)
+
+from . import box, errors
+from ._loop import loop_first_last, loop_last
+from ._pick import pick_bool
+from ._ratio import ratio_distribute, ratio_reduce
+from .align import VerticalAlignMethod
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .padding import Padding, PaddingDimensions
+from .protocol import is_renderable
+from .segment import Segment
+from .style import Style, StyleType
+from .text import Text, TextType
+
+if TYPE_CHECKING:
+ from .console import (
+ Console,
+ ConsoleOptions,
+ JustifyMethod,
+ OverflowMethod,
+ RenderableType,
+ RenderResult,
+ )
+
+
+@dataclass
+class Column:
+ """Defines a column within a ~Table.
+
+ Args:
+ title (Union[str, Text], optional): The title of the table rendered at the top. Defaults to None.
+ caption (Union[str, Text], optional): The table caption rendered below. Defaults to None.
+ width (int, optional): The width in characters of the table, or ``None`` to automatically fit. Defaults to None.
+ min_width (Optional[int], optional): The minimum width of the table, or ``None`` for no minimum. Defaults to None.
+ box (box.Box, optional): One of the constants in box.py used to draw the edges (see :ref:`appendix_box`), or ``None`` for no box lines. Defaults to box.HEAVY_HEAD.
+ safe_box (Optional[bool], optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
+ padding (PaddingDimensions, optional): Padding for cells (top, right, bottom, left). Defaults to (0, 1).
+ collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to False.
+ pad_edge (bool, optional): Enable padding of edge cells. Defaults to True.
+ expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
+ show_header (bool, optional): Show a header row. Defaults to True.
+ show_footer (bool, optional): Show a footer row. Defaults to False.
+ show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True.
+ show_lines (bool, optional): Draw lines between every row. Defaults to False.
+ leading (bool, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0.
+ style (Union[str, Style], optional): Default style for the table. Defaults to "none".
+ row_styles (List[Union, str], optional): Optional list of row styles, if more than one style is given then the styles will alternate. Defaults to None.
+ header_style (Union[str, Style], optional): Style of the header. Defaults to "table.header".
+ footer_style (Union[str, Style], optional): Style of the footer. Defaults to "table.footer".
+ border_style (Union[str, Style], optional): Style of the border. Defaults to None.
+ title_style (Union[str, Style], optional): Style of the title. Defaults to None.
+ caption_style (Union[str, Style], optional): Style of the caption. Defaults to None.
+ title_justify (str, optional): Justify method for title. Defaults to "center".
+ caption_justify (str, optional): Justify method for caption. Defaults to "center".
+ highlight (bool, optional): Highlight cell contents (if str). Defaults to False.
+ """
+
+ header: "RenderableType" = ""
+ """RenderableType: Renderable for the header (typically a string)"""
+
+ footer: "RenderableType" = ""
+ """RenderableType: Renderable for the footer (typically a string)"""
+
+ header_style: StyleType = ""
+ """StyleType: The style of the header."""
+
+ footer_style: StyleType = ""
+ """StyleType: The style of the footer."""
+
+ style: StyleType = ""
+ """StyleType: The style of the column."""
+
+ justify: "JustifyMethod" = "left"
+ """str: How to justify text within the column ("left", "center", "right", or "full")"""
+
+ vertical: "VerticalAlignMethod" = "top"
+ """str: How to vertically align content ("top", "middle", or "bottom")"""
+
+ overflow: "OverflowMethod" = "ellipsis"
+ """str: Overflow method."""
+
+ width: Optional[int] = None
+ """Optional[int]: Width of the column, or ``None`` (default) to auto calculate width."""
+
+ min_width: Optional[int] = None
+ """Optional[int]: Minimum width of column, or ``None`` for no minimum. Defaults to None."""
+
+ max_width: Optional[int] = None
+ """Optional[int]: Maximum width of column, or ``None`` for no maximum. Defaults to None."""
+
+ ratio: Optional[int] = None
+ """Optional[int]: Ratio to use when calculating column width, or ``None`` (default) to adapt to column contents."""
+
+ no_wrap: bool = False
+ """bool: Prevent wrapping of text within the column. Defaults to ``False``."""
+
+ _index: int = 0
+ """Index of column."""
+
+ _cells: List["RenderableType"] = field(default_factory=list)
+
+ def copy(self) -> "Column":
+ """Return a copy of this Column."""
+ return replace(self, _cells=[])
+
+ @property
+ def cells(self) -> Iterable["RenderableType"]:
+ """Get all cells in the column, not including header."""
+ yield from self._cells
+
+ @property
+ def flexible(self) -> bool:
+ """Check if this column is flexible."""
+ return self.ratio is not None
+
+
+@dataclass
+class Row:
+ """Information regarding a row."""
+
+ style: Optional[StyleType] = None
+ """Style to apply to row."""
+
+ end_section: bool = False
+ """Indicated end of section, which will force a line beneath the row."""
+
+
+class _Cell(NamedTuple):
+ """A single cell in a table."""
+
+ style: StyleType
+ """Style to apply to cell."""
+ renderable: "RenderableType"
+ """Cell renderable."""
+ vertical: VerticalAlignMethod
+ """Cell vertical alignment."""
+
+
+class Table(JupyterMixin):
+ """A console renderable to draw a table.
+
+ Args:
+ *headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance.
+ title (Union[str, Text], optional): The title of the table rendered at the top. Defaults to None.
+ caption (Union[str, Text], optional): The table caption rendered below. Defaults to None.
+ width (int, optional): The width in characters of the table, or ``None`` to automatically fit. Defaults to None.
+ min_width (Optional[int], optional): The minimum width of the table, or ``None`` for no minimum. Defaults to None.
+ box (box.Box, optional): One of the constants in box.py used to draw the edges (see :ref:`appendix_box`), or ``None`` for no box lines. Defaults to box.HEAVY_HEAD.
+ safe_box (Optional[bool], optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
+ padding (PaddingDimensions, optional): Padding for cells (top, right, bottom, left). Defaults to (0, 1).
+ collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to False.
+ pad_edge (bool, optional): Enable padding of edge cells. Defaults to True.
+ expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
+ show_header (bool, optional): Show a header row. Defaults to True.
+ show_footer (bool, optional): Show a footer row. Defaults to False.
+ show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True.
+ show_lines (bool, optional): Draw lines between every row. Defaults to False.
+ leading (bool, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0.
+ style (Union[str, Style], optional): Default style for the table. Defaults to "none".
+ row_styles (List[Union, str], optional): Optional list of row styles, if more than one style is given then the styles will alternate. Defaults to None.
+ header_style (Union[str, Style], optional): Style of the header. Defaults to "table.header".
+ footer_style (Union[str, Style], optional): Style of the footer. Defaults to "table.footer".
+ border_style (Union[str, Style], optional): Style of the border. Defaults to None.
+ title_style (Union[str, Style], optional): Style of the title. Defaults to None.
+ caption_style (Union[str, Style], optional): Style of the caption. Defaults to None.
+ title_justify (str, optional): Justify method for title. Defaults to "center".
+ caption_justify (str, optional): Justify method for caption. Defaults to "center".
+ highlight (bool, optional): Highlight cell contents (if str). Defaults to False.
+ """
+
+ columns: List[Column]
+ rows: List[Row]
+
+ def __init__(
+ self,
+ *headers: Union[Column, str],
+ title: Optional[TextType] = None,
+ caption: Optional[TextType] = None,
+ width: Optional[int] = None,
+ min_width: Optional[int] = None,
+ box: Optional[box.Box] = box.HEAVY_HEAD,
+ safe_box: Optional[bool] = None,
+ padding: PaddingDimensions = (0, 1),
+ collapse_padding: bool = False,
+ pad_edge: bool = True,
+ expand: bool = False,
+ show_header: bool = True,
+ show_footer: bool = False,
+ show_edge: bool = True,
+ show_lines: bool = False,
+ leading: int = 0,
+ style: StyleType = "none",
+ row_styles: Optional[Iterable[StyleType]] = None,
+ header_style: Optional[StyleType] = "table.header",
+ footer_style: Optional[StyleType] = "table.footer",
+ border_style: Optional[StyleType] = None,
+ title_style: Optional[StyleType] = None,
+ caption_style: Optional[StyleType] = None,
+ title_justify: "JustifyMethod" = "center",
+ caption_justify: "JustifyMethod" = "center",
+ highlight: bool = False,
+ ) -> None:
+
+ self.columns: List[Column] = []
+ self.rows: List[Row] = []
+ self.title = title
+ self.caption = caption
+ self.width = width
+ self.min_width = min_width
+ self.box = box
+ self.safe_box = safe_box
+ self._padding = Padding.unpack(padding)
+ self.pad_edge = pad_edge
+ self._expand = expand
+ self.show_header = show_header
+ self.show_footer = show_footer
+ self.show_edge = show_edge
+ self.show_lines = show_lines
+ self.leading = leading
+ self.collapse_padding = collapse_padding
+ self.style = style
+ self.header_style = header_style or ""
+ self.footer_style = footer_style or ""
+ self.border_style = border_style
+ self.title_style = title_style
+ self.caption_style = caption_style
+ self.title_justify: "JustifyMethod" = title_justify
+ self.caption_justify: "JustifyMethod" = caption_justify
+ self.highlight = highlight
+ self.row_styles: Sequence[StyleType] = list(row_styles or [])
+ append_column = self.columns.append
+ for header in headers:
+ if isinstance(header, str):
+ self.add_column(header=header)
+ else:
+ header._index = len(self.columns)
+ append_column(header)
+
+ @classmethod
+ def grid(
+ cls,
+ *headers: Union[Column, str],
+ padding: PaddingDimensions = 0,
+ collapse_padding: bool = True,
+ pad_edge: bool = False,
+ expand: bool = False,
+ ) -> "Table":
+ """Get a table with no lines, headers, or footer.
+
+ Args:
+ *headers (Union[Column, str]): Column headers, either as a string, or :class:`~rich.table.Column` instance.
+ padding (PaddingDimensions, optional): Get padding around cells. Defaults to 0.
+ collapse_padding (bool, optional): Enable collapsing of padding around cells. Defaults to True.
+ pad_edge (bool, optional): Enable padding around edges of table. Defaults to False.
+ expand (bool, optional): Expand the table to fit the available space if ``True``, otherwise the table width will be auto-calculated. Defaults to False.
+
+ Returns:
+ Table: A table instance.
+ """
+ return cls(
+ *headers,
+ box=None,
+ padding=padding,
+ collapse_padding=collapse_padding,
+ show_header=False,
+ show_footer=False,
+ show_edge=False,
+ pad_edge=pad_edge,
+ expand=expand,
+ )
+
+ @property
+ def expand(self) -> bool:
+ """Setting a non-None self.width implies expand."""
+ return self._expand or self.width is not None
+
+ @expand.setter
+ def expand(self, expand: bool) -> None:
+ """Set expand."""
+ self._expand = expand
+
+ @property
+ def _extra_width(self) -> int:
+ """Get extra width to add to cell content."""
+ width = 0
+ if self.box and self.show_edge:
+ width += 2
+ if self.box:
+ width += len(self.columns) - 1
+ return width
+
+ @property
+ def row_count(self) -> int:
+ """Get the current number of rows."""
+ return len(self.rows)
+
+ def get_row_style(self, console: "Console", index: int) -> StyleType:
+ """Get the current row style."""
+ style = Style.null()
+ if self.row_styles:
+ style += console.get_style(self.row_styles[index % len(self.row_styles)])
+ row_style = self.rows[index].style
+ if row_style is not None:
+ style += console.get_style(row_style)
+ return style
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> Measurement:
+ max_width = options.max_width
+ if self.width is not None:
+ max_width = self.width
+ if max_width < 0:
+ return Measurement(0, 0)
+
+ extra_width = self._extra_width
+ max_width = sum(
+ self._calculate_column_widths(
+ console, options.update_width(max_width - extra_width)
+ )
+ )
+ _measure_column = self._measure_column
+
+ measurements = [
+ _measure_column(console, options.update_width(max_width), column)
+ for column in self.columns
+ ]
+ minimum_width = (
+ sum(measurement.minimum for measurement in measurements) + extra_width
+ )
+ maximum_width = (
+ sum(measurement.maximum for measurement in measurements) + extra_width
+ if (self.width is None)
+ else self.width
+ )
+ measurement = Measurement(minimum_width, maximum_width)
+ measurement = measurement.clamp(self.min_width)
+ return measurement
+
+ @property
+ def padding(self) -> Tuple[int, int, int, int]:
+ """Get cell padding."""
+ return self._padding
+
+ @padding.setter
+ def padding(self, padding: PaddingDimensions) -> "Table":
+ """Set cell padding."""
+ self._padding = Padding.unpack(padding)
+ return self
+
+ def add_column(
+ self,
+ header: "RenderableType" = "",
+ footer: "RenderableType" = "",
+ *,
+ header_style: Optional[StyleType] = None,
+ footer_style: Optional[StyleType] = None,
+ style: Optional[StyleType] = None,
+ justify: "JustifyMethod" = "left",
+ vertical: "VerticalAlignMethod" = "top",
+ overflow: "OverflowMethod" = "ellipsis",
+ width: Optional[int] = None,
+ min_width: Optional[int] = None,
+ max_width: Optional[int] = None,
+ ratio: Optional[int] = None,
+ no_wrap: bool = False,
+ ) -> None:
+ """Add a column to the table.
+
+ Args:
+ header (RenderableType, optional): Text or renderable for the header.
+ Defaults to "".
+ footer (RenderableType, optional): Text or renderable for the footer.
+ Defaults to "".
+ header_style (Union[str, Style], optional): Style for the header, or None for default. Defaults to None.
+ footer_style (Union[str, Style], optional): Style for the footer, or None for default. Defaults to None.
+ style (Union[str, Style], optional): Style for the column cells, or None for default. Defaults to None.
+ justify (JustifyMethod, optional): Alignment for cells. Defaults to "left".
+ vertical (VerticalAlignMethod, optional): Vertical alignment, one of "top", "middle", or "bottom". Defaults to "top".
+ overflow (OverflowMethod): Overflow method: "crop", "fold", "ellipsis". Defaults to "ellipsis".
+ width (int, optional): Desired width of column in characters, or None to fit to contents. Defaults to None.
+ min_width (Optional[int], optional): Minimum width of column, or ``None`` for no minimum. Defaults to None.
+ max_width (Optional[int], optional): Maximum width of column, or ``None`` for no maximum. Defaults to None.
+ ratio (int, optional): Flexible ratio for the column (requires ``Table.expand`` or ``Table.width``). Defaults to None.
+ no_wrap (bool, optional): Set to ``True`` to disable wrapping of this column.
+ """
+
+ column = Column(
+ _index=len(self.columns),
+ header=header,
+ footer=footer,
+ header_style=header_style or "",
+ footer_style=footer_style or "",
+ style=style or "",
+ justify=justify,
+ vertical=vertical,
+ overflow=overflow,
+ width=width,
+ min_width=min_width,
+ max_width=max_width,
+ ratio=ratio,
+ no_wrap=no_wrap,
+ )
+ self.columns.append(column)
+
+ def add_row(
+ self,
+ *renderables: Optional["RenderableType"],
+ style: Optional[StyleType] = None,
+ end_section: bool = False,
+ ) -> None:
+ """Add a row of renderables.
+
+ Args:
+ *renderables (None or renderable): Each cell in a row must be a renderable object (including str),
+ or ``None`` for a blank cell.
+ style (StyleType, optional): An optional style to apply to the entire row. Defaults to None.
+ end_section (bool, optional): End a section and draw a line. Defaults to False.
+
+ Raises:
+ errors.NotRenderableError: If you add something that can't be rendered.
+ """
+
+ def add_cell(column: Column, renderable: "RenderableType") -> None:
+ column._cells.append(renderable)
+
+ cell_renderables: List[Optional["RenderableType"]] = list(renderables)
+
+ columns = self.columns
+ if len(cell_renderables) < len(columns):
+ cell_renderables = [
+ *cell_renderables,
+ *[None] * (len(columns) - len(cell_renderables)),
+ ]
+ for index, renderable in enumerate(cell_renderables):
+ if index == len(columns):
+ column = Column(_index=index)
+ for _ in self.rows:
+ add_cell(column, Text(""))
+ self.columns.append(column)
+ else:
+ column = columns[index]
+ if renderable is None:
+ add_cell(column, "")
+ elif is_renderable(renderable):
+ add_cell(column, renderable)
+ else:
+ raise errors.NotRenderableError(
+ f"unable to render {type(renderable).__name__}; a string or other renderable object is required"
+ )
+ self.rows.append(Row(style=style, end_section=end_section))
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+
+ if not self.columns:
+ yield Segment("\n")
+ return
+
+ max_width = options.max_width
+ if self.width is not None:
+ max_width = self.width
+
+ extra_width = self._extra_width
+ widths = self._calculate_column_widths(
+ console, options.update_width(max_width - extra_width)
+ )
+ table_width = sum(widths) + extra_width
+
+ render_options = options.update(
+ width=table_width, highlight=self.highlight, height=None
+ )
+
+ def render_annotation(
+ text: TextType, style: StyleType, justify: "JustifyMethod" = "center"
+ ) -> "RenderResult":
+ render_text = (
+ console.render_str(text, style=style, highlight=False)
+ if isinstance(text, str)
+ else text
+ )
+ return console.render(
+ render_text, options=render_options.update(justify=justify)
+ )
+
+ if self.title:
+ yield from render_annotation(
+ self.title,
+ style=Style.pick_first(self.title_style, "table.title"),
+ justify=self.title_justify,
+ )
+ yield from self._render(console, render_options, widths)
+ if self.caption:
+ yield from render_annotation(
+ self.caption,
+ style=Style.pick_first(self.caption_style, "table.caption"),
+ justify=self.caption_justify,
+ )
+
+ def _calculate_column_widths(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> List[int]:
+ """Calculate the widths of each column, including padding, not including borders."""
+ max_width = options.max_width
+ columns = self.columns
+ width_ranges = [
+ self._measure_column(console, options, column) for column in columns
+ ]
+ widths = [_range.maximum or 1 for _range in width_ranges]
+ get_padding_width = self._get_padding_width
+ extra_width = self._extra_width
+ if self.expand:
+ ratios = [col.ratio or 0 for col in columns if col.flexible]
+ if any(ratios):
+ fixed_widths = [
+ 0 if column.flexible else _range.maximum
+ for _range, column in zip(width_ranges, columns)
+ ]
+ flex_minimum = [
+ (column.width or 1) + get_padding_width(column._index)
+ for column in columns
+ if column.flexible
+ ]
+ flexible_width = max_width - sum(fixed_widths)
+ flex_widths = ratio_distribute(flexible_width, ratios, flex_minimum)
+ iter_flex_widths = iter(flex_widths)
+ for index, column in enumerate(columns):
+ if column.flexible:
+ widths[index] = fixed_widths[index] + next(iter_flex_widths)
+ table_width = sum(widths)
+
+ if table_width > max_width:
+ widths = self._collapse_widths(
+ widths,
+ [(column.width is None and not column.no_wrap) for column in columns],
+ max_width,
+ )
+ table_width = sum(widths)
+ # last resort, reduce columns evenly
+ if table_width > max_width:
+ excess_width = table_width - max_width
+ widths = ratio_reduce(excess_width, [1] * len(widths), widths, widths)
+ table_width = sum(widths)
+
+ width_ranges = [
+ self._measure_column(console, options.update_width(width), column)
+ for width, column in zip(widths, columns)
+ ]
+ widths = [_range.maximum or 0 for _range in width_ranges]
+
+ if (table_width < max_width and self.expand) or (
+ self.min_width is not None and table_width < (self.min_width - extra_width)
+ ):
+ _max_width = (
+ max_width
+ if self.min_width is None
+ else min(self.min_width - extra_width, max_width)
+ )
+ pad_widths = ratio_distribute(_max_width - table_width, widths)
+ widths = [_width + pad for _width, pad in zip(widths, pad_widths)]
+
+ return widths
+
+ @classmethod
+ def _collapse_widths(
+ cls, widths: List[int], wrapable: List[bool], max_width: int
+ ) -> List[int]:
+ """Reduce widths so that the total is under max_width.
+
+ Args:
+ widths (List[int]): List of widths.
+ wrapable (List[bool]): List of booleans that indicate if a column may shrink.
+ max_width (int): Maximum width to reduce to.
+
+ Returns:
+ List[int]: A new list of widths.
+ """
+ total_width = sum(widths)
+ excess_width = total_width - max_width
+ if any(wrapable):
+ while total_width and excess_width > 0:
+ max_column = max(
+ width for width, allow_wrap in zip(widths, wrapable) if allow_wrap
+ )
+ second_max_column = max(
+ width if allow_wrap and width != max_column else 0
+ for width, allow_wrap in zip(widths, wrapable)
+ )
+ column_difference = max_column - second_max_column
+ ratios = [
+ (1 if (width == max_column and allow_wrap) else 0)
+ for width, allow_wrap in zip(widths, wrapable)
+ ]
+ if not any(ratios) or not column_difference:
+ break
+ max_reduce = [min(excess_width, column_difference)] * len(widths)
+ widths = ratio_reduce(excess_width, ratios, max_reduce, widths)
+
+ total_width = sum(widths)
+ excess_width = total_width - max_width
+ return widths
+
+ def _get_cells(
+ self, console: "Console", column_index: int, column: Column
+ ) -> Iterable[_Cell]:
+ """Get all the cells with padding and optional header."""
+
+ collapse_padding = self.collapse_padding
+ pad_edge = self.pad_edge
+ padding = self.padding
+ any_padding = any(padding)
+
+ first_column = column_index == 0
+ last_column = column_index == len(self.columns) - 1
+
+ _padding_cache: Dict[Tuple[bool, bool], Tuple[int, int, int, int]] = {}
+
+ def get_padding(first_row: bool, last_row: bool) -> Tuple[int, int, int, int]:
+ cached = _padding_cache.get((first_row, last_row))
+ if cached:
+ return cached
+ top, right, bottom, left = padding
+
+ if collapse_padding:
+ if not first_column:
+ left = max(0, left - right)
+ if not last_row:
+ bottom = max(0, top - bottom)
+
+ if not pad_edge:
+ if first_column:
+ left = 0
+ if last_column:
+ right = 0
+ if first_row:
+ top = 0
+ if last_row:
+ bottom = 0
+ _padding = (top, right, bottom, left)
+ _padding_cache[(first_row, last_row)] = _padding
+ return _padding
+
+ raw_cells: List[Tuple[StyleType, "RenderableType"]] = []
+ _append = raw_cells.append
+ get_style = console.get_style
+ if self.show_header:
+ header_style = get_style(self.header_style or "") + get_style(
+ column.header_style
+ )
+ _append((header_style, column.header))
+ cell_style = get_style(column.style or "")
+ for cell in column.cells:
+ _append((cell_style, cell))
+ if self.show_footer:
+ footer_style = get_style(self.footer_style or "") + get_style(
+ column.footer_style
+ )
+ _append((footer_style, column.footer))
+
+ if any_padding:
+ _Padding = Padding
+ for first, last, (style, renderable) in loop_first_last(raw_cells):
+ yield _Cell(
+ style,
+ _Padding(renderable, get_padding(first, last)),
+ getattr(renderable, "vertical", None) or column.vertical,
+ )
+ else:
+ for (style, renderable) in raw_cells:
+ yield _Cell(
+ style,
+ renderable,
+ getattr(renderable, "vertical", None) or column.vertical,
+ )
+
+ def _get_padding_width(self, column_index: int) -> int:
+ """Get extra width from padding."""
+ _, pad_right, _, pad_left = self.padding
+ if self.collapse_padding:
+ if column_index > 0:
+ pad_left = max(0, pad_left - pad_right)
+ return pad_left + pad_right
+
+ def _measure_column(
+ self,
+ console: "Console",
+ options: "ConsoleOptions",
+ column: Column,
+ ) -> Measurement:
+ """Get the minimum and maximum width of the column."""
+
+ max_width = options.max_width
+ if max_width < 1:
+ return Measurement(0, 0)
+
+ padding_width = self._get_padding_width(column._index)
+
+ if column.width is not None:
+ # Fixed width column
+ return Measurement(
+ column.width + padding_width, column.width + padding_width
+ ).with_maximum(max_width)
+ # Flexible column, we need to measure contents
+ min_widths: List[int] = []
+ max_widths: List[int] = []
+ append_min = min_widths.append
+ append_max = max_widths.append
+ get_render_width = Measurement.get
+ for cell in self._get_cells(console, column._index, column):
+ _min, _max = get_render_width(console, options, cell.renderable)
+ append_min(_min)
+ append_max(_max)
+
+ measurement = Measurement(
+ max(min_widths) if min_widths else 1,
+ max(max_widths) if max_widths else max_width,
+ ).with_maximum(max_width)
+ measurement = measurement.clamp(
+ None if column.min_width is None else column.min_width + padding_width,
+ None if column.max_width is None else column.max_width + padding_width,
+ )
+ return measurement
+
+ def _render(
+ self, console: "Console", options: "ConsoleOptions", widths: List[int]
+ ) -> "RenderResult":
+ table_style = console.get_style(self.style or "")
+
+ border_style = table_style + console.get_style(self.border_style or "")
+ _column_cells = (
+ self._get_cells(console, column_index, column)
+ for column_index, column in enumerate(self.columns)
+ )
+ row_cells: List[Tuple[_Cell, ...]] = list(zip(*_column_cells))
+ _box = (
+ self.box.substitute(
+ options, safe=pick_bool(self.safe_box, console.safe_box)
+ )
+ if self.box
+ else None
+ )
+ _box = _box.get_plain_headed_box() if _box and not self.show_header else _box
+
+ new_line = Segment.line()
+
+ columns = self.columns
+ show_header = self.show_header
+ show_footer = self.show_footer
+ show_edge = self.show_edge
+ show_lines = self.show_lines
+ leading = self.leading
+
+ _Segment = Segment
+ if _box:
+ box_segments = [
+ (
+ _Segment(_box.head_left, border_style),
+ _Segment(_box.head_right, border_style),
+ _Segment(_box.head_vertical, border_style),
+ ),
+ (
+ _Segment(_box.foot_left, border_style),
+ _Segment(_box.foot_right, border_style),
+ _Segment(_box.foot_vertical, border_style),
+ ),
+ (
+ _Segment(_box.mid_left, border_style),
+ _Segment(_box.mid_right, border_style),
+ _Segment(_box.mid_vertical, border_style),
+ ),
+ ]
+ if show_edge:
+ yield _Segment(_box.get_top(widths), border_style)
+ yield new_line
+ else:
+ box_segments = []
+
+ get_row_style = self.get_row_style
+ get_style = console.get_style
+
+ for index, (first, last, row_cell) in enumerate(loop_first_last(row_cells)):
+ header_row = first and show_header
+ footer_row = last and show_footer
+ row = (
+ self.rows[index - show_header]
+ if (not header_row and not footer_row)
+ else None
+ )
+ max_height = 1
+ cells: List[List[List[Segment]]] = []
+ if header_row or footer_row:
+ row_style = Style.null()
+ else:
+ row_style = get_style(
+ get_row_style(console, index - 1 if show_header else index)
+ )
+ for width, cell, column in zip(widths, row_cell, columns):
+ render_options = options.update(
+ width=width,
+ justify=column.justify,
+ no_wrap=column.no_wrap,
+ overflow=column.overflow,
+ height=None,
+ )
+ lines = console.render_lines(
+ cell.renderable,
+ render_options,
+ style=get_style(cell.style) + row_style,
+ )
+ max_height = max(max_height, len(lines))
+ cells.append(lines)
+
+ row_height = max(len(cell) for cell in cells)
+
+ def align_cell(
+ cell: List[List[Segment]],
+ vertical: "VerticalAlignMethod",
+ width: int,
+ style: Style,
+ ) -> List[List[Segment]]:
+ if header_row:
+ vertical = "bottom"
+ elif footer_row:
+ vertical = "top"
+
+ if vertical == "top":
+ return _Segment.align_top(cell, width, row_height, style)
+ elif vertical == "middle":
+ return _Segment.align_middle(cell, width, row_height, style)
+ return _Segment.align_bottom(cell, width, row_height, style)
+
+ cells[:] = [
+ _Segment.set_shape(
+ align_cell(
+ cell,
+ _cell.vertical,
+ width,
+ get_style(_cell.style) + row_style,
+ ),
+ width,
+ max_height,
+ )
+ for width, _cell, cell, column in zip(widths, row_cell, cells, columns)
+ ]
+
+ if _box:
+ if last and show_footer:
+ yield _Segment(
+ _box.get_row(widths, "foot", edge=show_edge), border_style
+ )
+ yield new_line
+ left, right, _divider = box_segments[0 if first else (2 if last else 1)]
+
+ # If the column divider is whitespace also style it with the row background
+ divider = (
+ _divider
+ if _divider.text.strip()
+ else _Segment(
+ _divider.text, row_style.background_style + _divider.style
+ )
+ )
+ for line_no in range(max_height):
+ if show_edge:
+ yield left
+ for last_cell, rendered_cell in loop_last(cells):
+ yield from rendered_cell[line_no]
+ if not last_cell:
+ yield divider
+ if show_edge:
+ yield right
+ yield new_line
+ else:
+ for line_no in range(max_height):
+ for rendered_cell in cells:
+ yield from rendered_cell[line_no]
+ yield new_line
+ if _box and first and show_header:
+ yield _Segment(
+ _box.get_row(widths, "head", edge=show_edge), border_style
+ )
+ yield new_line
+ end_section = row and row.end_section
+ if _box and (show_lines or leading or end_section):
+ if (
+ not last
+ and not (show_footer and index >= len(row_cells) - 2)
+ and not (show_header and header_row)
+ ):
+ if leading:
+ yield _Segment(
+ _box.get_row(widths, "mid", edge=show_edge) * leading,
+ border_style,
+ )
+ else:
+ yield _Segment(
+ _box.get_row(widths, "row", edge=show_edge), border_style
+ )
+ yield new_line
+
+ if _box and show_edge:
+ yield _Segment(_box.get_bottom(widths), border_style)
+ yield new_line
+
+
+if __name__ == "__main__": # pragma: no cover
+ from pip._vendor.rich.console import Console
+ from pip._vendor.rich.highlighter import ReprHighlighter
+ from pip._vendor.rich.table import Table as Table
+
+ from ._timer import timer
+
+ with timer("Table render"):
+ table = Table(
+ title="Star Wars Movies",
+ caption="Rich example table",
+ caption_justify="right",
+ )
+
+ table.add_column(
+ "Released", header_style="bright_cyan", style="cyan", no_wrap=True
+ )
+ table.add_column("Title", style="magenta")
+ table.add_column("Box Office", justify="right", style="green")
+
+ table.add_row(
+ "Dec 20, 2019",
+ "Star Wars: The Rise of Skywalker",
+ "$952,110,690",
+ )
+ table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
+ table.add_row(
+ "Dec 15, 2017",
+ "Star Wars Ep. V111: The Last Jedi",
+ "$1,332,539,889",
+ style="on black",
+ end_section=True,
+ )
+ table.add_row(
+ "Dec 16, 2016",
+ "Rogue One: A Star Wars Story",
+ "$1,332,439,889",
+ )
+
+ def header(text: str) -> None:
+ console.print()
+ console.rule(highlight(text))
+ console.print()
+
+ console = Console()
+ highlight = ReprHighlighter()
+ header("Example Table")
+ console.print(table, justify="center")
+
+ table.expand = True
+ header("expand=True")
+ console.print(table)
+
+ table.width = 50
+ header("width=50")
+
+ console.print(table, justify="center")
+
+ table.width = None
+ table.expand = False
+ table.row_styles = ["dim", "none"]
+ header("row_styles=['dim', 'none']")
+
+ console.print(table, justify="center")
+
+ table.width = None
+ table.expand = False
+ table.row_styles = ["dim", "none"]
+ table.leading = 1
+ header("leading=1, row_styles=['dim', 'none']")
+ console.print(table, justify="center")
+
+ table.width = None
+ table.expand = False
+ table.row_styles = ["dim", "none"]
+ table.show_lines = True
+ table.leading = 0
+ header("show_lines=True, row_styles=['dim', 'none']")
+ console.print(table, justify="center")
diff --git a/src/pip/_vendor/rich/terminal_theme.py b/src/pip/_vendor/rich/terminal_theme.py
new file mode 100644
index 000000000..565e9d960
--- /dev/null
+++ b/src/pip/_vendor/rich/terminal_theme.py
@@ -0,0 +1,153 @@
+from typing import List, Optional, Tuple
+
+from .color_triplet import ColorTriplet
+from .palette import Palette
+
+_ColorTuple = Tuple[int, int, int]
+
+
+class TerminalTheme:
+ """A color theme used when exporting console content.
+
+ Args:
+ background (Tuple[int, int, int]): The background color.
+ foreground (Tuple[int, int, int]): The foreground (text) color.
+ normal (List[Tuple[int, int, int]]): A list of 8 normal intensity colors.
+ bright (List[Tuple[int, int, int]], optional): A list of 8 bright colors, or None
+ to repeat normal intensity. Defaults to None.
+ """
+
+ def __init__(
+ self,
+ background: _ColorTuple,
+ foreground: _ColorTuple,
+ normal: List[_ColorTuple],
+ bright: Optional[List[_ColorTuple]] = None,
+ ) -> None:
+ self.background_color = ColorTriplet(*background)
+ self.foreground_color = ColorTriplet(*foreground)
+ self.ansi_colors = Palette(normal + (bright or normal))
+
+
+DEFAULT_TERMINAL_THEME = TerminalTheme(
+ (255, 255, 255),
+ (0, 0, 0),
+ [
+ (0, 0, 0),
+ (128, 0, 0),
+ (0, 128, 0),
+ (128, 128, 0),
+ (0, 0, 128),
+ (128, 0, 128),
+ (0, 128, 128),
+ (192, 192, 192),
+ ],
+ [
+ (128, 128, 128),
+ (255, 0, 0),
+ (0, 255, 0),
+ (255, 255, 0),
+ (0, 0, 255),
+ (255, 0, 255),
+ (0, 255, 255),
+ (255, 255, 255),
+ ],
+)
+
+MONOKAI = TerminalTheme(
+ (12, 12, 12),
+ (217, 217, 217),
+ [
+ (26, 26, 26),
+ (244, 0, 95),
+ (152, 224, 36),
+ (253, 151, 31),
+ (157, 101, 255),
+ (244, 0, 95),
+ (88, 209, 235),
+ (196, 197, 181),
+ (98, 94, 76),
+ ],
+ [
+ (244, 0, 95),
+ (152, 224, 36),
+ (224, 213, 97),
+ (157, 101, 255),
+ (244, 0, 95),
+ (88, 209, 235),
+ (246, 246, 239),
+ ],
+)
+DIMMED_MONOKAI = TerminalTheme(
+ (25, 25, 25),
+ (185, 188, 186),
+ [
+ (58, 61, 67),
+ (190, 63, 72),
+ (135, 154, 59),
+ (197, 166, 53),
+ (79, 118, 161),
+ (133, 92, 141),
+ (87, 143, 164),
+ (185, 188, 186),
+ (136, 137, 135),
+ ],
+ [
+ (251, 0, 31),
+ (15, 114, 47),
+ (196, 112, 51),
+ (24, 109, 227),
+ (251, 0, 103),
+ (46, 112, 109),
+ (253, 255, 185),
+ ],
+)
+NIGHT_OWLISH = TerminalTheme(
+ (255, 255, 255),
+ (64, 63, 83),
+ [
+ (1, 22, 39),
+ (211, 66, 62),
+ (42, 162, 152),
+ (218, 170, 1),
+ (72, 118, 214),
+ (64, 63, 83),
+ (8, 145, 106),
+ (122, 129, 129),
+ (122, 129, 129),
+ ],
+ [
+ (247, 110, 110),
+ (73, 208, 197),
+ (218, 194, 107),
+ (92, 167, 228),
+ (105, 112, 152),
+ (0, 201, 144),
+ (152, 159, 177),
+ ],
+)
+
+SVG_EXPORT_THEME = TerminalTheme(
+ (41, 41, 41),
+ (197, 200, 198),
+ [
+ (75, 78, 85),
+ (204, 85, 90),
+ (152, 168, 75),
+ (208, 179, 68),
+ (96, 138, 177),
+ (152, 114, 159),
+ (104, 160, 179),
+ (197, 200, 198),
+ (154, 155, 153),
+ ],
+ [
+ (255, 38, 39),
+ (0, 130, 61),
+ (208, 132, 66),
+ (25, 132, 233),
+ (255, 44, 122),
+ (57, 130, 128),
+ (253, 253, 197),
+ ],
+)
diff --git a/src/pip/_vendor/rich/text.py b/src/pip/_vendor/rich/text.py
new file mode 100644
index 000000000..12037d0cf
--- /dev/null
+++ b/src/pip/_vendor/rich/text.py
@@ -0,0 +1,1286 @@
+import re
+from functools import partial, reduce
+from math import gcd
+from operator import itemgetter
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ List,
+ NamedTuple,
+ Optional,
+ Tuple,
+ Union,
+)
+
+from ._loop import loop_last
+from ._pick import pick_bool
+from ._wrap import divide_line
+from .align import AlignMethod
+from .cells import cell_len, set_cell_size
+from .containers import Lines
+from .control import strip_control_codes
+from .emoji import EmojiVariant
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .segment import Segment
+from .style import Style, StyleType
+
+if TYPE_CHECKING: # pragma: no cover
+ from .console import Console, ConsoleOptions, JustifyMethod, OverflowMethod
+
+DEFAULT_JUSTIFY: "JustifyMethod" = "default"
+DEFAULT_OVERFLOW: "OverflowMethod" = "fold"
+
+
+_re_whitespace = re.compile(r"\s+$")
+
+TextType = Union[str, "Text"]
+
+GetStyleCallable = Callable[[str], Optional[StyleType]]
+
+
+class Span(NamedTuple):
+ """A marked up region in some text."""
+
+ start: int
+ """Span start index."""
+ end: int
+ """Span end index."""
+ style: Union[str, Style]
+ """Style associated with the span."""
+
+ def __repr__(self) -> str:
+ return (
+ f"Span({self.start}, {self.end}, {self.style!r})"
+ if (isinstance(self.style, Style) and self.style._meta)
+ else f"Span({self.start}, {self.end}, {repr(self.style)})"
+ )
+
+ def __bool__(self) -> bool:
+ return self.end > self.start
+
+ def split(self, offset: int) -> Tuple["Span", Optional["Span"]]:
+ """Split a span in to 2 from a given offset."""
+
+ if offset < self.start:
+ return self, None
+ if offset >= self.end:
+ return self, None
+
+ start, end, style = self
+ span1 = Span(start, min(end, offset), style)
+ span2 = Span(span1.end, end, style)
+ return span1, span2
+
+ def move(self, offset: int) -> "Span":
+ """Move start and end by a given offset.
+
+ Args:
+ offset (int): Number of characters to add to start and end.
+
+ Returns:
+ TextSpan: A new TextSpan with adjusted position.
+ """
+ start, end, style = self
+ return Span(start + offset, end + offset, style)
+
+ def right_crop(self, offset: int) -> "Span":
+ """Crop the span at the given offset.
+
+ Args:
+ offset (int): A value between start and end.
+
+ Returns:
+ Span: A new (possibly smaller) span.
+ """
+ start, end, style = self
+ if offset >= end:
+ return self
+ return Span(start, min(offset, end), style)
+
+
+class Text(JupyterMixin):
+ """Text with color / style.
+
+ Args:
+ text (str, optional): Default unstyled text. Defaults to "".
+ style (Union[str, Style], optional): Base style for text. Defaults to "".
+ justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
+ overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
+ no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.
+ end (str, optional): Character to end text with. Defaults to "\\\\n".
+ tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
+ spans (List[Span], optional). A list of predefined style spans. Defaults to None.
+ """
+
+ __slots__ = [
+ "_text",
+ "style",
+ "justify",
+ "overflow",
+ "no_wrap",
+ "end",
+ "tab_size",
+ "_spans",
+ "_length",
+ ]
+
+ def __init__(
+ self,
+ text: str = "",
+ style: Union[str, Style] = "",
+ *,
+ justify: Optional["JustifyMethod"] = None,
+ overflow: Optional["OverflowMethod"] = None,
+ no_wrap: Optional[bool] = None,
+ end: str = "\n",
+ tab_size: Optional[int] = 8,
+ spans: Optional[List[Span]] = None,
+ ) -> None:
+ sanitized_text = strip_control_codes(text)
+ self._text = [sanitized_text]
+ self.style = style
+ self.justify: Optional["JustifyMethod"] = justify
+ self.overflow: Optional["OverflowMethod"] = overflow
+ self.no_wrap = no_wrap
+ self.end = end
+ self.tab_size = tab_size
+ self._spans: List[Span] = spans or []
+ self._length: int = len(sanitized_text)
+
+ def __len__(self) -> int:
+ return self._length
+
+ def __bool__(self) -> bool:
+ return bool(self._length)
+
+ def __str__(self) -> str:
+ return self.plain
+
+ def __repr__(self) -> str:
+ return f"<text {self.plain!r} {self._spans!r}>"
+
+ def __add__(self, other: Any) -> "Text":
+ if isinstance(other, (str, Text)):
+ result = self.copy()
+ result.append(other)
+ return result
+ return NotImplemented
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Text):
+ return NotImplemented
+ return self.plain == other.plain and self._spans == other._spans
+
+ def __contains__(self, other: object) -> bool:
+ if isinstance(other, str):
+ return other in self.plain
+ elif isinstance(other, Text):
+ return other.plain in self.plain
+ return False
+
+ def __getitem__(self, slice: Union[int, slice]) -> "Text":
+ def get_text_at(offset: int) -> "Text":
+ _Span = Span
+ text = Text(
+ self.plain[offset],
+ spans=[
+ _Span(0, 1, style)
+ for start, end, style in self._spans
+ if end > offset >= start
+ ],
+ end="",
+ )
+ return text
+
+ if isinstance(slice, int):
+ return get_text_at(slice)
+ else:
+ start, stop, step = slice.indices(len(self.plain))
+ if step == 1:
+ lines = self.divide([start, stop])
+ return lines[1]
+ else:
+ # This would be a bit of work to implement efficiently
+ # For now, its not required
+ raise TypeError("slices with step!=1 are not supported")
+
+ @property
+ def cell_len(self) -> int:
+ """Get the number of cells required to render this text."""
+ return cell_len(self.plain)
+
+ @property
+ def markup(self) -> str:
+ """Get console markup to render this Text.
+
+ Returns:
+ str: A string potentially creating markup tags.
+ """
+ from .markup import escape
+
+ output: List[str] = []
+
+ plain = self.plain
+ markup_spans = [
+ (0, False, self.style),
+ *((span.start, False, span.style) for span in self._spans),
+ *((span.end, True, span.style) for span in self._spans),
+ (len(plain), True, self.style),
+ ]
+ markup_spans.sort(key=itemgetter(0, 1))
+ position = 0
+ append = output.append
+ for offset, closing, style in markup_spans:
+ if offset > position:
+ append(escape(plain[position:offset]))
+ position = offset
+ if style:
+ append(f"[/{style}]" if closing else f"[{style}]")
+ markup = "".join(output)
+ return markup
+
+ @classmethod
+ def from_markup(
+ cls,
+ text: str,
+ *,
+ style: Union[str, Style] = "",
+ emoji: bool = True,
+ emoji_variant: Optional[EmojiVariant] = None,
+ justify: Optional["JustifyMethod"] = None,
+ overflow: Optional["OverflowMethod"] = None,
+ end: str = "\n",
+ ) -> "Text":
+ """Create Text instance from markup.
+
+ Args:
+ text (str): A string containing console markup.
+ emoji (bool, optional): Also render emoji code. Defaults to True.
+ justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
+ overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
+ end (str, optional): Character to end text with. Defaults to "\\\\n".
+
+ Returns:
+ Text: A Text instance with markup rendered.
+ """
+ from .markup import render
+
+ rendered_text = render(text, style, emoji=emoji, emoji_variant=emoji_variant)
+ rendered_text.justify = justify
+ rendered_text.overflow = overflow
+ rendered_text.end = end
+ return rendered_text
+
+ @classmethod
+ def from_ansi(
+ cls,
+ text: str,
+ *,
+ style: Union[str, Style] = "",
+ justify: Optional["JustifyMethod"] = None,
+ overflow: Optional["OverflowMethod"] = None,
+ no_wrap: Optional[bool] = None,
+ end: str = "\n",
+ tab_size: Optional[int] = 8,
+ ) -> "Text":
+ """Create a Text object from a string containing ANSI escape codes.
+
+ Args:
+ text (str): A string containing escape codes.
+ style (Union[str, Style], optional): Base style for text. Defaults to "".
+ justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
+ overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
+ no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.
+ end (str, optional): Character to end text with. Defaults to "\\\\n".
+ tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
+ """
+ from .ansi import AnsiDecoder
+
+ joiner = Text(
+ "\n",
+ justify=justify,
+ overflow=overflow,
+ no_wrap=no_wrap,
+ end=end,
+ tab_size=tab_size,
+ style=style,
+ )
+ decoder = AnsiDecoder()
+ result = joiner.join(line for line in decoder.decode(text))
+ return result
+
+ @classmethod
+ def styled(
+ cls,
+ text: str,
+ style: StyleType = "",
+ *,
+ justify: Optional["JustifyMethod"] = None,
+ overflow: Optional["OverflowMethod"] = None,
+ ) -> "Text":
+ """Construct a Text instance with a pre-applied styled. A style applied in this way won't be used
+ to pad the text when it is justified.
+
+ Args:
+ text (str): A string containing console markup.
+ style (Union[str, Style]): Style to apply to the text. Defaults to "".
+ justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
+ overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
+
+ Returns:
+ Text: A text instance with a style applied to the entire string.
+ """
+ styled_text = cls(text, justify=justify, overflow=overflow)
+ styled_text.stylize(style)
+ return styled_text
+
+ @classmethod
+ def assemble(
+ cls,
+ *parts: Union[str, "Text", Tuple[str, StyleType]],
+ style: Union[str, Style] = "",
+ justify: Optional["JustifyMethod"] = None,
+ overflow: Optional["OverflowMethod"] = None,
+ no_wrap: Optional[bool] = None,
+ end: str = "\n",
+ tab_size: int = 8,
+ meta: Optional[Dict[str, Any]] = None,
+ ) -> "Text":
+ """Construct a text instance by combining a sequence of strings with optional styles.
+ The positional arguments should be either strings, or a tuple of string + style.
+
+ Args:
+ style (Union[str, Style], optional): Base style for text. Defaults to "".
+ justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
+ overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
+ end (str, optional): Character to end text with. Defaults to "\\\\n".
+ tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
+ meta (Dict[str, Any], optional). Meta data to apply to text, or None for no meta data. Default to None
+
+ Returns:
+ Text: A new text instance.
+ """
+ text = cls(
+ style=style,
+ justify=justify,
+ overflow=overflow,
+ no_wrap=no_wrap,
+ end=end,
+ tab_size=tab_size,
+ )
+ append = text.append
+ _Text = Text
+ for part in parts:
+ if isinstance(part, (_Text, str)):
+ append(part)
+ else:
+ append(*part)
+ if meta:
+ text.apply_meta(meta)
+ return text
+
+ @property
+ def plain(self) -> str:
+ """Get the text as a single string."""
+ if len(self._text) != 1:
+ self._text[:] = ["".join(self._text)]
+ return self._text[0]
+
+ @plain.setter
+ def plain(self, new_text: str) -> None:
+ """Set the text to a new value."""
+ if new_text != self.plain:
+ sanitized_text = strip_control_codes(new_text)
+ self._text[:] = [sanitized_text]
+ old_length = self._length
+ self._length = len(sanitized_text)
+ if old_length > self._length:
+ self._trim_spans()
+
+ @property
+ def spans(self) -> List[Span]:
+ """Get a reference to the internal list of spans."""
+ return self._spans
+
+ @spans.setter
+ def spans(self, spans: List[Span]) -> None:
+ """Set spans."""
+ self._spans = spans[:]
+
+ def blank_copy(self, plain: str = "") -> "Text":
+ """Return a new Text instance with copied meta data (but not the string or spans)."""
+ copy_self = Text(
+ plain,
+ style=self.style,
+ justify=self.justify,
+ overflow=self.overflow,
+ no_wrap=self.no_wrap,
+ end=self.end,
+ tab_size=self.tab_size,
+ )
+ return copy_self
+
+ def copy(self) -> "Text":
+ """Return a copy of this instance."""
+ copy_self = Text(
+ self.plain,
+ style=self.style,
+ justify=self.justify,
+ overflow=self.overflow,
+ no_wrap=self.no_wrap,
+ end=self.end,
+ tab_size=self.tab_size,
+ )
+ copy_self._spans[:] = self._spans
+ return copy_self
+
+ def stylize(
+ self,
+ style: Union[str, Style],
+ start: int = 0,
+ end: Optional[int] = None,
+ ) -> None:
+ """Apply a style to the text, or a portion of the text.
+
+ Args:
+ style (Union[str, Style]): Style instance or style definition to apply.
+ start (int): Start offset (negative indexing is supported). Defaults to 0.
+ end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.
+
+ """
+ if style:
+ length = len(self)
+ if start < 0:
+ start = length + start
+ if end is None:
+ end = length
+ if end < 0:
+ end = length + end
+ if start >= length or end <= start:
+ # Span not in text or not valid
+ return
+ self._spans.append(Span(start, min(length, end), style))
+
+ def apply_meta(
+ self, meta: Dict[str, Any], start: int = 0, end: Optional[int] = None
+ ) -> None:
+ """Apply meta data to the text, or a portion of the text.
+
+ Args:
+ meta (Dict[str, Any]): A dict of meta information.
+ start (int): Start offset (negative indexing is supported). Defaults to 0.
+ end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.
+
+ """
+ style = Style.from_meta(meta)
+ self.stylize(style, start=start, end=end)
+
+ def on(self, meta: Optional[Dict[str, Any]] = None, **handlers: Any) -> "Text":
+ """Apply event handlers (used by Textual project).
+
+ Example:
+ >>> from rich.text import Text
+ >>> text = Text("hello world")
+ >>> text.on(click="view.toggle('world')")
+
+ Args:
+ meta (Dict[str, Any]): Mapping of meta information.
+ **handlers: Keyword args are prefixed with "@" to defined handlers.
+
+ Returns:
+ Text: Self is returned to method may be chained.
+ """
+ meta = {} if meta is None else meta
+ meta.update({f"@{key}": value for key, value in handlers.items()})
+ self.stylize(Style.from_meta(meta))
+ return self
+
+ def remove_suffix(self, suffix: str) -> None:
+ """Remove a suffix if it exists.
+
+ Args:
+ suffix (str): Suffix to remove.
+ """
+ if self.plain.endswith(suffix):
+ self.right_crop(len(suffix))
+
+ def get_style_at_offset(self, console: "Console", offset: int) -> Style:
+ """Get the style of a character at give offset.
+
+ Args:
+ console (~Console): Console where text will be rendered.
+ offset (int): Offset in to text (negative indexing supported)
+
+ Returns:
+ Style: A Style instance.
+ """
+ # TODO: This is a little inefficient, it is only used by full justify
+ if offset < 0:
+ offset = len(self) + offset
+ get_style = console.get_style
+ style = get_style(self.style).copy()
+ for start, end, span_style in self._spans:
+ if end > offset >= start:
+ style += get_style(span_style, default="")
+ return style
+
+ def highlight_regex(
+ self,
+ re_highlight: str,
+ style: Optional[Union[GetStyleCallable, StyleType]] = None,
+ *,
+ style_prefix: str = "",
+ ) -> int:
+ """Highlight text with a regular expression, where group names are
+ translated to styles.
+
+ Args:
+ re_highlight (str): A regular expression.
+ style (Union[GetStyleCallable, StyleType]): Optional style to apply to whole match, or a callable
+ which accepts the matched text and returns a style. Defaults to None.
+ style_prefix (str, optional): Optional prefix to add to style group names.
+
+ Returns:
+ int: Number of regex matches
+ """
+ count = 0
+ append_span = self._spans.append
+ _Span = Span
+ plain = self.plain
+ for match in re.finditer(re_highlight, plain):
+ get_span = match.span
+ if style:
+ start, end = get_span()
+ match_style = style(plain[start:end]) if callable(style) else style
+ if match_style is not None and end > start:
+ append_span(_Span(start, end, match_style))
+
+ count += 1
+ for name in match.groupdict().keys():
+ start, end = get_span(name)
+ if start != -1 and end > start:
+ append_span(_Span(start, end, f"{style_prefix}{name}"))
+ return count
+
+ def highlight_words(
+ self,
+ words: Iterable[str],
+ style: Union[str, Style],
+ *,
+ case_sensitive: bool = True,
+ ) -> int:
+ """Highlight words with a style.
+
+ Args:
+ words (Iterable[str]): Worlds to highlight.
+ style (Union[str, Style]): Style to apply.
+ case_sensitive (bool, optional): Enable case sensitive matchings. Defaults to True.
+
+ Returns:
+ int: Number of words highlighted.
+ """
+ re_words = "|".join(re.escape(word) for word in words)
+ add_span = self._spans.append
+ count = 0
+ _Span = Span
+ for match in re.finditer(
+ re_words, self.plain, flags=0 if case_sensitive else re.IGNORECASE
+ ):
+ start, end = match.span(0)
+ add_span(_Span(start, end, style))
+ count += 1
+ return count
+
+ def rstrip(self) -> None:
+ """Strip whitespace from end of text."""
+ self.plain = self.plain.rstrip()
+
+ def rstrip_end(self, size: int) -> None:
+ """Remove whitespace beyond a certain width at the end of the text.
+
+ Args:
+ size (int): The desired size of the text.
+ """
+ text_length = len(self)
+ if text_length > size:
+ excess = text_length - size
+ whitespace_match = _re_whitespace.search(self.plain)
+ if whitespace_match is not None:
+ whitespace_count = len(whitespace_match.group(0))
+ self.right_crop(min(whitespace_count, excess))
+
+ def set_length(self, new_length: int) -> None:
+ """Set new length of the text, clipping or padding is required."""
+ length = len(self)
+ if length != new_length:
+ if length < new_length:
+ self.pad_right(new_length - length)
+ else:
+ self.right_crop(length - new_length)
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> Iterable[Segment]:
+ tab_size: int = console.tab_size or self.tab_size or 8
+ justify = self.justify or options.justify or DEFAULT_JUSTIFY
+
+ overflow = self.overflow or options.overflow or DEFAULT_OVERFLOW
+
+ lines = self.wrap(
+ console,
+ options.max_width,
+ justify=justify,
+ overflow=overflow,
+ tab_size=tab_size or 8,
+ no_wrap=pick_bool(self.no_wrap, options.no_wrap, False),
+ )
+ all_lines = Text("\n").join(lines)
+ yield from all_lines.render(console, end=self.end)
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> Measurement:
+ text = self.plain
+ lines = text.splitlines()
+ max_text_width = max(cell_len(line) for line in lines) if lines else 0
+ words = text.split()
+ min_text_width = (
+ max(cell_len(word) for word in words) if words else max_text_width
+ )
+ return Measurement(min_text_width, max_text_width)
+
+ def render(self, console: "Console", end: str = "") -> Iterable["Segment"]:
+ """Render the text as Segments.
+
+ Args:
+ console (Console): Console instance.
+ end (Optional[str], optional): Optional end character.
+
+ Returns:
+ Iterable[Segment]: Result of render that may be written to the console.
+ """
+ _Segment = Segment
+ text = self.plain
+ if not self._spans:
+ yield Segment(text)
+ if end:
+ yield _Segment(end)
+ return
+ get_style = partial(console.get_style, default=Style.null())
+
+ enumerated_spans = list(enumerate(self._spans, 1))
+ style_map = {index: get_style(span.style) for index, span in enumerated_spans}
+ style_map[0] = get_style(self.style)
+
+ spans = [
+ (0, False, 0),
+ *((span.start, False, index) for index, span in enumerated_spans),
+ *((span.end, True, index) for index, span in enumerated_spans),
+ (len(text), True, 0),
+ ]
+ spans.sort(key=itemgetter(0, 1))
+
+ stack: List[int] = []
+ stack_append = stack.append
+ stack_pop = stack.remove
+
+ style_cache: Dict[Tuple[Style, ...], Style] = {}
+ style_cache_get = style_cache.get
+ combine = Style.combine
+
+ def get_current_style() -> Style:
+ """Construct current style from stack."""
+ styles = tuple(style_map[_style_id] for _style_id in sorted(stack))
+ cached_style = style_cache_get(styles)
+ if cached_style is not None:
+ return cached_style
+ current_style = combine(styles)
+ style_cache[styles] = current_style
+ return current_style
+
+ for (offset, leaving, style_id), (next_offset, _, _) in zip(spans, spans[1:]):
+ if leaving:
+ stack_pop(style_id)
+ else:
+ stack_append(style_id)
+ if next_offset > offset:
+ yield _Segment(text[offset:next_offset], get_current_style())
+ if end:
+ yield _Segment(end)
+
+ def join(self, lines: Iterable["Text"]) -> "Text":
+ """Join text together with this instance as the separator.
+
+ Args:
+ lines (Iterable[Text]): An iterable of Text instances to join.
+
+ Returns:
+ Text: A new text instance containing join text.
+ """
+
+ new_text = self.blank_copy()
+
+ def iter_text() -> Iterable["Text"]:
+ if self.plain:
+ for last, line in loop_last(lines):
+ yield line
+ if not last:
+ yield self
+ else:
+ yield from lines
+
+ extend_text = new_text._text.extend
+ append_span = new_text._spans.append
+ extend_spans = new_text._spans.extend
+ offset = 0
+ _Span = Span
+
+ for text in iter_text():
+ extend_text(text._text)
+ if text.style:
+ append_span(_Span(offset, offset + len(text), text.style))
+ extend_spans(
+ _Span(offset + start, offset + end, style)
+ for start, end, style in text._spans
+ )
+ offset += len(text)
+ new_text._length = offset
+ return new_text
+
+ def expand_tabs(self, tab_size: Optional[int] = None) -> None:
+ """Converts tabs to spaces.
+
+ Args:
+ tab_size (int, optional): Size of tabs. Defaults to 8.
+
+ """
+ if "\t" not in self.plain:
+ return
+ pos = 0
+ if tab_size is None:
+ tab_size = self.tab_size
+ assert tab_size is not None
+ result = self.blank_copy()
+ append = result.append
+
+ _style = self.style
+ for line in self.split("\n", include_separator=True):
+ parts = line.split("\t", include_separator=True)
+ for part in parts:
+ if part.plain.endswith("\t"):
+ part._text = [part.plain[:-1] + " "]
+ append(part)
+ pos += len(part)
+ spaces = tab_size - ((pos - 1) % tab_size) - 1
+ if spaces:
+ append(" " * spaces, _style)
+ pos += spaces
+ else:
+ append(part)
+ self._text = [result.plain]
+ self._length = len(self.plain)
+ self._spans[:] = result._spans
+
+ def truncate(
+ self,
+ max_width: int,
+ *,
+ overflow: Optional["OverflowMethod"] = None,
+ pad: bool = False,
+ ) -> None:
+ """Truncate text if it is longer that a given width.
+
+ Args:
+ max_width (int): Maximum number of characters in text.
+ overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None, to use self.overflow.
+ pad (bool, optional): Pad with spaces if the length is less than max_width. Defaults to False.
+ """
+ _overflow = overflow or self.overflow or DEFAULT_OVERFLOW
+ if _overflow != "ignore":
+ length = cell_len(self.plain)
+ if length > max_width:
+ if _overflow == "ellipsis":
+ self.plain = set_cell_size(self.plain, max_width - 1) + "…"
+ else:
+ self.plain = set_cell_size(self.plain, max_width)
+ if pad and length < max_width:
+ spaces = max_width - length
+ self._text = [f"{self.plain}{' ' * spaces}"]
+ self._length = len(self.plain)
+
+ def _trim_spans(self) -> None:
+ """Remove or modify any spans that are over the end of the text."""
+ max_offset = len(self.plain)
+ _Span = Span
+ self._spans[:] = [
+ (
+ span
+ if span.end < max_offset
+ else _Span(span.start, min(max_offset, span.end), span.style)
+ )
+ for span in self._spans
+ if span.start < max_offset
+ ]
+
+ def pad(self, count: int, character: str = " ") -> None:
+ """Pad left and right with a given number of characters.
+
+ Args:
+ count (int): Width of padding.
+ """
+ assert len(character) == 1, "Character must be a string of length 1"
+ if count:
+ pad_characters = character * count
+ self.plain = f"{pad_characters}{self.plain}{pad_characters}"
+ _Span = Span
+ self._spans[:] = [
+ _Span(start + count, end + count, style)
+ for start, end, style in self._spans
+ ]
+
+ def pad_left(self, count: int, character: str = " ") -> None:
+ """Pad the left with a given character.
+
+ Args:
+ count (int): Number of characters to pad.
+ character (str, optional): Character to pad with. Defaults to " ".
+ """
+ assert len(character) == 1, "Character must be a string of length 1"
+ if count:
+ self.plain = f"{character * count}{self.plain}"
+ _Span = Span
+ self._spans[:] = [
+ _Span(start + count, end + count, style)
+ for start, end, style in self._spans
+ ]
+
+ def pad_right(self, count: int, character: str = " ") -> None:
+ """Pad the right with a given character.
+
+ Args:
+ count (int): Number of characters to pad.
+ character (str, optional): Character to pad with. Defaults to " ".
+ """
+ assert len(character) == 1, "Character must be a string of length 1"
+ if count:
+ self.plain = f"{self.plain}{character * count}"
+
+ def align(self, align: AlignMethod, width: int, character: str = " ") -> None:
+ """Align text to a given width.
+
+ Args:
+ align (AlignMethod): One of "left", "center", or "right".
+ width (int): Desired width.
+ character (str, optional): Character to pad with. Defaults to " ".
+ """
+ self.truncate(width)
+ excess_space = width - cell_len(self.plain)
+ if excess_space:
+ if align == "left":
+ self.pad_right(excess_space, character)
+ elif align == "center":
+ left = excess_space // 2
+ self.pad_left(left, character)
+ self.pad_right(excess_space - left, character)
+ else:
+ self.pad_left(excess_space, character)
+
+ def append(
+ self, text: Union["Text", str], style: Optional[Union[str, "Style"]] = None
+ ) -> "Text":
+ """Add text with an optional style.
+
+ Args:
+ text (Union[Text, str]): A str or Text to append.
+ style (str, optional): A style name. Defaults to None.
+
+ Returns:
+ Text: Returns self for chaining.
+ """
+
+ if not isinstance(text, (str, Text)):
+ raise TypeError("Only str or Text can be appended to Text")
+
+ if len(text):
+ if isinstance(text, str):
+ sanitized_text = strip_control_codes(text)
+ self._text.append(sanitized_text)
+ offset = len(self)
+ text_length = len(sanitized_text)
+ if style is not None:
+ self._spans.append(Span(offset, offset + text_length, style))
+ self._length += text_length
+ elif isinstance(text, Text):
+ _Span = Span
+ if style is not None:
+ raise ValueError(
+ "style must not be set when appending Text instance"
+ )
+ text_length = self._length
+ if text.style is not None:
+ self._spans.append(
+ _Span(text_length, text_length + len(text), text.style)
+ )
+ self._text.append(text.plain)
+ self._spans.extend(
+ _Span(start + text_length, end + text_length, style)
+ for start, end, style in text._spans
+ )
+ self._length += len(text)
+ return self
+
+ def append_text(self, text: "Text") -> "Text":
+ """Append another Text instance. This method is more performant that Text.append, but
+ only works for Text.
+
+ Returns:
+ Text: Returns self for chaining.
+ """
+ _Span = Span
+ text_length = self._length
+ if text.style is not None:
+ self._spans.append(_Span(text_length, text_length + len(text), text.style))
+ self._text.append(text.plain)
+ self._spans.extend(
+ _Span(start + text_length, end + text_length, style)
+ for start, end, style in text._spans
+ )
+ self._length += len(text)
+ return self
+
+ def append_tokens(
+ self, tokens: Iterable[Tuple[str, Optional[StyleType]]]
+ ) -> "Text":
+ """Append iterable of str and style. Style may be a Style instance or a str style definition.
+
+ Args:
+ pairs (Iterable[Tuple[str, Optional[StyleType]]]): An iterable of tuples containing str content and style.
+
+ Returns:
+ Text: Returns self for chaining.
+ """
+ append_text = self._text.append
+ append_span = self._spans.append
+ _Span = Span
+ offset = len(self)
+ for content, style in tokens:
+ append_text(content)
+ if style is not None:
+ append_span(_Span(offset, offset + len(content), style))
+ offset += len(content)
+ self._length = offset
+ return self
+
+ def copy_styles(self, text: "Text") -> None:
+ """Copy styles from another Text instance.
+
+ Args:
+ text (Text): A Text instance to copy styles from, must be the same length.
+ """
+ self._spans.extend(text._spans)
+
+ def split(
+ self,
+ separator: str = "\n",
+ *,
+ include_separator: bool = False,
+ allow_blank: bool = False,
+ ) -> Lines:
+ """Split rich text in to lines, preserving styles.
+
+ Args:
+ separator (str, optional): String to split on. Defaults to "\\\\n".
+ include_separator (bool, optional): Include the separator in the lines. Defaults to False.
+ allow_blank (bool, optional): Return a blank line if the text ends with a separator. Defaults to False.
+
+ Returns:
+ List[RichText]: A list of rich text, one per line of the original.
+ """
+ assert separator, "separator must not be empty"
+
+ text = self.plain
+ if separator not in text:
+ return Lines([self.copy()])
+
+ if include_separator:
+ lines = self.divide(
+ match.end() for match in re.finditer(re.escape(separator), text)
+ )
+ else:
+
+ def flatten_spans() -> Iterable[int]:
+ for match in re.finditer(re.escape(separator), text):
+ start, end = match.span()
+ yield start
+ yield end
+
+ lines = Lines(
+ line for line in self.divide(flatten_spans()) if line.plain != separator
+ )
+
+ if not allow_blank and text.endswith(separator):
+ lines.pop()
+
+ return lines
+
+ def divide(self, offsets: Iterable[int]) -> Lines:
+ """Divide text in to a number of lines at given offsets.
+
+ Args:
+ offsets (Iterable[int]): Offsets used to divide text.
+
+ Returns:
+ Lines: New RichText instances between offsets.
+ """
+ _offsets = list(offsets)
+
+ if not _offsets:
+ return Lines([self.copy()])
+
+ text = self.plain
+ text_length = len(text)
+ divide_offsets = [0, *_offsets, text_length]
+ line_ranges = list(zip(divide_offsets, divide_offsets[1:]))
+
+ style = self.style
+ justify = self.justify
+ overflow = self.overflow
+ _Text = Text
+ new_lines = Lines(
+ _Text(
+ text[start:end],
+ style=style,
+ justify=justify,
+ overflow=overflow,
+ )
+ for start, end in line_ranges
+ )
+ if not self._spans:
+ return new_lines
+
+ _line_appends = [line._spans.append for line in new_lines._lines]
+ line_count = len(line_ranges)
+ _Span = Span
+
+ for span_start, span_end, style in self._spans:
+
+ lower_bound = 0
+ upper_bound = line_count
+ start_line_no = (lower_bound + upper_bound) // 2
+
+ while True:
+ line_start, line_end = line_ranges[start_line_no]
+ if span_start < line_start:
+ upper_bound = start_line_no - 1
+ elif span_start > line_end:
+ lower_bound = start_line_no + 1
+ else:
+ break
+ start_line_no = (lower_bound + upper_bound) // 2
+
+ if span_end < line_end:
+ end_line_no = start_line_no
+ else:
+ end_line_no = lower_bound = start_line_no
+ upper_bound = line_count
+
+ while True:
+ line_start, line_end = line_ranges[end_line_no]
+ if span_end < line_start:
+ upper_bound = end_line_no - 1
+ elif span_end > line_end:
+ lower_bound = end_line_no + 1
+ else:
+ break
+ end_line_no = (lower_bound + upper_bound) // 2
+
+ for line_no in range(start_line_no, end_line_no + 1):
+ line_start, line_end = line_ranges[line_no]
+ new_start = max(0, span_start - line_start)
+ new_end = min(span_end - line_start, line_end - line_start)
+ if new_end > new_start:
+ _line_appends[line_no](_Span(new_start, new_end, style))
+
+ return new_lines
+
+ def right_crop(self, amount: int = 1) -> None:
+ """Remove a number of characters from the end of the text."""
+ max_offset = len(self.plain) - amount
+ _Span = Span
+ self._spans[:] = [
+ (
+ span
+ if span.end < max_offset
+ else _Span(span.start, min(max_offset, span.end), span.style)
+ )
+ for span in self._spans
+ if span.start < max_offset
+ ]
+ self._text = [self.plain[:-amount]]
+ self._length -= amount
+
+ def wrap(
+ self,
+ console: "Console",
+ width: int,
+ *,
+ justify: Optional["JustifyMethod"] = None,
+ overflow: Optional["OverflowMethod"] = None,
+ tab_size: int = 8,
+ no_wrap: Optional[bool] = None,
+ ) -> Lines:
+ """Word wrap the text.
+
+ Args:
+ console (Console): Console instance.
+ width (int): Number of characters per line.
+ emoji (bool, optional): Also render emoji code. Defaults to True.
+ justify (str, optional): Justify method: "default", "left", "center", "full", "right". Defaults to "default".
+ overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None.
+ tab_size (int, optional): Default tab size. Defaults to 8.
+ no_wrap (bool, optional): Disable wrapping, Defaults to False.
+
+ Returns:
+ Lines: Number of lines.
+ """
+ wrap_justify = justify or self.justify or DEFAULT_JUSTIFY
+ wrap_overflow = overflow or self.overflow or DEFAULT_OVERFLOW
+
+ no_wrap = pick_bool(no_wrap, self.no_wrap, False) or overflow == "ignore"
+
+ lines = Lines()
+ for line in self.split(allow_blank=True):
+ if "\t" in line:
+ line.expand_tabs(tab_size)
+ if no_wrap:
+ new_lines = Lines([line])
+ else:
+ offsets = divide_line(str(line), width, fold=wrap_overflow == "fold")
+ new_lines = line.divide(offsets)
+ for line in new_lines:
+ line.rstrip_end(width)
+ if wrap_justify:
+ new_lines.justify(
+ console, width, justify=wrap_justify, overflow=wrap_overflow
+ )
+ for line in new_lines:
+ line.truncate(width, overflow=wrap_overflow)
+ lines.extend(new_lines)
+ return lines
+
+ def fit(self, width: int) -> Lines:
+ """Fit the text in to given width by chopping in to lines.
+
+ Args:
+ width (int): Maximum characters in a line.
+
+ Returns:
+ Lines: List of lines.
+ """
+ lines: Lines = Lines()
+ append = lines.append
+ for line in self.split():
+ line.set_length(width)
+ append(line)
+ return lines
+
+ def detect_indentation(self) -> int:
+ """Auto-detect indentation of code.
+
+ Returns:
+ int: Number of spaces used to indent code.
+ """
+
+ _indentations = {
+ len(match.group(1))
+ for match in re.finditer(r"^( *)(.*)$", self.plain, flags=re.MULTILINE)
+ }
+
+ try:
+ indentation = (
+ reduce(gcd, [indent for indent in _indentations if not indent % 2]) or 1
+ )
+ except TypeError:
+ indentation = 1
+
+ return indentation
+
+ def with_indent_guides(
+ self,
+ indent_size: Optional[int] = None,
+ *,
+ character: str = "│",
+ style: StyleType = "dim green",
+ ) -> "Text":
+ """Adds indent guide lines to text.
+
+ Args:
+ indent_size (Optional[int]): Size of indentation, or None to auto detect. Defaults to None.
+ character (str, optional): Character to use for indentation. Defaults to "│".
+ style (Union[Style, str], optional): Style of indent guides.
+
+ Returns:
+ Text: New text with indentation guides.
+ """
+
+ _indent_size = self.detect_indentation() if indent_size is None else indent_size
+
+ text = self.copy()
+ text.expand_tabs()
+ indent_line = f"{character}{' ' * (_indent_size - 1)}"
+
+ re_indent = re.compile(r"^( *)(.*)$")
+ new_lines: List[Text] = []
+ add_line = new_lines.append
+ blank_lines = 0
+ for line in text.split(allow_blank=True):
+ match = re_indent.match(line.plain)
+ if not match or not match.group(2):
+ blank_lines += 1
+ continue
+ indent = match.group(1)
+ full_indents, remaining_space = divmod(len(indent), _indent_size)
+ new_indent = f"{indent_line * full_indents}{' ' * remaining_space}"
+ line.plain = new_indent + line.plain[len(new_indent) :]
+ line.stylize(style, 0, len(new_indent))
+ if blank_lines:
+ new_lines.extend([Text(new_indent, style=style)] * blank_lines)
+ blank_lines = 0
+ add_line(line)
+ if blank_lines:
+ new_lines.extend([Text("", style=style)] * blank_lines)
+
+ new_text = text.blank_copy("\n").join(new_lines)
+ return new_text
+
+
+if __name__ == "__main__": # pragma: no cover
+ from pip._vendor.rich.console import Console
+
+ text = Text(
+ """\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n"""
+ )
+ text.highlight_words(["Lorem"], "bold")
+ text.highlight_words(["ipsum"], "italic")
+
+ console = Console()
+
+ console.rule("justify='left'")
+ console.print(text, style="red")
+ console.print()
+
+ console.rule("justify='center'")
+ console.print(text, style="green", justify="center")
+ console.print()
+
+ console.rule("justify='right'")
+ console.print(text, style="blue", justify="right")
+ console.print()
+
+ console.rule("justify='full'")
+ console.print(text, style="magenta", justify="full")
+ console.print()
diff --git a/src/pip/_vendor/rich/theme.py b/src/pip/_vendor/rich/theme.py
new file mode 100644
index 000000000..bfb3c7f82
--- /dev/null
+++ b/src/pip/_vendor/rich/theme.py
@@ -0,0 +1,112 @@
+import configparser
+from typing import Dict, List, IO, Mapping, Optional
+
+from .default_styles import DEFAULT_STYLES
+from .style import Style, StyleType
+
+
+class Theme:
+ """A container for style information, used by :class:`~rich.console.Console`.
+
+ Args:
+ styles (Dict[str, Style], optional): A mapping of style names on to styles. Defaults to None for a theme with no styles.
+ inherit (bool, optional): Inherit default styles. Defaults to True.
+ """
+
+ styles: Dict[str, Style]
+
+ def __init__(
+ self, styles: Optional[Mapping[str, StyleType]] = None, inherit: bool = True
+ ):
+ self.styles = DEFAULT_STYLES.copy() if inherit else {}
+ if styles is not None:
+ self.styles.update(
+ {
+ name: style if isinstance(style, Style) else Style.parse(style)
+ for name, style in styles.items()
+ }
+ )
+
+ @property
+ def config(self) -> str:
+ """Get contents of a config file for this theme."""
+ config = "[styles]\n" + "\n".join(
+ f"{name} = {style}" for name, style in sorted(self.styles.items())
+ )
+ return config
+
+ @classmethod
+ def from_file(
+ cls, config_file: IO[str], source: Optional[str] = None, inherit: bool = True
+ ) -> "Theme":
+ """Load a theme from a text mode file.
+
+ Args:
+ config_file (IO[str]): An open conf file.
+ source (str, optional): The filename of the open file. Defaults to None.
+ inherit (bool, optional): Inherit default styles. Defaults to True.
+
+ Returns:
+ Theme: A New theme instance.
+ """
+ config = configparser.ConfigParser()
+ config.read_file(config_file, source=source)
+ styles = {name: Style.parse(value) for name, value in config.items("styles")}
+ theme = Theme(styles, inherit=inherit)
+ return theme
+
+ @classmethod
+ def read(cls, path: str, inherit: bool = True) -> "Theme":
+ """Read a theme from a path.
+
+ Args:
+ path (str): Path to a config file readable by Python configparser module.
+ inherit (bool, optional): Inherit default styles. Defaults to True.
+
+ Returns:
+ Theme: A new theme instance.
+ """
+ with open(path, "rt") as config_file:
+ return cls.from_file(config_file, source=path, inherit=inherit)
+
+
+class ThemeStackError(Exception):
+ """Base exception for errors related to the theme stack."""
+
+
+class ThemeStack:
+ """A stack of themes.
+
+ Args:
+ theme (Theme): A theme instance
+ """
+
+ def __init__(self, theme: Theme) -> None:
+ self._entries: List[Dict[str, Style]] = [theme.styles]
+ self.get = self._entries[-1].get
+
+ def push_theme(self, theme: Theme, inherit: bool = True) -> None:
+ """Push a theme on the top of the stack.
+
+ Args:
+ theme (Theme): A Theme instance.
+ inherit (boolean, optional): Inherit styles from current top of stack.
+ """
+ styles: Dict[str, Style]
+ styles = (
+ {**self._entries[-1], **theme.styles} if inherit else theme.styles.copy()
+ )
+ self._entries.append(styles)
+ self.get = self._entries[-1].get
+
+ def pop_theme(self) -> None:
+ """Pop (and discard) the top-most theme."""
+ if len(self._entries) == 1:
+ raise ThemeStackError("Unable to pop base theme")
+ self._entries.pop()
+ self.get = self._entries[-1].get
+
+
+if __name__ == "__main__": # pragma: no cover
+ theme = Theme()
+ print(theme.config)
diff --git a/src/pip/_vendor/rich/themes.py b/src/pip/_vendor/rich/themes.py
new file mode 100644
index 000000000..bf6db104a
--- /dev/null
+++ b/src/pip/_vendor/rich/themes.py
@@ -0,0 +1,5 @@
+from .default_styles import DEFAULT_STYLES
+from .theme import Theme
+
+
+DEFAULT = Theme(DEFAULT_STYLES)
diff --git a/src/pip/_vendor/rich/traceback.py b/src/pip/_vendor/rich/traceback.py
new file mode 100644
index 000000000..e5023c77a
--- /dev/null
+++ b/src/pip/_vendor/rich/traceback.py
@@ -0,0 +1,679 @@
+from __future__ import absolute_import
+
+import os
+import platform
+import sys
+from dataclasses import dataclass, field
+from traceback import walk_tb
+from types import ModuleType, TracebackType
+from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Type, Union
+
+from pip._vendor.pygments.lexers import guess_lexer_for_filename
+from pip._vendor.pygments.token import Comment, Keyword, Name, Number, Operator, String
+from pip._vendor.pygments.token import Text as TextToken
+from pip._vendor.pygments.token import Token
+from pip._vendor.pygments.util import ClassNotFound
+
+from . import pretty
+from ._loop import loop_last
+from .columns import Columns
+from .console import Console, ConsoleOptions, ConsoleRenderable, RenderResult, group
+from .constrain import Constrain
+from .highlighter import RegexHighlighter, ReprHighlighter
+from .panel import Panel
+from .scope import render_scope
+from .style import Style
+from .syntax import Syntax
+from .text import Text
+from .theme import Theme
+
+WINDOWS = platform.system() == "Windows"
+
+LOCALS_MAX_LENGTH = 10
+LOCALS_MAX_STRING = 80
+
+
+def install(
+ *,
+ console: Optional[Console] = None,
+ width: Optional[int] = 100,
+ extra_lines: int = 3,
+ theme: Optional[str] = None,
+ word_wrap: bool = False,
+ show_locals: bool = False,
+ indent_guides: bool = True,
+ suppress: Iterable[Union[str, ModuleType]] = (),
+ max_frames: int = 100,
+) -> Callable[[Type[BaseException], BaseException, Optional[TracebackType]], Any]:
+ """Install a rich traceback handler.
+
+ Once installed, any tracebacks will be printed with syntax highlighting and rich formatting.
+
+
+ Args:
+ console (Optional[Console], optional): Console to write exception to. Default uses internal Console instance.
+ width (Optional[int], optional): Width (in characters) of traceback. Defaults to 100.
+ extra_lines (int, optional): Extra lines of code. Defaults to 3.
+ theme (Optional[str], optional): Pygments theme to use in traceback. Defaults to ``None`` which will pick
+ a theme appropriate for the platform.
+ word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
+ show_locals (bool, optional): Enable display of local variables. Defaults to False.
+ indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
+ suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
+
+ Returns:
+ Callable: The previous exception handler that was replaced.
+
+ """
+ traceback_console = Console(file=sys.stderr) if console is None else console
+
+ def excepthook(
+ type_: Type[BaseException],
+ value: BaseException,
+ traceback: Optional[TracebackType],
+ ) -> None:
+ traceback_console.print(
+ Traceback.from_exception(
+ type_,
+ value,
+ traceback,
+ width=width,
+ extra_lines=extra_lines,
+ theme=theme,
+ word_wrap=word_wrap,
+ show_locals=show_locals,
+ indent_guides=indent_guides,
+ suppress=suppress,
+ max_frames=max_frames,
+ )
+ )
+
+ def ipy_excepthook_closure(ip: Any) -> None: # pragma: no cover
+ tb_data = {} # store information about showtraceback call
+ default_showtraceback = ip.showtraceback # keep reference of default traceback
+
+ def ipy_show_traceback(*args: Any, **kwargs: Any) -> None:
+ """wrap the default ip.showtraceback to store info for ip._showtraceback"""
+ nonlocal tb_data
+ tb_data = kwargs
+ default_showtraceback(*args, **kwargs)
+
+ def ipy_display_traceback(
+ *args: Any, is_syntax: bool = False, **kwargs: Any
+ ) -> None:
+ """Internally called traceback from ip._showtraceback"""
+ nonlocal tb_data
+ exc_tuple = ip._get_exc_info()
+
+ # do not display trace on syntax error
+ tb: Optional[TracebackType] = None if is_syntax else exc_tuple[2]
+
+ # determine correct tb_offset
+ compiled = tb_data.get("running_compiled_code", False)
+ tb_offset = tb_data.get("tb_offset", 1 if compiled else 0)
+ # remove ipython internal frames from trace with tb_offset
+ for _ in range(tb_offset):
+ if tb is None:
+ break
+ tb = tb.tb_next
+
+ excepthook(exc_tuple[0], exc_tuple[1], tb)
+ tb_data = {} # clear data upon usage
+
+ # replace _showtraceback instead of showtraceback to allow ipython features such as debugging to work
+ # this is also what the ipython docs recommends to modify when subclassing InteractiveShell
+ ip._showtraceback = ipy_display_traceback
+ # add wrapper to capture tb_data
+ ip.showtraceback = ipy_show_traceback
+ ip.showsyntaxerror = lambda *args, **kwargs: ipy_display_traceback(
+ *args, is_syntax=True, **kwargs
+ )
+
+ try: # pragma: no cover
+ # if within ipython, use customized traceback
+ ip = get_ipython() # type: ignore[name-defined]
+ ipy_excepthook_closure(ip)
+ return sys.excepthook
+ except Exception:
+ # otherwise use default system hook
+ old_excepthook = sys.excepthook
+ sys.excepthook = excepthook
+ return old_excepthook
+
+
+@dataclass
+class Frame:
+ filename: str
+ lineno: int
+ name: str
+ line: str = ""
+ locals: Optional[Dict[str, pretty.Node]] = None
+
+
+@dataclass
+class _SyntaxError:
+ offset: int
+ filename: str
+ line: str
+ lineno: int
+ msg: str
+
+
+@dataclass
+class Stack:
+ exc_type: str
+ exc_value: str
+ syntax_error: Optional[_SyntaxError] = None
+ is_cause: bool = False
+ frames: List[Frame] = field(default_factory=list)
+
+
+@dataclass
+class Trace:
+ stacks: List[Stack]
+
+
+class PathHighlighter(RegexHighlighter):
+ highlights = [r"(?P<dim>.*/)(?P<bold>.+)"]
+
+
+class Traceback:
+ """A Console renderable that renders a traceback.
+
+ Args:
+ trace (Trace, optional): A `Trace` object produced from `extract`. Defaults to None, which uses
+ the last exception.
+ width (Optional[int], optional): Number of characters used to traceback. Defaults to 100.
+ extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
+ theme (str, optional): Override pygments theme used in traceback.
+ word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
+ show_locals (bool, optional): Enable display of local variables. Defaults to False.
+ indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
+ locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to 10.
+ locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
+ suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
+ max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
+
+ """
+
+ LEXERS = {
+ "": "text",
+ ".py": "python",
+ ".pxd": "cython",
+ ".pyx": "cython",
+ ".pxi": "pyrex",
+ }
+
+ def __init__(
+ self,
+ trace: Optional[Trace] = None,
+ width: Optional[int] = 100,
+ extra_lines: int = 3,
+ theme: Optional[str] = None,
+ word_wrap: bool = False,
+ show_locals: bool = False,
+ indent_guides: bool = True,
+ locals_max_length: int = LOCALS_MAX_LENGTH,
+ locals_max_string: int = LOCALS_MAX_STRING,
+ suppress: Iterable[Union[str, ModuleType]] = (),
+ max_frames: int = 100,
+ ):
+ if trace is None:
+ exc_type, exc_value, traceback = sys.exc_info()
+ if exc_type is None or exc_value is None or traceback is None:
+ raise ValueError(
+ "Value for 'trace' required if not called in except: block"
+ )
+ trace = self.extract(
+ exc_type, exc_value, traceback, show_locals=show_locals
+ )
+ self.trace = trace
+ self.width = width
+ self.extra_lines = extra_lines
+ self.theme = Syntax.get_theme(theme or "ansi_dark")
+ self.word_wrap = word_wrap
+ self.show_locals = show_locals
+ self.indent_guides = indent_guides
+ self.locals_max_length = locals_max_length
+ self.locals_max_string = locals_max_string
+
+ self.suppress: Sequence[str] = []
+ for suppress_entity in suppress:
+ if not isinstance(suppress_entity, str):
+ assert (
+ suppress_entity.__file__ is not None
+ ), f"{suppress_entity!r} must be a module with '__file__' attribute"
+ path = os.path.dirname(suppress_entity.__file__)
+ else:
+ path = suppress_entity
+ path = os.path.normpath(os.path.abspath(path))
+ self.suppress.append(path)
+ self.max_frames = max(4, max_frames) if max_frames > 0 else 0
+
+ @classmethod
+ def from_exception(
+ cls,
+ exc_type: Type[Any],
+ exc_value: BaseException,
+ traceback: Optional[TracebackType],
+ width: Optional[int] = 100,
+ extra_lines: int = 3,
+ theme: Optional[str] = None,
+ word_wrap: bool = False,
+ show_locals: bool = False,
+ indent_guides: bool = True,
+ locals_max_length: int = LOCALS_MAX_LENGTH,
+ locals_max_string: int = LOCALS_MAX_STRING,
+ suppress: Iterable[Union[str, ModuleType]] = (),
+ max_frames: int = 100,
+ ) -> "Traceback":
+ """Create a traceback from exception info
+
+ Args:
+ exc_type (Type[BaseException]): Exception type.
+ exc_value (BaseException): Exception value.
+ traceback (TracebackType): Python Traceback object.
+ width (Optional[int], optional): Number of characters used to traceback. Defaults to 100.
+ extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
+ theme (str, optional): Override pygments theme used in traceback.
+ word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
+ show_locals (bool, optional): Enable display of local variables. Defaults to False.
+ indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
+ locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to 10.
+ locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
+ suppress (Iterable[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
+ max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
+
+ Returns:
+ Traceback: A Traceback instance that may be printed.
+ """
+ rich_traceback = cls.extract(
+ exc_type, exc_value, traceback, show_locals=show_locals
+ )
+ return cls(
+ rich_traceback,
+ width=width,
+ extra_lines=extra_lines,
+ theme=theme,
+ word_wrap=word_wrap,
+ show_locals=show_locals,
+ indent_guides=indent_guides,
+ locals_max_length=locals_max_length,
+ locals_max_string=locals_max_string,
+ suppress=suppress,
+ max_frames=max_frames,
+ )
+
+ @classmethod
+ def extract(
+ cls,
+ exc_type: Type[BaseException],
+ exc_value: BaseException,
+ traceback: Optional[TracebackType],
+ show_locals: bool = False,
+ locals_max_length: int = LOCALS_MAX_LENGTH,
+ locals_max_string: int = LOCALS_MAX_STRING,
+ ) -> Trace:
+ """Extract traceback information.
+
+ Args:
+ exc_type (Type[BaseException]): Exception type.
+ exc_value (BaseException): Exception value.
+ traceback (TracebackType): Python Traceback object.
+ show_locals (bool, optional): Enable display of local variables. Defaults to False.
+ locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
+ Defaults to 10.
+ locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
+
+ Returns:
+ Trace: A Trace instance which you can use to construct a `Traceback`.
+ """
+
+ stacks: List[Stack] = []
+ is_cause = False
+
+ from pip._vendor.rich import _IMPORT_CWD
+
+ def safe_str(_object: Any) -> str:
+ """Don't allow exceptions from __str__ to propegate."""
+ try:
+ return str(_object)
+ except Exception:
+ return "<exception str() failed>"
+
+ while True:
+ stack = Stack(
+ exc_type=safe_str(exc_type.__name__),
+ exc_value=safe_str(exc_value),
+ is_cause=is_cause,
+ )
+
+ if isinstance(exc_value, SyntaxError):
+ stack.syntax_error = _SyntaxError(
+ offset=exc_value.offset or 0,
+ filename=exc_value.filename or "?",
+ lineno=exc_value.lineno or 0,
+ line=exc_value.text or "",
+ msg=exc_value.msg,
+ )
+
+ stacks.append(stack)
+ append = stack.frames.append
+
+ for frame_summary, line_no in walk_tb(traceback):
+ filename = frame_summary.f_code.co_filename
+ if filename and not filename.startswith("<"):
+ if not os.path.isabs(filename):
+ filename = os.path.join(_IMPORT_CWD, filename)
+ if frame_summary.f_locals.get("_rich_traceback_omit", False):
+ continue
+ frame = Frame(
+ filename=filename or "?",
+ lineno=line_no,
+ name=frame_summary.f_code.co_name,
+ locals={
+ key: pretty.traverse(
+ value,
+ max_length=locals_max_length,
+ max_string=locals_max_string,
+ )
+ for key, value in frame_summary.f_locals.items()
+ }
+ if show_locals
+ else None,
+ )
+ append(frame)
+ if frame_summary.f_locals.get("_rich_traceback_guard", False):
+ del stack.frames[:]
+
+ cause = getattr(exc_value, "__cause__", None)
+ if cause and cause.__traceback__:
+ exc_type = cause.__class__
+ exc_value = cause
+ traceback = cause.__traceback__
+ is_cause = True
+ continue
+
+ cause = exc_value.__context__
+ if (
+ cause
+ and cause.__traceback__
+ and not getattr(exc_value, "__suppress_context__", False)
+ ):
+ exc_type = cause.__class__
+ exc_value = cause
+ traceback = cause.__traceback__
+ is_cause = False
+ continue
+ # No cover, code is reached but coverage doesn't recognize it.
+ break # pragma: no cover
+
+ trace = Trace(stacks=stacks)
+ return trace
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ theme = self.theme
+ background_style = theme.get_background_style()
+ token_style = theme.get_style_for_token
+
+ traceback_theme = Theme(
+ {
+ "pretty": token_style(TextToken),
+ "pygments.text": token_style(Token),
+ "pygments.string": token_style(String),
+ "pygments.function": token_style(Name.Function),
+ "pygments.number": token_style(Number),
+ "repr.indent": token_style(Comment) + Style(dim=True),
+ "repr.str": token_style(String),
+ "repr.brace": token_style(TextToken) + Style(bold=True),
+ "repr.number": token_style(Number),
+ "repr.bool_true": token_style(Keyword.Constant),
+ "repr.bool_false": token_style(Keyword.Constant),
+ "repr.none": token_style(Keyword.Constant),
+ "scope.border": token_style(String.Delimiter),
+ "scope.equals": token_style(Operator),
+ "scope.key": token_style(Name),
+ "scope.key.special": token_style(Name.Constant) + Style(dim=True),
+ },
+ inherit=False,
+ )
+
+ highlighter = ReprHighlighter()
+ for last, stack in loop_last(reversed(self.trace.stacks)):
+ if stack.frames:
+ stack_renderable: ConsoleRenderable = Panel(
+ self._render_stack(stack),
+ title="[traceback.title]Traceback [dim](most recent call last)",
+ style=background_style,
+ border_style="traceback.border",
+ expand=True,
+ padding=(0, 1),
+ )
+ stack_renderable = Constrain(stack_renderable, self.width)
+ with console.use_theme(traceback_theme):
+ yield stack_renderable
+ if stack.syntax_error is not None:
+ with console.use_theme(traceback_theme):
+ yield Constrain(
+ Panel(
+ self._render_syntax_error(stack.syntax_error),
+ style=background_style,
+ border_style="traceback.border.syntax_error",
+ expand=True,
+ padding=(0, 1),
+ width=self.width,
+ ),
+ self.width,
+ )
+ yield Text.assemble(
+ (f"{stack.exc_type}: ", "traceback.exc_type"),
+ highlighter(stack.syntax_error.msg),
+ )
+ elif stack.exc_value:
+ yield Text.assemble(
+ (f"{stack.exc_type}: ", "traceback.exc_type"),
+ highlighter(stack.exc_value),
+ )
+ else:
+ yield Text.assemble((f"{stack.exc_type}", "traceback.exc_type"))
+
+ if not last:
+ if stack.is_cause:
+ yield Text.from_markup(
+ "\n[i]The above exception was the direct cause of the following exception:\n",
+ )
+ else:
+ yield Text.from_markup(
+ "\n[i]During handling of the above exception, another exception occurred:\n",
+ )
+
+ @group()
+ def _render_syntax_error(self, syntax_error: _SyntaxError) -> RenderResult:
+ highlighter = ReprHighlighter()
+ path_highlighter = PathHighlighter()
+ if syntax_error.filename != "<stdin>":
+ text = Text.assemble(
+ (f" {syntax_error.filename}", "pygments.string"),
+ (":", "pygments.text"),
+ (str(syntax_error.lineno), "pygments.number"),
+ style="pygments.text",
+ )
+ yield path_highlighter(text)
+ syntax_error_text = highlighter(syntax_error.line.rstrip())
+ syntax_error_text.no_wrap = True
+ offset = min(syntax_error.offset - 1, len(syntax_error_text))
+ syntax_error_text.stylize("bold underline", offset, offset)
+ syntax_error_text += Text.from_markup(
+ "\n" + " " * offset + "[traceback.offset]â–²[/]",
+ style="pygments.text",
+ )
+ yield syntax_error_text
+
+ @classmethod
+ def _guess_lexer(cls, filename: str, code: str) -> str:
+ ext = os.path.splitext(filename)[-1]
+ if not ext:
+ # No extension, look at first line to see if it is a hashbang
+ # Note, this is an educated guess and not a guarantee
+ # If it fails, the only downside is that the code is highlighted strangely
+ new_line_index = code.index("\n")
+ first_line = code[:new_line_index] if new_line_index != -1 else code
+ if first_line.startswith("#!") and "python" in first_line.lower():
+ return "python"
+ try:
+ return cls.LEXERS.get(ext) or guess_lexer_for_filename(filename, code).name
+ except ClassNotFound:
+ return "text"
+
+ @group()
+ def _render_stack(self, stack: Stack) -> RenderResult:
+ path_highlighter = PathHighlighter()
+ theme = self.theme
+ code_cache: Dict[str, str] = {}
+
+ def read_code(filename: str) -> str:
+ """Read files, and cache results on filename.
+
+ Args:
+ filename (str): Filename to read
+
+ Returns:
+ str: Contents of file
+ """
+ code = code_cache.get(filename)
+ if code is None:
+ with open(
+ filename, "rt", encoding="utf-8", errors="replace"
+ ) as code_file:
+ code = code_file.read()
+ code_cache[filename] = code
+ return code
+
+ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
+ if frame.locals:
+ yield render_scope(
+ frame.locals,
+ title="locals",
+ indent_guides=self.indent_guides,
+ max_length=self.locals_max_length,
+ max_string=self.locals_max_string,
+ )
+
+ exclude_frames: Optional[range] = None
+ if self.max_frames != 0:
+ exclude_frames = range(
+ self.max_frames // 2,
+ len(stack.frames) - self.max_frames // 2,
+ )
+
+ excluded = False
+ for frame_index, frame in enumerate(stack.frames):
+
+ if exclude_frames and frame_index in exclude_frames:
+ excluded = True
+ continue
+
+ if excluded:
+ assert exclude_frames is not None
+ yield Text(
+ f"\n... {len(exclude_frames)} frames hidden ...",
+ justify="center",
+ style="traceback.error",
+ )
+ excluded = False
+
+ first = frame_index == 0
+ frame_filename = frame.filename
+ suppressed = any(frame_filename.startswith(path) for path in self.suppress)
+
+ text = Text.assemble(
+ path_highlighter(Text(frame.filename, style="pygments.string")),
+ (":", "pygments.text"),
+ (str(frame.lineno), "pygments.number"),
+ " in ",
+ (frame.name, "pygments.function"),
+ style="pygments.text",
+ )
+ if not frame.filename.startswith("<") and not first:
+ yield ""
+ yield text
+ if frame.filename.startswith("<"):
+ yield from render_locals(frame)
+ continue
+ if not suppressed:
+ try:
+ code = read_code(frame.filename)
+ lexer_name = self._guess_lexer(frame.filename, code)
+ syntax = Syntax(
+ code,
+ lexer_name,
+ theme=theme,
+ line_numbers=True,
+ line_range=(
+ frame.lineno - self.extra_lines,
+ frame.lineno + self.extra_lines,
+ ),
+ highlight_lines={frame.lineno},
+ word_wrap=self.word_wrap,
+ code_width=88,
+ indent_guides=self.indent_guides,
+ dedent=False,
+ )
+ yield ""
+ except Exception as error:
+ yield Text.assemble(
+ (f"\n{error}", "traceback.error"),
+ )
+ else:
+ yield (
+ Columns(
+ [
+ syntax,
+ *render_locals(frame),
+ ],
+ padding=1,
+ )
+ if frame.locals
+ else syntax
+ )
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from .console import Console
+
+ console = Console()
+ import sys
+
+ def bar(a: Any) -> None: # 这是对亚洲语言支æŒçš„测试。é¢å¯¹æ¨¡æ£±ä¸¤å¯çš„想法,拒ç»çŒœæµ‹çš„诱惑
+ one = 1
+ print(one / a)
+
+ def foo(a: Any) -> None:
+ _rich_traceback_guard = True
+ zed = {
+ "characters": {
+ "Paul Atreides",
+ "Vladimir Harkonnen",
+ "Thufir Hawat",
+ "Duncan Idaho",
+ },
+ "atomic_types": (None, False, True),
+ }
+ bar(a)
+
+ def error() -> None:
+
+ try:
+ try:
+ foo(0)
+ except:
+ slfkjsldkfj # type: ignore[name-defined]
+ except:
+ console.print_exception(show_locals=True)
+
+ error()
diff --git a/src/pip/_vendor/rich/tree.py b/src/pip/_vendor/rich/tree.py
new file mode 100644
index 000000000..afe8da1a4
--- /dev/null
+++ b/src/pip/_vendor/rich/tree.py
@@ -0,0 +1,251 @@
+from typing import Iterator, List, Optional, Tuple
+
+from ._loop import loop_first, loop_last
+from .console import Console, ConsoleOptions, RenderableType, RenderResult
+from .jupyter import JupyterMixin
+from .measure import Measurement
+from .segment import Segment
+from .style import Style, StyleStack, StyleType
+from .styled import Styled
+
+
+class Tree(JupyterMixin):
+ """A renderable for a tree structure.
+
+ Args:
+ label (RenderableType): The renderable or str for the tree label.
+ style (StyleType, optional): Style of this tree. Defaults to "tree".
+ guide_style (StyleType, optional): Style of the guide lines. Defaults to "tree.line".
+ expanded (bool, optional): Also display children. Defaults to True.
+ highlight (bool, optional): Highlight renderable (if str). Defaults to False.
+ """
+
+ def __init__(
+ self,
+ label: RenderableType,
+ *,
+ style: StyleType = "tree",
+ guide_style: StyleType = "tree.line",
+ expanded: bool = True,
+ highlight: bool = False,
+ hide_root: bool = False,
+ ) -> None:
+ self.label = label
+ self.style = style
+ self.guide_style = guide_style
+ self.children: List[Tree] = []
+ self.expanded = expanded
+ self.highlight = highlight
+ self.hide_root = hide_root
+
+ def add(
+ self,
+ label: RenderableType,
+ *,
+ style: Optional[StyleType] = None,
+ guide_style: Optional[StyleType] = None,
+ expanded: bool = True,
+ highlight: Optional[bool] = False,
+ ) -> "Tree":
+ """Add a child tree.
+
+ Args:
+ label (RenderableType): The renderable or str for the tree label.
+ style (StyleType, optional): Style of this tree. Defaults to "tree".
+ guide_style (StyleType, optional): Style of the guide lines. Defaults to "tree.line".
+ expanded (bool, optional): Also display children. Defaults to True.
+ highlight (Optional[bool], optional): Highlight renderable (if str). Defaults to False.
+
+ Returns:
+ Tree: A new child Tree, which may be further modified.
+ """
+ node = Tree(
+ label,
+ style=self.style if style is None else style,
+ guide_style=self.guide_style if guide_style is None else guide_style,
+ expanded=expanded,
+ highlight=self.highlight if highlight is None else highlight,
+ )
+ self.children.append(node)
+ return node
+
+ def __rich_console__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "RenderResult":
+
+ stack: List[Iterator[Tuple[bool, Tree]]] = []
+ pop = stack.pop
+ push = stack.append
+ new_line = Segment.line()
+
+ get_style = console.get_style
+ null_style = Style.null()
+ guide_style = get_style(self.guide_style, default="") or null_style
+ SPACE, CONTINUE, FORK, END = range(4)
+
+ ASCII_GUIDES = (" ", "| ", "+-- ", "`-- ")
+ TREE_GUIDES = [
+ (" ", "│ ", "├── ", "└── "),
+ (" ", "┃ ", "┣â”â” ", "â”—â”â” "),
+ (" ", "â•‘ ", "â• â•â• ", "â•šâ•â• "),
+ ]
+ _Segment = Segment
+
+ def make_guide(index: int, style: Style) -> Segment:
+ """Make a Segment for a level of the guide lines."""
+ if options.ascii_only:
+ line = ASCII_GUIDES[index]
+ else:
+ guide = 1 if style.bold else (2 if style.underline2 else 0)
+ line = TREE_GUIDES[0 if options.legacy_windows else guide][index]
+ return _Segment(line, style)
+
+ levels: List[Segment] = [make_guide(CONTINUE, guide_style)]
+ push(iter(loop_last([self])))
+
+ guide_style_stack = StyleStack(get_style(self.guide_style))
+ style_stack = StyleStack(get_style(self.style))
+ remove_guide_styles = Style(bold=False, underline2=False)
+
+ depth = 0
+
+ while stack:
+ stack_node = pop()
+ try:
+ last, node = next(stack_node)
+ except StopIteration:
+ levels.pop()
+ if levels:
+ guide_style = levels[-1].style or null_style
+ levels[-1] = make_guide(FORK, guide_style)
+ guide_style_stack.pop()
+ style_stack.pop()
+ continue
+ push(stack_node)
+ if last:
+ levels[-1] = make_guide(END, levels[-1].style or null_style)
+
+ guide_style = guide_style_stack.current + get_style(node.guide_style)
+ style = style_stack.current + get_style(node.style)
+ prefix = levels[(2 if self.hide_root else 1) :]
+ renderable_lines = console.render_lines(
+ Styled(node.label, style),
+ options.update(
+ width=options.max_width
+ - sum(level.cell_length for level in prefix),
+ highlight=self.highlight,
+ height=None,
+ ),
+ pad=options.justify is not None,
+ )
+
+ if not (depth == 0 and self.hide_root):
+ for first, line in loop_first(renderable_lines):
+ if prefix:
+ yield from _Segment.apply_style(
+ prefix,
+ style.background_style,
+ post_style=remove_guide_styles,
+ )
+ yield from line
+ yield new_line
+ if first and prefix:
+ prefix[-1] = make_guide(
+ SPACE if last else CONTINUE, prefix[-1].style or null_style
+ )
+
+ if node.expanded and node.children:
+ levels[-1] = make_guide(
+ SPACE if last else CONTINUE, levels[-1].style or null_style
+ )
+ levels.append(
+ make_guide(END if len(node.children) == 1 else FORK, guide_style)
+ )
+ style_stack.push(get_style(node.style))
+ guide_style_stack.push(get_style(node.guide_style))
+ push(iter(loop_last(node.children)))
+ depth += 1
+
+ def __rich_measure__(
+ self, console: "Console", options: "ConsoleOptions"
+ ) -> "Measurement":
+ stack: List[Iterator[Tree]] = [iter([self])]
+ pop = stack.pop
+ push = stack.append
+ minimum = 0
+ maximum = 0
+ measure = Measurement.get
+ level = 0
+ while stack:
+ iter_tree = pop()
+ try:
+ tree = next(iter_tree)
+ except StopIteration:
+ level -= 1
+ continue
+ push(iter_tree)
+ min_measure, max_measure = measure(console, options, tree.label)
+ indent = level * 4
+ minimum = max(min_measure + indent, minimum)
+ maximum = max(max_measure + indent, maximum)
+ if tree.expanded and tree.children:
+ push(iter(tree.children))
+ level += 1
+ return Measurement(minimum, maximum)
+
+
+if __name__ == "__main__": # pragma: no cover
+
+ from pip._vendor.rich.console import Group
+ from pip._vendor.rich.markdown import Markdown
+ from pip._vendor.rich.panel import Panel
+ from pip._vendor.rich.syntax import Syntax
+ from pip._vendor.rich.table import Table
+
+ table = Table(row_styles=["", "dim"])
+
+ table.add_column("Released", style="cyan", no_wrap=True)
+ table.add_column("Title", style="magenta")
+ table.add_column("Box Office", justify="right", style="green")
+
+ table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690")
+ table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
+ table.add_row("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889")
+ table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,889")
+
+ code = """\
+class Segment(NamedTuple):
+ text: str = ""
+ style: Optional[Style] = None
+ is_control: bool = False
+"""
+ syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
+
+ markdown = Markdown(
+ """\
+### example.md
+> Hello, World!
+>
+> Markdown _all_ the things
+"""
+ )
+
+ root = Tree("🌲 [b green]Rich Tree", highlight=True, hide_root=True)
+
+ node = root.add(":file_folder: Renderables", guide_style="red")
+ simple_node = node.add(":file_folder: [bold yellow]Atomic", guide_style="uu green")
+ simple_node.add(Group("📄 Syntax", syntax))
+ simple_node.add(Group("📄 Markdown", Panel(markdown, border_style="green")))
+
+ containers_node = node.add(
+ ":file_folder: [bold magenta]Containers", guide_style="bold magenta"
+ )
+ containers_node.expanded = True
+ panel = Panel.fit("Just a panel", border_style="red")
+ containers_node.add(Group("📄 Panels", panel))
+
+ containers_node.add(Group("📄 [b magenta]Table", table))
+
+ console = Console()
+
+ console.print(root)
diff --git a/src/pip/_vendor/tenacity.pyi b/src/pip/_vendor/tenacity.pyi
deleted file mode 100644
index baf1de9dd..000000000
--- a/src/pip/_vendor/tenacity.pyi
+++ /dev/null
@@ -1 +0,0 @@
-from tenacity import * \ No newline at end of file
diff --git a/src/pip/_vendor/tomli.pyi b/src/pip/_vendor/tomli.pyi
deleted file mode 100644
index b894db691..000000000
--- a/src/pip/_vendor/tomli.pyi
+++ /dev/null
@@ -1 +0,0 @@
-from tomli import * \ No newline at end of file
diff --git a/src/pip/_vendor/tomli/__init__.py b/src/pip/_vendor/tomli/__init__.py
index 1cd8e0727..4c6ec97ec 100644
--- a/src/pip/_vendor/tomli/__init__.py
+++ b/src/pip/_vendor/tomli/__init__.py
@@ -1,6 +1,11 @@
-"""A lil' TOML parser."""
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
+# Licensed to PSF under a Contributor Agreement.
__all__ = ("loads", "load", "TOMLDecodeError")
-__version__ = "1.0.3" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT
+__version__ = "2.0.1" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT
-from pip._vendor.tomli._parser import TOMLDecodeError, load, loads
+from ._parser import TOMLDecodeError, load, loads
+
+# Pretend this exception was created here.
+TOMLDecodeError.__module__ = __name__
diff --git a/src/pip/_vendor/tomli/_parser.py b/src/pip/_vendor/tomli/_parser.py
index 730a74684..f1bb0aa19 100644
--- a/src/pip/_vendor/tomli/_parser.py
+++ b/src/pip/_vendor/tomli/_parser.py
@@ -1,42 +1,33 @@
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
+# Licensed to PSF under a Contributor Agreement.
+
+from __future__ import annotations
+
+from collections.abc import Iterable
import string
from types import MappingProxyType
-from typing import (
- TYPE_CHECKING,
- Any,
- Callable,
- Dict,
- FrozenSet,
- Iterable,
- Optional,
- TextIO,
- Tuple,
-)
+from typing import Any, BinaryIO, NamedTuple
-from pip._vendor.tomli._re import (
- RE_BIN,
+from ._re import (
RE_DATETIME,
- RE_HEX,
RE_LOCALTIME,
RE_NUMBER,
- RE_OCT,
match_to_datetime,
match_to_localtime,
match_to_number,
)
-
-if TYPE_CHECKING:
- from re import Pattern
-
+from ._types import Key, ParseFloat, Pos
ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
# Neither of these sets include quotation mark or backslash. They are
# currently handled as separate cases in the parser functions.
ILLEGAL_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t")
-ILLEGAL_MULTILINE_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t\n\r")
+ILLEGAL_MULTILINE_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t\n")
ILLEGAL_LITERAL_STR_CHARS = ILLEGAL_BASIC_STR_CHARS
-ILLEGAL_MULTILINE_LITERAL_STR_CHARS = ASCII_CTRL - frozenset("\t\n")
+ILLEGAL_MULTILINE_LITERAL_STR_CHARS = ILLEGAL_MULTILINE_BASIC_STR_CHARS
ILLEGAL_COMMENT_CHARS = ILLEGAL_BASIC_STR_CHARS
@@ -44,6 +35,7 @@ TOML_WS = frozenset(" \t")
TOML_WS_AND_NEWLINE = TOML_WS | frozenset("\n")
BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + "-_")
KEY_INITIAL_CHARS = BARE_KEY_CHARS | frozenset("\"'")
+HEXDIGIT_CHARS = frozenset(string.hexdigits)
BASIC_STR_ESCAPE_REPLACEMENTS = MappingProxyType(
{
@@ -57,30 +49,33 @@ BASIC_STR_ESCAPE_REPLACEMENTS = MappingProxyType(
}
)
-# Type annotations
-ParseFloat = Callable[[str], Any]
-Key = Tuple[str, ...]
-Pos = int
-
class TOMLDecodeError(ValueError):
"""An error raised if a document is not valid TOML."""
-def load(fp: TextIO, *, parse_float: ParseFloat = float) -> Dict[str, Any]:
- """Parse TOML from a file object."""
- s = fp.read()
+def load(__fp: BinaryIO, *, parse_float: ParseFloat = float) -> dict[str, Any]:
+ """Parse TOML from a binary file object."""
+ b = __fp.read()
+ try:
+ s = b.decode()
+ except AttributeError:
+ raise TypeError(
+ "File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`"
+ ) from None
return loads(s, parse_float=parse_float)
-def loads(s: str, *, parse_float: ParseFloat = float) -> Dict[str, Any]: # noqa: C901
+def loads(__s: str, *, parse_float: ParseFloat = float) -> dict[str, Any]: # noqa: C901
"""Parse TOML from a string."""
# The spec allows converting "\r\n" to "\n", even in string
# literals. Let's do so to simplify parsing.
- src = s.replace("\r\n", "\n")
+ src = __s.replace("\r\n", "\n")
pos = 0
- state = State()
+ out = Output(NestedDict(), Flags())
+ header: Key = ()
+ parse_float = make_safe_parse_float(parse_float)
# Parse one statement at a time
# (typically means one line in TOML source)
@@ -104,17 +99,18 @@ def loads(s: str, *, parse_float: ParseFloat = float) -> Dict[str, Any]: # noqa
pos += 1
continue
if char in KEY_INITIAL_CHARS:
- pos = key_value_rule(src, pos, state, parse_float)
+ pos = key_value_rule(src, pos, out, header, parse_float)
pos = skip_chars(src, pos, TOML_WS)
elif char == "[":
try:
- second_char: Optional[str] = src[pos + 1]
+ second_char: str | None = src[pos + 1]
except IndexError:
second_char = None
+ out.flags.finalize_pending()
if second_char == "[":
- pos = create_list_rule(src, pos, state)
+ pos, header = create_list_rule(src, pos, out)
else:
- pos = create_dict_rule(src, pos, state)
+ pos, header = create_dict_rule(src, pos, out)
pos = skip_chars(src, pos, TOML_WS)
elif char != "#":
raise suffixed_err(src, pos, "Invalid statement")
@@ -133,17 +129,7 @@ def loads(s: str, *, parse_float: ParseFloat = float) -> Dict[str, Any]: # noqa
)
pos += 1
- return state.out.dict
-
-
-class State:
- def __init__(self) -> None:
- # Mutable, read-only
- self.out = NestedDict()
- self.flags = Flags()
-
- # Immutable, read and write
- self.header_namespace: Key = ()
+ return out.data.dict
class Flags:
@@ -156,7 +142,16 @@ class Flags:
EXPLICIT_NEST = 1
def __init__(self) -> None:
- self._flags: Dict[str, dict] = {}
+ self._flags: dict[str, dict] = {}
+ self._pending_flags: set[tuple[Key, int]] = set()
+
+ def add_pending(self, key: Key, flag: int) -> None:
+ self._pending_flags.add((key, flag))
+
+ def finalize_pending(self) -> None:
+ for key, flag in self._pending_flags:
+ self.set(key, flag, recursive=False)
+ self._pending_flags.clear()
def unset_all(self, key: Key) -> None:
cont = self._flags
@@ -166,19 +161,6 @@ class Flags:
cont = cont[k]["nested"]
cont.pop(key[-1], None)
- def set_for_relative_key(self, head_key: Key, rel_key: Key, flag: int) -> None:
- cont = self._flags
- for k in head_key:
- if k not in cont:
- cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}}
- cont = cont[k]["nested"]
- for k in rel_key:
- if k in cont:
- cont[k]["flags"].add(flag)
- else:
- cont[k] = {"flags": {flag}, "recursive_flags": set(), "nested": {}}
- cont = cont[k]["nested"]
-
def set(self, key: Key, flag: int, *, recursive: bool) -> None: # noqa: A003
cont = self._flags
key_parent, key_stem = key[:-1], key[-1]
@@ -211,7 +193,7 @@ class Flags:
class NestedDict:
def __init__(self) -> None:
# The parsed content of the TOML document
- self.dict: Dict[str, Any] = {}
+ self.dict: dict[str, Any] = {}
def get_or_create_nest(
self,
@@ -242,6 +224,11 @@ class NestedDict:
cont[last_key] = [{}]
+class Output(NamedTuple):
+ data: NestedDict
+ flags: Flags
+
+
def skip_chars(src: str, pos: Pos, chars: Iterable[str]) -> Pos:
try:
while src[pos] in chars:
@@ -256,7 +243,7 @@ def skip_until(
pos: Pos,
expect: str,
*,
- error_on: FrozenSet[str],
+ error_on: frozenset[str],
error_on_eof: bool,
) -> Pos:
try:
@@ -264,19 +251,18 @@ def skip_until(
except ValueError:
new_pos = len(src)
if error_on_eof:
- raise suffixed_err(src, new_pos, f'Expected "{expect!r}"')
+ raise suffixed_err(src, new_pos, f"Expected {expect!r}") from None
- bad_chars = error_on.intersection(src[pos:new_pos])
- if bad_chars:
- bad_char = next(iter(bad_chars))
- bad_pos = src.index(bad_char, pos)
- raise suffixed_err(src, bad_pos, f'Found invalid character "{bad_char!r}"')
+ if not error_on.isdisjoint(src[pos:new_pos]):
+ while src[pos] not in error_on:
+ pos += 1
+ raise suffixed_err(src, pos, f"Found invalid character {src[pos]!r}")
return new_pos
def skip_comment(src: str, pos: Pos) -> Pos:
try:
- char: Optional[str] = src[pos]
+ char: str | None = src[pos]
except IndexError:
char = None
if char == "#":
@@ -295,115 +281,116 @@ def skip_comments_and_array_ws(src: str, pos: Pos) -> Pos:
return pos
-def create_dict_rule(src: str, pos: Pos, state: State) -> Pos:
+def create_dict_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:
pos += 1 # Skip "["
pos = skip_chars(src, pos, TOML_WS)
pos, key = parse_key(src, pos)
- if state.flags.is_(key, Flags.EXPLICIT_NEST) or state.flags.is_(key, Flags.FROZEN):
- raise suffixed_err(src, pos, f"Can not declare {key} twice")
- state.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
+ if out.flags.is_(key, Flags.EXPLICIT_NEST) or out.flags.is_(key, Flags.FROZEN):
+ raise suffixed_err(src, pos, f"Cannot declare {key} twice")
+ out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
try:
- state.out.get_or_create_nest(key)
+ out.data.get_or_create_nest(key)
except KeyError:
- raise suffixed_err(src, pos, "Can not overwrite a value")
- state.header_namespace = key
+ raise suffixed_err(src, pos, "Cannot overwrite a value") from None
- if src[pos : pos + 1] != "]":
- raise suffixed_err(src, pos, 'Expected "]" at the end of a table declaration')
- return pos + 1
+ if not src.startswith("]", pos):
+ raise suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
+ return pos + 1, key
-def create_list_rule(src: str, pos: Pos, state: State) -> Pos:
+def create_list_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:
pos += 2 # Skip "[["
pos = skip_chars(src, pos, TOML_WS)
pos, key = parse_key(src, pos)
- if state.flags.is_(key, Flags.FROZEN):
- raise suffixed_err(src, pos, f"Can not mutate immutable namespace {key}")
+ if out.flags.is_(key, Flags.FROZEN):
+ raise suffixed_err(src, pos, f"Cannot mutate immutable namespace {key}")
# Free the namespace now that it points to another empty list item...
- state.flags.unset_all(key)
+ out.flags.unset_all(key)
# ...but this key precisely is still prohibited from table declaration
- state.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
+ out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
try:
- state.out.append_nest_to_list(key)
+ out.data.append_nest_to_list(key)
except KeyError:
- raise suffixed_err(src, pos, "Can not overwrite a value")
- state.header_namespace = key
+ raise suffixed_err(src, pos, "Cannot overwrite a value") from None
- end_marker = src[pos : pos + 2]
- if end_marker != "]]":
- raise suffixed_err(
- src,
- pos,
- f'Found "{end_marker!r}" at the end of an array declaration.'
- ' Expected "]]"',
- )
- return pos + 2
+ if not src.startswith("]]", pos):
+ raise suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
+ return pos + 2, key
-def key_value_rule(src: str, pos: Pos, state: State, parse_float: ParseFloat) -> Pos:
+def key_value_rule(
+ src: str, pos: Pos, out: Output, header: Key, parse_float: ParseFloat
+) -> Pos:
pos, key, value = parse_key_value_pair(src, pos, parse_float)
key_parent, key_stem = key[:-1], key[-1]
- abs_key_parent = state.header_namespace + key_parent
-
- if state.flags.is_(abs_key_parent, Flags.FROZEN):
+ abs_key_parent = header + key_parent
+
+ relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
+ for cont_key in relative_path_cont_keys:
+ # Check that dotted key syntax does not redefine an existing table
+ if out.flags.is_(cont_key, Flags.EXPLICIT_NEST):
+ raise suffixed_err(src, pos, f"Cannot redefine namespace {cont_key}")
+ # Containers in the relative path can't be opened with the table syntax or
+ # dotted key/value syntax in following table sections.
+ out.flags.add_pending(cont_key, Flags.EXPLICIT_NEST)
+
+ if out.flags.is_(abs_key_parent, Flags.FROZEN):
raise suffixed_err(
- src, pos, f"Can not mutate immutable namespace {abs_key_parent}"
+ src, pos, f"Cannot mutate immutable namespace {abs_key_parent}"
)
- # Containers in the relative path can't be opened with the table syntax after this
- state.flags.set_for_relative_key(state.header_namespace, key, Flags.EXPLICIT_NEST)
+
try:
- nest = state.out.get_or_create_nest(abs_key_parent)
+ nest = out.data.get_or_create_nest(abs_key_parent)
except KeyError:
- raise suffixed_err(src, pos, "Can not overwrite a value")
+ raise suffixed_err(src, pos, "Cannot overwrite a value") from None
if key_stem in nest:
- raise suffixed_err(src, pos, "Can not overwrite a value")
+ raise suffixed_err(src, pos, "Cannot overwrite a value")
# Mark inline table and array namespaces recursively immutable
if isinstance(value, (dict, list)):
- abs_key = state.header_namespace + key
- state.flags.set(abs_key, Flags.FROZEN, recursive=True)
+ out.flags.set(header + key, Flags.FROZEN, recursive=True)
nest[key_stem] = value
return pos
def parse_key_value_pair(
src: str, pos: Pos, parse_float: ParseFloat
-) -> Tuple[Pos, Key, Any]:
+) -> tuple[Pos, Key, Any]:
pos, key = parse_key(src, pos)
try:
- char: Optional[str] = src[pos]
+ char: str | None = src[pos]
except IndexError:
char = None
if char != "=":
- raise suffixed_err(src, pos, 'Expected "=" after a key in a key/value pair')
+ raise suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
pos += 1
pos = skip_chars(src, pos, TOML_WS)
pos, value = parse_value(src, pos, parse_float)
return pos, key, value
-def parse_key(src: str, pos: Pos) -> Tuple[Pos, Key]:
+def parse_key(src: str, pos: Pos) -> tuple[Pos, Key]:
pos, key_part = parse_key_part(src, pos)
- key = [key_part]
+ key: Key = (key_part,)
pos = skip_chars(src, pos, TOML_WS)
while True:
try:
- char: Optional[str] = src[pos]
+ char: str | None = src[pos]
except IndexError:
char = None
if char != ".":
- return pos, tuple(key)
+ return pos, key
pos += 1
pos = skip_chars(src, pos, TOML_WS)
pos, key_part = parse_key_part(src, pos)
- key.append(key_part)
+ key += (key_part,)
pos = skip_chars(src, pos, TOML_WS)
-def parse_key_part(src: str, pos: Pos) -> Tuple[Pos, str]:
+def parse_key_part(src: str, pos: Pos) -> tuple[Pos, str]:
try:
- char: Optional[str] = src[pos]
+ char: str | None = src[pos]
except IndexError:
char = None
if char in BARE_KEY_CHARS:
@@ -417,17 +404,17 @@ def parse_key_part(src: str, pos: Pos) -> Tuple[Pos, str]:
raise suffixed_err(src, pos, "Invalid initial character for a key part")
-def parse_one_line_basic_str(src: str, pos: Pos) -> Tuple[Pos, str]:
+def parse_one_line_basic_str(src: str, pos: Pos) -> tuple[Pos, str]:
pos += 1
return parse_basic_str(src, pos, multiline=False)
-def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos, list]:
+def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos, list]:
pos += 1
array: list = []
pos = skip_comments_and_array_ws(src, pos)
- if src[pos : pos + 1] == "]":
+ if src.startswith("]", pos):
return pos + 1, array
while True:
pos, val = parse_value(src, pos, parse_float)
@@ -442,29 +429,29 @@ def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos, list]
pos += 1
pos = skip_comments_and_array_ws(src, pos)
- if src[pos : pos + 1] == "]":
+ if src.startswith("]", pos):
return pos + 1, array
-def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos, dict]:
+def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos, dict]:
pos += 1
nested_dict = NestedDict()
flags = Flags()
pos = skip_chars(src, pos, TOML_WS)
- if src[pos : pos + 1] == "}":
+ if src.startswith("}", pos):
return pos + 1, nested_dict.dict
while True:
pos, key, value = parse_key_value_pair(src, pos, parse_float)
key_parent, key_stem = key[:-1], key[-1]
if flags.is_(key, Flags.FROZEN):
- raise suffixed_err(src, pos, f"Can not mutate immutable namespace {key}")
+ raise suffixed_err(src, pos, f"Cannot mutate immutable namespace {key}")
try:
nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
except KeyError:
- raise suffixed_err(src, pos, "Can not overwrite a value")
+ raise suffixed_err(src, pos, "Cannot overwrite a value") from None
if key_stem in nest:
- raise suffixed_err(src, pos, f'Duplicate inline table key "{key_stem}"')
+ raise suffixed_err(src, pos, f"Duplicate inline table key {key_stem!r}")
nest[key_stem] = value
pos = skip_chars(src, pos, TOML_WS)
c = src[pos : pos + 1]
@@ -480,7 +467,7 @@ def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos
def parse_basic_str_escape(
src: str, pos: Pos, *, multiline: bool = False
-) -> Tuple[Pos, str]:
+) -> tuple[Pos, str]:
escape_id = src[pos : pos + 2]
pos += 2
if multiline and escape_id in {"\\ ", "\\\t", "\\\n"}:
@@ -488,11 +475,12 @@ def parse_basic_str_escape(
# the doc. Error if non-whitespace is found before newline.
if escape_id != "\\\n":
pos = skip_chars(src, pos, TOML_WS)
- char = src[pos : pos + 1]
- if not char:
+ try:
+ char = src[pos]
+ except IndexError:
return pos, ""
if char != "\n":
- raise suffixed_err(src, pos, 'Unescaped "\\" in a string')
+ raise suffixed_err(src, pos, "Unescaped '\\' in a string")
pos += 1
pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)
return pos, ""
@@ -503,18 +491,16 @@ def parse_basic_str_escape(
try:
return pos, BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
except KeyError:
- if len(escape_id) != 2:
- raise suffixed_err(src, pos, "Unterminated string")
- raise suffixed_err(src, pos, 'Unescaped "\\" in a string')
+ raise suffixed_err(src, pos, "Unescaped '\\' in a string") from None
-def parse_basic_str_escape_multiline(src: str, pos: Pos) -> Tuple[Pos, str]:
+def parse_basic_str_escape_multiline(src: str, pos: Pos) -> tuple[Pos, str]:
return parse_basic_str_escape(src, pos, multiline=True)
-def parse_hex_char(src: str, pos: Pos, hex_len: int) -> Tuple[Pos, str]:
+def parse_hex_char(src: str, pos: Pos, hex_len: int) -> tuple[Pos, str]:
hex_str = src[pos : pos + hex_len]
- if len(hex_str) != hex_len or any(c not in string.hexdigits for c in hex_str):
+ if len(hex_str) != hex_len or not HEXDIGIT_CHARS.issuperset(hex_str):
raise suffixed_err(src, pos, "Invalid hex value")
pos += hex_len
hex_int = int(hex_str, 16)
@@ -523,7 +509,7 @@ def parse_hex_char(src: str, pos: Pos, hex_len: int) -> Tuple[Pos, str]:
return pos, chr(hex_int)
-def parse_literal_str(src: str, pos: Pos) -> Tuple[Pos, str]:
+def parse_literal_str(src: str, pos: Pos) -> tuple[Pos, str]:
pos += 1 # Skip starting apostrophe
start_pos = pos
pos = skip_until(
@@ -532,9 +518,9 @@ def parse_literal_str(src: str, pos: Pos) -> Tuple[Pos, str]:
return pos + 1, src[start_pos:pos] # Skip ending apostrophe
-def parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> Tuple[Pos, str]:
+def parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> tuple[Pos, str]:
pos += 3
- if src[pos : pos + 1] == "\n":
+ if src.startswith("\n", pos):
pos += 1
if literal:
@@ -554,16 +540,16 @@ def parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> Tuple[Pos, str]
# Add at maximum two extra apostrophes/quotes if the end sequence
# is 4 or 5 chars long instead of just 3.
- if src[pos : pos + 1] != delim:
+ if not src.startswith(delim, pos):
return pos, result
pos += 1
- if src[pos : pos + 1] != delim:
+ if not src.startswith(delim, pos):
return pos, result + delim
pos += 1
return pos, result + (delim * 2)
-def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> Tuple[Pos, str]:
+def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> tuple[Pos, str]:
if multiline:
error_on = ILLEGAL_MULTILINE_BASIC_STR_CHARS
parse_escapes = parse_basic_str_escape_multiline
@@ -576,11 +562,11 @@ def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> Tuple[Pos, str]:
try:
char = src[pos]
except IndexError:
- raise suffixed_err(src, pos, "Unterminated string")
+ raise suffixed_err(src, pos, "Unterminated string") from None
if char == '"':
if not multiline:
return pos + 1, result + src[start_pos:pos]
- if src[pos + 1 : pos + 3] == '""':
+ if src.startswith('"""', pos):
return pos + 3, result + src[start_pos:pos]
pos += 1
continue
@@ -591,86 +577,67 @@ def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> Tuple[Pos, str]:
start_pos = pos
continue
if char in error_on:
- raise suffixed_err(src, pos, f'Illegal character "{char!r}"')
+ raise suffixed_err(src, pos, f"Illegal character {char!r}")
pos += 1
-def parse_regex(src: str, pos: Pos, regex: "Pattern") -> Tuple[Pos, str]:
- match = regex.match(src, pos)
- if not match:
- raise suffixed_err(src, pos, "Unexpected sequence")
- return match.end(), match.group()
-
-
def parse_value( # noqa: C901
src: str, pos: Pos, parse_float: ParseFloat
-) -> Tuple[Pos, Any]:
+) -> tuple[Pos, Any]:
try:
- char: Optional[str] = src[pos]
+ char: str | None = src[pos]
except IndexError:
char = None
+ # IMPORTANT: order conditions based on speed of checking and likelihood
+
# Basic strings
if char == '"':
- if src[pos + 1 : pos + 3] == '""':
+ if src.startswith('"""', pos):
return parse_multiline_str(src, pos, literal=False)
return parse_one_line_basic_str(src, pos)
# Literal strings
if char == "'":
- if src[pos + 1 : pos + 3] == "''":
+ if src.startswith("'''", pos):
return parse_multiline_str(src, pos, literal=True)
return parse_literal_str(src, pos)
# Booleans
if char == "t":
- if src[pos + 1 : pos + 4] == "rue":
+ if src.startswith("true", pos):
return pos + 4, True
if char == "f":
- if src[pos + 1 : pos + 5] == "alse":
+ if src.startswith("false", pos):
return pos + 5, False
+ # Arrays
+ if char == "[":
+ return parse_array(src, pos, parse_float)
+
+ # Inline tables
+ if char == "{":
+ return parse_inline_table(src, pos, parse_float)
+
# Dates and times
datetime_match = RE_DATETIME.match(src, pos)
if datetime_match:
try:
datetime_obj = match_to_datetime(datetime_match)
- except ValueError:
- raise suffixed_err(src, pos, "Invalid date or datetime")
+ except ValueError as e:
+ raise suffixed_err(src, pos, "Invalid date or datetime") from e
return datetime_match.end(), datetime_obj
localtime_match = RE_LOCALTIME.match(src, pos)
if localtime_match:
return localtime_match.end(), match_to_localtime(localtime_match)
- # Non-decimal integers
- if char == "0":
- second_char = src[pos + 1 : pos + 2]
- if second_char == "x":
- pos, hex_str = parse_regex(src, pos + 2, RE_HEX)
- return pos, int(hex_str, 16)
- if second_char == "o":
- pos, oct_str = parse_regex(src, pos + 2, RE_OCT)
- return pos, int(oct_str, 8)
- if second_char == "b":
- pos, bin_str = parse_regex(src, pos + 2, RE_BIN)
- return pos, int(bin_str, 2)
-
- # Decimal integers and "normal" floats.
+ # Integers and "normal" floats.
# The regex will greedily match any type starting with a decimal
- # char, so needs to be located after handling of non-decimal ints,
- # and dates and times.
+ # char, so needs to be located after handling of dates and times.
number_match = RE_NUMBER.match(src, pos)
if number_match:
return number_match.end(), match_to_number(number_match, parse_float)
- # Arrays
- if char == "[":
- return parse_array(src, pos, parse_float)
-
- # Inline tables
- if char == "{":
- return parse_inline_table(src, pos, parse_float)
-
# Special floats
first_three = src[pos : pos + 3]
if first_three in {"inf", "nan"}:
@@ -701,3 +668,24 @@ def suffixed_err(src: str, pos: Pos, msg: str) -> TOMLDecodeError:
def is_unicode_scalar_value(codepoint: int) -> bool:
return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
+
+
+def make_safe_parse_float(parse_float: ParseFloat) -> ParseFloat:
+ """A decorator to make `parse_float` safe.
+
+ `parse_float` must not return dicts or lists, because these types
+ would be mixed with parsed TOML tables and arrays, thus confusing
+ the parser. The returned decorated callable raises `ValueError`
+ instead of returning illegal types.
+ """
+ # The default `float` callable never returns illegal types. Optimize it.
+ if parse_float is float: # type: ignore[comparison-overlap]
+ return float
+
+ def safe_parse_float(float_str: str) -> Any:
+ float_value = parse_float(float_str)
+ if isinstance(float_value, (dict, list)):
+ raise ValueError("parse_float must not return dicts or lists")
+ return float_value
+
+ return safe_parse_float
diff --git a/src/pip/_vendor/tomli/_re.py b/src/pip/_vendor/tomli/_re.py
index 3883fdd9c..994bb7493 100644
--- a/src/pip/_vendor/tomli/_re.py
+++ b/src/pip/_vendor/tomli/_re.py
@@ -1,37 +1,55 @@
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
+# Licensed to PSF under a Contributor Agreement.
+
+from __future__ import annotations
+
from datetime import date, datetime, time, timedelta, timezone, tzinfo
+from functools import lru_cache
import re
-from typing import TYPE_CHECKING, Any, Optional, Union
+from typing import Any
-if TYPE_CHECKING:
- from re import Match
-
- from pip._vendor.tomli._parser import ParseFloat
+from ._types import ParseFloat
# E.g.
# - 00:32:00.999999
# - 00:32:00
-_TIME_RE_STR = r"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?"
+_TIME_RE_STR = r"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?"
-RE_HEX = re.compile(r"[0-9A-Fa-f](?:_?[0-9A-Fa-f])*")
-RE_BIN = re.compile(r"[01](?:_?[01])*")
-RE_OCT = re.compile(r"[0-7](?:_?[0-7])*")
RE_NUMBER = re.compile(
- r"[+-]?(?:0|[1-9](?:_?[0-9])*)" # integer
- + r"(?:\.[0-9](?:_?[0-9])*)?" # optional fractional part
- + r"(?:[eE][+-]?[0-9](?:_?[0-9])*)?" # optional exponent part
+ r"""
+0
+(?:
+ x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
+ |
+ b[01](?:_?[01])* # bin
+ |
+ o[0-7](?:_?[0-7])* # oct
+)
+|
+[+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
+(?P<floatpart>
+ (?:\.[0-9](?:_?[0-9])*)? # optional fractional part
+ (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
+)
+""",
+ flags=re.VERBOSE,
)
RE_LOCALTIME = re.compile(_TIME_RE_STR)
RE_DATETIME = re.compile(
- r"([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[01])" # date, e.g. 1988-10-27
- + r"(?:"
- + r"[T ]"
- + _TIME_RE_STR
- + r"(?:(Z)|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))?" # time offset
- + r")?"
+ rf"""
+([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
+(?:
+ [Tt ]
+ {_TIME_RE_STR}
+ (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
+)?
+""",
+ flags=re.VERBOSE,
)
-def match_to_datetime(match: "Match") -> Union[datetime, date]:
+def match_to_datetime(match: re.Match) -> datetime | date:
"""Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
Raises ValueError if the match does not correspond to a valid date
@@ -46,7 +64,7 @@ def match_to_datetime(match: "Match") -> Union[datetime, date]:
sec_str,
micros_str,
zulu_time,
- offset_dir_str,
+ offset_sign_str,
offset_hour_str,
offset_minute_str,
) = match.groups()
@@ -54,14 +72,10 @@ def match_to_datetime(match: "Match") -> Union[datetime, date]:
if hour_str is None:
return date(year, month, day)
hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
- micros = int(micros_str[1:].ljust(6, "0")[:6]) if micros_str else 0
- if offset_dir_str:
- offset_dir = 1 if offset_dir_str == "+" else -1
- tz: Optional[tzinfo] = timezone(
- timedelta(
- hours=offset_dir * int(offset_hour_str),
- minutes=offset_dir * int(offset_minute_str),
- )
+ micros = int(micros_str.ljust(6, "0")) if micros_str else 0
+ if offset_sign_str:
+ tz: tzinfo | None = cached_tz(
+ offset_hour_str, offset_minute_str, offset_sign_str
)
elif zulu_time:
tz = timezone.utc
@@ -70,14 +84,24 @@ def match_to_datetime(match: "Match") -> Union[datetime, date]:
return datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
-def match_to_localtime(match: "Match") -> time:
+@lru_cache(maxsize=None)
+def cached_tz(hour_str: str, minute_str: str, sign_str: str) -> timezone:
+ sign = 1 if sign_str == "+" else -1
+ return timezone(
+ timedelta(
+ hours=sign * int(hour_str),
+ minutes=sign * int(minute_str),
+ )
+ )
+
+
+def match_to_localtime(match: re.Match) -> time:
hour_str, minute_str, sec_str, micros_str = match.groups()
- micros = int(micros_str[1:].ljust(6, "0")[:6]) if micros_str else 0
+ micros = int(micros_str.ljust(6, "0")) if micros_str else 0
return time(int(hour_str), int(minute_str), int(sec_str), micros)
-def match_to_number(match: "Match", parse_float: "ParseFloat") -> Any:
- match_str = match.group()
- if "." in match_str or "e" in match_str or "E" in match_str:
- return parse_float(match_str)
- return int(match_str)
+def match_to_number(match: re.Match, parse_float: ParseFloat) -> Any:
+ if match.group("floatpart"):
+ return parse_float(match.group())
+ return int(match.group(), 0)
diff --git a/src/pip/_vendor/tomli/_types.py b/src/pip/_vendor/tomli/_types.py
new file mode 100644
index 000000000..d949412e0
--- /dev/null
+++ b/src/pip/_vendor/tomli/_types.py
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
+# Licensed to PSF under a Contributor Agreement.
+
+from typing import Any, Callable, Tuple
+
+# Type annotations
+ParseFloat = Callable[[str], Any]
+Key = Tuple[str, ...]
+Pos = int
diff --git a/src/pip/_vendor/typing_extensions.LICENSE b/src/pip/_vendor/typing_extensions.LICENSE
new file mode 100644
index 000000000..583f9f6e6
--- /dev/null
+++ b/src/pip/_vendor/typing_extensions.LICENSE
@@ -0,0 +1,254 @@
+A. HISTORY OF THE SOFTWARE
+==========================
+
+Python was created in the early 1990s by Guido van Rossum at Stichting
+Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
+as a successor of a language called ABC. Guido remains Python's
+principal author, although it includes many contributions from others.
+
+In 1995, Guido continued his work on Python at the Corporation for
+National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
+in Reston, Virginia where he released several versions of the
+software.
+
+In May 2000, Guido and the Python core development team moved to
+BeOpen.com to form the BeOpen PythonLabs team. In October of the same
+year, the PythonLabs team moved to Digital Creations (now Zope
+Corporation, see http://www.zope.com). In 2001, the Python Software
+Foundation (PSF, see http://www.python.org/psf/) was formed, a
+non-profit organization created specifically to own Python-related
+Intellectual Property. Zope Corporation is a sponsoring member of
+the PSF.
+
+All Python releases are Open Source (see http://www.opensource.org for
+the Open Source Definition). Historically, most, but not all, Python
+releases have also been GPL-compatible; the table below summarizes
+the various releases.
+
+ Release Derived Year Owner GPL-
+ from compatible? (1)
+
+ 0.9.0 thru 1.2 1991-1995 CWI yes
+ 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
+ 1.6 1.5.2 2000 CNRI no
+ 2.0 1.6 2000 BeOpen.com no
+ 1.6.1 1.6 2001 CNRI yes (2)
+ 2.1 2.0+1.6.1 2001 PSF no
+ 2.0.1 2.0+1.6.1 2001 PSF yes
+ 2.1.1 2.1+2.0.1 2001 PSF yes
+ 2.1.2 2.1.1 2002 PSF yes
+ 2.1.3 2.1.2 2002 PSF yes
+ 2.2 and above 2.1.1 2001-now PSF yes
+
+Footnotes:
+
+(1) GPL-compatible doesn't mean that we're distributing Python under
+ the GPL. All Python licenses, unlike the GPL, let you distribute
+ a modified version without making your changes open source. The
+ GPL-compatible licenses make it possible to combine Python with
+ other software that is released under the GPL; the others don't.
+
+(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
+ because its license has a choice of law clause. According to
+ CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
+ is "not incompatible" with the GPL.
+
+Thanks to the many outside volunteers who have worked under Guido's
+direction to make these releases possible.
+
+
+B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
+===============================================================
+
+PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+--------------------------------------------
+
+1. This LICENSE AGREEMENT is between the Python Software Foundation
+("PSF"), and the Individual or Organization ("Licensee") accessing and
+otherwise using this software ("Python") in source or binary form and
+its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, PSF hereby
+grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
+analyze, test, perform and/or display publicly, prepare derivative works,
+distribute, and otherwise use Python alone or in any derivative version,
+provided, however, that PSF's License Agreement and PSF's notice of copyright,
+i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+2011, 2012, 2013, 2014 Python Software Foundation; All Rights Reserved" are
+retained in Python alone or in any derivative version prepared by Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python.
+
+4. PSF is making Python available to Licensee on an "AS IS"
+basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between PSF and
+Licensee. This License Agreement does not grant permission to use PSF
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using Python, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
+-------------------------------------------
+
+BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
+
+1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
+office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
+Individual or Organization ("Licensee") accessing and otherwise using
+this software in source or binary form and its associated
+documentation ("the Software").
+
+2. Subject to the terms and conditions of this BeOpen Python License
+Agreement, BeOpen hereby grants Licensee a non-exclusive,
+royalty-free, world-wide license to reproduce, analyze, test, perform
+and/or display publicly, prepare derivative works, distribute, and
+otherwise use the Software alone or in any derivative version,
+provided, however, that the BeOpen Python License is retained in the
+Software, alone or in any derivative version prepared by Licensee.
+
+3. BeOpen is making the Software available to Licensee on an "AS IS"
+basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
+SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
+AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
+DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+5. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+6. This License Agreement shall be governed by and interpreted in all
+respects by the law of the State of California, excluding conflict of
+law provisions. Nothing in this License Agreement shall be deemed to
+create any relationship of agency, partnership, or joint venture
+between BeOpen and Licensee. This License Agreement does not grant
+permission to use BeOpen trademarks or trade names in a trademark
+sense to endorse or promote products or services of Licensee, or any
+third party. As an exception, the "BeOpen Python" logos available at
+http://www.pythonlabs.com/logos.html may be used according to the
+permissions granted on that web page.
+
+7. By copying, installing or otherwise using the software, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
+---------------------------------------
+
+1. This LICENSE AGREEMENT is between the Corporation for National
+Research Initiatives, having an office at 1895 Preston White Drive,
+Reston, VA 20191 ("CNRI"), and the Individual or Organization
+("Licensee") accessing and otherwise using Python 1.6.1 software in
+source or binary form and its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, CNRI
+hereby grants Licensee a nonexclusive, royalty-free, world-wide
+license to reproduce, analyze, test, perform and/or display publicly,
+prepare derivative works, distribute, and otherwise use Python 1.6.1
+alone or in any derivative version, provided, however, that CNRI's
+License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
+1995-2001 Corporation for National Research Initiatives; All Rights
+Reserved" are retained in Python 1.6.1 alone or in any derivative
+version prepared by Licensee. Alternately, in lieu of CNRI's License
+Agreement, Licensee may substitute the following text (omitting the
+quotes): "Python 1.6.1 is made available subject to the terms and
+conditions in CNRI's License Agreement. This Agreement together with
+Python 1.6.1 may be located on the Internet using the following
+unique, persistent identifier (known as a handle): 1895.22/1013. This
+Agreement may also be obtained from a proxy server on the Internet
+using the following URL: http://hdl.handle.net/1895.22/1013".
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python 1.6.1 or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python 1.6.1.
+
+4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
+basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. This License Agreement shall be governed by the federal
+intellectual property law of the United States, including without
+limitation the federal copyright law, and, to the extent such
+U.S. federal law does not apply, by the law of the Commonwealth of
+Virginia, excluding Virginia's conflict of law provisions.
+Notwithstanding the foregoing, with regard to derivative works based
+on Python 1.6.1 that incorporate non-separable material that was
+previously distributed under the GNU General Public License (GPL), the
+law of the Commonwealth of Virginia shall govern this License
+Agreement only as to issues arising under or with respect to
+Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
+License Agreement shall be deemed to create any relationship of
+agency, partnership, or joint venture between CNRI and Licensee. This
+License Agreement does not grant permission to use CNRI trademarks or
+trade name in a trademark sense to endorse or promote products or
+services of Licensee, or any third party.
+
+8. By clicking on the "ACCEPT" button where indicated, or by copying,
+installing or otherwise using Python 1.6.1, Licensee agrees to be
+bound by the terms and conditions of this License Agreement.
+
+ ACCEPT
+
+
+CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
+--------------------------------------------------
+
+Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
+The Netherlands. All rights reserved.
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation, and that the name of Stichting Mathematisch
+Centrum or CWI not be used in advertising or publicity pertaining to
+distribution of the software without specific, written prior
+permission.
+
+STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
+FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/src/pip/_vendor/typing_extensions.py b/src/pip/_vendor/typing_extensions.py
new file mode 100644
index 000000000..4fd824768
--- /dev/null
+++ b/src/pip/_vendor/typing_extensions.py
@@ -0,0 +1,2069 @@
+import abc
+import collections
+import collections.abc
+import functools
+import operator
+import sys
+import types as _types
+import typing
+
+
+# Please keep __all__ alphabetized within each category.
+__all__ = [
+ # Super-special typing primitives.
+ 'ClassVar',
+ 'Concatenate',
+ 'Final',
+ 'LiteralString',
+ 'ParamSpec',
+ 'ParamSpecArgs',
+ 'ParamSpecKwargs',
+ 'Self',
+ 'Type',
+ 'TypeVarTuple',
+ 'Unpack',
+
+ # ABCs (from collections.abc).
+ 'Awaitable',
+ 'AsyncIterator',
+ 'AsyncIterable',
+ 'Coroutine',
+ 'AsyncGenerator',
+ 'AsyncContextManager',
+ 'ChainMap',
+
+ # Concrete collection types.
+ 'ContextManager',
+ 'Counter',
+ 'Deque',
+ 'DefaultDict',
+ 'NamedTuple',
+ 'OrderedDict',
+ 'TypedDict',
+
+ # Structural checks, a.k.a. protocols.
+ 'SupportsIndex',
+
+ # One-off things.
+ 'Annotated',
+ 'assert_never',
+ 'assert_type',
+ 'clear_overloads',
+ 'dataclass_transform',
+ 'get_overloads',
+ 'final',
+ 'get_args',
+ 'get_origin',
+ 'get_type_hints',
+ 'IntVar',
+ 'is_typeddict',
+ 'Literal',
+ 'NewType',
+ 'overload',
+ 'Protocol',
+ 'reveal_type',
+ 'runtime',
+ 'runtime_checkable',
+ 'Text',
+ 'TypeAlias',
+ 'TypeGuard',
+ 'TYPE_CHECKING',
+ 'Never',
+ 'NoReturn',
+ 'Required',
+ 'NotRequired',
+]
+
+# for backward compatibility
+PEP_560 = True
+GenericMeta = type
+
+# The functions below are modified copies of typing internal helpers.
+# They are needed by _ProtocolMeta and they provide support for PEP 646.
+
+_marker = object()
+
+
+def _check_generic(cls, parameters, elen=_marker):
+ """Check correct count for parameters of a generic cls (internal helper).
+ This gives a nice error message in case of count mismatch.
+ """
+ if not elen:
+ raise TypeError(f"{cls} is not a generic class")
+ if elen is _marker:
+ if not hasattr(cls, "__parameters__") or not cls.__parameters__:
+ raise TypeError(f"{cls} is not a generic class")
+ elen = len(cls.__parameters__)
+ alen = len(parameters)
+ if alen != elen:
+ if hasattr(cls, "__parameters__"):
+ parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
+ num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters)
+ if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples):
+ return
+ raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};"
+ f" actual {alen}, expected {elen}")
+
+
+if sys.version_info >= (3, 10):
+ def _should_collect_from_parameters(t):
+ return isinstance(
+ t, (typing._GenericAlias, _types.GenericAlias, _types.UnionType)
+ )
+elif sys.version_info >= (3, 9):
+ def _should_collect_from_parameters(t):
+ return isinstance(t, (typing._GenericAlias, _types.GenericAlias))
+else:
+ def _should_collect_from_parameters(t):
+ return isinstance(t, typing._GenericAlias) and not t._special
+
+
+def _collect_type_vars(types, typevar_types=None):
+ """Collect all type variable contained in types in order of
+ first appearance (lexicographic order). For example::
+
+ _collect_type_vars((T, List[S, T])) == (T, S)
+ """
+ if typevar_types is None:
+ typevar_types = typing.TypeVar
+ tvars = []
+ for t in types:
+ if (
+ isinstance(t, typevar_types) and
+ t not in tvars and
+ not _is_unpack(t)
+ ):
+ tvars.append(t)
+ if _should_collect_from_parameters(t):
+ tvars.extend([t for t in t.__parameters__ if t not in tvars])
+ return tuple(tvars)
+
+
+NoReturn = typing.NoReturn
+
+# Some unconstrained type variables. These are used by the container types.
+# (These are not for export.)
+T = typing.TypeVar('T') # Any type.
+KT = typing.TypeVar('KT') # Key type.
+VT = typing.TypeVar('VT') # Value type.
+T_co = typing.TypeVar('T_co', covariant=True) # Any type covariant containers.
+T_contra = typing.TypeVar('T_contra', contravariant=True) # Ditto contravariant.
+
+ClassVar = typing.ClassVar
+
+# On older versions of typing there is an internal class named "Final".
+# 3.8+
+if hasattr(typing, 'Final') and sys.version_info[:2] >= (3, 7):
+ Final = typing.Final
+# 3.7
+else:
+ class _FinalForm(typing._SpecialForm, _root=True):
+
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ def __getitem__(self, parameters):
+ item = typing._type_check(parameters,
+ f'{self._name} accepts only a single type.')
+ return typing._GenericAlias(self, (item,))
+
+ Final = _FinalForm('Final',
+ doc="""A special typing construct to indicate that a name
+ cannot be re-assigned or overridden in a subclass.
+ For example:
+
+ MAX_SIZE: Final = 9000
+ MAX_SIZE += 1 # Error reported by type checker
+
+ class Connection:
+ TIMEOUT: Final[int] = 10
+ class FastConnector(Connection):
+ TIMEOUT = 1 # Error reported by type checker
+
+ There is no runtime checking of these properties.""")
+
+if sys.version_info >= (3, 11):
+ final = typing.final
+else:
+ # @final exists in 3.8+, but we backport it for all versions
+ # before 3.11 to keep support for the __final__ attribute.
+ # See https://bugs.python.org/issue46342
+ def final(f):
+ """This decorator can be used to indicate to type checkers that
+ the decorated method cannot be overridden, and decorated class
+ cannot be subclassed. For example:
+
+ class Base:
+ @final
+ def done(self) -> None:
+ ...
+ class Sub(Base):
+ def done(self) -> None: # Error reported by type checker
+ ...
+ @final
+ class Leaf:
+ ...
+ class Other(Leaf): # Error reported by type checker
+ ...
+
+ There is no runtime checking of these properties. The decorator
+ sets the ``__final__`` attribute to ``True`` on the decorated object
+ to allow runtime introspection.
+ """
+ try:
+ f.__final__ = True
+ except (AttributeError, TypeError):
+ # Skip the attribute silently if it is not writable.
+ # AttributeError happens if the object has __slots__ or a
+ # read-only property, TypeError if it's a builtin class.
+ pass
+ return f
+
+
+def IntVar(name):
+ return typing.TypeVar(name)
+
+
+# 3.8+:
+if hasattr(typing, 'Literal'):
+ Literal = typing.Literal
+# 3.7:
+else:
+ class _LiteralForm(typing._SpecialForm, _root=True):
+
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ def __getitem__(self, parameters):
+ return typing._GenericAlias(self, parameters)
+
+ Literal = _LiteralForm('Literal',
+ doc="""A type that can be used to indicate to type checkers
+ that the corresponding value has a value literally equivalent
+ to the provided parameter. For example:
+
+ var: Literal[4] = 4
+
+ The type checker understands that 'var' is literally equal to
+ the value 4 and no other value.
+
+ Literal[...] cannot be subclassed. There is no runtime
+ checking verifying that the parameter is actually a value
+ instead of a type.""")
+
+
+_overload_dummy = typing._overload_dummy # noqa
+
+
+if hasattr(typing, "get_overloads"): # 3.11+
+ overload = typing.overload
+ get_overloads = typing.get_overloads
+ clear_overloads = typing.clear_overloads
+else:
+ # {module: {qualname: {firstlineno: func}}}
+ _overload_registry = collections.defaultdict(
+ functools.partial(collections.defaultdict, dict)
+ )
+
+ def overload(func):
+ """Decorator for overloaded functions/methods.
+
+ In a stub file, place two or more stub definitions for the same
+ function in a row, each decorated with @overload. For example:
+
+ @overload
+ def utf8(value: None) -> None: ...
+ @overload
+ def utf8(value: bytes) -> bytes: ...
+ @overload
+ def utf8(value: str) -> bytes: ...
+
+ In a non-stub file (i.e. a regular .py file), do the same but
+ follow it with an implementation. The implementation should *not*
+ be decorated with @overload. For example:
+
+ @overload
+ def utf8(value: None) -> None: ...
+ @overload
+ def utf8(value: bytes) -> bytes: ...
+ @overload
+ def utf8(value: str) -> bytes: ...
+ def utf8(value):
+ # implementation goes here
+
+ The overloads for a function can be retrieved at runtime using the
+ get_overloads() function.
+ """
+ # classmethod and staticmethod
+ f = getattr(func, "__func__", func)
+ try:
+ _overload_registry[f.__module__][f.__qualname__][
+ f.__code__.co_firstlineno
+ ] = func
+ except AttributeError:
+ # Not a normal function; ignore.
+ pass
+ return _overload_dummy
+
+ def get_overloads(func):
+ """Return all defined overloads for *func* as a sequence."""
+ # classmethod and staticmethod
+ f = getattr(func, "__func__", func)
+ if f.__module__ not in _overload_registry:
+ return []
+ mod_dict = _overload_registry[f.__module__]
+ if f.__qualname__ not in mod_dict:
+ return []
+ return list(mod_dict[f.__qualname__].values())
+
+ def clear_overloads():
+ """Clear all overloads in the registry."""
+ _overload_registry.clear()
+
+
+# This is not a real generic class. Don't use outside annotations.
+Type = typing.Type
+
+# Various ABCs mimicking those in collections.abc.
+# A few are simply re-exported for completeness.
+
+
+Awaitable = typing.Awaitable
+Coroutine = typing.Coroutine
+AsyncIterable = typing.AsyncIterable
+AsyncIterator = typing.AsyncIterator
+Deque = typing.Deque
+ContextManager = typing.ContextManager
+AsyncContextManager = typing.AsyncContextManager
+DefaultDict = typing.DefaultDict
+
+# 3.7.2+
+if hasattr(typing, 'OrderedDict'):
+ OrderedDict = typing.OrderedDict
+# 3.7.0-3.7.2
+else:
+ OrderedDict = typing._alias(collections.OrderedDict, (KT, VT))
+
+Counter = typing.Counter
+ChainMap = typing.ChainMap
+AsyncGenerator = typing.AsyncGenerator
+NewType = typing.NewType
+Text = typing.Text
+TYPE_CHECKING = typing.TYPE_CHECKING
+
+
+_PROTO_WHITELIST = ['Callable', 'Awaitable',
+ 'Iterable', 'Iterator', 'AsyncIterable', 'AsyncIterator',
+ 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible',
+ 'ContextManager', 'AsyncContextManager']
+
+
+def _get_protocol_attrs(cls):
+ attrs = set()
+ for base in cls.__mro__[:-1]: # without object
+ if base.__name__ in ('Protocol', 'Generic'):
+ continue
+ annotations = getattr(base, '__annotations__', {})
+ for attr in list(base.__dict__.keys()) + list(annotations.keys()):
+ if (not attr.startswith('_abc_') and attr not in (
+ '__abstractmethods__', '__annotations__', '__weakref__',
+ '_is_protocol', '_is_runtime_protocol', '__dict__',
+ '__args__', '__slots__',
+ '__next_in_mro__', '__parameters__', '__origin__',
+ '__orig_bases__', '__extra__', '__tree_hash__',
+ '__doc__', '__subclasshook__', '__init__', '__new__',
+ '__module__', '_MutableMapping__marker', '_gorg')):
+ attrs.add(attr)
+ return attrs
+
+
+def _is_callable_members_only(cls):
+ return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls))
+
+
+def _maybe_adjust_parameters(cls):
+ """Helper function used in Protocol.__init_subclass__ and _TypedDictMeta.__new__.
+
+ The contents of this function are very similar
+ to logic found in typing.Generic.__init_subclass__
+ on the CPython main branch.
+ """
+ tvars = []
+ if '__orig_bases__' in cls.__dict__:
+ tvars = typing._collect_type_vars(cls.__orig_bases__)
+ # Look for Generic[T1, ..., Tn] or Protocol[T1, ..., Tn].
+ # If found, tvars must be a subset of it.
+ # If not found, tvars is it.
+ # Also check for and reject plain Generic,
+ # and reject multiple Generic[...] and/or Protocol[...].
+ gvars = None
+ for base in cls.__orig_bases__:
+ if (isinstance(base, typing._GenericAlias) and
+ base.__origin__ in (typing.Generic, Protocol)):
+ # for error messages
+ the_base = base.__origin__.__name__
+ if gvars is not None:
+ raise TypeError(
+ "Cannot inherit from Generic[...]"
+ " and/or Protocol[...] multiple types.")
+ gvars = base.__parameters__
+ if gvars is None:
+ gvars = tvars
+ else:
+ tvarset = set(tvars)
+ gvarset = set(gvars)
+ if not tvarset <= gvarset:
+ s_vars = ', '.join(str(t) for t in tvars if t not in gvarset)
+ s_args = ', '.join(str(g) for g in gvars)
+ raise TypeError(f"Some type variables ({s_vars}) are"
+ f" not listed in {the_base}[{s_args}]")
+ tvars = gvars
+ cls.__parameters__ = tuple(tvars)
+
+
+# 3.8+
+if hasattr(typing, 'Protocol'):
+ Protocol = typing.Protocol
+# 3.7
+else:
+
+ def _no_init(self, *args, **kwargs):
+ if type(self)._is_protocol:
+ raise TypeError('Protocols cannot be instantiated')
+
+ class _ProtocolMeta(abc.ABCMeta):
+ # This metaclass is a bit unfortunate and exists only because of the lack
+ # of __instancehook__.
+ def __instancecheck__(cls, instance):
+ # We need this method for situations where attributes are
+ # assigned in __init__.
+ if ((not getattr(cls, '_is_protocol', False) or
+ _is_callable_members_only(cls)) and
+ issubclass(instance.__class__, cls)):
+ return True
+ if cls._is_protocol:
+ if all(hasattr(instance, attr) and
+ (not callable(getattr(cls, attr, None)) or
+ getattr(instance, attr) is not None)
+ for attr in _get_protocol_attrs(cls)):
+ return True
+ return super().__instancecheck__(instance)
+
+ class Protocol(metaclass=_ProtocolMeta):
+ # There is quite a lot of overlapping code with typing.Generic.
+ # Unfortunately it is hard to avoid this while these live in two different
+ # modules. The duplicated code will be removed when Protocol is moved to typing.
+ """Base class for protocol classes. Protocol classes are defined as::
+
+ class Proto(Protocol):
+ def meth(self) -> int:
+ ...
+
+ Such classes are primarily used with static type checkers that recognize
+ structural subtyping (static duck-typing), for example::
+
+ class C:
+ def meth(self) -> int:
+ return 0
+
+ def func(x: Proto) -> int:
+ return x.meth()
+
+ func(C()) # Passes static type check
+
+ See PEP 544 for details. Protocol classes decorated with
+ @typing_extensions.runtime act as simple-minded runtime protocol that checks
+ only the presence of given attributes, ignoring their type signatures.
+
+ Protocol classes can be generic, they are defined as::
+
+ class GenProto(Protocol[T]):
+ def meth(self) -> T:
+ ...
+ """
+ __slots__ = ()
+ _is_protocol = True
+
+ def __new__(cls, *args, **kwds):
+ if cls is Protocol:
+ raise TypeError("Type Protocol cannot be instantiated; "
+ "it can only be used as a base class")
+ return super().__new__(cls)
+
+ @typing._tp_cache
+ def __class_getitem__(cls, params):
+ if not isinstance(params, tuple):
+ params = (params,)
+ if not params and cls is not typing.Tuple:
+ raise TypeError(
+ f"Parameter list to {cls.__qualname__}[...] cannot be empty")
+ msg = "Parameters to generic types must be types."
+ params = tuple(typing._type_check(p, msg) for p in params) # noqa
+ if cls is Protocol:
+ # Generic can only be subscripted with unique type variables.
+ if not all(isinstance(p, typing.TypeVar) for p in params):
+ i = 0
+ while isinstance(params[i], typing.TypeVar):
+ i += 1
+ raise TypeError(
+ "Parameters to Protocol[...] must all be type variables."
+ f" Parameter {i + 1} is {params[i]}")
+ if len(set(params)) != len(params):
+ raise TypeError(
+ "Parameters to Protocol[...] must all be unique")
+ else:
+ # Subscripting a regular Generic subclass.
+ _check_generic(cls, params, len(cls.__parameters__))
+ return typing._GenericAlias(cls, params)
+
+ def __init_subclass__(cls, *args, **kwargs):
+ if '__orig_bases__' in cls.__dict__:
+ error = typing.Generic in cls.__orig_bases__
+ else:
+ error = typing.Generic in cls.__bases__
+ if error:
+ raise TypeError("Cannot inherit from plain Generic")
+ _maybe_adjust_parameters(cls)
+
+ # Determine if this is a protocol or a concrete subclass.
+ if not cls.__dict__.get('_is_protocol', None):
+ cls._is_protocol = any(b is Protocol for b in cls.__bases__)
+
+ # Set (or override) the protocol subclass hook.
+ def _proto_hook(other):
+ if not cls.__dict__.get('_is_protocol', None):
+ return NotImplemented
+ if not getattr(cls, '_is_runtime_protocol', False):
+ if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']:
+ return NotImplemented
+ raise TypeError("Instance and class checks can only be used with"
+ " @runtime protocols")
+ if not _is_callable_members_only(cls):
+ if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']:
+ return NotImplemented
+ raise TypeError("Protocols with non-method members"
+ " don't support issubclass()")
+ if not isinstance(other, type):
+ # Same error as for issubclass(1, int)
+ raise TypeError('issubclass() arg 1 must be a class')
+ for attr in _get_protocol_attrs(cls):
+ for base in other.__mro__:
+ if attr in base.__dict__:
+ if base.__dict__[attr] is None:
+ return NotImplemented
+ break
+ annotations = getattr(base, '__annotations__', {})
+ if (isinstance(annotations, typing.Mapping) and
+ attr in annotations and
+ isinstance(other, _ProtocolMeta) and
+ other._is_protocol):
+ break
+ else:
+ return NotImplemented
+ return True
+ if '__subclasshook__' not in cls.__dict__:
+ cls.__subclasshook__ = _proto_hook
+
+ # We have nothing more to do for non-protocols.
+ if not cls._is_protocol:
+ return
+
+ # Check consistency of bases.
+ for base in cls.__bases__:
+ if not (base in (object, typing.Generic) or
+ base.__module__ == 'collections.abc' and
+ base.__name__ in _PROTO_WHITELIST or
+ isinstance(base, _ProtocolMeta) and base._is_protocol):
+ raise TypeError('Protocols can only inherit from other'
+ f' protocols, got {repr(base)}')
+ cls.__init__ = _no_init
+
+
+# 3.8+
+if hasattr(typing, 'runtime_checkable'):
+ runtime_checkable = typing.runtime_checkable
+# 3.7
+else:
+ def runtime_checkable(cls):
+ """Mark a protocol class as a runtime protocol, so that it
+ can be used with isinstance() and issubclass(). Raise TypeError
+ if applied to a non-protocol class.
+
+ This allows a simple-minded structural check very similar to the
+ one-offs in collections.abc such as Hashable.
+ """
+ if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol:
+ raise TypeError('@runtime_checkable can be only applied to protocol classes,'
+ f' got {cls!r}')
+ cls._is_runtime_protocol = True
+ return cls
+
+
+# Exists for backwards compatibility.
+runtime = runtime_checkable
+
+
+# 3.8+
+if hasattr(typing, 'SupportsIndex'):
+ SupportsIndex = typing.SupportsIndex
+# 3.7
+else:
+ @runtime_checkable
+ class SupportsIndex(Protocol):
+ __slots__ = ()
+
+ @abc.abstractmethod
+ def __index__(self) -> int:
+ pass
+
+
+if hasattr(typing, "Required"):
+ # The standard library TypedDict in Python 3.8 does not store runtime information
+ # about which (if any) keys are optional. See https://bugs.python.org/issue38834
+ # The standard library TypedDict in Python 3.9.0/1 does not honour the "total"
+ # keyword with old-style TypedDict(). See https://bugs.python.org/issue42059
+ # The standard library TypedDict below Python 3.11 does not store runtime
+ # information about optional and required keys when using Required or NotRequired.
+ # Generic TypedDicts are also impossible using typing.TypedDict on Python <3.11.
+ TypedDict = typing.TypedDict
+ _TypedDictMeta = typing._TypedDictMeta
+ is_typeddict = typing.is_typeddict
+else:
+ def _check_fails(cls, other):
+ try:
+ if sys._getframe(1).f_globals['__name__'] not in ['abc',
+ 'functools',
+ 'typing']:
+ # Typed dicts are only for static structural subtyping.
+ raise TypeError('TypedDict does not support instance and class checks')
+ except (AttributeError, ValueError):
+ pass
+ return False
+
+ def _dict_new(*args, **kwargs):
+ if not args:
+ raise TypeError('TypedDict.__new__(): not enough arguments')
+ _, args = args[0], args[1:] # allow the "cls" keyword be passed
+ return dict(*args, **kwargs)
+
+ _dict_new.__text_signature__ = '($cls, _typename, _fields=None, /, **kwargs)'
+
+ def _typeddict_new(*args, total=True, **kwargs):
+ if not args:
+ raise TypeError('TypedDict.__new__(): not enough arguments')
+ _, args = args[0], args[1:] # allow the "cls" keyword be passed
+ if args:
+ typename, args = args[0], args[1:] # allow the "_typename" keyword be passed
+ elif '_typename' in kwargs:
+ typename = kwargs.pop('_typename')
+ import warnings
+ warnings.warn("Passing '_typename' as keyword argument is deprecated",
+ DeprecationWarning, stacklevel=2)
+ else:
+ raise TypeError("TypedDict.__new__() missing 1 required positional "
+ "argument: '_typename'")
+ if args:
+ try:
+ fields, = args # allow the "_fields" keyword be passed
+ except ValueError:
+ raise TypeError('TypedDict.__new__() takes from 2 to 3 '
+ f'positional arguments but {len(args) + 2} '
+ 'were given')
+ elif '_fields' in kwargs and len(kwargs) == 1:
+ fields = kwargs.pop('_fields')
+ import warnings
+ warnings.warn("Passing '_fields' as keyword argument is deprecated",
+ DeprecationWarning, stacklevel=2)
+ else:
+ fields = None
+
+ if fields is None:
+ fields = kwargs
+ elif kwargs:
+ raise TypeError("TypedDict takes either a dict or keyword arguments,"
+ " but not both")
+
+ ns = {'__annotations__': dict(fields)}
+ try:
+ # Setting correct module is necessary to make typed dict classes pickleable.
+ ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__')
+ except (AttributeError, ValueError):
+ pass
+
+ return _TypedDictMeta(typename, (), ns, total=total)
+
+ _typeddict_new.__text_signature__ = ('($cls, _typename, _fields=None,'
+ ' /, *, total=True, **kwargs)')
+
+ class _TypedDictMeta(type):
+ def __init__(cls, name, bases, ns, total=True):
+ super().__init__(name, bases, ns)
+
+ def __new__(cls, name, bases, ns, total=True):
+ # Create new typed dict class object.
+ # This method is called directly when TypedDict is subclassed,
+ # or via _typeddict_new when TypedDict is instantiated. This way
+ # TypedDict supports all three syntaxes described in its docstring.
+ # Subclasses and instances of TypedDict return actual dictionaries
+ # via _dict_new.
+ ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new
+ # Don't insert typing.Generic into __bases__ here,
+ # or Generic.__init_subclass__ will raise TypeError
+ # in the super().__new__() call.
+ # Instead, monkey-patch __bases__ onto the class after it's been created.
+ tp_dict = super().__new__(cls, name, (dict,), ns)
+
+ if any(issubclass(base, typing.Generic) for base in bases):
+ tp_dict.__bases__ = (typing.Generic, dict)
+ _maybe_adjust_parameters(tp_dict)
+
+ annotations = {}
+ own_annotations = ns.get('__annotations__', {})
+ msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
+ own_annotations = {
+ n: typing._type_check(tp, msg) for n, tp in own_annotations.items()
+ }
+ required_keys = set()
+ optional_keys = set()
+
+ for base in bases:
+ annotations.update(base.__dict__.get('__annotations__', {}))
+ required_keys.update(base.__dict__.get('__required_keys__', ()))
+ optional_keys.update(base.__dict__.get('__optional_keys__', ()))
+
+ annotations.update(own_annotations)
+ for annotation_key, annotation_type in own_annotations.items():
+ annotation_origin = get_origin(annotation_type)
+ if annotation_origin is Annotated:
+ annotation_args = get_args(annotation_type)
+ if annotation_args:
+ annotation_type = annotation_args[0]
+ annotation_origin = get_origin(annotation_type)
+
+ if annotation_origin is Required:
+ required_keys.add(annotation_key)
+ elif annotation_origin is NotRequired:
+ optional_keys.add(annotation_key)
+ elif total:
+ required_keys.add(annotation_key)
+ else:
+ optional_keys.add(annotation_key)
+
+ tp_dict.__annotations__ = annotations
+ tp_dict.__required_keys__ = frozenset(required_keys)
+ tp_dict.__optional_keys__ = frozenset(optional_keys)
+ if not hasattr(tp_dict, '__total__'):
+ tp_dict.__total__ = total
+ return tp_dict
+
+ __instancecheck__ = __subclasscheck__ = _check_fails
+
+ TypedDict = _TypedDictMeta('TypedDict', (dict,), {})
+ TypedDict.__module__ = __name__
+ TypedDict.__doc__ = \
+ """A simple typed name space. At runtime it is equivalent to a plain dict.
+
+ TypedDict creates a dictionary type that expects all of its
+ instances to have a certain set of keys, with each key
+ associated with a value of a consistent type. This expectation
+ is not checked at runtime but is only enforced by type checkers.
+ Usage::
+
+ class Point2D(TypedDict):
+ x: int
+ y: int
+ label: str
+
+ a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK
+ b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check
+
+ assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first')
+
+ The type info can be accessed via the Point2D.__annotations__ dict, and
+ the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets.
+ TypedDict supports two additional equivalent forms::
+
+ Point2D = TypedDict('Point2D', x=int, y=int, label=str)
+ Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str})
+
+ The class syntax is only supported in Python 3.6+, while two other
+ syntax forms work for Python 2.7 and 3.2+
+ """
+
+ if hasattr(typing, "_TypedDictMeta"):
+ _TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta)
+ else:
+ _TYPEDDICT_TYPES = (_TypedDictMeta,)
+
+ def is_typeddict(tp):
+ """Check if an annotation is a TypedDict class
+
+ For example::
+ class Film(TypedDict):
+ title: str
+ year: int
+
+ is_typeddict(Film) # => True
+ is_typeddict(Union[list, str]) # => False
+ """
+ return isinstance(tp, tuple(_TYPEDDICT_TYPES))
+
+
+if hasattr(typing, "assert_type"):
+ assert_type = typing.assert_type
+
+else:
+ def assert_type(__val, __typ):
+ """Assert (to the type checker) that the value is of the given type.
+
+ When the type checker encounters a call to assert_type(), it
+ emits an error if the value is not of the specified type::
+
+ def greet(name: str) -> None:
+ assert_type(name, str) # ok
+ assert_type(name, int) # type checker error
+
+ At runtime this returns the first argument unchanged and otherwise
+ does nothing.
+ """
+ return __val
+
+
+if hasattr(typing, "Required"):
+ get_type_hints = typing.get_type_hints
+else:
+ import functools
+ import types
+
+ # replaces _strip_annotations()
+ def _strip_extras(t):
+ """Strips Annotated, Required and NotRequired from a given type."""
+ if isinstance(t, _AnnotatedAlias):
+ return _strip_extras(t.__origin__)
+ if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired):
+ return _strip_extras(t.__args__[0])
+ if isinstance(t, typing._GenericAlias):
+ stripped_args = tuple(_strip_extras(a) for a in t.__args__)
+ if stripped_args == t.__args__:
+ return t
+ return t.copy_with(stripped_args)
+ if hasattr(types, "GenericAlias") and isinstance(t, types.GenericAlias):
+ stripped_args = tuple(_strip_extras(a) for a in t.__args__)
+ if stripped_args == t.__args__:
+ return t
+ return types.GenericAlias(t.__origin__, stripped_args)
+ if hasattr(types, "UnionType") and isinstance(t, types.UnionType):
+ stripped_args = tuple(_strip_extras(a) for a in t.__args__)
+ if stripped_args == t.__args__:
+ return t
+ return functools.reduce(operator.or_, stripped_args)
+
+ return t
+
+ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
+ """Return type hints for an object.
+
+ This is often the same as obj.__annotations__, but it handles
+ forward references encoded as string literals, adds Optional[t] if a
+ default value equal to None is set and recursively replaces all
+ 'Annotated[T, ...]', 'Required[T]' or 'NotRequired[T]' with 'T'
+ (unless 'include_extras=True').
+
+ The argument may be a module, class, method, or function. The annotations
+ are returned as a dictionary. For classes, annotations include also
+ inherited members.
+
+ TypeError is raised if the argument is not of a type that can contain
+ annotations, and an empty dictionary is returned if no annotations are
+ present.
+
+ BEWARE -- the behavior of globalns and localns is counterintuitive
+ (unless you are familiar with how eval() and exec() work). The
+ search order is locals first, then globals.
+
+ - If no dict arguments are passed, an attempt is made to use the
+ globals from obj (or the respective module's globals for classes),
+ and these are also used as the locals. If the object does not appear
+ to have globals, an empty dictionary is used.
+
+ - If one dict argument is passed, it is used for both globals and
+ locals.
+
+ - If two dict arguments are passed, they specify globals and
+ locals, respectively.
+ """
+ if hasattr(typing, "Annotated"):
+ hint = typing.get_type_hints(
+ obj, globalns=globalns, localns=localns, include_extras=True
+ )
+ else:
+ hint = typing.get_type_hints(obj, globalns=globalns, localns=localns)
+ if include_extras:
+ return hint
+ return {k: _strip_extras(t) for k, t in hint.items()}
+
+
+# Python 3.9+ has PEP 593 (Annotated)
+if hasattr(typing, 'Annotated'):
+ Annotated = typing.Annotated
+ # Not exported and not a public API, but needed for get_origin() and get_args()
+ # to work.
+ _AnnotatedAlias = typing._AnnotatedAlias
+# 3.7-3.8
+else:
+ class _AnnotatedAlias(typing._GenericAlias, _root=True):
+ """Runtime representation of an annotated type.
+
+ At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't'
+ with extra annotations. The alias behaves like a normal typing alias,
+ instantiating is the same as instantiating the underlying type, binding
+ it to types is also the same.
+ """
+ def __init__(self, origin, metadata):
+ if isinstance(origin, _AnnotatedAlias):
+ metadata = origin.__metadata__ + metadata
+ origin = origin.__origin__
+ super().__init__(origin, origin)
+ self.__metadata__ = metadata
+
+ def copy_with(self, params):
+ assert len(params) == 1
+ new_type = params[0]
+ return _AnnotatedAlias(new_type, self.__metadata__)
+
+ def __repr__(self):
+ return (f"typing_extensions.Annotated[{typing._type_repr(self.__origin__)}, "
+ f"{', '.join(repr(a) for a in self.__metadata__)}]")
+
+ def __reduce__(self):
+ return operator.getitem, (
+ Annotated, (self.__origin__,) + self.__metadata__
+ )
+
+ def __eq__(self, other):
+ if not isinstance(other, _AnnotatedAlias):
+ return NotImplemented
+ if self.__origin__ != other.__origin__:
+ return False
+ return self.__metadata__ == other.__metadata__
+
+ def __hash__(self):
+ return hash((self.__origin__, self.__metadata__))
+
+ class Annotated:
+ """Add context specific metadata to a type.
+
+ Example: Annotated[int, runtime_check.Unsigned] indicates to the
+ hypothetical runtime_check module that this type is an unsigned int.
+ Every other consumer of this type can ignore this metadata and treat
+ this type as int.
+
+ The first argument to Annotated must be a valid type (and will be in
+ the __origin__ field), the remaining arguments are kept as a tuple in
+ the __extra__ field.
+
+ Details:
+
+ - It's an error to call `Annotated` with less than two arguments.
+ - Nested Annotated are flattened::
+
+ Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3]
+
+ - Instantiating an annotated type is equivalent to instantiating the
+ underlying type::
+
+ Annotated[C, Ann1](5) == C(5)
+
+ - Annotated can be used as a generic type alias::
+
+ Optimized = Annotated[T, runtime.Optimize()]
+ Optimized[int] == Annotated[int, runtime.Optimize()]
+
+ OptimizedList = Annotated[List[T], runtime.Optimize()]
+ OptimizedList[int] == Annotated[List[int], runtime.Optimize()]
+ """
+
+ __slots__ = ()
+
+ def __new__(cls, *args, **kwargs):
+ raise TypeError("Type Annotated cannot be instantiated.")
+
+ @typing._tp_cache
+ def __class_getitem__(cls, params):
+ if not isinstance(params, tuple) or len(params) < 2:
+ raise TypeError("Annotated[...] should be used "
+ "with at least two arguments (a type and an "
+ "annotation).")
+ allowed_special_forms = (ClassVar, Final)
+ if get_origin(params[0]) in allowed_special_forms:
+ origin = params[0]
+ else:
+ msg = "Annotated[t, ...]: t must be a type."
+ origin = typing._type_check(params[0], msg)
+ metadata = tuple(params[1:])
+ return _AnnotatedAlias(origin, metadata)
+
+ def __init_subclass__(cls, *args, **kwargs):
+ raise TypeError(
+ f"Cannot subclass {cls.__module__}.Annotated"
+ )
+
+# Python 3.8 has get_origin() and get_args() but those implementations aren't
+# Annotated-aware, so we can't use those. Python 3.9's versions don't support
+# ParamSpecArgs and ParamSpecKwargs, so only Python 3.10's versions will do.
+if sys.version_info[:2] >= (3, 10):
+ get_origin = typing.get_origin
+ get_args = typing.get_args
+# 3.7-3.9
+else:
+ try:
+ # 3.9+
+ from typing import _BaseGenericAlias
+ except ImportError:
+ _BaseGenericAlias = typing._GenericAlias
+ try:
+ # 3.9+
+ from typing import GenericAlias as _typing_GenericAlias
+ except ImportError:
+ _typing_GenericAlias = typing._GenericAlias
+
+ def get_origin(tp):
+ """Get the unsubscripted version of a type.
+
+ This supports generic types, Callable, Tuple, Union, Literal, Final, ClassVar
+ and Annotated. Return None for unsupported types. Examples::
+
+ get_origin(Literal[42]) is Literal
+ get_origin(int) is None
+ get_origin(ClassVar[int]) is ClassVar
+ get_origin(Generic) is Generic
+ get_origin(Generic[T]) is Generic
+ get_origin(Union[T, int]) is Union
+ get_origin(List[Tuple[T, T]][int]) == list
+ get_origin(P.args) is P
+ """
+ if isinstance(tp, _AnnotatedAlias):
+ return Annotated
+ if isinstance(tp, (typing._GenericAlias, _typing_GenericAlias, _BaseGenericAlias,
+ ParamSpecArgs, ParamSpecKwargs)):
+ return tp.__origin__
+ if tp is typing.Generic:
+ return typing.Generic
+ return None
+
+ def get_args(tp):
+ """Get type arguments with all substitutions performed.
+
+ For unions, basic simplifications used by Union constructor are performed.
+ Examples::
+ get_args(Dict[str, int]) == (str, int)
+ get_args(int) == ()
+ get_args(Union[int, Union[T, int], str][int]) == (int, str)
+ get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
+ get_args(Callable[[], T][int]) == ([], int)
+ """
+ if isinstance(tp, _AnnotatedAlias):
+ return (tp.__origin__,) + tp.__metadata__
+ if isinstance(tp, (typing._GenericAlias, _typing_GenericAlias)):
+ if getattr(tp, "_special", False):
+ return ()
+ res = tp.__args__
+ if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis:
+ res = (list(res[:-1]), res[-1])
+ return res
+ return ()
+
+
+# 3.10+
+if hasattr(typing, 'TypeAlias'):
+ TypeAlias = typing.TypeAlias
+# 3.9
+elif sys.version_info[:2] >= (3, 9):
+ class _TypeAliasForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ @_TypeAliasForm
+ def TypeAlias(self, parameters):
+ """Special marker indicating that an assignment should
+ be recognized as a proper type alias definition by type
+ checkers.
+
+ For example::
+
+ Predicate: TypeAlias = Callable[..., bool]
+
+ It's invalid when used anywhere except as in the example above.
+ """
+ raise TypeError(f"{self} is not subscriptable")
+# 3.7-3.8
+else:
+ class _TypeAliasForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ TypeAlias = _TypeAliasForm('TypeAlias',
+ doc="""Special marker indicating that an assignment should
+ be recognized as a proper type alias definition by type
+ checkers.
+
+ For example::
+
+ Predicate: TypeAlias = Callable[..., bool]
+
+ It's invalid when used anywhere except as in the example
+ above.""")
+
+
+# Python 3.10+ has PEP 612
+if hasattr(typing, 'ParamSpecArgs'):
+ ParamSpecArgs = typing.ParamSpecArgs
+ ParamSpecKwargs = typing.ParamSpecKwargs
+# 3.7-3.9
+else:
+ class _Immutable:
+ """Mixin to indicate that object should not be copied."""
+ __slots__ = ()
+
+ def __copy__(self):
+ return self
+
+ def __deepcopy__(self, memo):
+ return self
+
+ class ParamSpecArgs(_Immutable):
+ """The args for a ParamSpec object.
+
+ Given a ParamSpec object P, P.args is an instance of ParamSpecArgs.
+
+ ParamSpecArgs objects have a reference back to their ParamSpec:
+
+ P.args.__origin__ is P
+
+ This type is meant for runtime introspection and has no special meaning to
+ static type checkers.
+ """
+ def __init__(self, origin):
+ self.__origin__ = origin
+
+ def __repr__(self):
+ return f"{self.__origin__.__name__}.args"
+
+ def __eq__(self, other):
+ if not isinstance(other, ParamSpecArgs):
+ return NotImplemented
+ return self.__origin__ == other.__origin__
+
+ class ParamSpecKwargs(_Immutable):
+ """The kwargs for a ParamSpec object.
+
+ Given a ParamSpec object P, P.kwargs is an instance of ParamSpecKwargs.
+
+ ParamSpecKwargs objects have a reference back to their ParamSpec:
+
+ P.kwargs.__origin__ is P
+
+ This type is meant for runtime introspection and has no special meaning to
+ static type checkers.
+ """
+ def __init__(self, origin):
+ self.__origin__ = origin
+
+ def __repr__(self):
+ return f"{self.__origin__.__name__}.kwargs"
+
+ def __eq__(self, other):
+ if not isinstance(other, ParamSpecKwargs):
+ return NotImplemented
+ return self.__origin__ == other.__origin__
+
+# 3.10+
+if hasattr(typing, 'ParamSpec'):
+ ParamSpec = typing.ParamSpec
+# 3.7-3.9
+else:
+
+ # Inherits from list as a workaround for Callable checks in Python < 3.9.2.
+ class ParamSpec(list):
+ """Parameter specification variable.
+
+ Usage::
+
+ P = ParamSpec('P')
+
+ Parameter specification variables exist primarily for the benefit of static
+ type checkers. They are used to forward the parameter types of one
+ callable to another callable, a pattern commonly found in higher order
+ functions and decorators. They are only valid when used in ``Concatenate``,
+ or s the first argument to ``Callable``. In Python 3.10 and higher,
+ they are also supported in user-defined Generics at runtime.
+ See class Generic for more information on generic types. An
+ example for annotating a decorator::
+
+ T = TypeVar('T')
+ P = ParamSpec('P')
+
+ def add_logging(f: Callable[P, T]) -> Callable[P, T]:
+ '''A type-safe decorator to add logging to a function.'''
+ def inner(*args: P.args, **kwargs: P.kwargs) -> T:
+ logging.info(f'{f.__name__} was called')
+ return f(*args, **kwargs)
+ return inner
+
+ @add_logging
+ def add_two(x: float, y: float) -> float:
+ '''Add two numbers together.'''
+ return x + y
+
+ Parameter specification variables defined with covariant=True or
+ contravariant=True can be used to declare covariant or contravariant
+ generic types. These keyword arguments are valid, but their actual semantics
+ are yet to be decided. See PEP 612 for details.
+
+ Parameter specification variables can be introspected. e.g.:
+
+ P.__name__ == 'T'
+ P.__bound__ == None
+ P.__covariant__ == False
+ P.__contravariant__ == False
+
+ Note that only parameter specification variables defined in global scope can
+ be pickled.
+ """
+
+ # Trick Generic __parameters__.
+ __class__ = typing.TypeVar
+
+ @property
+ def args(self):
+ return ParamSpecArgs(self)
+
+ @property
+ def kwargs(self):
+ return ParamSpecKwargs(self)
+
+ def __init__(self, name, *, bound=None, covariant=False, contravariant=False):
+ super().__init__([self])
+ self.__name__ = name
+ self.__covariant__ = bool(covariant)
+ self.__contravariant__ = bool(contravariant)
+ if bound:
+ self.__bound__ = typing._type_check(bound, 'Bound must be a type.')
+ else:
+ self.__bound__ = None
+
+ # for pickling:
+ try:
+ def_mod = sys._getframe(1).f_globals.get('__name__', '__main__')
+ except (AttributeError, ValueError):
+ def_mod = None
+ if def_mod != 'typing_extensions':
+ self.__module__ = def_mod
+
+ def __repr__(self):
+ if self.__covariant__:
+ prefix = '+'
+ elif self.__contravariant__:
+ prefix = '-'
+ else:
+ prefix = '~'
+ return prefix + self.__name__
+
+ def __hash__(self):
+ return object.__hash__(self)
+
+ def __eq__(self, other):
+ return self is other
+
+ def __reduce__(self):
+ return self.__name__
+
+ # Hack to get typing._type_check to pass.
+ def __call__(self, *args, **kwargs):
+ pass
+
+
+# 3.7-3.9
+if not hasattr(typing, 'Concatenate'):
+ # Inherits from list as a workaround for Callable checks in Python < 3.9.2.
+ class _ConcatenateGenericAlias(list):
+
+ # Trick Generic into looking into this for __parameters__.
+ __class__ = typing._GenericAlias
+
+ # Flag in 3.8.
+ _special = False
+
+ def __init__(self, origin, args):
+ super().__init__(args)
+ self.__origin__ = origin
+ self.__args__ = args
+
+ def __repr__(self):
+ _type_repr = typing._type_repr
+ return (f'{_type_repr(self.__origin__)}'
+ f'[{", ".join(_type_repr(arg) for arg in self.__args__)}]')
+
+ def __hash__(self):
+ return hash((self.__origin__, self.__args__))
+
+ # Hack to get typing._type_check to pass in Generic.
+ def __call__(self, *args, **kwargs):
+ pass
+
+ @property
+ def __parameters__(self):
+ return tuple(
+ tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec))
+ )
+
+
+# 3.7-3.9
+@typing._tp_cache
+def _concatenate_getitem(self, parameters):
+ if parameters == ():
+ raise TypeError("Cannot take a Concatenate of no types.")
+ if not isinstance(parameters, tuple):
+ parameters = (parameters,)
+ if not isinstance(parameters[-1], ParamSpec):
+ raise TypeError("The last parameter to Concatenate should be a "
+ "ParamSpec variable.")
+ msg = "Concatenate[arg, ...]: each arg must be a type."
+ parameters = tuple(typing._type_check(p, msg) for p in parameters)
+ return _ConcatenateGenericAlias(self, parameters)
+
+
+# 3.10+
+if hasattr(typing, 'Concatenate'):
+ Concatenate = typing.Concatenate
+ _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa
+# 3.9
+elif sys.version_info[:2] >= (3, 9):
+ @_TypeAliasForm
+ def Concatenate(self, parameters):
+ """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a
+ higher order function which adds, removes or transforms parameters of a
+ callable.
+
+ For example::
+
+ Callable[Concatenate[int, P], int]
+
+ See PEP 612 for detailed information.
+ """
+ return _concatenate_getitem(self, parameters)
+# 3.7-8
+else:
+ class _ConcatenateForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ def __getitem__(self, parameters):
+ return _concatenate_getitem(self, parameters)
+
+ Concatenate = _ConcatenateForm(
+ 'Concatenate',
+ doc="""Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a
+ higher order function which adds, removes or transforms parameters of a
+ callable.
+
+ For example::
+
+ Callable[Concatenate[int, P], int]
+
+ See PEP 612 for detailed information.
+ """)
+
+# 3.10+
+if hasattr(typing, 'TypeGuard'):
+ TypeGuard = typing.TypeGuard
+# 3.9
+elif sys.version_info[:2] >= (3, 9):
+ class _TypeGuardForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ @_TypeGuardForm
+ def TypeGuard(self, parameters):
+ """Special typing form used to annotate the return type of a user-defined
+ type guard function. ``TypeGuard`` only accepts a single type argument.
+ At runtime, functions marked this way should return a boolean.
+
+ ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
+ type checkers to determine a more precise type of an expression within a
+ program's code flow. Usually type narrowing is done by analyzing
+ conditional code flow and applying the narrowing to a block of code. The
+ conditional expression here is sometimes referred to as a "type guard".
+
+ Sometimes it would be convenient to use a user-defined boolean function
+ as a type guard. Such a function should use ``TypeGuard[...]`` as its
+ return type to alert static type checkers to this intention.
+
+ Using ``-> TypeGuard`` tells the static type checker that for a given
+ function:
+
+ 1. The return value is a boolean.
+ 2. If the return value is ``True``, the type of its argument
+ is the type inside ``TypeGuard``.
+
+ For example::
+
+ def is_str(val: Union[str, float]):
+ # "isinstance" type guard
+ if isinstance(val, str):
+ # Type of ``val`` is narrowed to ``str``
+ ...
+ else:
+ # Else, type of ``val`` is narrowed to ``float``.
+ ...
+
+ Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
+ form of ``TypeA`` (it can even be a wider form) and this may lead to
+ type-unsafe results. The main reason is to allow for things like
+ narrowing ``List[object]`` to ``List[str]`` even though the latter is not
+ a subtype of the former, since ``List`` is invariant. The responsibility of
+ writing type-safe type guards is left to the user.
+
+ ``TypeGuard`` also works with type variables. For more information, see
+ PEP 647 (User-Defined Type Guards).
+ """
+ item = typing._type_check(parameters, f'{self} accepts only a single type.')
+ return typing._GenericAlias(self, (item,))
+# 3.7-3.8
+else:
+ class _TypeGuardForm(typing._SpecialForm, _root=True):
+
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ def __getitem__(self, parameters):
+ item = typing._type_check(parameters,
+ f'{self._name} accepts only a single type')
+ return typing._GenericAlias(self, (item,))
+
+ TypeGuard = _TypeGuardForm(
+ 'TypeGuard',
+ doc="""Special typing form used to annotate the return type of a user-defined
+ type guard function. ``TypeGuard`` only accepts a single type argument.
+ At runtime, functions marked this way should return a boolean.
+
+ ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
+ type checkers to determine a more precise type of an expression within a
+ program's code flow. Usually type narrowing is done by analyzing
+ conditional code flow and applying the narrowing to a block of code. The
+ conditional expression here is sometimes referred to as a "type guard".
+
+ Sometimes it would be convenient to use a user-defined boolean function
+ as a type guard. Such a function should use ``TypeGuard[...]`` as its
+ return type to alert static type checkers to this intention.
+
+ Using ``-> TypeGuard`` tells the static type checker that for a given
+ function:
+
+ 1. The return value is a boolean.
+ 2. If the return value is ``True``, the type of its argument
+ is the type inside ``TypeGuard``.
+
+ For example::
+
+ def is_str(val: Union[str, float]):
+ # "isinstance" type guard
+ if isinstance(val, str):
+ # Type of ``val`` is narrowed to ``str``
+ ...
+ else:
+ # Else, type of ``val`` is narrowed to ``float``.
+ ...
+
+ Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
+ form of ``TypeA`` (it can even be a wider form) and this may lead to
+ type-unsafe results. The main reason is to allow for things like
+ narrowing ``List[object]`` to ``List[str]`` even though the latter is not
+ a subtype of the former, since ``List`` is invariant. The responsibility of
+ writing type-safe type guards is left to the user.
+
+ ``TypeGuard`` also works with type variables. For more information, see
+ PEP 647 (User-Defined Type Guards).
+ """)
+
+
+# Vendored from cpython typing._SpecialFrom
+class _SpecialForm(typing._Final, _root=True):
+ __slots__ = ('_name', '__doc__', '_getitem')
+
+ def __init__(self, getitem):
+ self._getitem = getitem
+ self._name = getitem.__name__
+ self.__doc__ = getitem.__doc__
+
+ def __getattr__(self, item):
+ if item in {'__name__', '__qualname__'}:
+ return self._name
+
+ raise AttributeError(item)
+
+ def __mro_entries__(self, bases):
+ raise TypeError(f"Cannot subclass {self!r}")
+
+ def __repr__(self):
+ return f'typing_extensions.{self._name}'
+
+ def __reduce__(self):
+ return self._name
+
+ def __call__(self, *args, **kwds):
+ raise TypeError(f"Cannot instantiate {self!r}")
+
+ def __or__(self, other):
+ return typing.Union[self, other]
+
+ def __ror__(self, other):
+ return typing.Union[other, self]
+
+ def __instancecheck__(self, obj):
+ raise TypeError(f"{self} cannot be used with isinstance()")
+
+ def __subclasscheck__(self, cls):
+ raise TypeError(f"{self} cannot be used with issubclass()")
+
+ @typing._tp_cache
+ def __getitem__(self, parameters):
+ return self._getitem(self, parameters)
+
+
+if hasattr(typing, "LiteralString"):
+ LiteralString = typing.LiteralString
+else:
+ @_SpecialForm
+ def LiteralString(self, params):
+ """Represents an arbitrary literal string.
+
+ Example::
+
+ from pip._vendor.typing_extensions import LiteralString
+
+ def query(sql: LiteralString) -> ...:
+ ...
+
+ query("SELECT * FROM table") # ok
+ query(f"SELECT * FROM {input()}") # not ok
+
+ See PEP 675 for details.
+
+ """
+ raise TypeError(f"{self} is not subscriptable")
+
+
+if hasattr(typing, "Self"):
+ Self = typing.Self
+else:
+ @_SpecialForm
+ def Self(self, params):
+ """Used to spell the type of "self" in classes.
+
+ Example::
+
+ from typing import Self
+
+ class ReturnsSelf:
+ def parse(self, data: bytes) -> Self:
+ ...
+ return self
+
+ """
+
+ raise TypeError(f"{self} is not subscriptable")
+
+
+if hasattr(typing, "Never"):
+ Never = typing.Never
+else:
+ @_SpecialForm
+ def Never(self, params):
+ """The bottom type, a type that has no members.
+
+ This can be used to define a function that should never be
+ called, or a function that never returns::
+
+ from pip._vendor.typing_extensions import Never
+
+ def never_call_me(arg: Never) -> None:
+ pass
+
+ def int_or_str(arg: int | str) -> None:
+ never_call_me(arg) # type checker error
+ match arg:
+ case int():
+ print("It's an int")
+ case str():
+ print("It's a str")
+ case _:
+ never_call_me(arg) # ok, arg is of type Never
+
+ """
+
+ raise TypeError(f"{self} is not subscriptable")
+
+
+if hasattr(typing, 'Required'):
+ Required = typing.Required
+ NotRequired = typing.NotRequired
+elif sys.version_info[:2] >= (3, 9):
+ class _ExtensionsSpecialForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ @_ExtensionsSpecialForm
+ def Required(self, parameters):
+ """A special typing construct to mark a key of a total=False TypedDict
+ as required. For example:
+
+ class Movie(TypedDict, total=False):
+ title: Required[str]
+ year: int
+
+ m = Movie(
+ title='The Matrix', # typechecker error if key is omitted
+ year=1999,
+ )
+
+ There is no runtime checking that a required key is actually provided
+ when instantiating a related TypedDict.
+ """
+ item = typing._type_check(parameters, f'{self._name} accepts only a single type.')
+ return typing._GenericAlias(self, (item,))
+
+ @_ExtensionsSpecialForm
+ def NotRequired(self, parameters):
+ """A special typing construct to mark a key of a TypedDict as
+ potentially missing. For example:
+
+ class Movie(TypedDict):
+ title: str
+ year: NotRequired[int]
+
+ m = Movie(
+ title='The Matrix', # typechecker error if key is omitted
+ year=1999,
+ )
+ """
+ item = typing._type_check(parameters, f'{self._name} accepts only a single type.')
+ return typing._GenericAlias(self, (item,))
+
+else:
+ class _RequiredForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ def __getitem__(self, parameters):
+ item = typing._type_check(parameters,
+ f'{self._name} accepts only a single type.')
+ return typing._GenericAlias(self, (item,))
+
+ Required = _RequiredForm(
+ 'Required',
+ doc="""A special typing construct to mark a key of a total=False TypedDict
+ as required. For example:
+
+ class Movie(TypedDict, total=False):
+ title: Required[str]
+ year: int
+
+ m = Movie(
+ title='The Matrix', # typechecker error if key is omitted
+ year=1999,
+ )
+
+ There is no runtime checking that a required key is actually provided
+ when instantiating a related TypedDict.
+ """)
+ NotRequired = _RequiredForm(
+ 'NotRequired',
+ doc="""A special typing construct to mark a key of a TypedDict as
+ potentially missing. For example:
+
+ class Movie(TypedDict):
+ title: str
+ year: NotRequired[int]
+
+ m = Movie(
+ title='The Matrix', # typechecker error if key is omitted
+ year=1999,
+ )
+ """)
+
+
+if hasattr(typing, "Unpack"): # 3.11+
+ Unpack = typing.Unpack
+elif sys.version_info[:2] >= (3, 9):
+ class _UnpackSpecialForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ class _UnpackAlias(typing._GenericAlias, _root=True):
+ __class__ = typing.TypeVar
+
+ @_UnpackSpecialForm
+ def Unpack(self, parameters):
+ """A special typing construct to unpack a variadic type. For example:
+
+ Shape = TypeVarTuple('Shape')
+ Batch = NewType('Batch', int)
+
+ def add_batch_axis(
+ x: Array[Unpack[Shape]]
+ ) -> Array[Batch, Unpack[Shape]]: ...
+
+ """
+ item = typing._type_check(parameters, f'{self._name} accepts only a single type.')
+ return _UnpackAlias(self, (item,))
+
+ def _is_unpack(obj):
+ return isinstance(obj, _UnpackAlias)
+
+else:
+ class _UnpackAlias(typing._GenericAlias, _root=True):
+ __class__ = typing.TypeVar
+
+ class _UnpackForm(typing._SpecialForm, _root=True):
+ def __repr__(self):
+ return 'typing_extensions.' + self._name
+
+ def __getitem__(self, parameters):
+ item = typing._type_check(parameters,
+ f'{self._name} accepts only a single type.')
+ return _UnpackAlias(self, (item,))
+
+ Unpack = _UnpackForm(
+ 'Unpack',
+ doc="""A special typing construct to unpack a variadic type. For example:
+
+ Shape = TypeVarTuple('Shape')
+ Batch = NewType('Batch', int)
+
+ def add_batch_axis(
+ x: Array[Unpack[Shape]]
+ ) -> Array[Batch, Unpack[Shape]]: ...
+
+ """)
+
+ def _is_unpack(obj):
+ return isinstance(obj, _UnpackAlias)
+
+
+if hasattr(typing, "TypeVarTuple"): # 3.11+
+ TypeVarTuple = typing.TypeVarTuple
+else:
+ class TypeVarTuple:
+ """Type variable tuple.
+
+ Usage::
+
+ Ts = TypeVarTuple('Ts')
+
+ In the same way that a normal type variable is a stand-in for a single
+ type such as ``int``, a type variable *tuple* is a stand-in for a *tuple*
+ type such as ``Tuple[int, str]``.
+
+ Type variable tuples can be used in ``Generic`` declarations.
+ Consider the following example::
+
+ class Array(Generic[*Ts]): ...
+
+ The ``Ts`` type variable tuple here behaves like ``tuple[T1, T2]``,
+ where ``T1`` and ``T2`` are type variables. To use these type variables
+ as type parameters of ``Array``, we must *unpack* the type variable tuple using
+ the star operator: ``*Ts``. The signature of ``Array`` then behaves
+ as if we had simply written ``class Array(Generic[T1, T2]): ...``.
+ In contrast to ``Generic[T1, T2]``, however, ``Generic[*Shape]`` allows
+ us to parameterise the class with an *arbitrary* number of type parameters.
+
+ Type variable tuples can be used anywhere a normal ``TypeVar`` can.
+ This includes class definitions, as shown above, as well as function
+ signatures and variable annotations::
+
+ class Array(Generic[*Ts]):
+
+ def __init__(self, shape: Tuple[*Ts]):
+ self._shape: Tuple[*Ts] = shape
+
+ def get_shape(self) -> Tuple[*Ts]:
+ return self._shape
+
+ shape = (Height(480), Width(640))
+ x: Array[Height, Width] = Array(shape)
+ y = abs(x) # Inferred type is Array[Height, Width]
+ z = x + x # ... is Array[Height, Width]
+ x.get_shape() # ... is tuple[Height, Width]
+
+ """
+
+ # Trick Generic __parameters__.
+ __class__ = typing.TypeVar
+
+ def __iter__(self):
+ yield self.__unpacked__
+
+ def __init__(self, name):
+ self.__name__ = name
+
+ # for pickling:
+ try:
+ def_mod = sys._getframe(1).f_globals.get('__name__', '__main__')
+ except (AttributeError, ValueError):
+ def_mod = None
+ if def_mod != 'typing_extensions':
+ self.__module__ = def_mod
+
+ self.__unpacked__ = Unpack[self]
+
+ def __repr__(self):
+ return self.__name__
+
+ def __hash__(self):
+ return object.__hash__(self)
+
+ def __eq__(self, other):
+ return self is other
+
+ def __reduce__(self):
+ return self.__name__
+
+ def __init_subclass__(self, *args, **kwds):
+ if '_root' not in kwds:
+ raise TypeError("Cannot subclass special typing classes")
+
+
+if hasattr(typing, "reveal_type"):
+ reveal_type = typing.reveal_type
+else:
+ def reveal_type(__obj: T) -> T:
+ """Reveal the inferred type of a variable.
+
+ When a static type checker encounters a call to ``reveal_type()``,
+ it will emit the inferred type of the argument::
+
+ x: int = 1
+ reveal_type(x)
+
+ Running a static type checker (e.g., ``mypy``) on this example
+ will produce output similar to 'Revealed type is "builtins.int"'.
+
+ At runtime, the function prints the runtime type of the
+ argument and returns it unchanged.
+
+ """
+ print(f"Runtime type is {type(__obj).__name__!r}", file=sys.stderr)
+ return __obj
+
+
+if hasattr(typing, "assert_never"):
+ assert_never = typing.assert_never
+else:
+ def assert_never(__arg: Never) -> Never:
+ """Assert to the type checker that a line of code is unreachable.
+
+ Example::
+
+ def int_or_str(arg: int | str) -> None:
+ match arg:
+ case int():
+ print("It's an int")
+ case str():
+ print("It's a str")
+ case _:
+ assert_never(arg)
+
+ If a type checker finds that a call to assert_never() is
+ reachable, it will emit an error.
+
+ At runtime, this throws an exception when called.
+
+ """
+ raise AssertionError("Expected code to be unreachable")
+
+
+if hasattr(typing, 'dataclass_transform'):
+ dataclass_transform = typing.dataclass_transform
+else:
+ def dataclass_transform(
+ *,
+ eq_default: bool = True,
+ order_default: bool = False,
+ kw_only_default: bool = False,
+ field_specifiers: typing.Tuple[
+ typing.Union[typing.Type[typing.Any], typing.Callable[..., typing.Any]],
+ ...
+ ] = (),
+ **kwargs: typing.Any,
+ ) -> typing.Callable[[T], T]:
+ """Decorator that marks a function, class, or metaclass as providing
+ dataclass-like behavior.
+
+ Example:
+
+ from pip._vendor.typing_extensions import dataclass_transform
+
+ _T = TypeVar("_T")
+
+ # Used on a decorator function
+ @dataclass_transform()
+ def create_model(cls: type[_T]) -> type[_T]:
+ ...
+ return cls
+
+ @create_model
+ class CustomerModel:
+ id: int
+ name: str
+
+ # Used on a base class
+ @dataclass_transform()
+ class ModelBase: ...
+
+ class CustomerModel(ModelBase):
+ id: int
+ name: str
+
+ # Used on a metaclass
+ @dataclass_transform()
+ class ModelMeta(type): ...
+
+ class ModelBase(metaclass=ModelMeta): ...
+
+ class CustomerModel(ModelBase):
+ id: int
+ name: str
+
+ Each of the ``CustomerModel`` classes defined in this example will now
+ behave similarly to a dataclass created with the ``@dataclasses.dataclass``
+ decorator. For example, the type checker will synthesize an ``__init__``
+ method.
+
+ The arguments to this decorator can be used to customize this behavior:
+ - ``eq_default`` indicates whether the ``eq`` parameter is assumed to be
+ True or False if it is omitted by the caller.
+ - ``order_default`` indicates whether the ``order`` parameter is
+ assumed to be True or False if it is omitted by the caller.
+ - ``kw_only_default`` indicates whether the ``kw_only`` parameter is
+ assumed to be True or False if it is omitted by the caller.
+ - ``field_specifiers`` specifies a static list of supported classes
+ or functions that describe fields, similar to ``dataclasses.field()``.
+
+ At runtime, this decorator records its arguments in the
+ ``__dataclass_transform__`` attribute on the decorated object.
+
+ See PEP 681 for details.
+
+ """
+ def decorator(cls_or_fn):
+ cls_or_fn.__dataclass_transform__ = {
+ "eq_default": eq_default,
+ "order_default": order_default,
+ "kw_only_default": kw_only_default,
+ "field_specifiers": field_specifiers,
+ "kwargs": kwargs,
+ }
+ return cls_or_fn
+ return decorator
+
+
+# We have to do some monkey patching to deal with the dual nature of
+# Unpack/TypeVarTuple:
+# - We want Unpack to be a kind of TypeVar so it gets accepted in
+# Generic[Unpack[Ts]]
+# - We want it to *not* be treated as a TypeVar for the purposes of
+# counting generic parameters, so that when we subscript a generic,
+# the runtime doesn't try to substitute the Unpack with the subscripted type.
+if not hasattr(typing, "TypeVarTuple"):
+ typing._collect_type_vars = _collect_type_vars
+ typing._check_generic = _check_generic
+
+
+# Backport typing.NamedTuple as it exists in Python 3.11.
+# In 3.11, the ability to define generic `NamedTuple`s was supported.
+# This was explicitly disallowed in 3.9-3.10, and only half-worked in <=3.8.
+if sys.version_info >= (3, 11):
+ NamedTuple = typing.NamedTuple
+else:
+ def _caller():
+ try:
+ return sys._getframe(2).f_globals.get('__name__', '__main__')
+ except (AttributeError, ValueError): # For platforms without _getframe()
+ return None
+
+ def _make_nmtuple(name, types, module, defaults=()):
+ fields = [n for n, t in types]
+ annotations = {n: typing._type_check(t, f"field {n} annotation must be a type")
+ for n, t in types}
+ nm_tpl = collections.namedtuple(name, fields,
+ defaults=defaults, module=module)
+ nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = annotations
+ # The `_field_types` attribute was removed in 3.9;
+ # in earlier versions, it is the same as the `__annotations__` attribute
+ if sys.version_info < (3, 9):
+ nm_tpl._field_types = annotations
+ return nm_tpl
+
+ _prohibited_namedtuple_fields = typing._prohibited
+ _special_namedtuple_fields = frozenset({'__module__', '__name__', '__annotations__'})
+
+ class _NamedTupleMeta(type):
+ def __new__(cls, typename, bases, ns):
+ assert _NamedTuple in bases
+ for base in bases:
+ if base is not _NamedTuple and base is not typing.Generic:
+ raise TypeError(
+ 'can only inherit from a NamedTuple type and Generic')
+ bases = tuple(tuple if base is _NamedTuple else base for base in bases)
+ types = ns.get('__annotations__', {})
+ default_names = []
+ for field_name in types:
+ if field_name in ns:
+ default_names.append(field_name)
+ elif default_names:
+ raise TypeError(f"Non-default namedtuple field {field_name} "
+ f"cannot follow default field"
+ f"{'s' if len(default_names) > 1 else ''} "
+ f"{', '.join(default_names)}")
+ nm_tpl = _make_nmtuple(
+ typename, types.items(),
+ defaults=[ns[n] for n in default_names],
+ module=ns['__module__']
+ )
+ nm_tpl.__bases__ = bases
+ if typing.Generic in bases:
+ class_getitem = typing.Generic.__class_getitem__.__func__
+ nm_tpl.__class_getitem__ = classmethod(class_getitem)
+ # update from user namespace without overriding special namedtuple attributes
+ for key in ns:
+ if key in _prohibited_namedtuple_fields:
+ raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
+ elif key not in _special_namedtuple_fields and key not in nm_tpl._fields:
+ setattr(nm_tpl, key, ns[key])
+ if typing.Generic in bases:
+ nm_tpl.__init_subclass__()
+ return nm_tpl
+
+ def NamedTuple(__typename, __fields=None, **kwargs):
+ if __fields is None:
+ __fields = kwargs.items()
+ elif kwargs:
+ raise TypeError("Either list of fields or keywords"
+ " can be provided to NamedTuple, not both")
+ return _make_nmtuple(__typename, __fields, module=_caller())
+
+ NamedTuple.__doc__ = typing.NamedTuple.__doc__
+ _NamedTuple = type.__new__(_NamedTupleMeta, 'NamedTuple', (), {})
+
+ # On 3.8+, alter the signature so that it matches typing.NamedTuple.
+ # The signature of typing.NamedTuple on >=3.8 is invalid syntax in Python 3.7,
+ # so just leave the signature as it is on 3.7.
+ if sys.version_info >= (3, 8):
+ NamedTuple.__text_signature__ = '(typename, fields=None, /, **kwargs)'
+
+ def _namedtuple_mro_entries(bases):
+ assert NamedTuple in bases
+ return (_NamedTuple,)
+
+ NamedTuple.__mro_entries__ = _namedtuple_mro_entries
diff --git a/src/pip/_vendor/typing_extensions.pyi b/src/pip/_vendor/typing_extensions.pyi
new file mode 100644
index 000000000..547f7a1b0
--- /dev/null
+++ b/src/pip/_vendor/typing_extensions.pyi
@@ -0,0 +1 @@
+from typing_extensions import * \ No newline at end of file
diff --git a/src/pip/_vendor/urllib3/_version.py b/src/pip/_vendor/urllib3/_version.py
index e8ebee957..c8ac29d08 100644
--- a/src/pip/_vendor/urllib3/_version.py
+++ b/src/pip/_vendor/urllib3/_version.py
@@ -1,2 +1,2 @@
# This file is protected via CODEOWNERS
-__version__ = "1.26.6"
+__version__ = "1.26.10"
diff --git a/src/pip/_vendor/urllib3/connection.py b/src/pip/_vendor/urllib3/connection.py
index 4c996659c..10fb36c4e 100644
--- a/src/pip/_vendor/urllib3/connection.py
+++ b/src/pip/_vendor/urllib3/connection.py
@@ -51,15 +51,16 @@ from .exceptions import (
SubjectAltNameWarning,
SystemTimeWarning,
)
-from .packages.ssl_match_hostname import CertificateError, match_hostname
from .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection
from .util.ssl_ import (
assert_fingerprint,
create_urllib3_context,
+ is_ipaddress,
resolve_cert_reqs,
resolve_ssl_version,
ssl_wrap_socket,
)
+from .util.ssl_match_hostname import CertificateError, match_hostname
log = logging.getLogger(__name__)
@@ -67,7 +68,7 @@ port_by_scheme = {"http": 80, "https": 443}
# When it comes time to update this value as a part of regular maintenance
# (ie test_recent_date is failing) update it to ~6 months before the current date.
-RECENT_DATE = datetime.date(2020, 7, 1)
+RECENT_DATE = datetime.date(2022, 1, 1)
_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
@@ -107,6 +108,10 @@ class HTTPConnection(_HTTPConnection, object):
#: Whether this connection verifies the host's certificate.
is_verified = False
+ #: Whether this proxy connection (if used) verifies the proxy host's
+ #: certificate.
+ proxy_is_verified = None
+
def __init__(self, *args, **kw):
if not six.PY2:
kw.pop("strict", None)
@@ -350,17 +355,15 @@ class HTTPSConnection(HTTPConnection):
def connect(self):
# Add certificate verification
- conn = self._new_conn()
+ self.sock = conn = self._new_conn()
hostname = self.host
tls_in_tls = False
if self._is_using_tunnel():
if self.tls_in_tls_required:
- conn = self._connect_tls_proxy(hostname, conn)
+ self.sock = conn = self._connect_tls_proxy(hostname, conn)
tls_in_tls = True
- self.sock = conn
-
# Calls self._set_hostport(), so self.host is
# self._tunnel_host below.
self._tunnel()
@@ -490,14 +493,10 @@ class HTTPSConnection(HTTPConnection):
self.ca_cert_dir,
self.ca_cert_data,
)
- # By default urllib3's SSLContext disables `check_hostname` and uses
- # a custom check. For proxies we're good with relying on the default
- # verification.
- ssl_context.check_hostname = True
# If no cert was provided, use only the default options for server
# certificate validation
- return ssl_wrap_socket(
+ socket = ssl_wrap_socket(
sock=conn,
ca_certs=self.ca_certs,
ca_cert_dir=self.ca_cert_dir,
@@ -506,8 +505,37 @@ class HTTPSConnection(HTTPConnection):
ssl_context=ssl_context,
)
+ if ssl_context.verify_mode != ssl.CERT_NONE and not getattr(
+ ssl_context, "check_hostname", False
+ ):
+ # While urllib3 attempts to always turn off hostname matching from
+ # the TLS library, this cannot always be done. So we check whether
+ # the TLS Library still thinks it's matching hostnames.
+ cert = socket.getpeercert()
+ if not cert.get("subjectAltName", ()):
+ warnings.warn(
+ (
+ "Certificate for {0} has no `subjectAltName`, falling back to check for a "
+ "`commonName` for now. This feature is being removed by major browsers and "
+ "deprecated by RFC 2818. (See https://github.com/urllib3/urllib3/issues/497 "
+ "for details.)".format(hostname)
+ ),
+ SubjectAltNameWarning,
+ )
+ _match_hostname(cert, hostname)
+
+ self.proxy_is_verified = ssl_context.verify_mode == ssl.CERT_REQUIRED
+ return socket
+
def _match_hostname(cert, asserted_hostname):
+ # Our upstream implementation of ssl.match_hostname()
+ # only applies this normalization to IP addresses so it doesn't
+ # match DNS SANs so we do the same thing!
+ stripped_hostname = asserted_hostname.strip("u[]")
+ if is_ipaddress(stripped_hostname):
+ asserted_hostname = stripped_hostname
+
try:
match_hostname(cert, asserted_hostname)
except CertificateError as e:
diff --git a/src/pip/_vendor/urllib3/connectionpool.py b/src/pip/_vendor/urllib3/connectionpool.py
index 459bbe095..96339e90a 100644
--- a/src/pip/_vendor/urllib3/connectionpool.py
+++ b/src/pip/_vendor/urllib3/connectionpool.py
@@ -2,6 +2,7 @@ from __future__ import absolute_import
import errno
import logging
+import re
import socket
import sys
import warnings
@@ -35,7 +36,6 @@ from .exceptions import (
)
from .packages import six
from .packages.six.moves import queue
-from .packages.ssl_match_hostname import CertificateError
from .request import RequestMethods
from .response import HTTPResponse
from .util.connection import is_connection_dropped
@@ -44,6 +44,7 @@ from .util.queue import LifoQueue
from .util.request import set_file_position
from .util.response import assert_header_parsing
from .util.retry import Retry
+from .util.ssl_match_hostname import CertificateError
from .util.timeout import Timeout
from .util.url import Url, _encode_target
from .util.url import _normalize_host as normalize_host
@@ -301,8 +302,11 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
pass
except queue.Full:
# This should never happen if self.block == True
- log.warning("Connection pool is full, discarding connection: %s", self.host)
-
+ log.warning(
+ "Connection pool is full, discarding connection: %s. Connection pool size: %s",
+ self.host,
+ self.pool.qsize(),
+ )
# Connection never got put back into the pool, close it.
if conn:
conn.close()
@@ -745,7 +749,35 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
# Discard the connection for these exceptions. It will be
# replaced during the next _get_conn() call.
clean_exit = False
- if isinstance(e, (BaseSSLError, CertificateError)):
+
+ def _is_ssl_error_message_from_http_proxy(ssl_error):
+ # We're trying to detect the message 'WRONG_VERSION_NUMBER' but
+ # SSLErrors are kinda all over the place when it comes to the message,
+ # so we try to cover our bases here!
+ message = " ".join(re.split("[^a-z]", str(ssl_error).lower()))
+ return (
+ "wrong version number" in message or "unknown protocol" in message
+ )
+
+ # Try to detect a common user error with proxies which is to
+ # set an HTTP proxy to be HTTPS when it should be 'http://'
+ # (ie {'http': 'http://proxy', 'https': 'https://proxy'})
+ # Instead we add a nice error message and point to a URL.
+ if (
+ isinstance(e, BaseSSLError)
+ and self.proxy
+ and _is_ssl_error_message_from_http_proxy(e)
+ and conn.proxy
+ and conn.proxy.scheme == "https"
+ ):
+ e = ProxyError(
+ "Your proxy appears to only use HTTP and not HTTPS, "
+ "try changing your proxy URL to be HTTP. See: "
+ "https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html"
+ "#https-proxy-error-http-proxy",
+ SSLError(e),
+ )
+ elif isinstance(e, (BaseSSLError, CertificateError)):
e = SSLError(e)
elif isinstance(e, (SocketError, NewConnectionError)) and self.proxy:
e = ProxyError("Cannot connect to proxy.", e)
@@ -1020,6 +1052,17 @@ class HTTPSConnectionPool(HTTPConnectionPool):
InsecureRequestWarning,
)
+ if getattr(conn, "proxy_is_verified", None) is False:
+ warnings.warn(
+ (
+ "Unverified HTTPS connection done to an HTTPS proxy. "
+ "Adding certificate verification is strongly advised. See: "
+ "https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html"
+ "#ssl-warnings"
+ ),
+ InsecureRequestWarning,
+ )
+
def connection_from_url(url, **kw):
"""
diff --git a/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py b/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py
index 42526be7f..264d564db 100644
--- a/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py
+++ b/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py
@@ -48,7 +48,7 @@ from ctypes import (
)
from ctypes.util import find_library
-from pip._vendor.urllib3.packages.six import raise_from
+from ...packages.six import raise_from
if platform.system() != "Darwin":
raise ImportError("Only macOS is supported")
diff --git a/src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py b/src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py
index ed8120190..fa0b245d2 100644
--- a/src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py
+++ b/src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py
@@ -188,6 +188,7 @@ def _cert_array_from_pem(pem_bundle):
# We only want to do that if an error occurs: otherwise, the caller
# should free.
CoreFoundation.CFRelease(cert_array)
+ raise
return cert_array
diff --git a/src/pip/_vendor/urllib3/contrib/pyopenssl.py b/src/pip/_vendor/urllib3/contrib/pyopenssl.py
index c43146279..5f1d2d0b7 100644
--- a/src/pip/_vendor/urllib3/contrib/pyopenssl.py
+++ b/src/pip/_vendor/urllib3/contrib/pyopenssl.py
@@ -28,8 +28,8 @@ like this:
.. code-block:: python
try:
- import urllib3.contrib.pyopenssl
- urllib3.contrib.pyopenssl.inject_into_urllib3()
+ import pip._vendor.urllib3.contrib.pyopenssl as pyopenssl
+ pyopenssl.inject_into_urllib3()
except ImportError:
pass
@@ -406,7 +406,6 @@ if _fileobject: # Platform-specific: Python 2
self._makefile_refs += 1
return _fileobject(self, mode, bufsize, close=True)
-
else: # Platform-specific: Python 3
makefile = backport_makefile
diff --git a/src/pip/_vendor/urllib3/contrib/securetransport.py b/src/pip/_vendor/urllib3/contrib/securetransport.py
index b97555454..4a06bc69d 100644
--- a/src/pip/_vendor/urllib3/contrib/securetransport.py
+++ b/src/pip/_vendor/urllib3/contrib/securetransport.py
@@ -19,8 +19,8 @@ contrib module. So...here we are.
To use this module, simply import and inject it::
- import urllib3.contrib.securetransport
- urllib3.contrib.securetransport.inject_into_urllib3()
+ import pip._vendor.urllib3.contrib.securetransport as securetransport
+ securetransport.inject_into_urllib3()
Happy TLSing!
@@ -770,7 +770,6 @@ if _fileobject: # Platform-specific: Python 2
self._makefile_refs += 1
return _fileobject(self, mode, bufsize, close=True)
-
else: # Platform-specific: Python 3
def makefile(self, mode="r", buffering=None, *args, **kwargs):
diff --git a/src/pip/_vendor/urllib3/packages/__init__.py b/src/pip/_vendor/urllib3/packages/__init__.py
index fce4caa65..e69de29bb 100644
--- a/src/pip/_vendor/urllib3/packages/__init__.py
+++ b/src/pip/_vendor/urllib3/packages/__init__.py
@@ -1,5 +0,0 @@
-from __future__ import absolute_import
-
-from . import ssl_match_hostname
-
-__all__ = ("ssl_match_hostname",)
diff --git a/src/pip/_vendor/urllib3/packages/six.py b/src/pip/_vendor/urllib3/packages/six.py
index ba50acb06..f099a3dcd 100644
--- a/src/pip/_vendor/urllib3/packages/six.py
+++ b/src/pip/_vendor/urllib3/packages/six.py
@@ -772,7 +772,6 @@ if PY3:
value = None
tb = None
-
else:
def exec_(_code_, _globs_=None, _locs_=None):
diff --git a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/__init__.py b/src/pip/_vendor/urllib3/packages/ssl_match_hostname/__init__.py
deleted file mode 100644
index ef3fde520..000000000
--- a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import sys
-
-try:
- # Our match_hostname function is the same as 3.10's, so we only want to
- # import the match_hostname function if it's at least that good.
- # We also fallback on Python 3.10+ because our code doesn't emit
- # deprecation warnings and is the same as Python 3.10 otherwise.
- if sys.version_info < (3, 5) or sys.version_info >= (3, 10):
- raise ImportError("Fallback to vendored code")
-
- from ssl import CertificateError, match_hostname
-except ImportError:
- try:
- # Backport of the function from a pypi module
- from backports.ssl_match_hostname import ( # type: ignore
- CertificateError,
- match_hostname,
- )
- except ImportError:
- # Our vendored copy
- from ._implementation import CertificateError, match_hostname # type: ignore
-
-# Not needed, but documenting what we provide.
-__all__ = ("CertificateError", "match_hostname")
diff --git a/src/pip/_vendor/urllib3/poolmanager.py b/src/pip/_vendor/urllib3/poolmanager.py
index 3a31a285b..ca4ec3411 100644
--- a/src/pip/_vendor/urllib3/poolmanager.py
+++ b/src/pip/_vendor/urllib3/poolmanager.py
@@ -34,6 +34,7 @@ SSL_KEYWORDS = (
"ca_cert_dir",
"ssl_context",
"key_password",
+ "server_hostname",
)
# All known keyword arguments that could be provided to the pool manager, its
diff --git a/src/pip/_vendor/urllib3/response.py b/src/pip/_vendor/urllib3/response.py
index 38693f4fc..776e49dd2 100644
--- a/src/pip/_vendor/urllib3/response.py
+++ b/src/pip/_vendor/urllib3/response.py
@@ -7,10 +7,7 @@ from contextlib import contextmanager
from socket import error as SocketError
from socket import timeout as SocketTimeout
-try:
- import brotli
-except ImportError:
- brotli = None
+brotli = None
from ._collections import HTTPHeaderDict
from .connection import BaseSSLError, HTTPException
diff --git a/src/pip/_vendor/urllib3/util/connection.py b/src/pip/_vendor/urllib3/util/connection.py
index facfa0dd2..6af1138f2 100644
--- a/src/pip/_vendor/urllib3/util/connection.py
+++ b/src/pip/_vendor/urllib3/util/connection.py
@@ -2,9 +2,8 @@ from __future__ import absolute_import
import socket
-from pip._vendor.urllib3.exceptions import LocationParseError
-
from ..contrib import _appengine_environ
+from ..exceptions import LocationParseError
from ..packages import six
from .wait import NoWayToWaitForSocketError, wait_for_read
diff --git a/src/pip/_vendor/urllib3/util/proxy.py b/src/pip/_vendor/urllib3/util/proxy.py
index 34f884d5b..2199cc7b7 100644
--- a/src/pip/_vendor/urllib3/util/proxy.py
+++ b/src/pip/_vendor/urllib3/util/proxy.py
@@ -45,6 +45,7 @@ def create_proxy_ssl_context(
ssl_version=resolve_ssl_version(ssl_version),
cert_reqs=resolve_cert_reqs(cert_reqs),
)
+
if (
not ca_certs
and not ca_cert_dir
diff --git a/src/pip/_vendor/urllib3/util/request.py b/src/pip/_vendor/urllib3/util/request.py
index 25103383e..330766ef4 100644
--- a/src/pip/_vendor/urllib3/util/request.py
+++ b/src/pip/_vendor/urllib3/util/request.py
@@ -13,12 +13,6 @@ SKIP_HEADER = "@@@SKIP_HEADER@@@"
SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"])
ACCEPT_ENCODING = "gzip,deflate"
-try:
- import brotli as _unused_module_brotli # noqa: F401
-except ImportError:
- pass
-else:
- ACCEPT_ENCODING += ",br"
_FAILEDTELL = object()
diff --git a/src/pip/_vendor/urllib3/util/retry.py b/src/pip/_vendor/urllib3/util/retry.py
index c7dc42f1d..3398323fd 100644
--- a/src/pip/_vendor/urllib3/util/retry.py
+++ b/src/pip/_vendor/urllib3/util/retry.py
@@ -69,6 +69,24 @@ class _RetryMeta(type):
)
cls.DEFAULT_REMOVE_HEADERS_ON_REDIRECT = value
+ @property
+ def BACKOFF_MAX(cls):
+ warnings.warn(
+ "Using 'Retry.BACKOFF_MAX' is deprecated and "
+ "will be removed in v2.0. Use 'Retry.DEFAULT_BACKOFF_MAX' instead",
+ DeprecationWarning,
+ )
+ return cls.DEFAULT_BACKOFF_MAX
+
+ @BACKOFF_MAX.setter
+ def BACKOFF_MAX(cls, value):
+ warnings.warn(
+ "Using 'Retry.BACKOFF_MAX' is deprecated and "
+ "will be removed in v2.0. Use 'Retry.DEFAULT_BACKOFF_MAX' instead",
+ DeprecationWarning,
+ )
+ cls.DEFAULT_BACKOFF_MAX = value
+
@six.add_metaclass(_RetryMeta)
class Retry(object):
@@ -181,7 +199,7 @@ class Retry(object):
seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep
for [0.0s, 0.2s, 0.4s, ...] between retries. It will never be longer
- than :attr:`Retry.BACKOFF_MAX`.
+ than :attr:`Retry.DEFAULT_BACKOFF_MAX`.
By default, backoff is disabled (set to 0).
@@ -220,7 +238,7 @@ class Retry(object):
DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(["Authorization"])
#: Maximum backoff time.
- BACKOFF_MAX = 120
+ DEFAULT_BACKOFF_MAX = 120
def __init__(
self,
@@ -348,7 +366,7 @@ class Retry(object):
return 0
backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1))
- return min(self.BACKOFF_MAX, backoff_value)
+ return min(self.DEFAULT_BACKOFF_MAX, backoff_value)
def parse_retry_after(self, retry_after):
# Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4
diff --git a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py b/src/pip/_vendor/urllib3/util/ssl_match_hostname.py
index 689208d3c..1dd950c48 100644
--- a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py
+++ b/src/pip/_vendor/urllib3/util/ssl_match_hostname.py
@@ -9,7 +9,7 @@ import sys
# ipaddress has been backported to 2.6+ in pypi. If it is installed on the
# system, use it to handle IPAddress ServerAltnames (this was added in
# python-3.5) otherwise only do DNS matching. This allows
-# backports.ssl_match_hostname to continue to be used in Python 2.7.
+# util.ssl_match_hostname to continue to be used in Python 2.7.
try:
import ipaddress
except ImportError:
@@ -78,7 +78,8 @@ def _dnsname_match(dn, hostname, max_wildcards=1):
def _to_unicode(obj):
if isinstance(obj, str) and sys.version_info < (3,):
- obj = unicode(obj, encoding="ascii", errors="strict")
+ # ignored flake8 # F821 to support python 2.7 function
+ obj = unicode(obj, encoding="ascii", errors="strict") # noqa: F821
return obj
@@ -111,11 +112,9 @@ def match_hostname(cert, hostname):
try:
# Divergence from upstream: ipaddress can't handle byte str
host_ip = ipaddress.ip_address(_to_unicode(hostname))
- except ValueError:
- # Not an IP address (common case)
- host_ip = None
- except UnicodeError:
- # Divergence from upstream: Have to deal with ipaddress not taking
+ except (UnicodeError, ValueError):
+ # ValueError: Not an IP address (common case)
+ # UnicodeError: Divergence from upstream: Have to deal with ipaddress not taking
# byte strings. addresses should be all ascii, so we consider it not
# an ipaddress in this case
host_ip = None
@@ -123,7 +122,7 @@ def match_hostname(cert, hostname):
# Divergence from upstream: Make ipaddress library optional
if ipaddress is None:
host_ip = None
- else:
+ else: # Defensive
raise
dnsnames = []
san = cert.get("subjectAltName", ())
diff --git a/src/pip/_vendor/urllib3/util/ssltransport.py b/src/pip/_vendor/urllib3/util/ssltransport.py
index 0ed97b644..4a7105d17 100644
--- a/src/pip/_vendor/urllib3/util/ssltransport.py
+++ b/src/pip/_vendor/urllib3/util/ssltransport.py
@@ -2,8 +2,8 @@ import io
import socket
import ssl
-from pip._vendor.urllib3.exceptions import ProxySchemeUnsupported
-from pip._vendor.urllib3.packages import six
+from ..exceptions import ProxySchemeUnsupported
+from ..packages import six
SSL_BLOCKSIZE = 16384
diff --git a/src/pip/_vendor/urllib3/util/url.py b/src/pip/_vendor/urllib3/util/url.py
index 3651c4318..86bd8b48a 100644
--- a/src/pip/_vendor/urllib3/util/url.py
+++ b/src/pip/_vendor/urllib3/util/url.py
@@ -279,6 +279,9 @@ def _normalize_host(host, scheme):
if scheme in NORMALIZABLE_SCHEMES:
is_ipv6 = IPV6_ADDRZ_RE.match(host)
if is_ipv6:
+ # IPv6 hosts of the form 'a::b%zone' are encoded in a URL as
+ # such per RFC 6874: 'a::b%25zone'. Unquote the ZoneID
+ # separator as necessary to return a valid RFC 4007 scoped IP.
match = ZONE_ID_RE.search(host)
if match:
start, end = match.span(1)
@@ -331,7 +334,7 @@ def parse_url(url):
"""
Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is
performed to parse incomplete urls. Fields not provided will be None.
- This parser is RFC 3986 compliant.
+ This parser is RFC 3986 and RFC 6874 compliant.
The parser logic and helper functions are based heavily on
work done in the ``rfc3986`` module.
diff --git a/src/pip/_vendor/urllib3/util/wait.py b/src/pip/_vendor/urllib3/util/wait.py
index c280646c7..21b4590b3 100644
--- a/src/pip/_vendor/urllib3/util/wait.py
+++ b/src/pip/_vendor/urllib3/util/wait.py
@@ -42,7 +42,6 @@ if sys.version_info >= (3, 5):
def _retry_on_intr(fn, timeout):
return fn(timeout)
-
else:
# Old and broken Pythons.
def _retry_on_intr(fn, timeout):
diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt
index 73e493434..375b6411a 100644
--- a/src/pip/_vendor/vendor.txt
+++ b/src/pip/_vendor/vendor.txt
@@ -1,22 +1,23 @@
-appdirs==1.4.4
-CacheControl==0.12.6
-colorama==0.4.4
-distlib==0.3.2
-distro==1.5.0
-html5lib==1.1
-msgpack==1.0.2
-packaging==21.0
-pep517==0.11.0
-progress==1.5
-pyparsing==2.4.7
-requests==2.26.0
- certifi==2021.05.30
- chardet==4.0.0
- idna==3.2
- urllib3==1.26.6
-resolvelib==0.7.1
+CacheControl==0.12.11 # Make sure to update the license in pyproject.toml for this.
+colorama==0.4.5
+distlib==0.3.5
+distro==1.7.0
+msgpack==1.0.4
+packaging==21.3
+pep517==0.12.0
+platformdirs==2.5.2
+pyparsing==3.0.9
+requests==2.28.1
+ certifi==2022.06.15
+ chardet==5.0.0
+ idna==3.3
+ urllib3==1.26.10
+rich==12.5.1
+ pygments==2.12.0
+ typing_extensions==4.3.0
+resolvelib==0.8.1
setuptools==44.0.0
six==1.16.0
tenacity==8.0.1
-tomli==1.0.3
+tomli==2.0.1
webencodings==0.5.1
diff --git a/tests/conftest.py b/tests/conftest.py
index a53e0c4f7..44aa56026 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,27 +6,56 @@ import re
import shutil
import subprocess
import sys
-import time
from contextlib import ExitStack, contextmanager
-from typing import Dict, Iterable
+from pathlib import Path
+from typing import (
+ TYPE_CHECKING,
+ Callable,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Union,
+)
from unittest.mock import patch
+from zipfile import ZipFile
import pytest
-from setuptools.wheel import Wheel
+# Config will be available from the public API in pytest >= 7.0.0:
+# https://github.com/pytest-dev/pytest/commit/88d84a57916b592b070f4201dc84f0286d1f9fef
+from _pytest.config import Config
+
+# Parser will be available from the public API in pytest >= 7.0.0:
+# https://github.com/pytest-dev/pytest/commit/538b5c24999e9ebb4fab43faabc8bcc28737bcdf
+from _pytest.config.argparsing import Parser
+from installer import install
+from installer.destinations import SchemeDictionaryDestination
+from installer.sources import WheelFile
+
+from pip import __file__ as pip_location
from pip._internal.cli.main import main as pip_entry_point
+from pip._internal.locations import _USE_SYSCONFIG
from pip._internal.utils.temp_dir import global_tempdir_manager
from tests.lib import DATA_DIR, SRC_DIR, PipTestEnvironment, TestData
-from tests.lib.certs import make_tls_cert, serialize_cert, serialize_key
-from tests.lib.path import Path
from tests.lib.server import MockServer as _MockServer
-from tests.lib.server import Responder, make_mock_server, server_running
-from tests.lib.venv import VirtualEnvironment
+from tests.lib.server import make_mock_server, server_running
+from tests.lib.venv import VirtualEnvironment, VirtualEnvironmentType
from .lib.compat import nullcontext
+if TYPE_CHECKING:
+ from typing import Protocol
+
+ from wsgi import WSGIApplication
+else:
+ # TODO: Protocol was introduced in Python 3.8. Remove this branch when
+ # dropping support for Python 3.7.
+ Protocol = object
-def pytest_addoption(parser):
+
+def pytest_addoption(parser: Parser) -> None:
parser.addoption(
"--keep-tmpdir",
action="store_true",
@@ -52,9 +81,21 @@ def pytest_addoption(parser):
default=False,
help="run 'pip search' tests",
)
+ parser.addoption(
+ "--proxy",
+ action="store",
+ default=None,
+ help="use given proxy in session network tests",
+ )
+ parser.addoption(
+ "--use-zipapp",
+ action="store_true",
+ default=False,
+ help="use a zipapp when running pip in tests",
+ )
-def pytest_collection_modifyitems(config, items):
+def pytest_collection_modifyitems(config: Config, items: List[pytest.Function]) -> None:
for item in items:
if not hasattr(item, "module"): # e.g.: DoctestTextfile
continue
@@ -77,9 +118,12 @@ def pytest_collection_modifyitems(config, items):
):
item.add_marker(pytest.mark.skip("Incompatible with venv"))
+ if item.get_closest_marker("incompatible_with_sysconfig") and _USE_SYSCONFIG:
+ item.add_marker(pytest.mark.skip("Incompatible with sysconfig"))
+
+ module_file = item.module.__file__
module_path = os.path.relpath(
- item.module.__file__,
- os.path.commonprefix([__file__, item.module.__file__]),
+ module_file, os.path.commonprefix([__file__, module_file])
)
module_root_dir = module_path.split(os.pathsep)[0]
@@ -91,12 +135,20 @@ def pytest_collection_modifyitems(config, items):
item.add_marker(pytest.mark.integration)
elif module_root_dir.startswith("unit"):
item.add_marker(pytest.mark.unit)
+
+ # We don't want to allow using the script resource if this is a
+ # unit test, as unit tests should not need all that heavy lifting
+ if "script" in item.fixturenames:
+ raise RuntimeError(
+ "Cannot use the ``script`` funcarg in a unit test: "
+ "(filename = {}, item = {})".format(module_path, item)
+ )
else:
raise RuntimeError(f"Unknown test type (filename = {module_path})")
@pytest.fixture(scope="session", autouse=True)
-def resolver_variant(request):
+def resolver_variant(request: pytest.FixtureRequest) -> Iterator[str]:
"""Set environment variable to make pip default to the correct resolver."""
resolver = request.config.getoption("--resolver")
@@ -118,40 +170,59 @@ def resolver_variant(request):
@pytest.fixture(scope="session")
-def tmpdir_factory(request, tmpdir_factory):
+def tmp_path_factory(
+ request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory
+) -> Iterator[pytest.TempPathFactory]:
"""Modified `tmpdir_factory` session fixture
that will automatically cleanup after itself.
"""
- yield tmpdir_factory
+ yield tmp_path_factory
if not request.config.getoption("--keep-tmpdir"):
shutil.rmtree(
- tmpdir_factory.getbasetemp(),
+ tmp_path_factory.getbasetemp(),
ignore_errors=True,
)
+@pytest.fixture(scope="session")
+def tmpdir_factory(tmp_path_factory: pytest.TempPathFactory) -> pytest.TempPathFactory:
+ """Override Pytest's ``tmpdir_factory`` with our pathlib implementation.
+
+ This prevents mis-use of this fixture.
+ """
+ return tmp_path_factory
+
+
@pytest.fixture
-def tmpdir(request, tmpdir):
+def tmp_path(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[Path]:
"""
Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
- directory. The returned object is a ``tests.lib.path.Path`` object.
+ directory. The returned object is a ``Path`` object.
- This uses the built-in tmpdir fixture from pytest itself but modified
- to return our typical path object instead of py.path.local as well as
- deleting the temporary directories at the end of each test case.
+ This uses the built-in tmp_path fixture from pytest itself, but deletes the
+ temporary directories at the end of each test case.
"""
- assert tmpdir.isdir()
- yield Path(str(tmpdir))
+ assert tmp_path.is_dir()
+ yield tmp_path
# Clear out the temporary directory after the test has finished using it.
# This should prevent us from needing a multiple gigabyte temporary
# directory while running the tests.
if not request.config.getoption("--keep-tmpdir"):
- tmpdir.remove(ignore_errors=True)
+ shutil.rmtree(tmp_path, ignore_errors=True)
+
+
+@pytest.fixture()
+def tmpdir(tmp_path: Path) -> Path:
+ """Override Pytest's ``tmpdir`` with our pathlib implementation.
+
+ This prevents mis-use of this fixture.
+ """
+ return tmp_path
@pytest.fixture(autouse=True)
-def isolate(tmpdir, monkeypatch):
+def isolate(tmpdir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""
Isolate our tests so that things like global configuration files and the
like do not affect our test results.
@@ -241,7 +312,7 @@ def isolate(tmpdir, monkeypatch):
monkeypatch.setenv("PIP_DISABLE_PIP_VERSION_CHECK", "true")
# Make sure tests don't share a requirements tracker.
- monkeypatch.delenv("PIP_REQ_TRACKER", False)
+ monkeypatch.delenv("PIP_BUILD_TRACKER", False)
# FIXME: Windows...
os.makedirs(os.path.join(home_dir, ".config", "git"))
@@ -250,7 +321,7 @@ def isolate(tmpdir, monkeypatch):
@pytest.fixture(autouse=True)
-def scoped_global_tempdir_manager(request):
+def scoped_global_tempdir_manager(request: pytest.FixtureRequest) -> Iterator[None]:
"""Make unit tests with globally-managed tempdirs easier
Each test function gets its own individual scope for globally-managed
@@ -266,12 +337,14 @@ def scoped_global_tempdir_manager(request):
@pytest.fixture(scope="session")
-def pip_src(tmpdir_factory):
- def not_code_files_and_folders(path, names):
+def pip_src(tmpdir_factory: pytest.TempPathFactory) -> Path:
+ def not_code_files_and_folders(path: str, names: List[str]) -> Iterable[str]:
# In the root directory...
- if path == SRC_DIR:
+ if os.path.samefile(path, SRC_DIR):
# ignore all folders except "src"
- folders = {name for name in names if os.path.isdir(path / name)}
+ folders = {
+ name for name in names if os.path.isdir(os.path.join(path, name))
+ }
to_ignore = folders - {"src"}
# and ignore ".git" if present (which may be a file if in a linked
# worktree).
@@ -285,7 +358,7 @@ def pip_src(tmpdir_factory):
ignored.update(fnmatch.filter(names, pattern))
return ignored
- pip_src = Path(str(tmpdir_factory.mktemp("pip_src"))).joinpath("pip_src")
+ pip_src = tmpdir_factory.mktemp("pip_src").joinpath("pip_src")
# Copy over our source tree so that each use is self contained
shutil.copytree(
SRC_DIR,
@@ -295,55 +368,86 @@ def pip_src(tmpdir_factory):
return pip_src
-def _common_wheel_editable_install(tmpdir_factory, common_wheels, package):
+def _common_wheel_editable_install(
+ tmpdir_factory: pytest.TempPathFactory, common_wheels: Path, package: str
+) -> Path:
wheel_candidates = list(common_wheels.glob(f"{package}-*.whl"))
assert len(wheel_candidates) == 1, wheel_candidates
- install_dir = Path(str(tmpdir_factory.mktemp(package))) / "install"
- Wheel(wheel_candidates[0]).install_as_egg(install_dir)
- (install_dir / "EGG-INFO").rename(install_dir / f"{package}.egg-info")
- assert compileall.compile_dir(str(install_dir), quiet=1)
- return install_dir
+ install_dir = tmpdir_factory.mktemp(package) / "install"
+ lib_install_dir = install_dir / "lib"
+ bin_install_dir = install_dir / "bin"
+ with WheelFile.open(wheel_candidates[0]) as source:
+ install(
+ source,
+ SchemeDictionaryDestination(
+ {
+ "purelib": os.fspath(lib_install_dir),
+ "platlib": os.fspath(lib_install_dir),
+ "scripts": os.fspath(bin_install_dir),
+ },
+ interpreter=sys.executable,
+ script_kind="posix",
+ ),
+ additional_metadata={},
+ )
+ # The scripts are not necessary for our use cases, and they would be installed with
+ # the wrong interpreter, so remove them.
+ # TODO consider a refactoring by adding a install_from_wheel(path) method
+ # to the virtualenv fixture.
+ if bin_install_dir.exists():
+ shutil.rmtree(bin_install_dir)
+ return lib_install_dir
@pytest.fixture(scope="session")
-def setuptools_install(tmpdir_factory, common_wheels):
+def setuptools_install(
+ tmpdir_factory: pytest.TempPathFactory, common_wheels: Path
+) -> Path:
return _common_wheel_editable_install(tmpdir_factory, common_wheels, "setuptools")
@pytest.fixture(scope="session")
-def wheel_install(tmpdir_factory, common_wheels):
+def wheel_install(tmpdir_factory: pytest.TempPathFactory, common_wheels: Path) -> Path:
return _common_wheel_editable_install(tmpdir_factory, common_wheels, "wheel")
@pytest.fixture(scope="session")
-def coverage_install(tmpdir_factory, common_wheels):
+def coverage_install(
+ tmpdir_factory: pytest.TempPathFactory, common_wheels: Path
+) -> Path:
return _common_wheel_editable_install(tmpdir_factory, common_wheels, "coverage")
-def install_egg_link(venv, project_name, egg_info_dir):
- with open(venv.site / "easy-install.pth", "a") as fp:
- fp.write(str(egg_info_dir.resolve()) + "\n")
- with open(venv.site / (project_name + ".egg-link"), "w") as fp:
- fp.write(str(egg_info_dir) + "\n.")
+def install_pth_link(
+ venv: VirtualEnvironment, project_name: str, lib_dir: Path
+) -> None:
+ venv.site.joinpath(f"_pip_testsuite_{project_name}.pth").write_text(
+ str(lib_dir.resolve()), encoding="utf-8"
+ )
@pytest.fixture(scope="session")
def virtualenv_template(
- request, tmpdir_factory, pip_src, setuptools_install, coverage_install
-):
-
+ request: pytest.FixtureRequest,
+ tmpdir_factory: pytest.TempPathFactory,
+ pip_src: Path,
+ setuptools_install: Path,
+ coverage_install: Path,
+) -> Iterator[VirtualEnvironment]:
+
+ venv_type: VirtualEnvironmentType
if request.config.getoption("--use-venv"):
venv_type = "venv"
else:
venv_type = "virtualenv"
# Create the virtual environment
- tmpdir = Path(str(tmpdir_factory.mktemp("virtualenv")))
+ tmpdir = tmpdir_factory.mktemp("virtualenv")
venv = VirtualEnvironment(tmpdir.joinpath("venv_orig"), venv_type=venv_type)
# Install setuptools and pip.
- install_egg_link(venv, "setuptools", setuptools_install)
- pip_editable = Path(str(tmpdir_factory.mktemp("pip"))) / "pip"
+ install_pth_link(venv, "setuptools", setuptools_install)
+ pip_editable = tmpdir_factory.mktemp("pip") / "pip"
shutil.copytree(pip_src, pip_editable, symlinks=True)
# noxfile.py is Python 3 only
assert compileall.compile_dir(
@@ -352,12 +456,12 @@ def virtualenv_template(
rx=re.compile("noxfile.py$"),
)
subprocess.check_call(
- [venv.bin / "python", "setup.py", "-q", "develop"], cwd=pip_editable
+ [os.fspath(venv.bin / "python"), "setup.py", "-q", "develop"], cwd=pip_editable
)
# Install coverage and pth file for executing it in any spawned processes
# in this virtual environment.
- install_egg_link(venv, "coverage", coverage_install)
+ install_pth_link(venv, "coverage", coverage_install)
# zz prefix ensures the file is after easy-install.pth.
with open(venv.site / "zz-coverage-helper.pth", "a") as f:
f.write("import coverage; coverage.process_startup()")
@@ -381,15 +485,19 @@ def virtualenv_template(
@pytest.fixture(scope="session")
-def virtualenv_factory(virtualenv_template):
- def factory(tmpdir):
+def virtualenv_factory(
+ virtualenv_template: VirtualEnvironment,
+) -> Callable[[Path], VirtualEnvironment]:
+ def factory(tmpdir: Path) -> VirtualEnvironment:
return VirtualEnvironment(tmpdir, virtualenv_template)
return factory
@pytest.fixture
-def virtualenv(virtualenv_factory, tmpdir):
+def virtualenv(
+ virtualenv_factory: Callable[[Path], VirtualEnvironment], tmpdir: Path
+) -> Iterator[VirtualEnvironment]:
"""
Return a virtual environment which is unique to each test function
invocation created inside of a sub directory of the test function's
@@ -400,13 +508,27 @@ def virtualenv(virtualenv_factory, tmpdir):
@pytest.fixture
-def with_wheel(virtualenv, wheel_install):
- install_egg_link(virtualenv, "wheel", wheel_install)
+def with_wheel(virtualenv: VirtualEnvironment, wheel_install: Path) -> None:
+ install_pth_link(virtualenv, "wheel", wheel_install)
+
+
+class ScriptFactory(Protocol):
+ def __call__(
+ self, tmpdir: Path, virtualenv: Optional[VirtualEnvironment] = None
+ ) -> PipTestEnvironment:
+ ...
@pytest.fixture(scope="session")
-def script_factory(virtualenv_factory, deprecated_python):
- def factory(tmpdir, virtualenv=None):
+def script_factory(
+ virtualenv_factory: Callable[[Path], VirtualEnvironment],
+ deprecated_python: bool,
+ zipapp: Optional[str],
+) -> ScriptFactory:
+ def factory(
+ tmpdir: Path,
+ virtualenv: Optional[VirtualEnvironment] = None,
+ ) -> PipTestEnvironment:
if virtualenv is None:
virtualenv = virtualenv_factory(tmpdir.joinpath("venv"))
return PipTestEnvironment(
@@ -424,13 +546,68 @@ def script_factory(virtualenv_factory, deprecated_python):
assert_no_temp=True,
# Deprecated python versions produce an extra deprecation warning
pip_expect_warning=deprecated_python,
+ # Tell the Test Environment if we want to run pip via a zipapp
+ zipapp=zipapp,
)
return factory
+ZIPAPP_MAIN = """\
+#!/usr/bin/env python
+
+import os
+import runpy
+import sys
+
+lib = os.path.join(os.path.dirname(__file__), "lib")
+sys.path.insert(0, lib)
+
+runpy.run_module("pip", run_name="__main__")
+"""
+
+
+def make_zipapp_from_pip(zipapp_name: Path) -> None:
+ pip_dir = Path(pip_location).parent
+ with zipapp_name.open("wb") as zipapp_file:
+ zipapp_file.write(b"#!/usr/bin/env python\n")
+ with ZipFile(zipapp_file, "w") as zipapp:
+ for pip_file in pip_dir.rglob("*"):
+ if pip_file.suffix == ".pyc":
+ continue
+ if pip_file.name == "__pycache__":
+ continue
+ rel_name = pip_file.relative_to(pip_dir.parent)
+ zipapp.write(pip_file, arcname=f"lib/{rel_name}")
+ zipapp.writestr("__main__.py", ZIPAPP_MAIN)
+
+
+@pytest.fixture(scope="session")
+def zipapp(
+ request: pytest.FixtureRequest, tmpdir_factory: pytest.TempPathFactory
+) -> Optional[str]:
+ """
+ If the user requested for pip to be run from a zipapp, build that zipapp
+ and return its location. If the user didn't request a zipapp, return None.
+
+ This fixture is session scoped, so the zipapp will only be created once.
+ """
+ if not request.config.getoption("--use-zipapp"):
+ return None
+
+ temp_location = tmpdir_factory.mktemp("zipapp")
+ pyz_file = temp_location / "pip.pyz"
+ make_zipapp_from_pip(pyz_file)
+ return str(pyz_file)
+
+
@pytest.fixture
-def script(tmpdir, virtualenv, script_factory):
+def script(
+ request: pytest.FixtureRequest,
+ tmpdir: Path,
+ virtualenv: VirtualEnvironment,
+ script_factory: ScriptFactory,
+) -> PipTestEnvironment:
"""
Return a PipTestEnvironment which is unique to each test function and
will execute all commands inside of the unique virtual environment for this
@@ -441,34 +618,34 @@ def script(tmpdir, virtualenv, script_factory):
@pytest.fixture(scope="session")
-def common_wheels():
+def common_wheels() -> Path:
"""Provide a directory with latest setuptools and wheel wheels"""
return DATA_DIR.joinpath("common_wheels")
@pytest.fixture(scope="session")
-def shared_data(tmpdir_factory):
- return TestData.copy(Path(str(tmpdir_factory.mktemp("data"))))
+def shared_data(tmpdir_factory: pytest.TempPathFactory) -> TestData:
+ return TestData.copy(tmpdir_factory.mktemp("data"))
@pytest.fixture
-def data(tmpdir):
+def data(tmpdir: Path) -> TestData:
return TestData.copy(tmpdir.joinpath("data"))
class InMemoryPipResult:
- def __init__(self, returncode, stdout):
+ def __init__(self, returncode: int, stdout: str) -> None:
self.returncode = returncode
self.stdout = stdout
class InMemoryPip:
- def pip(self, *args):
+ def pip(self, *args: Union[str, Path]) -> InMemoryPipResult:
orig_stdout = sys.stdout
stdout = io.StringIO()
sys.stdout = stdout
try:
- returncode = pip_entry_point(list(args))
+ returncode = pip_entry_point([os.fspath(a) for a in args])
except SystemExit as e:
returncode = e.code or 0
finally:
@@ -477,22 +654,29 @@ class InMemoryPip:
@pytest.fixture
-def in_memory_pip():
+def in_memory_pip() -> InMemoryPip:
return InMemoryPip()
@pytest.fixture(scope="session")
-def deprecated_python():
+def deprecated_python() -> bool:
"""Used to indicate whether pip deprecated this Python version"""
return sys.version_info[:2] in []
+CertFactory = Callable[[], str]
+
+
@pytest.fixture(scope="session")
-def cert_factory(tmpdir_factory):
- def factory():
- # type: () -> str
+def cert_factory(tmpdir_factory: pytest.TempPathFactory) -> CertFactory:
+ # Delay the import requiring cryptography in order to make it possible
+ # to deselect relevant tests on systems where cryptography cannot
+ # be installed.
+ from tests.lib.certs import make_tls_cert, serialize_cert, serialize_key
+
+ def factory() -> str:
"""Returns path to cert/key file."""
- output_path = Path(str(tmpdir_factory.mktemp("certs"))) / "cert.pem"
+ output_path = tmpdir_factory.mktemp("certs") / "cert.pem"
# Must be Text on PY2.
cert, key = make_tls_cert("localhost")
with open(str(output_path), "wb") as f:
@@ -505,46 +689,41 @@ def cert_factory(tmpdir_factory):
class MockServer:
- def __init__(self, server):
- # type: (_MockServer) -> None
+ def __init__(self, server: _MockServer) -> None:
self._server = server
self._running = False
self.context = ExitStack()
@property
- def port(self):
+ def port(self) -> int:
return self._server.port
@property
- def host(self):
+ def host(self) -> str:
return self._server.host
- def set_responses(self, responses):
- # type: (Iterable[Responder]) -> None
+ def set_responses(self, responses: Iterable["WSGIApplication"]) -> None:
assert not self._running, "responses cannot be set on running server"
self._server.mock.side_effect = responses
- def start(self):
- # type: () -> None
+ def start(self) -> None:
assert not self._running, "running server cannot be started"
self.context.enter_context(server_running(self._server))
self.context.enter_context(self._set_running())
@contextmanager
- def _set_running(self):
+ def _set_running(self) -> Iterator[None]:
self._running = True
try:
yield
finally:
self._running = False
- def stop(self):
- # type: () -> None
+ def stop(self) -> None:
assert self._running, "idle server cannot be stopped"
self.context.close()
- def get_requests(self):
- # type: () -> Dict[str, str]
+ def get_requests(self) -> List[Dict[str, str]]:
"""Get environ for each received request."""
assert not self._running, "cannot get mock from running server"
# Legacy: replace call[0][0] with call.args[0]
@@ -553,7 +732,7 @@ class MockServer:
@pytest.fixture
-def mock_server():
+def mock_server() -> Iterator[MockServer]:
server = make_mock_server()
test_server = MockServer(server)
with test_server.context:
@@ -561,10 +740,5 @@ def mock_server():
@pytest.fixture
-def utc():
- # time.tzset() is not implemented on some platforms, e.g. Windows.
- tzset = getattr(time, "tzset", lambda: None)
- with patch.dict(os.environ, {"TZ": "UTC"}):
- tzset()
- yield
- tzset()
+def proxy(request: pytest.FixtureRequest) -> str:
+ return request.config.getoption("proxy")
diff --git a/tests/data/indexes/datarequire/fakepackage/index.html b/tests/data/indexes/datarequire/fakepackage/index.html
index 0ca8b9dc3..25bf4aa21 100644
--- a/tests/data/indexes/datarequire/fakepackage/index.html
+++ b/tests/data/indexes/datarequire/fakepackage/index.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
<html><head><title>Links for fakepackage</title><meta name="api-version" value="2" /></head><body><h1>Links for fakepackage</h1>
<a data-requires-python='' href="/fakepackage-1.0.0.tar.gz#md5=00000000000000000000000000000000" rel="internal">fakepackage-1.0.0.tar.gz</a><br/>
<a data-requires-python='&lt;2.7' href="/fakepackage-2.6.0.tar.gz#md5=00000000000000000000000000000000" rel="internal">fakepackage-2.6.0.tar.gz</a><br/>
diff --git a/tests/data/indexes/dev/bar/index.html b/tests/data/indexes/dev/bar/index.html
index bcee30921..c0da65613 100644
--- a/tests/data/indexes/dev/bar/index.html
+++ b/tests/data/indexes/dev/bar/index.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
<html>
<body>
<a href="bar-1.0.tar.gz">bar-1.0.tar.gz</a>
diff --git a/tests/data/indexes/in dex/simple/index.html b/tests/data/indexes/in dex/simple/index.html
index dba6cc3eb..cb078ea7b 100644
--- a/tests/data/indexes/in dex/simple/index.html
+++ b/tests/data/indexes/in dex/simple/index.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
<html>
<body>
<a href="../../../packages/simple-1.0.tar.gz#md5=4bdf78ebb7911f215c1972cf71b378f0">simple-1.0.tar.gz</a>
diff --git a/tests/data/indexes/pre/bar/index.html b/tests/data/indexes/pre/bar/index.html
index c50d88bc8..da76454f6 100644
--- a/tests/data/indexes/pre/bar/index.html
+++ b/tests/data/indexes/pre/bar/index.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
<html>
<body>
<a href="bar-1.0.tar.gz">bar-1.0.tar.gz</a>
diff --git a/tests/data/indexes/simple/simple/index.html b/tests/data/indexes/simple/simple/index.html
index dba6cc3eb..cb078ea7b 100644
--- a/tests/data/indexes/simple/simple/index.html
+++ b/tests/data/indexes/simple/simple/index.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
<html>
<body>
<a href="../../../packages/simple-1.0.tar.gz#md5=4bdf78ebb7911f215c1972cf71b378f0">simple-1.0.tar.gz</a>
diff --git a/tests/data/indexes/yanked/simple/index.html b/tests/data/indexes/yanked/simple/index.html
index bf4994310..67a2585ae 100644
--- a/tests/data/indexes/yanked/simple/index.html
+++ b/tests/data/indexes/yanked/simple/index.html
@@ -1,3 +1,4 @@
+<!doctype html>
<html>
<body>
<a href="../../../packages/simple-1.0.tar.gz">simple-1.0.tar.gz</a>
diff --git a/tests/data/indexes/yanked_all/simple/index.html b/tests/data/indexes/yanked_all/simple/index.html
new file mode 100644
index 000000000..060f99044
--- /dev/null
+++ b/tests/data/indexes/yanked_all/simple/index.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <a data-yanked="test reason message" href="../../../packages/simple-1.0.tar.gz">simple-1.0.tar.gz</a>
+ <a data-yanked="test reason message" href="../../../packages/simple-2.0.tar.gz">simple-2.0.tar.gz</a>
+ <a data-yanked="test reason message" href="../../../packages/simple-3.0.tar.gz">simple-3.0.tar.gz</a>
+ </body>
+</html>
diff --git a/tests/data/packages/BrokenEmitsUTF8/setup.py b/tests/data/packages/BrokenEmitsUTF8/setup.py
index 81c5baead..a40bc60c1 100644
--- a/tests/data/packages/BrokenEmitsUTF8/setup.py
+++ b/tests/data/packages/BrokenEmitsUTF8/setup.py
@@ -7,20 +7,32 @@ from distutils.core import setup
class FakeError(Exception):
pass
-if sys.argv[1] == 'install':
- if hasattr(sys.stdout, 'buffer'):
- sys.stdout.buffer.write('\nThis package prints out UTF-8 stuff like:\n'.encode('utf-8'))
- sys.stdout.buffer.write('* return type of ‘main’ is not ‘int’\n'.encode('utf-8'))
- sys.stdout.buffer.write('* Björk Guðmundsdóttir [ˈpjÅ“rÌ¥k ˈkvÊðmÊntsËŒtoÊŠhtɪr]'.encode('utf-8'))
+
+if sys.argv[1] == "install":
+ if hasattr(sys.stdout, "buffer"):
+ sys.stdout.buffer.write(
+ "\nThis package prints out UTF-8 stuff like:\n".encode("utf-8")
+ )
+ sys.stdout.buffer.write(
+ "* return type of ‘main’ is not ‘int’\n".encode("utf-8")
+ )
+ sys.stdout.buffer.write(
+ "* Björk Guðmundsdóttir [ˈpjÅ“rÌ¥k ˈkvÊðmÊntsËŒtoÊŠhtɪr]".encode("utf-8")
+ )
else:
pass
- sys.stdout.write('\nThis package prints out UTF-8 stuff like:\n')
- sys.stdout.write('* return type of \xe2\x80\x98main\xe2\x80\x99 is not \xe2\x80\x98int\xe2\x80\x99\n')
- sys.stdout.write('* Bj\xc3\xb6rk Gu\xc3\xb0mundsd\xc3\xb3ttir [\xcb\x88pj\xc5\x93r\xcc\xa5k \xcb\x88kv\xca\x8f\xc3\xb0m\xca\x8fnts\xcb\x8cto\xca\x8aht\xc9\xaar]\n')
+ sys.stdout.write("\nThis package prints out UTF-8 stuff like:\n")
+ sys.stdout.write(
+ "* return type of \xe2\x80\x98main\xe2\x80\x99 is not \xe2\x80\x98int\xe2\x80\x99\n"
+ )
+ sys.stdout.write(
+ "* Bj\xc3\xb6rk Gu\xc3\xb0mundsd\xc3\xb3ttir [\xcb\x88pj\xc5\x93r\xcc\xa5k \xcb\x88kv\xca\x8f\xc3\xb0m\xca\x8fnts\xcb\x8cto\xca\x8aht\xc9\xaar]\n"
+ )
- raise FakeError('this package designed to fail on install')
+ raise FakeError("this package designed to fail on install")
-setup(name='broken',
- version='0.2',
- py_modules=['broken'],
- )
+setup(
+ name="broken",
+ version="0.2",
+ py_modules=["broken"],
+)
diff --git a/tests/data/packages/FSPkg/setup.py b/tests/data/packages/FSPkg/setup.py
index d1b725e8c..58a784133 100644
--- a/tests/data/packages/FSPkg/setup.py
+++ b/tests/data/packages/FSPkg/setup.py
@@ -1,25 +1,26 @@
from setuptools import find_packages, setup
-version = '0.1dev'
+version = "0.1dev"
-setup(name='FSPkg',
- version=version,
- description="File system test package",
- long_description="""\
+setup(
+ name="FSPkg",
+ version=version,
+ description="File system test package",
+ long_description="""\
File system test package""",
- classifiers=[], # Get strings from https://pypi.org/pypi?%3Aaction=list_classifiers
- keywords='pip tests',
- author='pip',
- author_email='pip@openplans.org',
- url='http://pip.openplans.org',
- license='',
- packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
- include_package_data=True,
- zip_safe=False,
- install_requires=[
- # -*- Extra requirements: -*-
- ],
- entry_points="""
+ classifiers=[], # Get strings from https://pypi.org/pypi?%3Aaction=list_classifiers
+ keywords="pip tests",
+ author="pip",
+ author_email="pip@openplans.org",
+ url="http://pip.openplans.org",
+ license="",
+ packages=find_packages(exclude=["ez_setup", "examples", "tests"]),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=[
+ # -*- Extra requirements: -*-
+ ],
+ entry_points="""
# -*- Entry points: -*-
""",
- )
+)
diff --git a/tests/data/packages/HackedEggInfo/setup.py b/tests/data/packages/HackedEggInfo/setup.py
index 9e872e0b5..8f6aad433 100644
--- a/tests/data/packages/HackedEggInfo/setup.py
+++ b/tests/data/packages/HackedEggInfo/setup.py
@@ -2,14 +2,14 @@ from setuptools import setup
from setuptools.command import egg_info as orig_egg_info
-class egg_info (orig_egg_info.egg_info):
+class egg_info(orig_egg_info.egg_info):
def run(self):
orig_egg_info.egg_info.run(self)
setup(
name="hackedegginfo",
- version='0.0.0',
- cmdclass={'egg_info':egg_info},
+ version="0.0.0",
+ cmdclass={"egg_info": egg_info},
zip_safe=False,
)
diff --git a/tests/data/packages/LocalEnvironMarker/setup.py b/tests/data/packages/LocalEnvironMarker/setup.py
index 36ceb214c..0ab6c8a57 100644
--- a/tests/data/packages/LocalEnvironMarker/setup.py
+++ b/tests/data/packages/LocalEnvironMarker/setup.py
@@ -11,17 +11,17 @@ def path_to_url(path):
path = os.path.normpath(os.path.abspath(path))
drive, path = os.path.splitdrive(path)
filepath = path.split(os.path.sep)
- url = '/'.join(filepath)
+ url = "/".join(filepath)
if drive:
- return 'file:///' + drive + url
- return 'file://' + url
+ return "file:///" + drive + url
+ return "file://" + url
setup(
- name='LocalEnvironMarker',
- version='0.0.1',
+ name="LocalEnvironMarker",
+ version="0.0.1",
packages=find_packages(),
extras_require={
- ":python_version == '2.7'": ['simple'],
- }
+ ":python_version == '2.7'": ["simple"],
+ },
)
diff --git a/tests/data/packages/LocalExtras-0.0.2/setup.py b/tests/data/packages/LocalExtras-0.0.2/setup.py
index cc5c83283..6e910d1cb 100644
--- a/tests/data/packages/LocalExtras-0.0.2/setup.py
+++ b/tests/data/packages/LocalExtras-0.0.2/setup.py
@@ -11,16 +11,16 @@ def path_to_url(path):
path = os.path.normpath(os.path.abspath(path))
drive, path = os.path.splitdrive(path)
filepath = path.split(os.path.sep)
- url = '/'.join(filepath)
+ url = "/".join(filepath)
if drive:
- return 'file:///' + drive + url
- return 'file://' + url
+ return "file:///" + drive + url
+ return "file://" + url
setup(
- name='LocalExtras',
- version='0.0.2',
+ name="LocalExtras",
+ version="0.0.2",
packages=find_packages(),
- install_requires=['simple==1.0'],
- extras_require={'bar': ['simple==2.0'], 'baz': ['singlemodule']}
+ install_requires=["simple==1.0"],
+ extras_require={"bar": ["simple==2.0"], "baz": ["singlemodule"]},
)
diff --git a/tests/data/packages/LocalExtras/setup.py b/tests/data/packages/LocalExtras/setup.py
index eb390e32e..4bf2179da 100644
--- a/tests/data/packages/LocalExtras/setup.py
+++ b/tests/data/packages/LocalExtras/setup.py
@@ -11,15 +11,15 @@ def path_to_url(path):
path = os.path.normpath(os.path.abspath(path))
drive, path = os.path.splitdrive(path)
filepath = path.split(os.path.sep)
- url = '/'.join(filepath)
+ url = "/".join(filepath)
if drive:
- return 'file:///' + drive + url
- return 'file://' + url
+ return "file:///" + drive + url
+ return "file://" + url
setup(
- name='LocalExtras',
- version='0.0.1',
+ name="LocalExtras",
+ version="0.0.1",
packages=find_packages(),
- extras_require={'bar': ['simple'], 'baz': ['singlemodule']}
+ extras_require={"bar": ["simple"], "baz": ["singlemodule"]},
)
diff --git a/tests/data/packages/SetupPyLatin1/setup.py b/tests/data/packages/SetupPyLatin1/setup.py
index 3ca77c00c..80b10310e 100644
--- a/tests/data/packages/SetupPyLatin1/setup.py
+++ b/tests/data/packages/SetupPyLatin1/setup.py
@@ -2,6 +2,7 @@
from distutils.core import setup
-setup(name="SetupPyUTF8",
- author=u"Saúl Ibarra Corretgé",
- )
+setup(
+ name="SetupPyUTF8",
+ author="Saúl Ibarra Corretgé",
+)
diff --git a/tests/data/packages/SetupPyUTF8/setup.py b/tests/data/packages/SetupPyUTF8/setup.py
index 1962a0060..bd9fd2a29 100644
--- a/tests/data/packages/SetupPyUTF8/setup.py
+++ b/tests/data/packages/SetupPyUTF8/setup.py
@@ -1,5 +1,6 @@
from distutils.core import setup
-setup(name="SetupPyUTF8",
- author="Saúl Ibarra Corretgé",
- )
+setup(
+ name="SetupPyUTF8",
+ author="Saúl Ibarra Corretgé",
+)
diff --git a/tests/data/packages/corruptwheel-1.0-py2.py3-none-any.whl b/tests/data/packages/corruptwheel-1.0-py2.py3-none-any.whl
new file mode 100644
index 000000000..bf285f13f
--- /dev/null
+++ b/tests/data/packages/corruptwheel-1.0-py2.py3-none-any.whl
@@ -0,0 +1 @@
+This is a corrupt wheel which _clearly_ is not a zip file.
diff --git a/tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py b/tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py
index e2f920ba3..08645e8bd 100644
--- a/tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py
+++ b/tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py
@@ -2,9 +2,11 @@ import os
from setuptools.build_meta import build_sdist
from setuptools.build_meta import build_wheel as setuptools_build_wheel
-from setuptools.build_meta import (get_requires_for_build_sdist,
- get_requires_for_build_wheel,
- prepare_metadata_for_build_wheel)
+from setuptools.build_meta import (
+ get_requires_for_build_sdist,
+ get_requires_for_build_wheel,
+ prepare_metadata_for_build_wheel,
+)
def build_wheel(*a, **kw):
@@ -12,7 +14,7 @@ def build_wheel(*a, **kw):
raise RuntimeError("Failing build_wheel, as requested.")
# Create the marker file to record that the hook was called
- with open(os.environ['PIP_TEST_MARKER_FILE'], 'wb'):
+ with open(os.environ["PIP_TEST_MARKER_FILE"], "wb"):
pass
return setuptools_build_wheel(*a, **kw)
diff --git a/tests/data/packages/requiresPaste/requiresPaste.py b/tests/data/packages/requiresPaste/requiresPaste.py
index c74209e44..84d4ed536 100644
--- a/tests/data/packages/requiresPaste/requiresPaste.py
+++ b/tests/data/packages/requiresPaste/requiresPaste.py
@@ -1,3 +1,3 @@
"""Module requiring Paste to test dependencies download of pip wheel."""
-__version__ = '3.1.4'
+__version__ = "3.1.4"
diff --git a/tests/data/packages/requires_wheelbroken_upper/setup.py b/tests/data/packages/requires_wheelbroken_upper/setup.py
index 150f98dfb..210b7c67a 100644
--- a/tests/data/packages/requires_wheelbroken_upper/setup.py
+++ b/tests/data/packages/requires_wheelbroken_upper/setup.py
@@ -3,4 +3,5 @@ import setuptools
setuptools.setup(
name="requires_wheelbroken_upper",
version="0",
- install_requires=['wheelbroken', 'upper'])
+ install_requires=["wheelbroken", "upper"],
+)
diff --git a/tests/data/packages/symlinks/setup.py b/tests/data/packages/symlinks/setup.py
index b71e35f1e..8605b6fae 100644
--- a/tests/data/packages/symlinks/setup.py
+++ b/tests/data/packages/symlinks/setup.py
@@ -1,8 +1,9 @@
from setuptools import setup
-version = '0.1'
+version = "0.1"
-setup(name='symlinks',
- version=version,
- packages=["symlinks"],
- )
+setup(
+ name="symlinks",
+ version=version,
+ packages=["symlinks"],
+)
diff --git a/tests/data/packages3/dinner/index.html b/tests/data/packages3/dinner/index.html
index e258eb16b..52a16b116 100644
--- a/tests/data/packages3/dinner/index.html
+++ b/tests/data/packages3/dinner/index.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
<html><head><title>PyPI Mirror</title></head>
<body>
<h1>PyPI Mirror</h1>
diff --git a/tests/data/packages3/index.html b/tests/data/packages3/index.html
index d66e70ec6..262207b6a 100644
--- a/tests/data/packages3/index.html
+++ b/tests/data/packages3/index.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
<html><head><title>PyPI Mirror</title></head>
<body>
<h1>PyPI Mirror</h1>
diff --git a/tests/data/packages3/requiredinner/index.html b/tests/data/packages3/requiredinner/index.html
index 0981c9c72..52a4e6667 100644
--- a/tests/data/packages3/requiredinner/index.html
+++ b/tests/data/packages3/requiredinner/index.html
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
<html><head><title>PyPI Mirror</title></head>
<body>
<h1>PyPI Mirror</h1>
diff --git a/tests/data/src/TopoRequires/setup.py b/tests/data/src/TopoRequires/setup.py
index 6cd29a7b6..c4b1029f5 100644
--- a/tests/data/src/TopoRequires/setup.py
+++ b/tests/data/src/TopoRequires/setup.py
@@ -1,7 +1,7 @@
from setuptools import setup
setup(
- name='TopoRequires',
- version='0.0.1',
- packages=['toporequires'],
+ name="TopoRequires",
+ version="0.0.1",
+ packages=["toporequires"],
)
diff --git a/tests/data/src/TopoRequires2/setup.py b/tests/data/src/TopoRequires2/setup.py
index 019f43cb2..11d009c41 100644
--- a/tests/data/src/TopoRequires2/setup.py
+++ b/tests/data/src/TopoRequires2/setup.py
@@ -1,8 +1,8 @@
from setuptools import setup
setup(
- name='TopoRequires2',
- version='0.0.1',
- packages=['toporequires2'],
- install_requires=['TopoRequires'],
+ name="TopoRequires2",
+ version="0.0.1",
+ packages=["toporequires2"],
+ install_requires=["TopoRequires"],
)
diff --git a/tests/data/src/TopoRequires3/setup.py b/tests/data/src/TopoRequires3/setup.py
index 772ed618e..550bb008e 100644
--- a/tests/data/src/TopoRequires3/setup.py
+++ b/tests/data/src/TopoRequires3/setup.py
@@ -1,8 +1,8 @@
from setuptools import setup
setup(
- name='TopoRequires3',
- version='0.0.1',
- packages=['toporequires3'],
- install_requires=['TopoRequires'],
+ name="TopoRequires3",
+ version="0.0.1",
+ packages=["toporequires3"],
+ install_requires=["TopoRequires"],
)
diff --git a/tests/data/src/TopoRequires4/setup.py b/tests/data/src/TopoRequires4/setup.py
index e276f55a2..077eec765 100644
--- a/tests/data/src/TopoRequires4/setup.py
+++ b/tests/data/src/TopoRequires4/setup.py
@@ -1,8 +1,8 @@
from setuptools import setup
setup(
- name='TopoRequires4',
- version='0.0.1',
- packages=['toporequires4'],
- install_requires=['TopoRequires2', 'TopoRequires', 'TopoRequires3'],
+ name="TopoRequires4",
+ version="0.0.1",
+ packages=["toporequires4"],
+ install_requires=["TopoRequires2", "TopoRequires", "TopoRequires3"],
)
diff --git a/tests/data/src/chattymodule/setup.py b/tests/data/src/chattymodule/setup.py
index 68099f2f8..9f411b6fd 100644
--- a/tests/data/src/chattymodule/setup.py
+++ b/tests/data/src/chattymodule/setup.py
@@ -6,15 +6,17 @@ import sys
from setuptools import setup
print(f"HELLO FROM CHATTYMODULE {sys.argv[1]}")
-print(os.environ)
print(sys.argv)
+print(sys.executable)
+print(sys.version)
+
if "--fail" in sys.argv:
print("I DIE, I DIE")
sys.exit(1)
setup(
name="chattymodule",
- version='0.0.1',
+ version="0.0.1",
description="A sample Python project with a single module",
- py_modules=['chattymodule'],
+ py_modules=["chattymodule"],
)
diff --git a/tests/data/src/compilewheel/setup.py b/tests/data/src/compilewheel/setup.py
index 8319a2a5c..da9949450 100644
--- a/tests/data/src/compilewheel/setup.py
+++ b/tests/data/src/compilewheel/setup.py
@@ -1,7 +1,4 @@
#!/usr/bin/env python
from setuptools import find_packages, setup
-setup(name='compilewheel',
- version='1.0',
- packages=find_packages()
- )
+setup(name="compilewheel", version="1.0", packages=find_packages())
diff --git a/tests/data/src/extension/setup.py b/tests/data/src/extension/setup.py
index b26302b05..83624965d 100644
--- a/tests/data/src/extension/setup.py
+++ b/tests/data/src/extension/setup.py
@@ -1,4 +1,4 @@
from setuptools import Extension, setup
-module = Extension('extension', sources=['extension.c'])
-setup(name='extension', version='0.0.1', ext_modules = [module])
+module = Extension("extension", sources=["extension.c"])
+setup(name="extension", version="0.0.1", ext_modules=[module])
diff --git a/tests/data/src/pep517_setup_cfg_only/setup.cfg b/tests/data/src/pep517_setup_cfg_only/setup.cfg
new file mode 100644
index 000000000..4d62ef58d
--- /dev/null
+++ b/tests/data/src/pep517_setup_cfg_only/setup.cfg
@@ -0,0 +1,3 @@
+[metadata]
+name = "dummy"
+version = "0.1"
diff --git a/tests/data/src/pep518-3.0/pep518.py b/tests/data/src/pep518-3.0/pep518.py
index 7986d1137..9ce06a81e 100644
--- a/tests/data/src/pep518-3.0/pep518.py
+++ b/tests/data/src/pep518-3.0/pep518.py
@@ -1 +1 @@
-#dummy
+# dummy
diff --git a/tests/data/src/pep518-3.0/setup.py b/tests/data/src/pep518-3.0/setup.py
index b63f59926..587c04fc0 100644
--- a/tests/data/src/pep518-3.0/setup.py
+++ b/tests/data/src/pep518-3.0/setup.py
@@ -3,7 +3,8 @@ from setuptools import setup
import simplewheel # ensure dependency is installed
-setup(name='pep518',
- version='3.0',
- py_modules=['pep518'],
- )
+setup(
+ name="pep518",
+ version="3.0",
+ py_modules=["pep518"],
+)
diff --git a/tests/data/src/pep518_conflicting_requires/pep518.py b/tests/data/src/pep518_conflicting_requires/pep518.py
index 7986d1137..9ce06a81e 100644
--- a/tests/data/src/pep518_conflicting_requires/pep518.py
+++ b/tests/data/src/pep518_conflicting_requires/pep518.py
@@ -1 +1 @@
-#dummy
+# dummy
diff --git a/tests/data/src/pep518_conflicting_requires/setup.py b/tests/data/src/pep518_conflicting_requires/setup.py
index 34bdc16b5..28f3db53d 100644
--- a/tests/data/src/pep518_conflicting_requires/setup.py
+++ b/tests/data/src/pep518_conflicting_requires/setup.py
@@ -2,7 +2,7 @@
from setuptools import setup
setup(
- name='pep518_conflicting_requires',
- version='1.0.0',
- py_modules=['pep518'],
+ name="pep518_conflicting_requires",
+ version="1.0.0",
+ py_modules=["pep518"],
)
diff --git a/tests/data/src/pep518_forkbomb-235/setup.py b/tests/data/src/pep518_forkbomb-235/setup.py
index c8bc29287..f69346cac 100644
--- a/tests/data/src/pep518_forkbomb-235/setup.py
+++ b/tests/data/src/pep518_forkbomb-235/setup.py
@@ -1,5 +1,3 @@
from setuptools import setup
-setup(name='pep518_forkbomb',
- version='235',
- py_modules=['pep518_forkbomb'])
+setup(name="pep518_forkbomb", version="235", py_modules=["pep518_forkbomb"])
diff --git a/tests/data/src/pep518_invalid_build_system/pep518.py b/tests/data/src/pep518_invalid_build_system/pep518.py
index 7986d1137..9ce06a81e 100644
--- a/tests/data/src/pep518_invalid_build_system/pep518.py
+++ b/tests/data/src/pep518_invalid_build_system/pep518.py
@@ -1 +1 @@
-#dummy
+# dummy
diff --git a/tests/data/src/pep518_invalid_build_system/setup.py b/tests/data/src/pep518_invalid_build_system/setup.py
index ba23cf24a..de4161d3a 100644
--- a/tests/data/src/pep518_invalid_build_system/setup.py
+++ b/tests/data/src/pep518_invalid_build_system/setup.py
@@ -2,7 +2,7 @@
from setuptools import setup
setup(
- name='pep518_invalid_build_system',
- version='1.0.0',
- py_modules=['pep518'],
+ name="pep518_invalid_build_system",
+ version="1.0.0",
+ py_modules=["pep518"],
)
diff --git a/tests/data/src/pep518_invalid_requires/pep518.py b/tests/data/src/pep518_invalid_requires/pep518.py
index 7986d1137..9ce06a81e 100644
--- a/tests/data/src/pep518_invalid_requires/pep518.py
+++ b/tests/data/src/pep518_invalid_requires/pep518.py
@@ -1 +1 @@
-#dummy
+# dummy
diff --git a/tests/data/src/pep518_invalid_requires/setup.py b/tests/data/src/pep518_invalid_requires/setup.py
index e8a92da31..ff6ac8b32 100644
--- a/tests/data/src/pep518_invalid_requires/setup.py
+++ b/tests/data/src/pep518_invalid_requires/setup.py
@@ -2,7 +2,7 @@
from setuptools import setup
setup(
- name='pep518_invalid_requires',
- version='1.0.0',
- py_modules=['pep518'],
+ name="pep518_invalid_requires",
+ version="1.0.0",
+ py_modules=["pep518"],
)
diff --git a/tests/data/src/pep518_missing_requires/pep518.py b/tests/data/src/pep518_missing_requires/pep518.py
index 7986d1137..9ce06a81e 100644
--- a/tests/data/src/pep518_missing_requires/pep518.py
+++ b/tests/data/src/pep518_missing_requires/pep518.py
@@ -1 +1 @@
-#dummy
+# dummy
diff --git a/tests/data/src/pep518_missing_requires/setup.py b/tests/data/src/pep518_missing_requires/setup.py
index cbc5d28af..4d3c5f3e5 100644
--- a/tests/data/src/pep518_missing_requires/setup.py
+++ b/tests/data/src/pep518_missing_requires/setup.py
@@ -2,7 +2,7 @@
from setuptools import setup
setup(
- name='pep518_missing_requires',
- version='1.0.0',
- py_modules=['pep518'],
+ name="pep518_missing_requires",
+ version="1.0.0",
+ py_modules=["pep518"],
)
diff --git a/tests/data/src/pep518_twin_forkbombs_first-234/setup.py b/tests/data/src/pep518_twin_forkbombs_first-234/setup.py
index 55e9bbfb1..acb97e18e 100644
--- a/tests/data/src/pep518_twin_forkbombs_first-234/setup.py
+++ b/tests/data/src/pep518_twin_forkbombs_first-234/setup.py
@@ -1,5 +1,7 @@
from setuptools import setup
-setup(name='pep518_twin_forkbombs_first',
- version='234',
- py_modules=['pep518_twin_forkbombs_first'])
+setup(
+ name="pep518_twin_forkbombs_first",
+ version="234",
+ py_modules=["pep518_twin_forkbombs_first"],
+)
diff --git a/tests/data/src/pep518_twin_forkbombs_second-238/setup.py b/tests/data/src/pep518_twin_forkbombs_second-238/setup.py
index 985af51df..c14c1cfb0 100644
--- a/tests/data/src/pep518_twin_forkbombs_second-238/setup.py
+++ b/tests/data/src/pep518_twin_forkbombs_second-238/setup.py
@@ -1,5 +1,7 @@
from setuptools import setup
-setup(name='pep518_twin_forkbombs_second',
- version='238',
- py_modules=['pep518_twin_forkbombs_second'])
+setup(
+ name="pep518_twin_forkbombs_second",
+ version="238",
+ py_modules=["pep518_twin_forkbombs_second"],
+)
diff --git a/tests/data/src/pep518_with_extra_and_markers-1.0/pep518_with_extra_and_markers.py b/tests/data/src/pep518_with_extra_and_markers-1.0/pep518_with_extra_and_markers.py
index 7986d1137..9ce06a81e 100644
--- a/tests/data/src/pep518_with_extra_and_markers-1.0/pep518_with_extra_and_markers.py
+++ b/tests/data/src/pep518_with_extra_and_markers-1.0/pep518_with_extra_and_markers.py
@@ -1 +1 @@
-#dummy
+# dummy
diff --git a/tests/data/src/pep518_with_extra_and_markers-1.0/setup.py b/tests/data/src/pep518_with_extra_and_markers-1.0/setup.py
index 29a8175e4..bfac5b467 100644
--- a/tests/data/src/pep518_with_extra_and_markers-1.0/setup.py
+++ b/tests/data/src/pep518_with_extra_and_markers-1.0/setup.py
@@ -1,15 +1,14 @@
#!/usr/bin/env python
-import sys
-
from setuptools import setup
# ensure dependencies are installed
import simple
import simplewheel
-assert simplewheel.__version__ == '1.0' if sys.version_info < (3,) else '2.0'
+assert simplewheel.__version__ == "2.0"
-setup(name='pep518_with_extra_and_markers',
- version='1.0',
- py_modules=['pep518_with_extra_and_markers'],
- )
+setup(
+ name="pep518_with_extra_and_markers",
+ version="1.0",
+ py_modules=["pep518_with_extra_and_markers"],
+)
diff --git a/tests/data/src/pep518_with_namespace_package-1.0/setup.py b/tests/data/src/pep518_with_namespace_package-1.0/setup.py
index 540ede4cf..263ba1988 100644
--- a/tests/data/src/pep518_with_namespace_package-1.0/setup.py
+++ b/tests/data/src/pep518_with_namespace_package-1.0/setup.py
@@ -3,7 +3,7 @@ from setuptools import setup
import simple_namespace.module
setup(
- name='pep518_with_namespace_package',
- version='1.0',
- py_modules=['pep518_with_namespace_package'],
+ name="pep518_with_namespace_package",
+ version="1.0",
+ py_modules=["pep518_with_namespace_package"],
)
diff --git a/tests/data/src/prjwithdatafile/setup.py b/tests/data/src/prjwithdatafile/setup.py
index 240b7ea10..e2c29e6d3 100755
--- a/tests/data/src/prjwithdatafile/setup.py
+++ b/tests/data/src/prjwithdatafile/setup.py
@@ -1,11 +1,11 @@
from setuptools import setup
setup(
- name='prjwithdatafile',
+ name="prjwithdatafile",
version="1.0",
- packages=['prjwithdatafile'],
+ packages=["prjwithdatafile"],
data_files=[
- (r'packages1', ['prjwithdatafile/README.txt']),
- (r'packages2', ['prjwithdatafile/README.txt'])
- ]
+ (r"packages1", ["prjwithdatafile/README.txt"]),
+ (r"packages2", ["prjwithdatafile/README.txt"]),
+ ],
)
diff --git a/tests/data/src/requires_capitalized/setup.py b/tests/data/src/requires_capitalized/setup.py
index b3f37b919..287704f86 100644
--- a/tests/data/src/requires_capitalized/setup.py
+++ b/tests/data/src/requires_capitalized/setup.py
@@ -1,6 +1,3 @@
from setuptools import setup
-setup(name='Requires_Capitalized',
- version='0.1',
- install_requires=['simple==1.0']
- )
+setup(name="Requires_Capitalized", version="0.1", install_requires=["simple==1.0"])
diff --git a/tests/data/src/requires_requires_capitalized/setup.py b/tests/data/src/requires_requires_capitalized/setup.py
index d124d0728..8d099fddd 100644
--- a/tests/data/src/requires_requires_capitalized/setup.py
+++ b/tests/data/src/requires_requires_capitalized/setup.py
@@ -1,6 +1,7 @@
from setuptools import setup
-setup(name='requires_requires_capitalized',
- version='1.0',
- install_requires=['requires_Capitalized==0.1']
- )
+setup(
+ name="requires_requires_capitalized",
+ version="1.0",
+ install_requires=["requires_Capitalized==0.1"],
+)
diff --git a/tests/data/src/requires_simple/setup.py b/tests/data/src/requires_simple/setup.py
index 57122041a..5eebde770 100644
--- a/tests/data/src/requires_simple/setup.py
+++ b/tests/data/src/requires_simple/setup.py
@@ -1,6 +1,3 @@
from setuptools import find_packages, setup
-setup(name='requires_simple',
- version='0.1',
- install_requires=['simple==1.0']
- )
+setup(name="requires_simple", version="0.1", install_requires=["simple==1.0"])
diff --git a/tests/data/src/requires_simple_extra/setup.py b/tests/data/src/requires_simple_extra/setup.py
index 3378c2ce7..5562ebc95 100644
--- a/tests/data/src/requires_simple_extra/setup.py
+++ b/tests/data/src/requires_simple_extra/setup.py
@@ -1,9 +1,8 @@
from setuptools import setup
-setup(name='requires_simple_extra',
- version='0.1',
- py_modules=['requires_simple_extra'],
- extras_require={
- 'extra': ['simple==1.0']
- }
+setup(
+ name="requires_simple_extra",
+ version="0.1",
+ py_modules=["requires_simple_extra"],
+ extras_require={"extra": ["simple==1.0"]},
)
diff --git a/tests/data/src/setup_error/setup.py b/tests/data/src/setup_error/setup.py
new file mode 100644
index 000000000..d942355ca
--- /dev/null
+++ b/tests/data/src/setup_error/setup.py
@@ -0,0 +1,11 @@
+from setuptools import setup
+
+# This is to get an error that originates from setuptools, which generates a
+# decently sized output.
+setup(
+ cmdclass={
+ "egg_info": "<make-me-fail>",
+ "install": "<make-me-fail>",
+ "bdist_wheel": "<make-me-fail>",
+ }
+)
diff --git a/tests/data/src/simple_namespace/setup.py b/tests/data/src/simple_namespace/setup.py
index 9a49d52b7..c6b597864 100644
--- a/tests/data/src/simple_namespace/setup.py
+++ b/tests/data/src/simple_namespace/setup.py
@@ -1,8 +1,8 @@
from setuptools import setup
setup(
- name='simple_namespace',
- version='1.0',
- namespace_packages=['simple_namespace'],
- packages=['simple_namespace.module'],
+ name="simple_namespace",
+ version="1.0",
+ namespace_packages=["simple_namespace"],
+ packages=["simple_namespace.module"],
)
diff --git a/tests/data/src/simplewheel-1.0/setup.py b/tests/data/src/simplewheel-1.0/setup.py
index 461536dce..28ee850cd 100644
--- a/tests/data/src/simplewheel-1.0/setup.py
+++ b/tests/data/src/simplewheel-1.0/setup.py
@@ -3,7 +3,8 @@ from setuptools import setup
import simplewheel
-setup(name='simplewheel',
- version=simplewheel.__version__,
- packages=['simplewheel'],
- )
+setup(
+ name="simplewheel",
+ version=simplewheel.__version__,
+ packages=["simplewheel"],
+)
diff --git a/tests/data/src/simplewheel-1.0/simplewheel/__init__.py b/tests/data/src/simplewheel-1.0/simplewheel/__init__.py
index 7e49527e3..4802e90f8 100644
--- a/tests/data/src/simplewheel-1.0/simplewheel/__init__.py
+++ b/tests/data/src/simplewheel-1.0/simplewheel/__init__.py
@@ -1 +1 @@
-__version__ = '1.0'
+__version__ = "1.0"
diff --git a/tests/data/src/simplewheel-2.0/setup.py b/tests/data/src/simplewheel-2.0/setup.py
index 461536dce..28ee850cd 100644
--- a/tests/data/src/simplewheel-2.0/setup.py
+++ b/tests/data/src/simplewheel-2.0/setup.py
@@ -3,7 +3,8 @@ from setuptools import setup
import simplewheel
-setup(name='simplewheel',
- version=simplewheel.__version__,
- packages=['simplewheel'],
- )
+setup(
+ name="simplewheel",
+ version=simplewheel.__version__,
+ packages=["simplewheel"],
+)
diff --git a/tests/data/src/simplewheel-2.0/simplewheel/__init__.py b/tests/data/src/simplewheel-2.0/simplewheel/__init__.py
index 3b3dacb9a..f2dc0e400 100644
--- a/tests/data/src/simplewheel-2.0/simplewheel/__init__.py
+++ b/tests/data/src/simplewheel-2.0/simplewheel/__init__.py
@@ -1 +1 @@
-__version__ = '2.0'
+__version__ = "2.0"
diff --git a/tests/data/src/singlemodule/setup.py b/tests/data/src/singlemodule/setup.py
index 622af1f8e..e6358e2f7 100644
--- a/tests/data/src/singlemodule/setup.py
+++ b/tests/data/src/singlemodule/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup
setup(
name="singlemodule",
- version='0.0.1',
+ version="0.0.1",
description="A sample Python project with a single module",
- py_modules=['singlemodule'],
+ py_modules=["singlemodule"],
)
diff --git a/tests/data/src/withpyproject/setup.py b/tests/data/src/withpyproject/setup.py
index 1ea9e3e41..af10b3e3f 100644
--- a/tests/data/src/withpyproject/setup.py
+++ b/tests/data/src/withpyproject/setup.py
@@ -1,3 +1,3 @@
from setuptools import setup
-setup(name='withpyproject', version='0.0.1')
+setup(name="withpyproject", version="0.0.1")
diff --git a/tests/functional/test_bad_url.py b/tests/functional/test_bad_url.py
new file mode 100644
index 000000000..bc3a987e6
--- /dev/null
+++ b/tests/functional/test_bad_url.py
@@ -0,0 +1,16 @@
+# test the error message returned by pip when
+# a bad "file:" URL is passed to it.
+
+from typing import Any
+
+
+def test_filenotfound_error_message(script: Any) -> None:
+ # Test the error message returned when using a bad 'file:' URL.
+ # make pip to fail and get an error message
+ # by running "pip install -r file:nonexistent_file"
+ proc = script.pip("install", "-r", "file:unexistent_file", expect_error=True)
+ assert proc.returncode == 1
+ expect = (
+ "ERROR: 404 Client Error: FileNotFoundError for url: file:///unexistent_file"
+ )
+ assert proc.stderr.rstrip() == expect
diff --git a/tests/functional/test_broken_stdout.py b/tests/functional/test_broken_stdout.py
index 4baa4348b..ae0fdc1ba 100644
--- a/tests/functional/test_broken_stdout.py
+++ b/tests/functional/test_broken_stdout.py
@@ -1,25 +1,27 @@
import os
import subprocess
-import sys
+from pathlib import Path
+from typing import List, Tuple
-if sys.version_info < (3, 6):
- _BROKEN_STDOUT_RETURN_CODE = 1
-else:
- # The new exit status was added in Python 3.6 as a result of:
- # https://bugs.python.org/issue5319
- _BROKEN_STDOUT_RETURN_CODE = 120
+_BROKEN_STDOUT_RETURN_CODE = 120
-def setup_broken_stdout_test(args, deprecated_python):
+def setup_broken_stdout_test(
+ args: List[str], deprecated_python: bool
+) -> Tuple[str, int]:
proc = subprocess.Popen(
- args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
)
# Call close() on stdout to cause a broken pipe.
+ assert proc.stdout is not None
proc.stdout.close()
returncode = proc.wait()
- stderr = proc.stderr.read().decode('utf-8')
+ assert proc.stderr is not None
+ stderr = proc.stderr.read().decode("utf-8")
- expected_msg = 'ERROR: Pipe to stdout was broken'
+ expected_msg = "ERROR: Pipe to stdout was broken"
if deprecated_python:
assert expected_msg in stderr
else:
@@ -28,49 +30,51 @@ def setup_broken_stdout_test(args, deprecated_python):
return stderr, returncode
-def test_broken_stdout_pipe(deprecated_python):
+def test_broken_stdout_pipe(deprecated_python: bool) -> None:
"""
Test a broken pipe to stdout.
"""
stderr, returncode = setup_broken_stdout_test(
- ['pip', 'list'], deprecated_python=deprecated_python,
+ ["pip", "list"],
+ deprecated_python=deprecated_python,
)
# Check that no traceback occurs.
- assert 'raise BrokenStdoutLoggingError()' not in stderr
- assert stderr.count('Traceback') == 0
+ assert "raise BrokenStdoutLoggingError()" not in stderr
+ assert stderr.count("Traceback") == 0
assert returncode == _BROKEN_STDOUT_RETURN_CODE
-def test_broken_stdout_pipe__log_option(deprecated_python, tmpdir):
+def test_broken_stdout_pipe__log_option(deprecated_python: bool, tmpdir: Path) -> None:
"""
Test a broken pipe to stdout when --log is passed.
"""
- log_path = os.path.join(str(tmpdir), 'log.txt')
+ log_path = os.path.join(str(tmpdir), "log.txt")
stderr, returncode = setup_broken_stdout_test(
- ['pip', '--log', log_path, 'list'],
+ ["pip", "--log", log_path, "list"],
deprecated_python=deprecated_python,
)
# Check that no traceback occurs.
- assert 'raise BrokenStdoutLoggingError()' not in stderr
- assert stderr.count('Traceback') == 0
+ assert "raise BrokenStdoutLoggingError()" not in stderr
+ assert stderr.count("Traceback") == 0
assert returncode == _BROKEN_STDOUT_RETURN_CODE
-def test_broken_stdout_pipe__verbose(deprecated_python):
+def test_broken_stdout_pipe__verbose(deprecated_python: bool) -> None:
"""
Test a broken pipe to stdout with verbose logging enabled.
"""
stderr, returncode = setup_broken_stdout_test(
- ['pip', '-vv', 'list'], deprecated_python=deprecated_python,
+ ["pip", "-vv", "list"],
+ deprecated_python=deprecated_python,
)
# Check that a traceback occurs and that it occurs at most once.
# We permit up to two because the exception can be chained.
- assert 'raise BrokenStdoutLoggingError()' in stderr
- assert 1 <= stderr.count('Traceback') <= 2
+ assert "raise BrokenStdoutLoggingError()" in stderr
+ assert 1 <= stderr.count("Traceback") <= 2
assert returncode == _BROKEN_STDOUT_RETURN_CODE
diff --git a/tests/functional/test_build_env.py b/tests/functional/test_build_env.py
index b3f51a880..437adb995 100644
--- a/tests/functional/test_build_env.py
+++ b/tests/functional/test_build_env.py
@@ -1,22 +1,31 @@
+import os
from textwrap import dedent
+from typing import Optional
import pytest
from pip._internal.build_env import BuildEnvironment
-from tests.lib import create_basic_wheel_for_package, make_test_finder
+from tests.lib import (
+ PipTestEnvironment,
+ TestPipResult,
+ create_basic_wheel_for_package,
+ make_test_finder,
+)
-def indent(text, prefix):
- return '\n'.join((prefix if line else '') + line
- for line in text.split('\n'))
+def indent(text: str, prefix: str) -> str:
+ return "\n".join((prefix if line else "") + line for line in text.split("\n"))
-def run_with_build_env(script, setup_script_contents,
- test_script_contents=None):
- build_env_script = script.scratch_path / 'build_env.py'
+def run_with_build_env(
+ script: PipTestEnvironment,
+ setup_script_contents: str,
+ test_script_contents: Optional[str] = None,
+) -> TestPipResult:
+ build_env_script = script.scratch_path / "build_env.py"
build_env_script.write_text(
dedent(
- '''
+ """
import subprocess
import sys
@@ -32,7 +41,7 @@ def run_with_build_env(script, setup_script_contents,
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope.create([{scratch!r}], []),
+ search_scope=SearchScope.create([{scratch!r}], [], False),
)
selection_prefs = SelectionPreferences(
allow_yanked=True,
@@ -44,64 +53,70 @@ def run_with_build_env(script, setup_script_contents,
with global_tempdir_manager():
build_env = BuildEnvironment()
- '''.format(scratch=str(script.scratch_path))) +
- indent(dedent(setup_script_contents), ' ') +
- indent(
+ """.format(
+ scratch=str(script.scratch_path)
+ )
+ )
+ + indent(dedent(setup_script_contents), " ")
+ + indent(
dedent(
- '''
+ """
if len(sys.argv) > 1:
with build_env:
subprocess.check_call((sys.executable, sys.argv[1]))
- '''
+ """
),
- ' '
+ " ",
)
)
- args = ['python', build_env_script]
+ args = ["python", os.fspath(build_env_script)]
if test_script_contents is not None:
- test_script = script.scratch_path / 'test.py'
+ test_script = script.scratch_path / "test.py"
test_script.write_text(dedent(test_script_contents))
- args.append(test_script)
+ args.append(os.fspath(test_script))
return script.run(*args)
-def test_build_env_allow_empty_requirements_install():
+def test_build_env_allow_empty_requirements_install() -> None:
+ finder = make_test_finder()
build_env = BuildEnvironment()
- for prefix in ('normal', 'overlay'):
- build_env.install_requirements(None, [], prefix, None)
+ for prefix in ("normal", "overlay"):
+ build_env.install_requirements(
+ finder, [], prefix, kind="Installing build dependencies"
+ )
-def test_build_env_allow_only_one_install(script):
- create_basic_wheel_for_package(script, 'foo', '1.0')
- create_basic_wheel_for_package(script, 'bar', '1.0')
- finder = make_test_finder(find_links=[script.scratch_path])
+def test_build_env_allow_only_one_install(script: PipTestEnvironment) -> None:
+ create_basic_wheel_for_package(script, "foo", "1.0")
+ create_basic_wheel_for_package(script, "bar", "1.0")
+ finder = make_test_finder(find_links=[os.fspath(script.scratch_path)])
build_env = BuildEnvironment()
- for prefix in ('normal', 'overlay'):
+ for prefix in ("normal", "overlay"):
build_env.install_requirements(
- finder, ['foo'], prefix,
- f'installing foo in {prefix}')
+ finder, ["foo"], prefix, kind=f"installing foo in {prefix}"
+ )
with pytest.raises(AssertionError):
build_env.install_requirements(
- finder, ['bar'], prefix,
- f'installing bar in {prefix}')
+ finder, ["bar"], prefix, kind=f"installing bar in {prefix}"
+ )
with pytest.raises(AssertionError):
build_env.install_requirements(
- finder, [], prefix,
- f'installing in {prefix}')
+ finder, [], prefix, kind=f"installing in {prefix}"
+ )
-def test_build_env_requirements_check(script):
+def test_build_env_requirements_check(script: PipTestEnvironment) -> None:
- create_basic_wheel_for_package(script, 'foo', '2.0')
- create_basic_wheel_for_package(script, 'bar', '1.0')
- create_basic_wheel_for_package(script, 'bar', '3.0')
- create_basic_wheel_for_package(script, 'other', '0.5')
+ create_basic_wheel_for_package(script, "foo", "2.0")
+ create_basic_wheel_for_package(script, "bar", "1.0")
+ create_basic_wheel_for_package(script, "bar", "3.0")
+ create_basic_wheel_for_package(script, "other", "0.5")
- script.pip_install_local('-f', script.scratch_path, 'foo', 'bar', 'other')
+ script.pip_install_local("-f", script.scratch_path, "foo", "bar", "other")
run_with_build_env(
script,
- '''
+ """
r = build_env.check_requirements(['foo', 'bar', 'other'])
assert r == (set(), {'foo', 'bar', 'other'}), repr(r)
@@ -110,13 +125,14 @@ def test_build_env_requirements_check(script):
r = build_env.check_requirements(['foo>3.0', 'bar>=2.5'])
assert r == (set(), {'foo>3.0', 'bar>=2.5'}), repr(r)
- ''')
+ """,
+ )
run_with_build_env(
script,
- '''
+ """
build_env.install_requirements(finder, ['foo', 'bar==3.0'], 'normal',
- 'installing foo in normal')
+ kind='installing foo in normal')
r = build_env.check_requirements(['foo', 'bar', 'other'])
assert r == (set(), {'other'}), repr(r)
@@ -126,15 +142,16 @@ def test_build_env_requirements_check(script):
r = build_env.check_requirements(['foo>3.0', 'bar>=2.5'])
assert r == ({('foo==2.0', 'foo>3.0')}, set()), repr(r)
- ''')
+ """,
+ )
run_with_build_env(
script,
- '''
+ """
build_env.install_requirements(finder, ['foo', 'bar==3.0'], 'normal',
- 'installing foo in normal')
+ kind='installing foo in normal')
build_env.install_requirements(finder, ['bar==1.0'], 'overlay',
- 'installing foo in overlay')
+ kind='installing foo in overlay')
r = build_env.check_requirements(['foo', 'bar', 'other'])
assert r == (set(), {'other'}), repr(r)
@@ -145,53 +162,74 @@ def test_build_env_requirements_check(script):
r = build_env.check_requirements(['foo>3.0', 'bar>=2.5'])
assert r == ({('bar==1.0', 'bar>=2.5'), ('foo==2.0', 'foo>3.0')}, \
set()), repr(r)
- ''')
+ """,
+ )
+
+ run_with_build_env(
+ script,
+ """
+ build_env.install_requirements(
+ finder,
+ ["bar==3.0"],
+ "normal",
+ kind="installing bar in normal",
+ )
+ r = build_env.check_requirements(
+ [
+ "bar==2.0; python_version < '3.0'",
+ "bar==3.0; python_version >= '3.0'",
+ "foo==4.0; extra == 'dev'",
+ ],
+ )
+ assert r == (set(), set()), repr(r)
+ """,
+ )
-def test_build_env_overlay_prefix_has_priority(script):
- create_basic_wheel_for_package(script, 'pkg', '2.0')
- create_basic_wheel_for_package(script, 'pkg', '4.3')
+def test_build_env_overlay_prefix_has_priority(script: PipTestEnvironment) -> None:
+ create_basic_wheel_for_package(script, "pkg", "2.0")
+ create_basic_wheel_for_package(script, "pkg", "4.3")
result = run_with_build_env(
script,
- '''
+ """
build_env.install_requirements(finder, ['pkg==2.0'], 'overlay',
- 'installing pkg==2.0 in overlay')
+ kind='installing pkg==2.0 in overlay')
build_env.install_requirements(finder, ['pkg==4.3'], 'normal',
- 'installing pkg==4.3 in normal')
- ''',
- '''
+ kind='installing pkg==4.3 in normal')
+ """,
+ """
print(__import__('pkg').__version__)
- ''')
- assert result.stdout.strip() == '2.0', str(result)
+ """,
+ )
+ assert result.stdout.strip() == "2.0", str(result)
@pytest.mark.incompatible_with_test_venv
-def test_build_env_isolation(script):
+def test_build_env_isolation(script: PipTestEnvironment) -> None:
# Create dummy `pkg` wheel.
- pkg_whl = create_basic_wheel_for_package(script, 'pkg', '1.0')
+ pkg_whl = create_basic_wheel_for_package(script, "pkg", "1.0")
# Install it to site packages.
script.pip_install_local(pkg_whl)
# And a copy in the user site.
- script.pip_install_local('--ignore-installed', '--user', pkg_whl)
+ script.pip_install_local("--ignore-installed", "--user", pkg_whl)
# And to another directory available through a .pth file.
- target = script.scratch_path / 'pth_install'
- script.pip_install_local('-t', target, pkg_whl)
- (script.site_packages_path / 'build_requires.pth').write_text(
- str(target) + '\n'
- )
+ target = script.scratch_path / "pth_install"
+ script.pip_install_local("-t", target, pkg_whl)
+ (script.site_packages_path / "build_requires.pth").write_text(str(target) + "\n")
# And finally to yet another directory available through PYTHONPATH.
- target = script.scratch_path / 'pypath_install'
- script.pip_install_local('-t', target, pkg_whl)
+ target = script.scratch_path / "pypath_install"
+ script.pip_install_local("-t", target, pkg_whl)
script.environ["PYTHONPATH"] = target
run_with_build_env(
- script, '',
- r'''
+ script,
+ "",
+ r"""
from distutils.sysconfig import get_python_lib
import sys
@@ -209,4 +247,5 @@ def test_build_env_isolation(script):
})), file=sys.stderr)
print('sys.path:\n ' + '\n '.join(sys.path), file=sys.stderr)
sys.exit(1)
- ''')
+ """,
+ )
diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py
index 0dc791081..7d20f5e31 100644
--- a/tests/functional/test_cache.py
+++ b/tests/functional/test_cache.py
@@ -1,38 +1,41 @@
import os
import shutil
from glob import glob
+from typing import Callable, List, Tuple
import pytest
+from tests.lib import PipTestEnvironment, TestPipResult
+
@pytest.fixture
-def cache_dir(script):
+def cache_dir(script: PipTestEnvironment) -> str:
result = script.run(
- 'python', '-c',
- 'from pip._internal.locations import USER_CACHE_DIR;'
- 'print(USER_CACHE_DIR)'
+ "python",
+ "-c",
+ "from pip._internal.locations import USER_CACHE_DIR;print(USER_CACHE_DIR)",
)
return result.stdout.strip()
@pytest.fixture
-def http_cache_dir(cache_dir):
- return os.path.normcase(os.path.join(cache_dir, 'http'))
+def http_cache_dir(cache_dir: str) -> str:
+ return os.path.normcase(os.path.join(cache_dir, "http"))
@pytest.fixture
-def wheel_cache_dir(cache_dir):
- return os.path.normcase(os.path.join(cache_dir, 'wheels'))
+def wheel_cache_dir(cache_dir: str) -> str:
+ return os.path.normcase(os.path.join(cache_dir, "wheels"))
@pytest.fixture
-def http_cache_files(http_cache_dir):
- destination = os.path.join(http_cache_dir, 'arbitrary', 'pathname')
+def http_cache_files(http_cache_dir: str) -> List[str]:
+ destination = os.path.join(http_cache_dir, "arbitrary", "pathname")
if not os.path.exists(destination):
return []
- filenames = glob(os.path.join(destination, '*'))
+ filenames = glob(os.path.join(destination, "*"))
files = []
for filename in filenames:
files.append(os.path.join(destination, filename))
@@ -40,13 +43,13 @@ def http_cache_files(http_cache_dir):
@pytest.fixture
-def wheel_cache_files(wheel_cache_dir):
- destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname')
+def wheel_cache_files(wheel_cache_dir: str) -> List[str]:
+ destination = os.path.join(wheel_cache_dir, "arbitrary", "pathname")
if not os.path.exists(destination):
return []
- filenames = glob(os.path.join(destination, '*.whl'))
+ filenames = glob(os.path.join(destination, "*.whl"))
files = []
for filename in filenames:
files.append(os.path.join(destination, filename))
@@ -54,60 +57,60 @@ def wheel_cache_files(wheel_cache_dir):
@pytest.fixture
-def populate_http_cache(http_cache_dir):
- destination = os.path.join(http_cache_dir, 'arbitrary', 'pathname')
+def populate_http_cache(http_cache_dir: str) -> List[Tuple[str, str]]:
+ destination = os.path.join(http_cache_dir, "arbitrary", "pathname")
os.makedirs(destination)
files = [
- ('aaaaaaaaa', os.path.join(destination, 'aaaaaaaaa')),
- ('bbbbbbbbb', os.path.join(destination, 'bbbbbbbbb')),
- ('ccccccccc', os.path.join(destination, 'ccccccccc')),
+ ("aaaaaaaaa", os.path.join(destination, "aaaaaaaaa")),
+ ("bbbbbbbbb", os.path.join(destination, "bbbbbbbbb")),
+ ("ccccccccc", os.path.join(destination, "ccccccccc")),
]
for _name, filename in files:
- with open(filename, 'w'):
+ with open(filename, "w"):
pass
return files
@pytest.fixture
-def populate_wheel_cache(wheel_cache_dir):
- destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname')
+def populate_wheel_cache(wheel_cache_dir: str) -> List[Tuple[str, str]]:
+ destination = os.path.join(wheel_cache_dir, "arbitrary", "pathname")
os.makedirs(destination)
files = [
- ('yyy-1.2.3', os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl')),
- ('zzz-4.5.6', os.path.join(destination, 'zzz-4.5.6-py3-none-any.whl')),
- ('zzz-4.5.7', os.path.join(destination, 'zzz-4.5.7-py3-none-any.whl')),
- ('zzz-7.8.9', os.path.join(destination, 'zzz-7.8.9-py3-none-any.whl')),
+ ("yyy-1.2.3", os.path.join(destination, "yyy-1.2.3-py3-none-any.whl")),
+ ("zzz-4.5.6", os.path.join(destination, "zzz-4.5.6-py3-none-any.whl")),
+ ("zzz-4.5.7", os.path.join(destination, "zzz-4.5.7-py3-none-any.whl")),
+ ("zzz-7.8.9", os.path.join(destination, "zzz-7.8.9-py3-none-any.whl")),
]
for _name, filename in files:
- with open(filename, 'w'):
+ with open(filename, "w"):
pass
return files
@pytest.fixture
-def empty_wheel_cache(wheel_cache_dir):
+def empty_wheel_cache(wheel_cache_dir: str) -> None:
if os.path.exists(wheel_cache_dir):
shutil.rmtree(wheel_cache_dir)
-def list_matches_wheel(wheel_name, result):
+def list_matches_wheel(wheel_name: str, result: TestPipResult) -> bool:
"""Returns True if any line in `result`, which should be the output of
a `pip cache list` call, matches `wheel_name`.
E.g., If wheel_name is `foo-1.2.3` it searches for a line starting with
`- foo-1.2.3-py3-none-any.whl `."""
lines = result.stdout.splitlines()
- expected = f' - {wheel_name}-py3-none-any.whl '
+ expected = f" - {wheel_name}-py3-none-any.whl "
return any(map(lambda l: l.startswith(expected), lines))
-def list_matches_wheel_abspath(wheel_name, result):
+def list_matches_wheel_abspath(wheel_name: str, result: TestPipResult) -> bool:
"""Returns True if any line in `result`, which should be the output of
a `pip cache list --format=abspath` call, is a valid path and belongs to
`wheel_name`.
@@ -115,13 +118,20 @@ def list_matches_wheel_abspath(wheel_name, result):
E.g., If wheel_name is `foo-1.2.3` it searches for a line starting with
`foo-1.2.3-py3-none-any.whl`."""
lines = result.stdout.splitlines()
- expected = f'{wheel_name}-py3-none-any.whl'
- return any(map(lambda l: os.path.basename(l).startswith(expected)
- and os.path.exists(l), lines))
+ expected = f"{wheel_name}-py3-none-any.whl"
+ return any(
+ map(
+ lambda l: os.path.basename(l).startswith(expected) and os.path.exists(l),
+ lines,
+ )
+ )
+
+
+RemoveMatches = Callable[[str, TestPipResult], bool]
@pytest.fixture
-def remove_matches_http(http_cache_dir):
+def remove_matches_http(http_cache_dir: str) -> RemoveMatches:
"""Returns True if any line in `result`, which should be the output of
a `pip cache purge` call, matches `http_filename`.
@@ -129,22 +139,25 @@ def remove_matches_http(http_cache_dir):
`Removed <http files cache dir>/arbitrary/pathname/aaaaaaaaa`.
"""
- def _remove_matches_http(http_filename, result):
+ def _remove_matches_http(http_filename: str, result: TestPipResult) -> bool:
lines = result.stdout.splitlines()
# The "/arbitrary/pathname/" bit is an implementation detail of how
# the `populate_http_cache` fixture is implemented.
path = os.path.join(
- http_cache_dir, 'arbitrary', 'pathname', http_filename,
+ http_cache_dir,
+ "arbitrary",
+ "pathname",
+ http_filename,
)
- expected = f'Removed {path}'
+ expected = f"Removed {path}"
return expected in lines
return _remove_matches_http
@pytest.fixture
-def remove_matches_wheel(wheel_cache_dir):
+def remove_matches_wheel(wheel_cache_dir: str) -> RemoveMatches:
"""Returns True if any line in `result`, which should be the output of
a `pip cache remove`/`pip cache purge` call, matches `wheel_name`.
@@ -152,214 +165,240 @@ def remove_matches_wheel(wheel_cache_dir):
`Removed <wheel cache dir>/arbitrary/pathname/foo-1.2.3-py3-none-any.whl`.
"""
- def _remove_matches_wheel(wheel_name, result):
+ def _remove_matches_wheel(wheel_name: str, result: TestPipResult) -> bool:
lines = result.stdout.splitlines()
- wheel_filename = f'{wheel_name}-py3-none-any.whl'
+ wheel_filename = f"{wheel_name}-py3-none-any.whl"
# The "/arbitrary/pathname/" bit is an implementation detail of how
# the `populate_wheel_cache` fixture is implemented.
path = os.path.join(
- wheel_cache_dir, 'arbitrary', 'pathname', wheel_filename,
+ wheel_cache_dir,
+ "arbitrary",
+ "pathname",
+ wheel_filename,
)
- expected = f'Removed {path}'
+ expected = f"Removed {path}"
return expected in lines
return _remove_matches_wheel
-def test_cache_dir(script, cache_dir):
- result = script.pip('cache', 'dir')
+def test_cache_dir(script: PipTestEnvironment, cache_dir: str) -> None:
+ result = script.pip("cache", "dir")
assert os.path.normcase(cache_dir) == result.stdout.strip()
-def test_cache_dir_too_many_args(script, cache_dir):
- result = script.pip('cache', 'dir', 'aaa', expect_error=True)
+def test_cache_dir_too_many_args(script: PipTestEnvironment, cache_dir: str) -> None:
+ result = script.pip("cache", "dir", "aaa", expect_error=True)
- assert result.stdout == ''
+ assert result.stdout == ""
# This would be `result.stderr == ...`, but pip prints deprecation
# warnings on Python 2.7, so we check if the _line_ is in stderr.
- assert 'ERROR: Too many arguments' in result.stderr.splitlines()
+ assert "ERROR: Too many arguments" in result.stderr.splitlines()
@pytest.mark.usefixtures("populate_http_cache", "populate_wheel_cache")
def test_cache_info(
- script, http_cache_dir, wheel_cache_dir, wheel_cache_files
-):
- result = script.pip('cache', 'info')
-
- assert (
- f'Package index page cache location: {http_cache_dir}'
- in result.stdout
- )
- assert f'Wheels location: {wheel_cache_dir}' in result.stdout
+ script: PipTestEnvironment,
+ http_cache_dir: str,
+ wheel_cache_dir: str,
+ wheel_cache_files: List[str],
+) -> None:
+ result = script.pip("cache", "info")
+
+ assert f"Package index page cache location: {http_cache_dir}" in result.stdout
+ assert f"Locally built wheels location: {wheel_cache_dir}" in result.stdout
num_wheels = len(wheel_cache_files)
- assert f'Number of wheels: {num_wheels}' in result.stdout
+ assert f"Number of locally built wheels: {num_wheels}" in result.stdout
@pytest.mark.usefixtures("populate_wheel_cache")
-def test_cache_list(script):
+def test_cache_list(script: PipTestEnvironment) -> None:
"""Running `pip cache list` should return exactly what the
populate_wheel_cache fixture adds."""
- result = script.pip('cache', 'list')
+ result = script.pip("cache", "list")
- assert list_matches_wheel('yyy-1.2.3', result)
- assert list_matches_wheel('zzz-4.5.6', result)
- assert list_matches_wheel('zzz-4.5.7', result)
- assert list_matches_wheel('zzz-7.8.9', result)
+ assert list_matches_wheel("yyy-1.2.3", result)
+ assert list_matches_wheel("zzz-4.5.6", result)
+ assert list_matches_wheel("zzz-4.5.7", result)
+ assert list_matches_wheel("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_wheel_cache")
-def test_cache_list_abspath(script):
+def test_cache_list_abspath(script: PipTestEnvironment) -> None:
"""Running `pip cache list --format=abspath` should return full
paths of exactly what the populate_wheel_cache fixture adds."""
- result = script.pip('cache', 'list', '--format=abspath')
+ result = script.pip("cache", "list", "--format=abspath")
- assert list_matches_wheel_abspath('yyy-1.2.3', result)
- assert list_matches_wheel_abspath('zzz-4.5.6', result)
- assert list_matches_wheel_abspath('zzz-4.5.7', result)
- assert list_matches_wheel_abspath('zzz-7.8.9', result)
+ assert list_matches_wheel_abspath("yyy-1.2.3", result)
+ assert list_matches_wheel_abspath("zzz-4.5.6", result)
+ assert list_matches_wheel_abspath("zzz-4.5.7", result)
+ assert list_matches_wheel_abspath("zzz-7.8.9", result)
@pytest.mark.usefixtures("empty_wheel_cache")
-def test_cache_list_with_empty_cache(script):
+def test_cache_list_with_empty_cache(script: PipTestEnvironment) -> None:
"""Running `pip cache list` with an empty cache should print
- "Nothing cached." and exit."""
- result = script.pip('cache', 'list')
- assert result.stdout == "Nothing cached.\n"
+ "No locally built wheels cached." and exit."""
+ result = script.pip("cache", "list")
+ assert result.stdout == "No locally built wheels cached.\n"
@pytest.mark.usefixtures("empty_wheel_cache")
-def test_cache_list_with_empty_cache_abspath(script):
+def test_cache_list_with_empty_cache_abspath(script: PipTestEnvironment) -> None:
"""Running `pip cache list --format=abspath` with an empty cache should not
print anything and exit."""
- result = script.pip('cache', 'list', '--format=abspath')
+ result = script.pip("cache", "list", "--format=abspath")
assert result.stdout.strip() == ""
-def test_cache_list_too_many_args(script):
+@pytest.mark.usefixtures("empty_wheel_cache")
+def test_cache_purge_with_empty_cache(script: PipTestEnvironment) -> None:
+ """Running `pip cache purge` with an empty cache should print a warning
+ and exit without an error code."""
+ result = script.pip("cache", "purge", allow_stderr_warning=True)
+ assert result.stderr == "WARNING: No matching packages\n"
+ assert result.stdout == "Files removed: 0\n"
+
+
+@pytest.mark.usefixtures("populate_wheel_cache")
+def test_cache_remove_with_bad_pattern(script: PipTestEnvironment) -> None:
+ """Running `pip cache remove` with a bad pattern should print a warning
+ and exit without an error code."""
+ result = script.pip("cache", "remove", "aaa", allow_stderr_warning=True)
+ assert result.stderr == 'WARNING: No matching packages for pattern "aaa"\n'
+ assert result.stdout == "Files removed: 0\n"
+
+
+def test_cache_list_too_many_args(script: PipTestEnvironment) -> None:
"""Passing `pip cache list` too many arguments should cause an error."""
- script.pip('cache', 'list', 'aaa', 'bbb',
- expect_error=True)
+ script.pip("cache", "list", "aaa", "bbb", expect_error=True)
@pytest.mark.usefixtures("populate_wheel_cache")
-def test_cache_list_name_match(script):
+def test_cache_list_name_match(script: PipTestEnvironment) -> None:
"""Running `pip cache list zzz` should list zzz-4.5.6, zzz-4.5.7,
zzz-7.8.9, but nothing else."""
- result = script.pip('cache', 'list', 'zzz', '--verbose')
+ result = script.pip("cache", "list", "zzz", "--verbose")
- assert not list_matches_wheel('yyy-1.2.3', result)
- assert list_matches_wheel('zzz-4.5.6', result)
- assert list_matches_wheel('zzz-4.5.7', result)
- assert list_matches_wheel('zzz-7.8.9', result)
+ assert not list_matches_wheel("yyy-1.2.3", result)
+ assert list_matches_wheel("zzz-4.5.6", result)
+ assert list_matches_wheel("zzz-4.5.7", result)
+ assert list_matches_wheel("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_wheel_cache")
-def test_cache_list_name_match_abspath(script):
+def test_cache_list_name_match_abspath(script: PipTestEnvironment) -> None:
"""Running `pip cache list zzz --format=abspath` should list paths of
zzz-4.5.6, zzz-4.5.7, zzz-7.8.9, but nothing else."""
- result = script.pip('cache', 'list', 'zzz', '--format=abspath',
- '--verbose')
+ result = script.pip("cache", "list", "zzz", "--format=abspath", "--verbose")
- assert not list_matches_wheel_abspath('yyy-1.2.3', result)
- assert list_matches_wheel_abspath('zzz-4.5.6', result)
- assert list_matches_wheel_abspath('zzz-4.5.7', result)
- assert list_matches_wheel_abspath('zzz-7.8.9', result)
+ assert not list_matches_wheel_abspath("yyy-1.2.3", result)
+ assert list_matches_wheel_abspath("zzz-4.5.6", result)
+ assert list_matches_wheel_abspath("zzz-4.5.7", result)
+ assert list_matches_wheel_abspath("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_wheel_cache")
-def test_cache_list_name_and_version_match(script):
+def test_cache_list_name_and_version_match(script: PipTestEnvironment) -> None:
"""Running `pip cache list zzz-4.5.6` should list zzz-4.5.6, but
nothing else."""
- result = script.pip('cache', 'list', 'zzz-4.5.6', '--verbose')
+ result = script.pip("cache", "list", "zzz-4.5.6", "--verbose")
- assert not list_matches_wheel('yyy-1.2.3', result)
- assert list_matches_wheel('zzz-4.5.6', result)
- assert not list_matches_wheel('zzz-4.5.7', result)
- assert not list_matches_wheel('zzz-7.8.9', result)
+ assert not list_matches_wheel("yyy-1.2.3", result)
+ assert list_matches_wheel("zzz-4.5.6", result)
+ assert not list_matches_wheel("zzz-4.5.7", result)
+ assert not list_matches_wheel("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_wheel_cache")
-def test_cache_list_name_and_version_match_abspath(script):
+def test_cache_list_name_and_version_match_abspath(script: PipTestEnvironment) -> None:
"""Running `pip cache list zzz-4.5.6 --format=abspath` should list path of
zzz-4.5.6, but nothing else."""
- result = script.pip('cache', 'list', 'zzz-4.5.6', '--format=abspath',
- '--verbose')
+ result = script.pip("cache", "list", "zzz-4.5.6", "--format=abspath", "--verbose")
- assert not list_matches_wheel_abspath('yyy-1.2.3', result)
- assert list_matches_wheel_abspath('zzz-4.5.6', result)
- assert not list_matches_wheel_abspath('zzz-4.5.7', result)
- assert not list_matches_wheel_abspath('zzz-7.8.9', result)
+ assert not list_matches_wheel_abspath("yyy-1.2.3", result)
+ assert list_matches_wheel_abspath("zzz-4.5.6", result)
+ assert not list_matches_wheel_abspath("zzz-4.5.7", result)
+ assert not list_matches_wheel_abspath("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_wheel_cache")
-def test_cache_remove_no_arguments(script):
+def test_cache_remove_no_arguments(script: PipTestEnvironment) -> None:
"""Running `pip cache remove` with no arguments should cause an error."""
- script.pip('cache', 'remove', expect_error=True)
+ script.pip("cache", "remove", expect_error=True)
-def test_cache_remove_too_many_args(script):
+def test_cache_remove_too_many_args(script: PipTestEnvironment) -> None:
"""Passing `pip cache remove` too many arguments should cause an error."""
- script.pip('cache', 'remove', 'aaa', 'bbb',
- expect_error=True)
+ script.pip("cache", "remove", "aaa", "bbb", expect_error=True)
@pytest.mark.usefixtures("populate_wheel_cache")
-def test_cache_remove_name_match(script, remove_matches_wheel):
+def test_cache_remove_name_match(
+ script: PipTestEnvironment, remove_matches_wheel: RemoveMatches
+) -> None:
"""Running `pip cache remove zzz` should remove zzz-4.5.6 and zzz-7.8.9,
but nothing else."""
- result = script.pip('cache', 'remove', 'zzz', '--verbose')
+ result = script.pip("cache", "remove", "zzz", "--verbose")
- assert not remove_matches_wheel('yyy-1.2.3', result)
- assert remove_matches_wheel('zzz-4.5.6', result)
- assert remove_matches_wheel('zzz-4.5.7', result)
- assert remove_matches_wheel('zzz-7.8.9', result)
+ assert not remove_matches_wheel("yyy-1.2.3", result)
+ assert remove_matches_wheel("zzz-4.5.6", result)
+ assert remove_matches_wheel("zzz-4.5.7", result)
+ assert remove_matches_wheel("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_wheel_cache")
-def test_cache_remove_name_and_version_match(script, remove_matches_wheel):
+def test_cache_remove_name_and_version_match(
+ script: PipTestEnvironment, remove_matches_wheel: RemoveMatches
+) -> None:
"""Running `pip cache remove zzz-4.5.6` should remove zzz-4.5.6, but
nothing else."""
- result = script.pip('cache', 'remove', 'zzz-4.5.6', '--verbose')
+ result = script.pip("cache", "remove", "zzz-4.5.6", "--verbose")
- assert not remove_matches_wheel('yyy-1.2.3', result)
- assert remove_matches_wheel('zzz-4.5.6', result)
- assert not remove_matches_wheel('zzz-4.5.7', result)
- assert not remove_matches_wheel('zzz-7.8.9', result)
+ assert not remove_matches_wheel("yyy-1.2.3", result)
+ assert remove_matches_wheel("zzz-4.5.6", result)
+ assert not remove_matches_wheel("zzz-4.5.7", result)
+ assert not remove_matches_wheel("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_http_cache", "populate_wheel_cache")
-def test_cache_purge(script, remove_matches_http, remove_matches_wheel):
+def test_cache_purge(
+ script: PipTestEnvironment,
+ remove_matches_http: RemoveMatches,
+ remove_matches_wheel: RemoveMatches,
+) -> None:
"""Running `pip cache purge` should remove all cached http files and
wheels."""
- result = script.pip('cache', 'purge', '--verbose')
+ result = script.pip("cache", "purge", "--verbose")
- assert remove_matches_http('aaaaaaaaa', result)
- assert remove_matches_http('bbbbbbbbb', result)
- assert remove_matches_http('ccccccccc', result)
+ assert remove_matches_http("aaaaaaaaa", result)
+ assert remove_matches_http("bbbbbbbbb", result)
+ assert remove_matches_http("ccccccccc", result)
- assert remove_matches_wheel('yyy-1.2.3', result)
- assert remove_matches_wheel('zzz-4.5.6', result)
- assert remove_matches_wheel('zzz-4.5.7', result)
- assert remove_matches_wheel('zzz-7.8.9', result)
+ assert remove_matches_wheel("yyy-1.2.3", result)
+ assert remove_matches_wheel("zzz-4.5.6", result)
+ assert remove_matches_wheel("zzz-4.5.7", result)
+ assert remove_matches_wheel("zzz-7.8.9", result)
@pytest.mark.usefixtures("populate_http_cache", "populate_wheel_cache")
def test_cache_purge_too_many_args(
- script, http_cache_files, wheel_cache_files
-):
+ script: PipTestEnvironment,
+ http_cache_files: List[str],
+ wheel_cache_files: List[str],
+) -> None:
"""Running `pip cache purge aaa` should raise an error and remove no
cached http files or wheels."""
- result = script.pip('cache', 'purge', 'aaa', '--verbose',
- expect_error=True)
- assert result.stdout == ''
+ result = script.pip("cache", "purge", "aaa", "--verbose", expect_error=True)
+ assert result.stdout == ""
# This would be `result.stderr == ...`, but pip prints deprecation
# warnings on Python 2.7, so we check if the _line_ is in stderr.
- assert 'ERROR: Too many arguments' in result.stderr.splitlines()
+ assert "ERROR: Too many arguments" in result.stderr.splitlines()
# Make sure nothing was deleted.
for filename in http_cache_files + wheel_cache_files:
@@ -367,12 +406,15 @@ def test_cache_purge_too_many_args(
@pytest.mark.parametrize("command", ["info", "list", "remove", "purge"])
-def test_cache_abort_when_no_cache_dir(script, command):
+def test_cache_abort_when_no_cache_dir(
+ script: PipTestEnvironment, command: str
+) -> None:
"""Running any pip cache command when cache is disabled should
abort and log an informative error"""
- result = script.pip('cache', command, '--no-cache-dir',
- expect_error=True)
- assert result.stdout == ''
+ result = script.pip("cache", command, "--no-cache-dir", expect_error=True)
+ assert result.stdout == ""
- assert ('ERROR: pip cache commands can not function'
- ' since cache is disabled.' in result.stderr.splitlines())
+ assert (
+ "ERROR: pip cache commands can not function"
+ " since cache is disabled." in result.stderr.splitlines()
+ )
diff --git a/tests/functional/test_check.py b/tests/functional/test_check.py
index 5cb41a97e..e2b1c60ef 100644
--- a/tests/functional/test_check.py
+++ b/tests/functional/test_check.py
@@ -1,146 +1,158 @@
-from tests.lib import create_test_package_with_setup
+from typing import Collection
+from tests.lib import PipTestEnvironment, create_test_package_with_setup
-def matches_expected_lines(string, expected_lines):
+
+def matches_expected_lines(string: str, expected_lines: Collection[str]) -> bool:
# Ignore empty lines
output_lines = list(filter(None, string.splitlines()))
# We'll match the last n lines, given n lines to match.
- last_few_output_lines = output_lines[-len(expected_lines):]
+ last_few_output_lines = output_lines[-len(expected_lines) :]
# And order does not matter
return set(last_few_output_lines) == set(expected_lines)
-def test_basic_check_clean(script):
- """On a clean environment, check should print a helpful message.
-
- """
- result = script.pip('check')
+def test_basic_check_clean(script: PipTestEnvironment) -> None:
+ """On a clean environment, check should print a helpful message."""
+ result = script.pip("check")
- expected_lines = (
- "No broken requirements found.",
- )
+ expected_lines = ("No broken requirements found.",)
assert matches_expected_lines(result.stdout, expected_lines)
assert result.returncode == 0
-def test_basic_check_missing_dependency(script):
+def test_basic_check_missing_dependency(script: PipTestEnvironment) -> None:
# Setup a small project
pkga_path = create_test_package_with_setup(
script,
- name='pkga', version='1.0', install_requires=['missing==0.1'],
+ name="pkga",
+ version="1.0",
+ install_requires=["missing==0.1"],
)
# Let's install pkga without its dependency
- res = script.pip('install', '--no-index', pkga_path, '--no-deps')
+ res = script.pip("install", "--no-index", pkga_path, "--no-deps")
assert "Successfully installed pkga-1.0" in res.stdout, str(res)
- result = script.pip('check', expect_error=True)
+ result = script.pip("check", expect_error=True)
- expected_lines = (
- "pkga 1.0 requires missing, which is not installed.",
- )
+ expected_lines = ("pkga 1.0 requires missing, which is not installed.",)
assert matches_expected_lines(result.stdout, expected_lines)
assert result.returncode == 1
-def test_basic_check_broken_dependency(script):
+def test_basic_check_broken_dependency(script: PipTestEnvironment) -> None:
# Setup pkga depending on pkgb>=1.0
pkga_path = create_test_package_with_setup(
script,
- name='pkga', version='1.0', install_requires=['broken>=1.0'],
+ name="pkga",
+ version="1.0",
+ install_requires=["broken>=1.0"],
)
# Let's install pkga without its dependency
- res = script.pip('install', '--no-index', pkga_path, '--no-deps')
+ res = script.pip("install", "--no-index", pkga_path, "--no-deps")
assert "Successfully installed pkga-1.0" in res.stdout, str(res)
# Setup broken==0.1
broken_path = create_test_package_with_setup(
script,
- name='broken', version='0.1',
+ name="broken",
+ version="0.1",
)
# Let's install broken==0.1
res = script.pip(
- 'install', '--no-index', broken_path, '--no-warn-conflicts',
+ "install",
+ "--no-index",
+ broken_path,
+ "--no-warn-conflicts",
)
assert "Successfully installed broken-0.1" in res.stdout, str(res)
- result = script.pip('check', expect_error=True)
+ result = script.pip("check", expect_error=True)
- expected_lines = (
- "pkga 1.0 has requirement broken>=1.0, but you have broken 0.1.",
- )
+ expected_lines = ("pkga 1.0 has requirement broken>=1.0, but you have broken 0.1.",)
assert matches_expected_lines(result.stdout, expected_lines)
assert result.returncode == 1
-def test_basic_check_broken_dependency_and_missing_dependency(script):
+def test_basic_check_broken_dependency_and_missing_dependency(
+ script: PipTestEnvironment,
+) -> None:
pkga_path = create_test_package_with_setup(
script,
- name='pkga', version='1.0', install_requires=['broken>=1.0'],
+ name="pkga",
+ version="1.0",
+ install_requires=["broken>=1.0"],
)
# Let's install pkga without its dependency
- res = script.pip('install', '--no-index', pkga_path, '--no-deps')
+ res = script.pip("install", "--no-index", pkga_path, "--no-deps")
assert "Successfully installed pkga-1.0" in res.stdout, str(res)
# Setup broken==0.1
broken_path = create_test_package_with_setup(
script,
- name='broken', version='0.1', install_requires=['missing'],
+ name="broken",
+ version="0.1",
+ install_requires=["missing"],
)
# Let's install broken==0.1
- res = script.pip('install', '--no-index', broken_path, '--no-deps')
+ res = script.pip("install", "--no-index", broken_path, "--no-deps")
assert "Successfully installed broken-0.1" in res.stdout, str(res)
- result = script.pip('check', expect_error=True)
+ result = script.pip("check", expect_error=True)
expected_lines = (
"broken 0.1 requires missing, which is not installed.",
- "pkga 1.0 has requirement broken>=1.0, but you have broken 0.1."
+ "pkga 1.0 has requirement broken>=1.0, but you have broken 0.1.",
)
assert matches_expected_lines(result.stdout, expected_lines)
assert result.returncode == 1
-def test_check_complicated_name_missing(script):
+def test_check_complicated_name_missing(script: PipTestEnvironment) -> None:
package_a_path = create_test_package_with_setup(
script,
- name='package_A', version='1.0',
- install_requires=['Dependency-B>=1.0'],
+ name="package_A",
+ version="1.0",
+ install_requires=["Dependency-B>=1.0"],
)
# Without dependency
- result = script.pip('install', '--no-index', package_a_path, '--no-deps')
+ result = script.pip("install", "--no-index", package_a_path, "--no-deps")
assert "Successfully installed package-A-1.0" in result.stdout, str(result)
- result = script.pip('check', expect_error=True)
- expected_lines = (
- "package-a 1.0 requires dependency-b, which is not installed.",
- )
+ result = script.pip("check", expect_error=True)
+ expected_lines = ("package-a 1.0 requires dependency-b, which is not installed.",)
assert matches_expected_lines(result.stdout, expected_lines)
assert result.returncode == 1
-def test_check_complicated_name_broken(script):
+def test_check_complicated_name_broken(script: PipTestEnvironment) -> None:
package_a_path = create_test_package_with_setup(
script,
- name='package_A', version='1.0',
- install_requires=['Dependency-B>=1.0'],
+ name="package_A",
+ version="1.0",
+ install_requires=["Dependency-B>=1.0"],
)
dependency_b_path_incompatible = create_test_package_with_setup(
script,
- name='dependency-b', version='0.1',
+ name="dependency-b",
+ version="0.1",
)
# With broken dependency
- result = script.pip('install', '--no-index', package_a_path, '--no-deps')
+ result = script.pip("install", "--no-index", package_a_path, "--no-deps")
assert "Successfully installed package-A-1.0" in result.stdout, str(result)
result = script.pip(
- 'install', '--no-index', dependency_b_path_incompatible, '--no-deps',
+ "install",
+ "--no-index",
+ dependency_b_path_incompatible,
+ "--no-deps",
)
assert "Successfully installed dependency-b-0.1" in result.stdout
- result = script.pip('check', expect_error=True)
+ result = script.pip("check", expect_error=True)
expected_lines = (
"package-a 1.0 has requirement Dependency-B>=1.0, but you have "
"dependency-b 0.1.",
@@ -149,101 +161,110 @@ def test_check_complicated_name_broken(script):
assert result.returncode == 1
-def test_check_complicated_name_clean(script):
+def test_check_complicated_name_clean(script: PipTestEnvironment) -> None:
package_a_path = create_test_package_with_setup(
script,
- name='package_A', version='1.0',
- install_requires=['Dependency-B>=1.0'],
+ name="package_A",
+ version="1.0",
+ install_requires=["Dependency-B>=1.0"],
)
dependency_b_path = create_test_package_with_setup(
script,
- name='dependency-b', version='1.0',
+ name="dependency-b",
+ version="1.0",
)
- result = script.pip('install', '--no-index', package_a_path, '--no-deps')
+ result = script.pip("install", "--no-index", package_a_path, "--no-deps")
assert "Successfully installed package-A-1.0" in result.stdout, str(result)
result = script.pip(
- 'install', '--no-index', dependency_b_path, '--no-deps',
+ "install",
+ "--no-index",
+ dependency_b_path,
+ "--no-deps",
)
assert "Successfully installed dependency-b-1.0" in result.stdout
- result = script.pip('check')
- expected_lines = (
- "No broken requirements found.",
- )
+ result = script.pip("check")
+ expected_lines = ("No broken requirements found.",)
assert matches_expected_lines(result.stdout, expected_lines)
assert result.returncode == 0
-def test_check_considers_conditional_reqs(script):
+def test_check_considers_conditional_reqs(script: PipTestEnvironment) -> None:
package_a_path = create_test_package_with_setup(
script,
- name='package_A', version='1.0',
+ name="package_A",
+ version="1.0",
install_requires=[
"Dependency-B>=1.0; python_version != '2.7'",
"Dependency-B>=2.0; python_version == '2.7'",
],
)
- result = script.pip('install', '--no-index', package_a_path, '--no-deps')
+ result = script.pip("install", "--no-index", package_a_path, "--no-deps")
assert "Successfully installed package-A-1.0" in result.stdout, str(result)
- result = script.pip('check', expect_error=True)
- expected_lines = (
- "package-a 1.0 requires dependency-b, which is not installed.",
- )
+ result = script.pip("check", expect_error=True)
+ expected_lines = ("package-a 1.0 requires dependency-b, which is not installed.",)
assert matches_expected_lines(result.stdout, expected_lines)
assert result.returncode == 1
-def test_check_development_versions_are_also_considered(script):
+def test_check_development_versions_are_also_considered(
+ script: PipTestEnvironment,
+) -> None:
# Setup pkga depending on pkgb>=1.0
pkga_path = create_test_package_with_setup(
script,
- name='pkga', version='1.0', install_requires=['depend>=1.0'],
+ name="pkga",
+ version="1.0",
+ install_requires=["depend>=1.0"],
)
# Let's install pkga without its dependency
- res = script.pip('install', '--no-index', pkga_path, '--no-deps')
+ res = script.pip("install", "--no-index", pkga_path, "--no-deps")
assert "Successfully installed pkga-1.0" in res.stdout, str(res)
# Setup depend==1.1.0.dev0
depend_path = create_test_package_with_setup(
script,
- name='depend', version='1.1.0.dev0',
+ name="depend",
+ version="1.1.0.dev0",
)
# Let's install depend==1.1.0.dev0
res = script.pip(
- 'install', '--no-index', depend_path, '--no-warn-conflicts',
+ "install",
+ "--no-index",
+ depend_path,
+ "--no-warn-conflicts",
)
assert "Successfully installed depend-1.1.0.dev0" in res.stdout, str(res)
- result = script.pip('check')
- expected_lines = (
- "No broken requirements found.",
- )
+ result = script.pip("check")
+ expected_lines = ("No broken requirements found.",)
assert matches_expected_lines(result.stdout, expected_lines)
assert result.returncode == 0
-def test_basic_check_broken_metadata(script):
+def test_basic_check_broken_metadata(script: PipTestEnvironment) -> None:
# Create some corrupt metadata
- dist_info_dir = script.site_packages_path / 'pkga-1.0.dist-info'
+ dist_info_dir = script.site_packages_path / "pkga-1.0.dist-info"
dist_info_dir.mkdir()
- with open(dist_info_dir / 'METADATA', 'w') as f:
- f.write('Metadata-Version: 2.1\n'
- 'Name: pkga\n'
- 'Version: 1.0\n'
- 'Requires-Dist: pip; python_version == "3.4";extra == "test"\n'
- )
+ with open(dist_info_dir / "METADATA", "w") as f:
+ f.write(
+ "Metadata-Version: 2.1\n"
+ "Name: pkga\n"
+ "Version: 1.0\n"
+ 'Requires-Dist: pip; python_version == "3.4";extra == "test"\n'
+ )
- result = script.pip('check', expect_error=True)
+ result = script.pip("check", expect_error=True)
- assert 'Error parsing requirements' in result.stderr
+ assert "Error parsing requirements" in result.stderr
assert result.returncode == 1
-def test_check_skip_work_dir_pkg(script):
+def test_check_skip_work_dir_pkg(script: PipTestEnvironment) -> None:
"""
Test that check should not include package
present in working directory
@@ -252,23 +273,20 @@ def test_check_skip_work_dir_pkg(script):
# Create a test package with dependency missing
# and create .egg-info dir
pkg_path = create_test_package_with_setup(
- script, name='simple', version='1.0',
- install_requires=['missing==0.1'])
+ script, name="simple", version="1.0", install_requires=["missing==0.1"]
+ )
- script.run('python', 'setup.py', 'egg_info',
- expect_stderr=True, cwd=pkg_path)
+ script.run("python", "setup.py", "egg_info", expect_stderr=True, cwd=pkg_path)
# Check should not complain about broken requirements
# when run from package directory
- result = script.pip('check', cwd=pkg_path)
- expected_lines = (
- "No broken requirements found.",
- )
+ result = script.pip("check", cwd=pkg_path)
+ expected_lines = ("No broken requirements found.",)
assert matches_expected_lines(result.stdout, expected_lines)
assert result.returncode == 0
-def test_check_include_work_dir_pkg(script):
+def test_check_include_work_dir_pkg(script: PipTestEnvironment) -> None:
"""
Test that check should include package in working directory
if working directory is added in PYTHONPATH
@@ -277,20 +295,17 @@ def test_check_include_work_dir_pkg(script):
# Create a test package with dependency missing
# and create .egg-info dir
pkg_path = create_test_package_with_setup(
- script, name='simple', version='1.0',
- install_requires=['missing==0.1'])
+ script, name="simple", version="1.0", install_requires=["missing==0.1"]
+ )
- script.run('python', 'setup.py', 'egg_info',
- expect_stderr=True, cwd=pkg_path)
+ script.run("python", "setup.py", "egg_info", expect_stderr=True, cwd=pkg_path)
- script.environ.update({'PYTHONPATH': pkg_path})
+ script.environ.update({"PYTHONPATH": pkg_path})
# Check should mention about missing requirement simple
# when run from package directory, when package directory
# is in PYTHONPATH
- result = script.pip('check', expect_error=True, cwd=pkg_path)
- expected_lines = (
- "simple 1.0 requires missing, which is not installed.",
- )
+ result = script.pip("check", expect_error=True, cwd=pkg_path)
+ expected_lines = ("simple 1.0 requires missing, which is not installed.",)
assert matches_expected_lines(result.stdout, expected_lines)
assert result.returncode == 1
diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py
index e41631512..a1b69b721 100644
--- a/tests/functional/test_cli.py
+++ b/tests/functional/test_cli.py
@@ -4,16 +4,26 @@ from textwrap import dedent
import pytest
+from tests.lib import PipTestEnvironment
+
+
+@pytest.mark.parametrize(
+ "entrypoint",
+ [
+ ("fake_pip = pip._internal.main:main",),
+ ("fake_pip = pip._internal:main",),
+ ("fake_pip = pip:main",),
+ ],
+)
+def test_entrypoints_work(entrypoint: str, script: PipTestEnvironment) -> None:
+ if script.zipapp:
+ pytest.skip("Zipapp does not include entrypoints")
-@pytest.mark.parametrize("entrypoint", [
- ("fake_pip = pip._internal.main:main",),
- ("fake_pip = pip._internal:main",),
- ("fake_pip = pip:main",),
-])
-def test_entrypoints_work(entrypoint, script):
fake_pkg = script.temp_path / "fake_pkg"
fake_pkg.mkdir()
- fake_pkg.joinpath("setup.py").write_text(dedent("""
+ fake_pkg.joinpath("setup.py").write_text(
+ dedent(
+ """
from setuptools import setup
setup(
@@ -25,9 +35,14 @@ def test_entrypoints_work(entrypoint, script):
]
}}
)
- """.format(entrypoint)))
+ """.format(
+ entrypoint
+ )
+ )
+ )
- script.pip("install", "-vvv", str(fake_pkg))
+ # expect_temp because pip install will generate fake_pkg.egg-info
+ script.pip("install", "-vvv", str(fake_pkg), expect_temp=True)
result = script.pip("-V")
result2 = script.run("fake_pip", "-V", allow_stderr_warning=True)
assert result.stdout == result2.stdout
diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py
index 8a7464982..b02cd4fa3 100644
--- a/tests/functional/test_completion.py
+++ b/tests/functional/test_completion.py
@@ -1,20 +1,36 @@
import os
import sys
+from pathlib import Path
+from typing import TYPE_CHECKING, Tuple, Union
import pytest
-from tests.lib.path import Path
+from tests.conftest import ScriptFactory
+from tests.lib import PipTestEnvironment, TestData, TestPipResult
+
+if TYPE_CHECKING:
+ from typing import Protocol
+else:
+ # TODO: Protocol was introduced in Python 3.8. Remove this branch when
+ # dropping support for Python 3.7.
+ Protocol = object
+
COMPLETION_FOR_SUPPORTED_SHELLS_TESTS = (
- ('bash', """\
+ (
+ "bash",
+ """\
_pip_completion()
{
COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \\
COMP_CWORD=$COMP_CWORD \\
PIP_AUTO_COMPLETE=1 $1 2>/dev/null ) )
}
-complete -o default -F _pip_completion pip"""),
- ('fish', """\
+complete -o default -F _pip_completion pip""",
+ ),
+ (
+ "fish",
+ """\
function __fish_complete_pip
set -lx COMP_WORDS (commandline -o) ""
set -lx COMP_CWORD ( \\
@@ -23,8 +39,11 @@ function __fish_complete_pip
set -lx PIP_AUTO_COMPLETE 1
string split \\ -- (eval $COMP_WORDS[1])
end
-complete -fa "(__fish_complete_pip)" -c pip"""),
- ('zsh', """\
+complete -fa "(__fish_complete_pip)" -c pip""",
+ ),
+ (
+ "zsh",
+ """\
function _pip_completion {
local words cword
read -Ac words
@@ -33,56 +52,101 @@ function _pip_completion {
COMP_CWORD=$(( cword-1 )) \\
PIP_AUTO_COMPLETE=1 $words[1] 2>/dev/null ))
}
-compctl -K _pip_completion pip"""),
+compctl -K _pip_completion pip""",
+ ),
+ (
+ "powershell",
+ """\
+if ((Test-Path Function:\\TabExpansion) -and -not `
+ (Test-Path Function:\\_pip_completeBackup)) {
+ Rename-Item Function:\\TabExpansion _pip_completeBackup
+}
+function TabExpansion($line, $lastWord) {
+ $lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
+ if ($lastBlock.StartsWith("pip ")) {
+ $Env:COMP_WORDS=$lastBlock
+ $Env:COMP_CWORD=$lastBlock.Split().Length - 1
+ $Env:PIP_AUTO_COMPLETE=1
+ (& pip).Split()
+ Remove-Item Env:COMP_WORDS
+ Remove-Item Env:COMP_CWORD
+ Remove-Item Env:PIP_AUTO_COMPLETE
+ }
+ elseif (Test-Path Function:\\_pip_completeBackup) {
+ # Fall back on existing tab expansion
+ _pip_completeBackup $line $lastWord
+ }
+}""",
+ ),
)
@pytest.fixture(scope="session")
def script_with_launchers(
- tmpdir_factory, script_factory, common_wheels, pip_src
-):
- tmpdir = Path(str(tmpdir_factory.mktemp("script_with_launchers")))
+ tmpdir_factory: pytest.TempPathFactory,
+ script_factory: ScriptFactory,
+ common_wheels: Path,
+ pip_src: Path,
+) -> PipTestEnvironment:
+ tmpdir = tmpdir_factory.mktemp("script_with_launchers")
script = script_factory(tmpdir.joinpath("workspace"))
# Re-install pip so we get the launchers.
- script.pip_install_local('-f', common_wheels, pip_src)
+ script.pip_install_local("-f", common_wheels, pip_src)
return script
@pytest.mark.parametrize(
- 'shell, completion',
+ "shell, completion",
COMPLETION_FOR_SUPPORTED_SHELLS_TESTS,
ids=[t[0] for t in COMPLETION_FOR_SUPPORTED_SHELLS_TESTS],
)
def test_completion_for_supported_shells(
- script_with_launchers, shell, completion
-):
+ script_with_launchers: PipTestEnvironment, shell: str, completion: str
+) -> None:
"""
Test getting completion for bash shell
"""
- result = script_with_launchers.pip(
- 'completion', '--' + shell, use_module=False
- )
- assert completion in result.stdout, str(result.stdout)
+ result = script_with_launchers.pip("completion", "--" + shell, use_module=False)
+ actual = str(result.stdout)
+ if script_with_launchers.zipapp:
+ # The zipapp reports its name as "pip.pyz", but the expected
+ # output assumes "pip"
+ actual = actual.replace("pip.pyz", "pip")
+ assert completion in actual, actual
@pytest.fixture(scope="session")
-def autocomplete_script(tmpdir_factory, script_factory):
- tmpdir = Path(str(tmpdir_factory.mktemp("autocomplete_script")))
+def autocomplete_script(
+ tmpdir_factory: pytest.TempPathFactory, script_factory: ScriptFactory
+) -> PipTestEnvironment:
+ tmpdir = tmpdir_factory.mktemp("autocomplete_script")
return script_factory(tmpdir.joinpath("workspace"))
-@pytest.fixture
-def autocomplete(autocomplete_script, monkeypatch):
- monkeypatch.setattr(autocomplete_script, 'environ', os.environ.copy())
- autocomplete_script.environ['PIP_AUTO_COMPLETE'] = '1'
+class DoAutocomplete(Protocol):
+ def __call__(
+ self, words: str, cword: str, cwd: Union[Path, str, None] = None
+ ) -> Tuple[TestPipResult, PipTestEnvironment]:
+ ...
+
- def do_autocomplete(words, cword, cwd=None):
- autocomplete_script.environ['COMP_WORDS'] = words
- autocomplete_script.environ['COMP_CWORD'] = cword
+@pytest.fixture
+def autocomplete(
+ autocomplete_script: PipTestEnvironment, monkeypatch: pytest.MonkeyPatch
+) -> DoAutocomplete:
+ monkeypatch.setattr(autocomplete_script, "environ", os.environ.copy())
+ autocomplete_script.environ["PIP_AUTO_COMPLETE"] = "1"
+
+ def do_autocomplete(
+ words: str, cword: str, cwd: Union[Path, str, None] = None
+ ) -> Tuple[TestPipResult, PipTestEnvironment]:
+ autocomplete_script.environ["COMP_WORDS"] = words
+ autocomplete_script.environ["COMP_CWORD"] = cword
result = autocomplete_script.run(
- 'python', '-c',
- 'from pip._internal.cli.autocompletion import autocomplete;'
- 'autocomplete()',
+ "python",
+ "-c",
+ "from pip._internal.cli.autocompletion import autocomplete;"
+ "autocomplete()",
expect_error=True,
cwd=cwd,
)
@@ -92,225 +156,252 @@ def autocomplete(autocomplete_script, monkeypatch):
return do_autocomplete
-def test_completion_for_unknown_shell(autocomplete_script):
+def test_completion_for_unknown_shell(autocomplete_script: PipTestEnvironment) -> None:
"""
Test getting completion for an unknown shell
"""
- error_msg = 'no such option: --myfooshell'
- result = autocomplete_script.pip(
- 'completion', '--myfooshell', expect_error=True
- )
- assert error_msg in result.stderr, 'tests for an unknown shell failed'
+ error_msg = "no such option: --myfooshell"
+ result = autocomplete_script.pip("completion", "--myfooshell", expect_error=True)
+ assert error_msg in result.stderr, "tests for an unknown shell failed"
-def test_completion_alone(autocomplete_script):
+def test_completion_alone(autocomplete_script: PipTestEnvironment) -> None:
"""
Test getting completion for none shell, just pip completion
"""
- result = autocomplete_script.pip('completion', allow_stderr_error=True)
- assert 'ERROR: You must pass --bash or --fish or --zsh' in result.stderr, \
- 'completion alone failed -- ' + result.stderr
+ result = autocomplete_script.pip("completion", allow_stderr_error=True)
+ assert (
+ "ERROR: You must pass --bash or --fish or --powershell or --zsh"
+ in result.stderr
+ ), ("completion alone failed -- " + result.stderr)
-def test_completion_for_un_snippet(autocomplete):
+def test_completion_for_un_snippet(autocomplete: DoAutocomplete) -> None:
"""
Test getting completion for ``un`` should return uninstall
"""
- res, env = autocomplete('pip un', '1')
- assert res.stdout.strip().split() == ['uninstall'], res.stdout
+ res, env = autocomplete("pip un", "1")
+ assert res.stdout.strip().split() == ["uninstall"], res.stdout
-def test_completion_for_default_parameters(autocomplete):
+def test_completion_for_default_parameters(autocomplete: DoAutocomplete) -> None:
"""
Test getting completion for ``--`` should contain --help
"""
- res, env = autocomplete('pip --', '1')
- assert '--help' in res.stdout,\
- "autocomplete function could not complete ``--``"
+ res, env = autocomplete("pip --", "1")
+ assert "--help" in res.stdout, "autocomplete function could not complete ``--``"
-def test_completion_option_for_command(autocomplete):
+def test_completion_option_for_command(autocomplete: DoAutocomplete) -> None:
"""
Test getting completion for ``--`` in command (e.g. ``pip search --``)
"""
- res, env = autocomplete('pip search --', '2')
- assert '--help' in res.stdout,\
- "autocomplete function could not complete ``--``"
+ res, env = autocomplete("pip search --", "2")
+ assert "--help" in res.stdout, "autocomplete function could not complete ``--``"
-def test_completion_short_option(autocomplete):
+def test_completion_short_option(autocomplete: DoAutocomplete) -> None:
"""
Test getting completion for short options after ``-`` (eg. pip -)
"""
- res, env = autocomplete('pip -', '1')
+ res, env = autocomplete("pip -", "1")
- assert '-h' in res.stdout.split(),\
- "autocomplete function could not complete short options after ``-``"
+ assert (
+ "-h" in res.stdout.split()
+ ), "autocomplete function could not complete short options after ``-``"
-def test_completion_short_option_for_command(autocomplete):
+def test_completion_short_option_for_command(autocomplete: DoAutocomplete) -> None:
"""
Test getting completion for short options after ``-`` in command
(eg. pip search -)
"""
- res, env = autocomplete('pip search -', '2')
+ res, env = autocomplete("pip search -", "2")
- assert '-h' in res.stdout.split(),\
- "autocomplete function could not complete short options after ``-``"
+ assert (
+ "-h" in res.stdout.split()
+ ), "autocomplete function could not complete short options after ``-``"
-def test_completion_files_after_option(autocomplete, data):
+def test_completion_files_after_option(
+ autocomplete: DoAutocomplete, data: TestData
+) -> None:
"""
Test getting completion for <file> or <dir> after options in command
(e.g. ``pip install -r``)
"""
res, env = autocomplete(
- words=('pip install -r r'),
- cword='3',
+ words=("pip install -r r"),
+ cword="3",
cwd=data.completion_paths,
)
- assert 'requirements.txt' in res.stdout, (
- "autocomplete function could not complete <file> "
- "after options in command"
- )
- assert os.path.join('resources', '') in res.stdout, (
- "autocomplete function could not complete <dir> "
- "after options in command"
- )
- assert not any(out in res.stdout for out in
- (os.path.join('REPLAY', ''), 'README.txt')), (
+ assert (
+ "requirements.txt" in res.stdout
+ ), "autocomplete function could not complete <file> after options in command"
+ assert (
+ os.path.join("resources", "") in res.stdout
+ ), "autocomplete function could not complete <dir> after options in command"
+ assert not any(
+ out in res.stdout for out in (os.path.join("REPLAY", ""), "README.txt")
+ ), (
"autocomplete function completed <file> or <dir> that "
"should not be completed"
)
- if sys.platform != 'win32':
+ if sys.platform != "win32":
return
- assert 'readme.txt' in res.stdout, (
- "autocomplete function could not complete <file> "
- "after options in command"
- )
- assert os.path.join('replay', '') in res.stdout, (
- "autocomplete function could not complete <dir> "
- "after options in command"
- )
+ assert (
+ "readme.txt" in res.stdout
+ ), "autocomplete function could not complete <file> after options in command"
+ assert (
+ os.path.join("replay", "") in res.stdout
+ ), "autocomplete function could not complete <dir> after options in command"
-def test_completion_not_files_after_option(autocomplete, data):
+def test_completion_not_files_after_option(
+ autocomplete: DoAutocomplete, data: TestData
+) -> None:
"""
Test not getting completion files after options which not applicable
- (e.g. ``pip install``)
+ (e.g. ``pip wheel``)
"""
res, env = autocomplete(
- words=('pip install r'),
- cword='2',
+ words=("pip wheel r"),
+ cword="2",
cwd=data.completion_paths,
)
- assert not any(out in res.stdout for out in
- ('requirements.txt', 'readme.txt',)), (
- "autocomplete function completed <file> when "
- "it should not complete"
- )
- assert not any(os.path.join(out, '') in res.stdout
- for out in ('replay', 'resources')), (
- "autocomplete function completed <dir> when "
- "it should not complete"
+ assert not any(
+ out in res.stdout
+ for out in (
+ "requirements.txt",
+ "readme.txt",
+ )
+ ), "autocomplete function completed <file> when it should not complete"
+ assert not any(
+ os.path.join(out, "") in res.stdout for out in ("replay", "resources")
+ ), "autocomplete function completed <dir> when it should not complete"
+
+
+def test_pip_install_complete_files(
+ autocomplete: DoAutocomplete, data: TestData
+) -> None:
+ """``pip install`` autocompletes wheel and sdist files."""
+ res, env = autocomplete(
+ words=("pip install r"),
+ cword="2",
+ cwd=data.completion_paths,
)
+ assert all(
+ out in res.stdout
+ for out in (
+ "requirements.txt",
+ "resources",
+ )
+ ), "autocomplete function could not complete <path>"
@pytest.mark.parametrize("cl_opts", ["-U", "--user", "-h"])
def test_completion_not_files_after_nonexpecting_option(
- autocomplete, data, cl_opts
-):
+ autocomplete: DoAutocomplete, data: TestData, cl_opts: str
+) -> None:
"""
Test not getting completion files after options which not applicable
(e.g. ``pip install``)
"""
res, env = autocomplete(
- words=(f'pip install {cl_opts} r'),
- cword='2',
+ words=(f"pip install {cl_opts} r"),
+ cword="2",
cwd=data.completion_paths,
)
- assert not any(out in res.stdout for out in
- ('requirements.txt', 'readme.txt',)), (
- "autocomplete function completed <file> when "
- "it should not complete"
- )
- assert not any(os.path.join(out, '') in res.stdout
- for out in ('replay', 'resources')), (
- "autocomplete function completed <dir> when "
- "it should not complete"
- )
+ assert not any(
+ out in res.stdout
+ for out in (
+ "requirements.txt",
+ "readme.txt",
+ )
+ ), "autocomplete function completed <file> when it should not complete"
+ assert not any(
+ os.path.join(out, "") in res.stdout for out in ("replay", "resources")
+ ), "autocomplete function completed <dir> when it should not complete"
-def test_completion_directories_after_option(autocomplete, data):
+def test_completion_directories_after_option(
+ autocomplete: DoAutocomplete, data: TestData
+) -> None:
"""
Test getting completion <dir> after options in command
(e.g. ``pip --cache-dir``)
"""
res, env = autocomplete(
- words=('pip --cache-dir r'),
- cword='2',
+ words=("pip --cache-dir r"),
+ cword="2",
cwd=data.completion_paths,
)
- assert os.path.join('resources', '') in res.stdout, (
- "autocomplete function could not complete <dir> after options"
- )
- assert not any(out in res.stdout for out in (
- 'requirements.txt', 'README.txt', os.path.join('REPLAY', ''))), (
- "autocomplete function completed <dir> when "
- "it should not complete"
- )
- if sys.platform == 'win32':
- assert os.path.join('replay', '') in res.stdout, (
- "autocomplete function could not complete <dir> after options"
- )
-
-
-def test_completion_subdirectories_after_option(autocomplete, data):
+ assert (
+ os.path.join("resources", "") in res.stdout
+ ), "autocomplete function could not complete <dir> after options"
+ assert not any(
+ out in res.stdout
+ for out in ("requirements.txt", "README.txt", os.path.join("REPLAY", ""))
+ ), "autocomplete function completed <dir> when it should not complete"
+ if sys.platform == "win32":
+ assert (
+ os.path.join("replay", "") in res.stdout
+ ), "autocomplete function could not complete <dir> after options"
+
+
+def test_completion_subdirectories_after_option(
+ autocomplete: DoAutocomplete, data: TestData
+) -> None:
"""
Test getting completion <dir> after options in command
given path of a directory
"""
res, env = autocomplete(
- words=('pip --cache-dir ' + os.path.join('resources', '')),
- cword='2',
+ words=("pip --cache-dir " + os.path.join("resources", "")),
+ cword="2",
cwd=data.completion_paths,
)
- assert os.path.join('resources',
- os.path.join('images', '')) in res.stdout, (
+ assert os.path.join("resources", os.path.join("images", "")) in res.stdout, (
"autocomplete function could not complete <dir> "
"given path of a directory after options"
)
-def test_completion_path_after_option(autocomplete, data):
+def test_completion_path_after_option(
+ autocomplete: DoAutocomplete, data: TestData
+) -> None:
"""
Test getting completion <path> after options in command
given absolute path
"""
res, env = autocomplete(
- words=('pip install -e ' + os.path.join(data.completion_paths, 'R')),
- cword='3',
+ words=("pip install -e " + os.path.join(data.completion_paths, "R")),
+ cword="3",
)
- assert all(os.path.normcase(os.path.join(data.completion_paths, out))
- in res.stdout for out in (
- 'README.txt', os.path.join('REPLAY', ''))), (
+ assert all(
+ os.path.normcase(os.path.join(data.completion_paths, out)) in res.stdout
+ for out in ("README.txt", os.path.join("REPLAY", ""))
+ ), (
"autocomplete function could not complete <path> "
"after options in command given absolute path"
)
-@pytest.mark.parametrize('flag', ['--bash', '--zsh', '--fish'])
+@pytest.mark.parametrize("flag", ["--bash", "--zsh", "--fish", "--powershell"])
def test_completion_uses_same_executable_name(
- autocomplete_script, flag, deprecated_python
-):
- executable_name = 'pip{}'.format(sys.version_info[0])
+ autocomplete_script: PipTestEnvironment, flag: str, deprecated_python: bool
+) -> None:
+ executable_name = "pip{}".format(sys.version_info[0])
# Deprecated python versions produce an extra deprecation warning
result = autocomplete_script.run(
- executable_name, 'completion', flag, expect_stderr=deprecated_python,
+ executable_name,
+ "completion",
+ flag,
+ expect_stderr=deprecated_python,
)
assert executable_name in result.stdout
diff --git a/tests/functional/test_config_settings.py b/tests/functional/test_config_settings.py
new file mode 100644
index 000000000..b1e15c010
--- /dev/null
+++ b/tests/functional/test_config_settings.py
@@ -0,0 +1,139 @@
+import json
+from pathlib import Path
+from typing import Tuple
+from zipfile import ZipFile
+
+from tests.lib import PipTestEnvironment
+
+PYPROJECT_TOML = """\
+[build-system]
+requires = []
+build-backend = "dummy_backend:main"
+backend-path = ["backend"]
+"""
+
+BACKEND_SRC = '''
+import csv
+import json
+import os.path
+from zipfile import ZipFile
+import hashlib
+import base64
+import io
+
+WHEEL = """\
+Wheel-Version: 1.0
+Generator: dummy_backend 1.0
+Root-Is-Purelib: true
+Tag: py3-none-any
+"""
+
+METADATA = """\
+Metadata-Version: 2.1
+Name: {project}
+Version: {version}
+Summary: A dummy package
+Author: None
+Author-email: none@example.org
+License: MIT
+"""
+
+def make_wheel(z, project, version, files):
+ record = []
+ def add_file(name, data):
+ data = data.encode("utf-8")
+ z.writestr(name, data)
+ digest = hashlib.sha256(data).digest()
+ hash = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ASCII")
+ record.append((name, f"sha256={hash}", len(data)))
+ distinfo = f"{project}-{version}.dist-info"
+ add_file(f"{distinfo}/WHEEL", WHEEL)
+ add_file(f"{distinfo}/METADATA", METADATA.format(project=project, version=version))
+ for name, data in files:
+ add_file(name, data)
+ record_name = f"{distinfo}/RECORD"
+ record.append((record_name, "", ""))
+ b = io.BytesIO()
+ rec = io.TextIOWrapper(b, newline="", encoding="utf-8")
+ w = csv.writer(rec)
+ w.writerows(record)
+ z.writestr(record_name, b.getvalue())
+ rec.close()
+
+
+class Backend:
+ def build_wheel(
+ self,
+ wheel_directory,
+ config_settings=None,
+ metadata_directory=None
+ ):
+ if config_settings is None:
+ config_settings = {}
+ w = os.path.join(wheel_directory, "foo-1.0-py3-none-any.whl")
+ with open(w, "wb") as f:
+ with ZipFile(f, "w") as z:
+ make_wheel(
+ z, "foo", "1.0",
+ [("config.json", json.dumps(config_settings))]
+ )
+ return "foo-1.0-py3-none-any.whl"
+
+ build_editable = build_wheel
+
+main = Backend()
+'''
+
+
+def make_project(path: Path) -> Tuple[str, str, Path]:
+ name = "foo"
+ version = "1.0"
+ project_dir = path / name
+ backend = project_dir / "backend"
+ backend.mkdir(parents=True)
+ (project_dir / "pyproject.toml").write_text(PYPROJECT_TOML)
+ (backend / "dummy_backend.py").write_text(BACKEND_SRC)
+ return name, version, project_dir
+
+
+def test_backend_sees_config(script: PipTestEnvironment) -> None:
+ name, version, project_dir = make_project(script.scratch_path)
+ script.pip(
+ "wheel",
+ "--config-settings",
+ "FOO=Hello",
+ project_dir,
+ )
+ wheel_file_name = f"{name}-{version}-py3-none-any.whl"
+ wheel_file_path = script.cwd / wheel_file_name
+ with open(wheel_file_path, "rb") as f:
+ with ZipFile(f) as z:
+ output = z.read("config.json")
+ assert json.loads(output) == {"FOO": "Hello"}
+
+
+def test_install_sees_config(script: PipTestEnvironment) -> None:
+ _, _, project_dir = make_project(script.scratch_path)
+ script.pip(
+ "install",
+ "--config-settings",
+ "FOO=Hello",
+ project_dir,
+ )
+ config = script.site_packages_path / "config.json"
+ with open(config, "rb") as f:
+ assert json.load(f) == {"FOO": "Hello"}
+
+
+def test_install_editable_sees_config(script: PipTestEnvironment) -> None:
+ _, _, project_dir = make_project(script.scratch_path)
+ script.pip(
+ "install",
+ "--config-settings",
+ "FOO=Hello",
+ "--editable",
+ project_dir,
+ )
+ config = script.site_packages_path / "config.json"
+ with open(config, "rb") as f:
+ assert json.load(f) == {"FOO": "Hello"}
diff --git a/tests/functional/test_configuration.py b/tests/functional/test_configuration.py
index 72c09bd36..b3de3f697 100644
--- a/tests/functional/test_configuration.py
+++ b/tests/functional/test_configuration.py
@@ -1,36 +1,22 @@
"""Tests for the config command
"""
-
import re
import textwrap
-import pytest
-
from pip._internal.cli.status_codes import ERROR
from pip._internal.configuration import CONFIG_BASENAME, get_configuration_files
+from tests.lib import PipTestEnvironment
from tests.lib.configuration_helpers import ConfigurationMixin, kinds
+from tests.lib.venv import VirtualEnvironment
-def test_no_options_passed_should_error(script):
- result = script.pip('config', expect_error=True)
+def test_no_options_passed_should_error(script: PipTestEnvironment) -> None:
+ result = script.pip("config", expect_error=True)
assert result.returncode == ERROR
class TestBasicLoading(ConfigurationMixin):
-
- @pytest.mark.skip("Can't modify underlying file for any mode")
- def test_reads_file_appropriately(self, script):
- contents = """
- [test]
- hello = 1
- """
-
- with self.patched_file(kinds.USER, contents):
- result = script.pip("config", "list")
-
- assert "test.hello=1" in result.stdout
-
- def test_basic_modification_pipeline(self, script):
+ def test_basic_modification_pipeline(self, script: PipTestEnvironment) -> None:
script.pip("config", "get", "test.blah", expect_error=True)
script.pip("config", "set", "test.blah", "1")
@@ -40,17 +26,16 @@ class TestBasicLoading(ConfigurationMixin):
script.pip("config", "unset", "test.blah")
script.pip("config", "get", "test.blah", expect_error=True)
- def test_listing_is_correct(self, script):
+ def test_listing_is_correct(self, script: PipTestEnvironment) -> None:
script.pip("config", "set", "test.listing-beta", "2")
script.pip("config", "set", "test.listing-alpha", "1")
script.pip("config", "set", "test.listing-gamma", "3")
result = script.pip("config", "list")
- lines = list(filter(
- lambda x: x.startswith("test.listing-"),
- result.stdout.splitlines()
- ))
+ lines = list(
+ filter(lambda x: x.startswith("test.listing-"), result.stdout.splitlines())
+ )
expected = """
test.listing-alpha='1'
@@ -60,19 +45,18 @@ class TestBasicLoading(ConfigurationMixin):
assert lines == textwrap.dedent(expected).strip().splitlines()
- def test_forget_section(self, script):
- result = script.pip("config", "set", "isolated", "true",
- expect_error=True)
+ def test_forget_section(self, script: PipTestEnvironment) -> None:
+ result = script.pip("config", "set", "isolated", "true", expect_error=True)
assert "global.isolated" in result.stderr
- def test_env_var_values(self, script):
+ def test_env_var_values(self, script: PipTestEnvironment) -> None:
"""Test that pip configuration set with environment variables
is correctly displayed under "env_var".
"""
env_vars = {
"PIP_DEFAULT_TIMEOUT": "60",
- "PIP_FIND_LINKS": "http://mirror.example.com"
+ "PIP_FIND_LINKS": "http://mirror.example.com",
}
script.environ.update(env_vars)
@@ -81,21 +65,25 @@ class TestBasicLoading(ConfigurationMixin):
assert "PIP_FIND_LINKS='http://mirror.example.com'" in result.stdout
assert re.search(r"env_var:\n( .+\n)+", result.stdout)
- def test_env_values(self, script):
+ def test_env_values(self, script: PipTestEnvironment) -> None:
"""Test that custom pip configuration using the environment variable
PIP_CONFIG_FILE is correctly displayed under "env". This configuration
takes place of per-user configuration file displayed under "user".
"""
config_file = script.scratch_path / "test-pip.cfg"
- script.environ['PIP_CONFIG_FILE'] = str(config_file)
- config_file.write_text(textwrap.dedent("""\
+ script.environ["PIP_CONFIG_FILE"] = str(config_file)
+ config_file.write_text(
+ textwrap.dedent(
+ """\
[global]
timeout = 60
[freeze]
timeout = 10
- """))
+ """
+ )
+ )
result = script.pip("config", "debug")
assert f"{config_file}, exists: True" in result.stdout
@@ -103,7 +91,7 @@ class TestBasicLoading(ConfigurationMixin):
assert "freeze.timeout: 10" in result.stdout
assert re.search(r"env:\n( .+\n)+", result.stdout)
- def test_user_values(self, script,):
+ def test_user_values(self, script: PipTestEnvironment) -> None:
"""Test that the user pip configuration set using --user
is correctly displayed under "user". This configuration takes place
of custom path location using the environment variable PIP_CONFIG_FILE
@@ -122,7 +110,9 @@ class TestBasicLoading(ConfigurationMixin):
assert "freeze.timeout: 10" in result.stdout
assert re.search(r"user:\n( .+\n)+", result.stdout)
- def test_site_values(self, script, virtualenv):
+ def test_site_values(
+ self, script: PipTestEnvironment, virtualenv: VirtualEnvironment
+ ) -> None:
"""Test that the current environment configuration set using --site
is correctly displayed under "site".
"""
@@ -139,7 +129,7 @@ class TestBasicLoading(ConfigurationMixin):
assert "freeze.timeout: 10" in result.stdout
assert re.search(r"site:\n( .+\n)+", result.stdout)
- def test_global_config_file(self, script):
+ def test_global_config_file(self, script: PipTestEnvironment) -> None:
"""Test that the system-wide configuration can be identified"""
# We cannot write to system-wide files which might have permissions
@@ -150,3 +140,10 @@ class TestBasicLoading(ConfigurationMixin):
global_config_file = get_configuration_files()[kinds.GLOBAL][0]
result = script.pip("config", "debug")
assert f"{global_config_file}, exists:" in result.stdout
+
+ def test_editor_does_not_exist(self, script: PipTestEnvironment) -> None:
+ """Ensure that FileNotFoundError sets filename correctly"""
+ result = script.pip(
+ "config", "edit", "--editor", "notrealeditor", expect_error=True
+ )
+ assert "notrealeditor" in result.stderr
diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py
index 0e2261e1a..41374f8cb 100644
--- a/tests/functional/test_debug.py
+++ b/tests/functional/test_debug.py
@@ -1,86 +1,93 @@
+from typing import List
+
import pytest
from pip._internal.commands.debug import create_vendor_txt_map
from pip._internal.utils import compatibility_tags
+from tests.lib import PipTestEnvironment
-@pytest.mark.parametrize('expected_text', [
- 'sys.executable: ',
- 'sys.getdefaultencoding: ',
- 'sys.getfilesystemencoding: ',
- 'locale.getpreferredencoding: ',
- 'sys.platform: ',
- 'sys.implementation:',
- '\'cert\' config value: ',
- 'REQUESTS_CA_BUNDLE: ',
- 'CURL_CA_BUNDLE: ',
- 'pip._vendor.certifi.where(): ',
- 'pip._vendor.DEBUNDLED: ',
- 'vendored library versions:',
-
-])
-def test_debug(script, expected_text):
+@pytest.mark.parametrize(
+ "expected_text",
+ [
+ "sys.executable: ",
+ "sys.getdefaultencoding: ",
+ "sys.getfilesystemencoding: ",
+ "locale.getpreferredencoding: ",
+ "sys.platform: ",
+ "sys.implementation:",
+ "'cert' config value: ",
+ "REQUESTS_CA_BUNDLE: ",
+ "CURL_CA_BUNDLE: ",
+ "pip._vendor.certifi.where(): ",
+ "pip._vendor.DEBUNDLED: ",
+ "vendored library versions:",
+ ],
+)
+def test_debug(script: PipTestEnvironment, expected_text: str) -> None:
"""
Check that certain strings are present in the output.
"""
- args = ['debug']
+ args = ["debug"]
result = script.pip(*args, allow_stderr_warning=True)
stdout = result.stdout
assert expected_text in stdout
-def test_debug__library_versions(script):
+def test_debug__library_versions(script: PipTestEnvironment) -> None:
"""
Check the library versions normal output.
"""
- args = ['debug']
+ args = ["debug"]
result = script.pip(*args, allow_stderr_warning=True)
print(result.stdout)
vendored_versions = create_vendor_txt_map()
for name, value in vendored_versions.items():
- assert f'{name}=={value}' in result.stdout
+ assert f"{name}=={value}" in result.stdout
@pytest.mark.parametrize(
- 'args',
+ "args",
[
[],
- ['--verbose'],
- ]
+ ["--verbose"],
+ ],
)
-def test_debug__tags(script, args):
+def test_debug__tags(script: PipTestEnvironment, args: List[str]) -> None:
"""
Check the compatible tag output.
"""
- args = ['debug'] + args
+ args = ["debug"] + args
result = script.pip(*args, allow_stderr_warning=True)
stdout = result.stdout
tags = compatibility_tags.get_supported()
- expected_tag_header = 'Compatible tags: {}'.format(len(tags))
+ expected_tag_header = "Compatible tags: {}".format(len(tags))
assert expected_tag_header in stdout
- show_verbose_note = '--verbose' not in args
+ show_verbose_note = "--verbose" not in args
assert (
- '...\n [First 10 tags shown. Pass --verbose to show all.]' in stdout
+ "...\n [First 10 tags shown. Pass --verbose to show all.]" in stdout
) == show_verbose_note
@pytest.mark.parametrize(
- 'args, expected',
+ "args, expected",
[
- (['--python-version', '3.7'], "(target: version_info='3.7')"),
- ]
+ (["--python-version", "3.7"], "(target: version_info='3.7')"),
+ ],
)
-def test_debug__target_options(script, args, expected):
+def test_debug__target_options(
+ script: PipTestEnvironment, args: List[str], expected: str
+) -> None:
"""
Check passing target-related options.
"""
- args = ['debug'] + args
+ args = ["debug"] + args
result = script.pip(*args, allow_stderr_warning=True)
stdout = result.stdout
- assert 'Compatible tags: ' in stdout
+ assert "Compatible tags: " in stdout
assert expected in stdout
diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py
index 217157343..ede2213aa 100644
--- a/tests/functional/test_download.py
+++ b/tests/functional/test_download.py
@@ -1,18 +1,31 @@
-import os.path
+import os
+import re
import shutil
import textwrap
+import uuid
+from dataclasses import dataclass
+from enum import Enum
from hashlib import sha256
+from pathlib import Path
+from textwrap import dedent
+from typing import Callable, Dict, List, Tuple
import pytest
from pip._internal.cli.status_codes import ERROR
from pip._internal.utils.urls import path_to_url
-from tests.lib import create_really_basic_wheel
-from tests.lib.path import Path
+from tests.conftest import MockServer, ScriptFactory
+from tests.lib import (
+ PipTestEnvironment,
+ TestData,
+ TestPipResult,
+ create_basic_sdist_for_package,
+ create_really_basic_wheel,
+)
from tests.lib.server import file_response
-def fake_wheel(data, wheel_path):
+def fake_wheel(data: TestData, wheel_path: str) -> None:
wheel_name = os.path.basename(wheel_path)
name, version, rest = wheel_name.split("-", 2)
wheel_data = create_really_basic_wheel(name, version)
@@ -20,308 +33,382 @@ def fake_wheel(data, wheel_path):
@pytest.mark.network
-def test_download_if_requested(script):
+def test_download_if_requested(script: PipTestEnvironment) -> None:
"""
It should download (in the scratch path) and not install if requested.
"""
- result = script.pip(
- 'download', '-d', 'pip_downloads', 'INITools==0.1'
- )
- result.did_create(
- Path('scratch') / 'pip_downloads' / 'INITools-0.1.tar.gz'
- )
- result.did_not_create(script.site_packages / 'initools')
+ result = script.pip("download", "-d", "pip_downloads", "INITools==0.1")
+ result.did_create(Path("scratch") / "pip_downloads" / "INITools-0.1.tar.gz")
+ result.did_not_create(script.site_packages / "initools")
@pytest.mark.network
-def test_basic_download_setuptools(script):
+def test_basic_download_setuptools(script: PipTestEnvironment) -> None:
"""
It should download (in the scratch path) and not install if requested.
"""
- result = script.pip('download', 'setuptools')
- setuptools_prefix = str(Path('scratch') / 'setuptools')
- assert any(
- path.startswith(setuptools_prefix) for path in result.files_created
- )
+ result = script.pip("download", "setuptools")
+ setuptools_prefix = str(Path("scratch") / "setuptools")
+ assert any(os.fspath(p).startswith(setuptools_prefix) for p in result.files_created)
-def test_download_wheel(script, data):
+def test_download_wheel(script: PipTestEnvironment, data: TestData) -> None:
"""
Test using "pip download" to download a *.whl archive.
"""
result = script.pip(
- 'download',
- '--no-index',
- '-f', data.packages,
- '-d', '.', 'meta'
+ "download", "--no-index", "-f", data.packages, "-d", ".", "meta"
)
- result.did_create(Path('scratch') / 'meta-1.0-py2.py3-none-any.whl')
- result.did_not_create(script.site_packages / 'piptestpackage')
+ result.did_create(Path("scratch") / "meta-1.0-py2.py3-none-any.whl")
+ result.did_not_create(script.site_packages / "piptestpackage")
@pytest.mark.network
-def test_single_download_from_requirements_file(script):
+def test_single_download_from_requirements_file(script: PipTestEnvironment) -> None:
"""
It should support download (in the scratch path) from PyPI from a
requirements file
"""
- script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""
+ script.scratch_path.joinpath("test-req.txt").write_text(
+ textwrap.dedent(
+ """
INITools==0.1
- """))
+ """
+ )
+ )
result = script.pip(
- 'download', '-r', script.scratch_path / 'test-req.txt', '-d', '.',
+ "download",
+ "-r",
+ script.scratch_path / "test-req.txt",
+ "-d",
+ ".",
)
- result.did_create(Path('scratch') / 'INITools-0.1.tar.gz')
- result.did_not_create(script.site_packages / 'initools')
+ result.did_create(Path("scratch") / "INITools-0.1.tar.gz")
+ result.did_not_create(script.site_packages / "initools")
@pytest.mark.network
-def test_basic_download_should_download_dependencies(script):
+def test_basic_download_should_download_dependencies(
+ script: PipTestEnvironment,
+) -> None:
"""
It should download dependencies (in the scratch path)
"""
- result = script.pip(
- 'download', 'Paste[openid]==1.7.5.1', '-d', '.'
- )
- result.did_create(Path('scratch') / 'Paste-1.7.5.1.tar.gz')
- openid_tarball_prefix = str(Path('scratch') / 'python-openid-')
+ result = script.pip("download", "Paste[openid]==1.7.5.1", "-d", ".")
+ result.did_create(Path("scratch") / "Paste-1.7.5.1.tar.gz")
+ openid_tarball_prefix = str(Path("scratch") / "python-openid-")
assert any(
- path.startswith(openid_tarball_prefix) for path in result.files_created
+ os.fspath(path).startswith(openid_tarball_prefix)
+ for path in result.files_created
)
- result.did_not_create(script.site_packages / 'openid')
+ result.did_not_create(script.site_packages / "openid")
-def test_download_wheel_archive(script, data):
+def test_download_wheel_archive(script: PipTestEnvironment, data: TestData) -> None:
"""
It should download a wheel archive path
"""
- wheel_filename = 'colander-0.9.9-py2.py3-none-any.whl'
- wheel_path = '/'.join((data.find_links, wheel_filename))
- result = script.pip(
- 'download', wheel_path,
- '-d', '.', '--no-deps'
- )
- result.did_create(Path('scratch') / wheel_filename)
+ wheel_filename = "colander-0.9.9-py2.py3-none-any.whl"
+ wheel_path = "/".join((data.find_links, wheel_filename))
+ result = script.pip("download", wheel_path, "-d", ".", "--no-deps")
+ result.did_create(Path("scratch") / wheel_filename)
-def test_download_should_download_wheel_deps(script, data):
+def test_download_should_download_wheel_deps(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
It should download dependencies for wheels(in the scratch path)
"""
- wheel_filename = 'colander-0.9.9-py2.py3-none-any.whl'
- dep_filename = 'translationstring-1.1.tar.gz'
- wheel_path = '/'.join((data.find_links, wheel_filename))
+ wheel_filename = "colander-0.9.9-py2.py3-none-any.whl"
+ dep_filename = "translationstring-1.1.tar.gz"
+ wheel_path = "/".join((data.find_links, wheel_filename))
result = script.pip(
- 'download', wheel_path,
- '-d', '.', '--find-links', data.find_links, '--no-index'
+ "download", wheel_path, "-d", ".", "--find-links", data.find_links, "--no-index"
)
- result.did_create(Path('scratch') / wheel_filename)
- result.did_create(Path('scratch') / dep_filename)
+ result.did_create(Path("scratch") / wheel_filename)
+ result.did_create(Path("scratch") / dep_filename)
@pytest.mark.network
-def test_download_should_skip_existing_files(script):
+def test_download_should_skip_existing_files(script: PipTestEnvironment) -> None:
"""
It should not download files already existing in the scratch dir
"""
- script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""
+ script.scratch_path.joinpath("test-req.txt").write_text(
+ textwrap.dedent(
+ """
INITools==0.1
- """))
+ """
+ )
+ )
result = script.pip(
- 'download', '-r', script.scratch_path / 'test-req.txt', '-d', '.',
+ "download",
+ "-r",
+ script.scratch_path / "test-req.txt",
+ "-d",
+ ".",
)
- result.did_create(Path('scratch') / 'INITools-0.1.tar.gz')
- result.did_not_create(script.site_packages / 'initools')
+ result.did_create(Path("scratch") / "INITools-0.1.tar.gz")
+ result.did_not_create(script.site_packages / "initools")
# adding second package to test-req.txt
- script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""
+ script.scratch_path.joinpath("test-req.txt").write_text(
+ textwrap.dedent(
+ """
INITools==0.1
python-openid==2.2.5
- """))
+ """
+ )
+ )
# only the second package should be downloaded
result = script.pip(
- 'download', '-r', script.scratch_path / 'test-req.txt', '-d', '.',
+ "download",
+ "-r",
+ script.scratch_path / "test-req.txt",
+ "-d",
+ ".",
)
- openid_tarball_prefix = str(Path('scratch') / 'python-openid-')
+ openid_tarball_prefix = str(Path("scratch") / "python-openid-")
assert any(
- path.startswith(openid_tarball_prefix) for path in result.files_created
+ os.fspath(path).startswith(openid_tarball_prefix)
+ for path in result.files_created
)
- result.did_not_create(Path('scratch') / 'INITools-0.1.tar.gz')
- result.did_not_create(script.site_packages / 'initools')
- result.did_not_create(script.site_packages / 'openid')
+ result.did_not_create(Path("scratch") / "INITools-0.1.tar.gz")
+ result.did_not_create(script.site_packages / "initools")
+ result.did_not_create(script.site_packages / "openid")
@pytest.mark.network
-def test_download_vcs_link(script):
+def test_download_vcs_link(script: PipTestEnvironment) -> None:
"""
It should allow -d flag for vcs links, regression test for issue #798.
"""
result = script.pip(
- 'download', '-d', '.', 'git+git://github.com/pypa/pip-test-package.git'
+ "download", "-d", ".", "git+https://github.com/pypa/pip-test-package.git"
)
- result.did_create(Path('scratch') / 'pip-test-package-0.1.1.zip')
- result.did_not_create(script.site_packages / 'piptestpackage')
+ result.did_create(Path("scratch") / "pip-test-package-0.1.1.zip")
+ result.did_not_create(script.site_packages / "piptestpackage")
-def test_only_binary_set_then_download_specific_platform(script, data):
+def test_only_binary_set_then_download_specific_platform(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Confirm that specifying an interpreter/platform constraint
is allowed when ``--only-binary=:all:`` is set.
"""
- fake_wheel(data, 'fake-1.0-py2.py3-none-any.whl')
+ fake_wheel(data, "fake-1.0-py2.py3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--platform', 'linux_x86_64',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--platform",
+ "linux_x86_64",
+ "fake",
)
- result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl')
+ result.did_create(Path("scratch") / "fake-1.0-py2.py3-none-any.whl")
-def test_no_deps_set_then_download_specific_platform(script, data):
+def test_no_deps_set_then_download_specific_platform(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Confirm that specifying an interpreter/platform constraint
is allowed when ``--no-deps`` is set.
"""
- fake_wheel(data, 'fake-1.0-py2.py3-none-any.whl')
+ fake_wheel(data, "fake-1.0-py2.py3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--no-deps',
- '--dest', '.',
- '--platform', 'linux_x86_64',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--no-deps",
+ "--dest",
+ ".",
+ "--platform",
+ "linux_x86_64",
+ "fake",
)
- result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl')
+ result.did_create(Path("scratch") / "fake-1.0-py2.py3-none-any.whl")
-def test_download_specific_platform_fails(script, data):
+def test_download_specific_platform_fails(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Confirm that specifying an interpreter/platform constraint
enforces that ``--no-deps`` or ``--only-binary=:all:`` is set.
"""
- fake_wheel(data, 'fake-1.0-py2.py3-none-any.whl')
+ fake_wheel(data, "fake-1.0-py2.py3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--dest', '.',
- '--platform', 'linux_x86_64',
- 'fake',
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--dest",
+ ".",
+ "--platform",
+ "linux_x86_64",
+ "fake",
expect_error=True,
)
- assert '--only-binary=:all:' in result.stderr
+ assert "--only-binary=:all:" in result.stderr
-def test_no_binary_set_then_download_specific_platform_fails(script, data):
+def test_no_binary_set_then_download_specific_platform_fails(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Confirm that specifying an interpreter/platform constraint
enforces that ``--only-binary=:all:`` is set without ``--no-binary``.
"""
- fake_wheel(data, 'fake-1.0-py2.py3-none-any.whl')
+ fake_wheel(data, "fake-1.0-py2.py3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--no-binary=fake',
- '--dest', '.',
- '--platform', 'linux_x86_64',
- 'fake',
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--no-binary=fake",
+ "--dest",
+ ".",
+ "--platform",
+ "linux_x86_64",
+ "fake",
expect_error=True,
)
- assert '--only-binary=:all:' in result.stderr
+ assert "--only-binary=:all:" in result.stderr
-def test_download_specify_platform(script, data):
+def test_download_specify_platform(script: PipTestEnvironment, data: TestData) -> None:
"""
Test using "pip download --platform" to download a .whl archive
supported for a specific platform
"""
- fake_wheel(data, 'fake-1.0-py2.py3-none-any.whl')
+ fake_wheel(data, "fake-1.0-py2.py3-none-any.whl")
# Confirm that universal wheels are returned even for specific
# platforms.
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--platform', 'linux_x86_64',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--platform",
+ "linux_x86_64",
+ "fake",
)
- result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl')
+ result.did_create(Path("scratch") / "fake-1.0-py2.py3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--platform', 'macosx_10_9_x86_64',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--platform",
+ "macosx_10_9_x86_64",
+ "fake",
)
data.reset()
- fake_wheel(data, 'fake-1.0-py2.py3-none-macosx_10_9_x86_64.whl')
- fake_wheel(data, 'fake-2.0-py2.py3-none-linux_x86_64.whl')
+ fake_wheel(data, "fake-1.0-py2.py3-none-macosx_10_9_x86_64.whl")
+ fake_wheel(data, "fake-2.0-py2.py3-none-linux_x86_64.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--platform', 'macosx_10_10_x86_64',
- 'fake'
- )
- result.did_create(
- Path('scratch') /
- 'fake-1.0-py2.py3-none-macosx_10_9_x86_64.whl'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--platform",
+ "macosx_10_10_x86_64",
+ "fake",
)
+ result.did_create(Path("scratch") / "fake-1.0-py2.py3-none-macosx_10_9_x86_64.whl")
# OSX platform wheels are not backward-compatible.
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--platform', 'macosx_10_8_x86_64',
- 'fake',
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--platform",
+ "macosx_10_8_x86_64",
+ "fake",
expect_error=True,
)
# No linux wheel provided for this version.
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--platform', 'linux_x86_64',
- 'fake==1',
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--platform",
+ "linux_x86_64",
+ "fake==1",
expect_error=True,
)
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--platform', 'linux_x86_64',
- 'fake==2'
- )
- result.did_create(
- Path('scratch') / 'fake-2.0-py2.py3-none-linux_x86_64.whl'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--platform",
+ "linux_x86_64",
+ "fake==2",
)
+ result.did_create(Path("scratch") / "fake-2.0-py2.py3-none-linux_x86_64.whl")
# Test with multiple supported platforms specified.
data.reset()
- fake_wheel(data, 'fake-3.0-py2.py3-none-linux_x86_64.whl')
+ fake_wheel(data, "fake-3.0-py2.py3-none-linux_x86_64.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--platform', 'manylinux1_x86_64', '--platform', 'linux_x86_64',
- '--platform', 'any',
- 'fake==3'
- )
- result.did_create(
- Path('scratch') / 'fake-3.0-py2.py3-none-linux_x86_64.whl'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--platform",
+ "manylinux1_x86_64",
+ "--platform",
+ "linux_x86_64",
+ "--platform",
+ "any",
+ "fake==3",
)
+ result.did_create(Path("scratch") / "fake-3.0-py2.py3-none-linux_x86_64.whl")
class TestDownloadPlatformManylinuxes:
@@ -330,148 +417,219 @@ class TestDownloadPlatformManylinuxes:
manylinux platforms.
"""
- @pytest.mark.parametrize("platform", [
- "linux_x86_64",
- "manylinux1_x86_64",
- "manylinux2010_x86_64",
- "manylinux2014_x86_64",
- ])
- def test_download_universal(self, platform, script, data):
+ @pytest.mark.parametrize(
+ "platform",
+ [
+ "linux_x86_64",
+ "manylinux1_x86_64",
+ "manylinux2010_x86_64",
+ "manylinux2014_x86_64",
+ ],
+ )
+ def test_download_universal(
+ self, platform: str, script: PipTestEnvironment, data: TestData
+ ) -> None:
"""
Universal wheels are returned even for specific platforms.
"""
- fake_wheel(data, 'fake-1.0-py2.py3-none-any.whl')
+ fake_wheel(data, "fake-1.0-py2.py3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--platform', platform,
- 'fake',
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--platform",
+ platform,
+ "fake",
)
- result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl')
-
- @pytest.mark.parametrize("wheel_abi,platform", [
- ("manylinux1_x86_64", "manylinux1_x86_64"),
- ("manylinux1_x86_64", "manylinux2010_x86_64"),
- ("manylinux2010_x86_64", "manylinux2010_x86_64"),
- ("manylinux1_x86_64", "manylinux2014_x86_64"),
- ("manylinux2010_x86_64", "manylinux2014_x86_64"),
- ("manylinux2014_x86_64", "manylinux2014_x86_64"),
- ])
+ result.did_create(Path("scratch") / "fake-1.0-py2.py3-none-any.whl")
+
+ @pytest.mark.parametrize(
+ "wheel_abi,platform",
+ [
+ ("manylinux1_x86_64", "manylinux1_x86_64"),
+ ("manylinux1_x86_64", "manylinux2010_x86_64"),
+ ("manylinux2010_x86_64", "manylinux2010_x86_64"),
+ ("manylinux1_x86_64", "manylinux2014_x86_64"),
+ ("manylinux2010_x86_64", "manylinux2014_x86_64"),
+ ("manylinux2014_x86_64", "manylinux2014_x86_64"),
+ ],
+ )
def test_download_compatible_manylinuxes(
- self, wheel_abi, platform, script, data,
- ):
+ self,
+ wheel_abi: str,
+ platform: str,
+ script: PipTestEnvironment,
+ data: TestData,
+ ) -> None:
"""
Earlier manylinuxes are compatible with later manylinuxes.
"""
- wheel = f'fake-1.0-py2.py3-none-{wheel_abi}.whl'
+ wheel = f"fake-1.0-py2.py3-none-{wheel_abi}.whl"
fake_wheel(data, wheel)
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--platform', platform,
- 'fake',
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--platform",
+ platform,
+ "fake",
)
- result.did_create(Path('scratch') / wheel)
+ result.did_create(Path("scratch") / wheel)
- def test_explicit_platform_only(self, data, script):
+ def test_explicit_platform_only(
+ self, data: TestData, script: PipTestEnvironment
+ ) -> None:
"""
When specifying the platform, manylinux1 needs to be the
explicit platform--it won't ever be added to the compatible
tags.
"""
- fake_wheel(data, 'fake-1.0-py2.py3-none-linux_x86_64.whl')
+ fake_wheel(data, "fake-1.0-py2.py3-none-linux_x86_64.whl")
script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--platform', 'linux_x86_64',
- 'fake',
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--platform",
+ "linux_x86_64",
+ "fake",
)
-def test_download__python_version(script, data):
+def test_download__python_version(script: PipTestEnvironment, data: TestData) -> None:
"""
Test using "pip download --python-version" to download a .whl archive
supported for a specific interpreter
"""
- fake_wheel(data, 'fake-1.0-py2.py3-none-any.whl')
+ fake_wheel(data, "fake-1.0-py2.py3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--python-version', '2',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--python-version",
+ "2",
+ "fake",
)
- result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl')
+ result.did_create(Path("scratch") / "fake-1.0-py2.py3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--python-version', '3',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--python-version",
+ "3",
+ "fake",
)
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--python-version', '27',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--python-version",
+ "27",
+ "fake",
)
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--python-version', '33',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--python-version",
+ "33",
+ "fake",
)
data.reset()
- fake_wheel(data, 'fake-1.0-py2-none-any.whl')
- fake_wheel(data, 'fake-2.0-py3-none-any.whl')
+ fake_wheel(data, "fake-1.0-py2-none-any.whl")
+ fake_wheel(data, "fake-2.0-py3-none-any.whl")
# No py3 provided for version 1.
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--python-version', '3',
- 'fake==1.0',
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--python-version",
+ "3",
+ "fake==1.0",
expect_error=True,
)
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--python-version', '2',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--python-version",
+ "2",
+ "fake",
)
- result.did_create(Path('scratch') / 'fake-1.0-py2-none-any.whl')
+ result.did_create(Path("scratch") / "fake-1.0-py2-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--python-version', '26',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--python-version",
+ "26",
+ "fake",
)
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--python-version', '3',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--python-version",
+ "3",
+ "fake",
)
- result.did_create(Path('scratch') / 'fake-2.0-py3-none-any.whl')
+ result.did_create(Path("scratch") / "fake-2.0-py3-none-any.whl")
-def make_wheel_with_python_requires(script, package_name, python_requires):
+def make_wheel_with_python_requires(
+ script: PipTestEnvironment, package_name: str, python_requires: str
+) -> Path:
"""
Create a wheel using the given python_requires.
@@ -480,58 +638,72 @@ def make_wheel_with_python_requires(script, package_name, python_requires):
package_dir = script.scratch_path / package_name
package_dir.mkdir()
- text = textwrap.dedent("""\
+ text = textwrap.dedent(
+ """\
from setuptools import setup
setup(name='{}',
python_requires='{}',
version='1.0')
- """).format(package_name, python_requires)
- package_dir.joinpath('setup.py').write_text(text)
+ """
+ ).format(package_name, python_requires)
+ package_dir.joinpath("setup.py").write_text(text)
script.run(
- 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=package_dir,
+ "python",
+ "setup.py",
+ "bdist_wheel",
+ "--universal",
+ cwd=package_dir,
)
- file_name = f'{package_name}-1.0-py2.py3-none-any.whl'
- return package_dir / 'dist' / file_name
+ file_name = f"{package_name}-1.0-py2.py3-none-any.whl"
+ return package_dir / "dist" / file_name
+@pytest.mark.usefixtures("with_wheel")
def test_download__python_version_used_for_python_requires(
- script, data, with_wheel,
-):
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test that --python-version is used for the Requires-Python check.
"""
wheel_path = make_wheel_with_python_requires(
- script, 'mypackage', python_requires='==3.2',
+ script,
+ "mypackage",
+ python_requires="==3.2",
)
wheel_dir = os.path.dirname(wheel_path)
- def make_args(python_version):
+ def make_args(python_version: str) -> List[str]:
return [
- 'download', '--no-index', '--find-links', wheel_dir,
- '--only-binary=:all:',
- '--dest', '.',
- '--python-version', python_version,
- 'mypackage==1.0',
+ "download",
+ "--no-index",
+ "--find-links",
+ wheel_dir,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--python-version",
+ python_version,
+ "mypackage==1.0",
]
- args = make_args('33')
+ args = make_args("33")
result = script.pip(*args, expect_error=True)
expected_err = (
"ERROR: Package 'mypackage' requires a different Python: "
"3.3.0 not in '==3.2'"
)
- assert expected_err in result.stderr, f'stderr: {result.stderr}'
+ assert expected_err in result.stderr, f"stderr: {result.stderr}"
# Now try with a --python-version that satisfies the Requires-Python.
- args = make_args('32')
+ args = make_args("32")
script.pip(*args) # no exception
+@pytest.mark.usefixtures("with_wheel")
def test_download_ignore_requires_python_dont_fail_with_wrong_python(
- script,
- with_wheel,
-):
+ script: PipTestEnvironment,
+) -> None:
"""
Test that --ignore-requires-python ignores Requires-Python check.
"""
@@ -553,256 +725,367 @@ def test_download_ignore_requires_python_dont_fail_with_wrong_python(
".",
"mypackage==1.0",
)
- result.did_create(Path('scratch') / 'mypackage-1.0-py2.py3-none-any.whl')
+ result.did_create(Path("scratch") / "mypackage-1.0-py2.py3-none-any.whl")
-def test_download_specify_abi(script, data):
+def test_download_specify_abi(script: PipTestEnvironment, data: TestData) -> None:
"""
Test using "pip download --abi" to download a .whl archive
supported for a specific abi
"""
- fake_wheel(data, 'fake-1.0-py2.py3-none-any.whl')
+ fake_wheel(data, "fake-1.0-py2.py3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--implementation', 'fk',
- '--abi', 'fake_abi',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--implementation",
+ "fk",
+ "--abi",
+ "fake_abi",
+ "fake",
)
- result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl')
+ result.did_create(Path("scratch") / "fake-1.0-py2.py3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--implementation', 'fk',
- '--abi', 'none',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--implementation",
+ "fk",
+ "--abi",
+ "none",
+ "fake",
)
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--abi', 'cp27m',
- 'fake',
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--abi",
+ "cp27m",
+ "fake",
)
data.reset()
- fake_wheel(data, 'fake-1.0-fk2-fakeabi-fake_platform.whl')
+ fake_wheel(data, "fake-1.0-fk2-fakeabi-fake_platform.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--python-version', '2',
- '--implementation', 'fk',
- '--platform', 'fake_platform',
- '--abi', 'fakeabi',
- 'fake'
- )
- result.did_create(
- Path('scratch') / 'fake-1.0-fk2-fakeabi-fake_platform.whl'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--python-version",
+ "2",
+ "--implementation",
+ "fk",
+ "--platform",
+ "fake_platform",
+ "--abi",
+ "fakeabi",
+ "fake",
)
+ result.did_create(Path("scratch") / "fake-1.0-fk2-fakeabi-fake_platform.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--implementation', 'fk',
- '--platform', 'fake_platform',
- '--abi', 'none',
- 'fake',
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--implementation",
+ "fk",
+ "--platform",
+ "fake_platform",
+ "--abi",
+ "none",
+ "fake",
expect_error=True,
)
data.reset()
- fake_wheel(data, 'fake-1.0-fk2-otherabi-fake_platform.whl')
+ fake_wheel(data, "fake-1.0-fk2-otherabi-fake_platform.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--python-version', '2',
- '--implementation', 'fk',
- '--platform', 'fake_platform',
- '--abi', 'fakeabi', '--abi', 'otherabi', '--abi', 'none',
- 'fake'
- )
- result.did_create(
- Path('scratch') / 'fake-1.0-fk2-otherabi-fake_platform.whl'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--python-version",
+ "2",
+ "--implementation",
+ "fk",
+ "--platform",
+ "fake_platform",
+ "--abi",
+ "fakeabi",
+ "--abi",
+ "otherabi",
+ "--abi",
+ "none",
+ "fake",
)
+ result.did_create(Path("scratch") / "fake-1.0-fk2-otherabi-fake_platform.whl")
-def test_download_specify_implementation(script, data):
+def test_download_specify_implementation(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test using "pip download --abi" to download a .whl archive
supported for a specific abi
"""
- fake_wheel(data, 'fake-1.0-py2.py3-none-any.whl')
+ fake_wheel(data, "fake-1.0-py2.py3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--implementation', 'fk',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--implementation",
+ "fk",
+ "fake",
)
- result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl')
+ result.did_create(Path("scratch") / "fake-1.0-py2.py3-none-any.whl")
data.reset()
- fake_wheel(data, 'fake-1.0-fk3-none-any.whl')
+ fake_wheel(data, "fake-1.0-fk3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--implementation', 'fk',
- '--python-version', '3',
- 'fake'
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--implementation",
+ "fk",
+ "--python-version",
+ "3",
+ "fake",
)
- result.did_create(Path('scratch') / 'fake-1.0-fk3-none-any.whl')
+ result.did_create(Path("scratch") / "fake-1.0-fk3-none-any.whl")
result = script.pip(
- 'download', '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--dest', '.',
- '--implementation', 'fk',
- '--python-version', '2',
- 'fake',
+ "download",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--dest",
+ ".",
+ "--implementation",
+ "fk",
+ "--python-version",
+ "2",
+ "fake",
expect_error=True,
)
-def test_download_exit_status_code_when_no_requirements(script):
+def test_download_exit_status_code_when_no_requirements(
+ script: PipTestEnvironment,
+) -> None:
"""
Test download exit status code when no requirements specified
"""
- result = script.pip('download', expect_error=True)
- assert (
- "You must give at least one requirement to download" in result.stderr
- )
+ result = script.pip("download", expect_error=True)
+ assert "You must give at least one requirement to download" in result.stderr
assert result.returncode == ERROR
-def test_download_exit_status_code_when_blank_requirements_file(script):
+def test_download_exit_status_code_when_blank_requirements_file(
+ script: PipTestEnvironment,
+) -> None:
"""
Test download exit status code when blank requirements file specified
"""
script.scratch_path.joinpath("blank.txt").write_text("\n")
- script.pip('download', '-r', 'blank.txt')
+ script.pip("download", "-r", "blank.txt")
-def test_download_prefer_binary_when_tarball_higher_than_wheel(script, data):
- fake_wheel(data, 'source-0.8-py2.py3-none-any.whl')
+def test_download_prefer_binary_when_tarball_higher_than_wheel(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ fake_wheel(data, "source-0.8-py2.py3-none-any.whl")
result = script.pip(
- 'download',
- '--prefer-binary',
- '--no-index',
- '-f', data.packages,
- '-d', '.', 'source'
+ "download",
+ "--prefer-binary",
+ "--no-index",
+ "-f",
+ data.packages,
+ "-d",
+ ".",
+ "source",
)
- result.did_create(Path('scratch') / 'source-0.8-py2.py3-none-any.whl')
- result.did_not_create(Path('scratch') / 'source-1.0.tar.gz')
+ result.did_create(Path("scratch") / "source-0.8-py2.py3-none-any.whl")
+ result.did_not_create(Path("scratch") / "source-1.0.tar.gz")
-def test_prefer_binary_tarball_higher_than_wheel_req_file(script, data):
- fake_wheel(data, 'source-0.8-py2.py3-none-any.whl')
- script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""
+def test_prefer_binary_tarball_higher_than_wheel_req_file(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ fake_wheel(data, "source-0.8-py2.py3-none-any.whl")
+ script.scratch_path.joinpath("test-req.txt").write_text(
+ textwrap.dedent(
+ """
--prefer-binary
source
- """))
+ """
+ )
+ )
result = script.pip(
- 'download',
- '-r', script.scratch_path / 'test-req.txt',
- '--no-index',
- '-f', data.packages,
- '-d', '.'
+ "download",
+ "-r",
+ script.scratch_path / "test-req.txt",
+ "--no-index",
+ "-f",
+ data.packages,
+ "-d",
+ ".",
)
- result.did_create(Path('scratch') / 'source-0.8-py2.py3-none-any.whl')
- result.did_not_create(Path('scratch') / 'source-1.0.tar.gz')
+ result.did_create(Path("scratch") / "source-0.8-py2.py3-none-any.whl")
+ result.did_not_create(Path("scratch") / "source-1.0.tar.gz")
-def test_download_prefer_binary_when_wheel_doesnt_satisfy_req(script, data):
- fake_wheel(data, 'source-0.8-py2.py3-none-any.whl')
- script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""
+def test_download_prefer_binary_when_wheel_doesnt_satisfy_req(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ fake_wheel(data, "source-0.8-py2.py3-none-any.whl")
+ script.scratch_path.joinpath("test-req.txt").write_text(
+ textwrap.dedent(
+ """
source>0.9
- """))
+ """
+ )
+ )
result = script.pip(
- 'download',
- '--prefer-binary',
- '--no-index',
- '-f', data.packages,
- '-d', '.',
- '-r', script.scratch_path / 'test-req.txt'
+ "download",
+ "--prefer-binary",
+ "--no-index",
+ "-f",
+ data.packages,
+ "-d",
+ ".",
+ "-r",
+ script.scratch_path / "test-req.txt",
)
- result.did_create(Path('scratch') / 'source-1.0.tar.gz')
- result.did_not_create(Path('scratch') / 'source-0.8-py2.py3-none-any.whl')
+ result.did_create(Path("scratch") / "source-1.0.tar.gz")
+ result.did_not_create(Path("scratch") / "source-0.8-py2.py3-none-any.whl")
-def test_prefer_binary_when_wheel_doesnt_satisfy_req_req_file(script, data):
- fake_wheel(data, 'source-0.8-py2.py3-none-any.whl')
- script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""
+def test_prefer_binary_when_wheel_doesnt_satisfy_req_req_file(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ fake_wheel(data, "source-0.8-py2.py3-none-any.whl")
+ script.scratch_path.joinpath("test-req.txt").write_text(
+ textwrap.dedent(
+ """
--prefer-binary
source>0.9
- """))
+ """
+ )
+ )
result = script.pip(
- 'download',
- '--no-index',
- '-f', data.packages,
- '-d', '.',
- '-r', script.scratch_path / 'test-req.txt'
+ "download",
+ "--no-index",
+ "-f",
+ data.packages,
+ "-d",
+ ".",
+ "-r",
+ script.scratch_path / "test-req.txt",
)
- result.did_create(Path('scratch') / 'source-1.0.tar.gz')
- result.did_not_create(Path('scratch') / 'source-0.8-py2.py3-none-any.whl')
+ result.did_create(Path("scratch") / "source-1.0.tar.gz")
+ result.did_not_create(Path("scratch") / "source-0.8-py2.py3-none-any.whl")
-def test_download_prefer_binary_when_only_tarball_exists(script, data):
+def test_download_prefer_binary_when_only_tarball_exists(
+ script: PipTestEnvironment, data: TestData
+) -> None:
result = script.pip(
- 'download',
- '--prefer-binary',
- '--no-index',
- '-f', data.packages,
- '-d', '.', 'source'
+ "download",
+ "--prefer-binary",
+ "--no-index",
+ "-f",
+ data.packages,
+ "-d",
+ ".",
+ "source",
)
- result.did_create(Path('scratch') / 'source-1.0.tar.gz')
+ result.did_create(Path("scratch") / "source-1.0.tar.gz")
-def test_prefer_binary_when_only_tarball_exists_req_file(script, data):
- script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""
+def test_prefer_binary_when_only_tarball_exists_req_file(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ script.scratch_path.joinpath("test-req.txt").write_text(
+ textwrap.dedent(
+ """
--prefer-binary
source
- """))
+ """
+ )
+ )
result = script.pip(
- 'download',
- '--no-index',
- '-f', data.packages,
- '-d', '.',
- '-r', script.scratch_path / 'test-req.txt'
+ "download",
+ "--no-index",
+ "-f",
+ data.packages,
+ "-d",
+ ".",
+ "-r",
+ script.scratch_path / "test-req.txt",
)
- result.did_create(Path('scratch') / 'source-1.0.tar.gz')
+ result.did_create(Path("scratch") / "source-1.0.tar.gz")
@pytest.fixture(scope="session")
-def shared_script(tmpdir_factory, script_factory):
- tmpdir = Path(str(tmpdir_factory.mktemp("download_shared_script")))
+def shared_script(
+ tmpdir_factory: pytest.TempPathFactory, script_factory: ScriptFactory
+) -> PipTestEnvironment:
+ tmpdir = tmpdir_factory.mktemp("download_shared_script")
script = script_factory(tmpdir.joinpath("workspace"))
return script
-def test_download_file_url(shared_script, shared_data, tmpdir):
- download_dir = tmpdir / 'download'
+def test_download_file_url(
+ shared_script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
+ download_dir = tmpdir / "download"
download_dir.mkdir()
- downloaded_path = download_dir / 'simple-1.0.tar.gz'
+ downloaded_path = download_dir / "simple-1.0.tar.gz"
- simple_pkg = shared_data.packages / 'simple-1.0.tar.gz'
+ simple_pkg = shared_data.packages / "simple-1.0.tar.gz"
shared_script.pip(
- 'download',
- '-d',
+ "download",
+ "-d",
str(download_dir),
- '--no-index',
- path_to_url(str(simple_pkg)),
+ "--no-index",
+ simple_pkg.as_uri(),
)
assert downloaded_path.exists()
@@ -810,87 +1093,493 @@ def test_download_file_url(shared_script, shared_data, tmpdir):
def test_download_file_url_existing_ok_download(
- shared_script, shared_data, tmpdir
-):
- download_dir = tmpdir / 'download'
+ shared_script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
+ download_dir = tmpdir / "download"
download_dir.mkdir()
- downloaded_path = download_dir / 'simple-1.0.tar.gz'
- fake_existing_package = shared_data.packages / 'simple-2.0.tar.gz'
+ downloaded_path = download_dir / "simple-1.0.tar.gz"
+ fake_existing_package = shared_data.packages / "simple-2.0.tar.gz"
shutil.copy(str(fake_existing_package), str(downloaded_path))
downloaded_path_bytes = downloaded_path.read_bytes()
- digest = sha256(downloaded_path_bytes).hexdigest()
- simple_pkg = shared_data.packages / 'simple-1.0.tar.gz'
- url = "{}#sha256={}".format(path_to_url(simple_pkg), digest)
+ simple_pkg = shared_data.packages / "simple-1.0.tar.gz"
+ url = f"{simple_pkg.as_uri()}#sha256={sha256(downloaded_path_bytes).hexdigest()}"
- shared_script.pip('download', '-d', str(download_dir), url)
+ shared_script.pip("download", "-d", str(download_dir), url)
assert downloaded_path_bytes == downloaded_path.read_bytes()
def test_download_file_url_existing_bad_download(
- shared_script, shared_data, tmpdir
-):
- download_dir = tmpdir / 'download'
+ shared_script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
+ download_dir = tmpdir / "download"
download_dir.mkdir()
- downloaded_path = download_dir / 'simple-1.0.tar.gz'
- fake_existing_package = shared_data.packages / 'simple-2.0.tar.gz'
+ downloaded_path = download_dir / "simple-1.0.tar.gz"
+ fake_existing_package = shared_data.packages / "simple-2.0.tar.gz"
shutil.copy(str(fake_existing_package), str(downloaded_path))
- simple_pkg = shared_data.packages / 'simple-1.0.tar.gz'
+ simple_pkg = shared_data.packages / "simple-1.0.tar.gz"
simple_pkg_bytes = simple_pkg.read_bytes()
- digest = sha256(simple_pkg_bytes).hexdigest()
- url = "{}#sha256={}".format(path_to_url(simple_pkg), digest)
+ url = f"{simple_pkg.as_uri()}#sha256={sha256(simple_pkg_bytes).hexdigest()}"
- shared_script.pip('download', '-d', str(download_dir), url)
+ result = shared_script.pip(
+ "download",
+ "-d",
+ str(download_dir),
+ url,
+ allow_stderr_warning=True, # bad hash
+ )
assert simple_pkg_bytes == downloaded_path.read_bytes()
+ assert "WARNING: Previously-downloaded file" in result.stderr
+ assert "has bad hash. Re-downloading." in result.stderr
def test_download_http_url_bad_hash(
- shared_script, shared_data, tmpdir, mock_server
-):
+ shared_script: PipTestEnvironment,
+ shared_data: TestData,
+ tmpdir: Path,
+ mock_server: MockServer,
+) -> None:
"""
If already-downloaded file has bad checksum, re-download.
"""
- download_dir = tmpdir / 'download'
+ download_dir = tmpdir / "download"
download_dir.mkdir()
- downloaded_path = download_dir / 'simple-1.0.tar.gz'
- fake_existing_package = shared_data.packages / 'simple-2.0.tar.gz'
+ downloaded_path = download_dir / "simple-1.0.tar.gz"
+ fake_existing_package = shared_data.packages / "simple-2.0.tar.gz"
shutil.copy(str(fake_existing_package), str(downloaded_path))
- simple_pkg = shared_data.packages / 'simple-1.0.tar.gz'
+ simple_pkg = shared_data.packages / "simple-1.0.tar.gz"
simple_pkg_bytes = simple_pkg.read_bytes()
digest = sha256(simple_pkg_bytes).hexdigest()
- mock_server.set_responses([
- file_response(simple_pkg)
- ])
+ mock_server.set_responses([file_response(simple_pkg)])
mock_server.start()
- base_address = f'http://{mock_server.host}:{mock_server.port}'
+ base_address = f"http://{mock_server.host}:{mock_server.port}"
url = f"{base_address}/simple-1.0.tar.gz#sha256={digest}"
- shared_script.pip('download', '-d', str(download_dir), url)
+ result = shared_script.pip(
+ "download",
+ "-d",
+ str(download_dir),
+ url,
+ allow_stderr_warning=True, # bad hash
+ )
assert simple_pkg_bytes == downloaded_path.read_bytes()
+ assert "WARNING: Previously-downloaded file" in result.stderr
+ assert "has bad hash. Re-downloading." in result.stderr
mock_server.stop()
requests = mock_server.get_requests()
assert len(requests) == 1
- assert requests[0]['PATH_INFO'] == '/simple-1.0.tar.gz'
- assert requests[0]['HTTP_ACCEPT_ENCODING'] == 'identity'
+ assert requests[0]["PATH_INFO"] == "/simple-1.0.tar.gz"
+ assert requests[0]["HTTP_ACCEPT_ENCODING"] == "identity"
-def test_download_editable(script, data, tmpdir):
+def test_download_editable(
+ script: PipTestEnvironment, data: TestData, tmpdir: Path
+) -> None:
"""
Test 'pip download' of editables in requirement file.
"""
- editable_path = str(data.src / 'simplewheel-1.0').replace(os.path.sep, "/")
+ editable_path = str(data.src / "simplewheel-1.0").replace(os.path.sep, "/")
requirements_path = tmpdir / "requirements.txt"
requirements_path.write_text("-e " + editable_path + "\n")
download_dir = tmpdir / "download_dir"
script.pip(
- 'download', '--no-deps', '-r', str(requirements_path), '-d', str(download_dir)
+ "download", "--no-deps", "-r", str(requirements_path), "-d", str(download_dir)
)
downloads = os.listdir(download_dir)
assert len(downloads) == 1
assert downloads[0].endswith(".zip")
+
+
+def test_download_use_pep517_propagation(
+ script: PipTestEnvironment, tmpdir: Path, common_wheels: Path
+) -> None:
+ """
+ Check that --use-pep517 applies not just to the requirements specified
+ on the command line, but to their dependencies too.
+ """
+
+ create_basic_sdist_for_package(script, "fake_proj", "1.0", depends=["fake_dep"])
+
+ # If --use-pep517 is in effect, then setup.py should be running in an isolated
+ # environment that doesn't have pip in it.
+ create_basic_sdist_for_package(
+ script,
+ "fake_dep",
+ "1.0",
+ setup_py_prelude=textwrap.dedent(
+ """\
+ try:
+ import pip
+ except ImportError:
+ pass
+ else:
+ raise Exception(f"not running in isolation")
+ """
+ ),
+ )
+
+ download_dir = tmpdir / "download_dir"
+ script.pip(
+ "download",
+ f"--dest={download_dir}",
+ "--no-index",
+ f"--find-links={common_wheels}",
+ f"--find-links={script.scratch_path}",
+ "--use-pep517",
+ "fake_proj",
+ )
+
+ downloads = os.listdir(download_dir)
+ assert len(downloads) == 2
+
+
+class MetadataKind(Enum):
+ """All the types of values we might be provided for the data-dist-info-metadata
+ attribute from PEP 658."""
+
+ # Valid: will read metadata from the dist instead.
+ No = "none"
+ # Valid: will read the .metadata file, but won't check its hash.
+ Unhashed = "unhashed"
+ # Valid: will read the .metadata file and check its hash matches.
+ Sha256 = "sha256"
+ # Invalid: will error out after checking the hash.
+ WrongHash = "wrong-hash"
+ # Invalid: will error out after failing to fetch the .metadata file.
+ NoFile = "no-file"
+
+
+@dataclass(frozen=True)
+class Package:
+ """Mock package structure used to generate a PyPI repository.
+
+ Package name and version should correspond to sdists (.tar.gz files) in our test
+ data."""
+
+ name: str
+ version: str
+ filename: str
+ metadata: MetadataKind
+ # This will override any dependencies specified in the actual dist's METADATA.
+ requires_dist: Tuple[str, ...] = ()
+
+ def metadata_filename(self) -> str:
+ """This is specified by PEP 658."""
+ return f"{self.filename}.metadata"
+
+ def generate_additional_tag(self) -> str:
+ """This gets injected into the <a> tag in the generated PyPI index page for this
+ package."""
+ if self.metadata == MetadataKind.No:
+ return ""
+ if self.metadata in [MetadataKind.Unhashed, MetadataKind.NoFile]:
+ return 'data-dist-info-metadata="true"'
+ if self.metadata == MetadataKind.WrongHash:
+ return 'data-dist-info-metadata="sha256=WRONG-HASH"'
+ assert self.metadata == MetadataKind.Sha256
+ checksum = sha256(self.generate_metadata()).hexdigest()
+ return f'data-dist-info-metadata="sha256={checksum}"'
+
+ def requires_str(self) -> str:
+ if not self.requires_dist:
+ return ""
+ joined = " and ".join(self.requires_dist)
+ return f"Requires-Dist: {joined}"
+
+ def generate_metadata(self) -> bytes:
+ """This is written to `self.metadata_filename()` and will override the actual
+ dist's METADATA, unless `self.metadata == MetadataKind.NoFile`."""
+ return dedent(
+ f"""\
+ Metadata-Version: 2.1
+ Name: {self.name}
+ Version: {self.version}
+ {self.requires_str()}
+ """
+ ).encode("utf-8")
+
+
+@pytest.fixture(scope="function")
+def write_index_html_content(tmpdir: Path) -> Callable[[str], Path]:
+ """Generate a PyPI package index.html within a temporary local directory."""
+ html_dir = tmpdir / "index_html_content"
+ html_dir.mkdir()
+
+ def generate_index_html_subdir(index_html: str) -> Path:
+ """Create a new subdirectory after a UUID and write an index.html."""
+ new_subdir = html_dir / uuid.uuid4().hex
+ new_subdir.mkdir()
+
+ with open(new_subdir / "index.html", "w") as f:
+ f.write(index_html)
+
+ return new_subdir
+
+ return generate_index_html_subdir
+
+
+@pytest.fixture(scope="function")
+def html_index_for_packages(
+ shared_data: TestData,
+ write_index_html_content: Callable[[str], Path],
+) -> Callable[..., Path]:
+ """Generate a PyPI HTML package index within a local directory pointing to
+ blank data."""
+
+ def generate_html_index_for_packages(packages: Dict[str, List[Package]]) -> Path:
+ """
+ Produce a PyPI directory structure pointing to the specified packages.
+ """
+ # (1) Generate the content for a PyPI index.html.
+ pkg_links = "\n".join(
+ f' <a href="{pkg}/index.html">{pkg}</a>' for pkg in packages.keys()
+ )
+ index_html = f"""\
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta name="pypi:repository-version" content="1.0">
+ <title>Simple index</title>
+ </head>
+ <body>
+{pkg_links}
+ </body>
+</html>"""
+ # (2) Generate the index.html in a new subdirectory of the temp directory.
+ index_html_subdir = write_index_html_content(index_html)
+
+ # (3) Generate subdirectories for individual packages, each with their own
+ # index.html.
+ for pkg, links in packages.items():
+ pkg_subdir = index_html_subdir / pkg
+ pkg_subdir.mkdir()
+
+ download_links: List[str] = []
+ for package_link in links:
+ # (3.1) Generate the <a> tag which pip can crawl pointing to this
+ # specific package version.
+ download_links.append(
+ f' <a href="{package_link.filename}" {package_link.generate_additional_tag()}>{package_link.filename}</a><br/>' # noqa: E501
+ )
+ # (3.2) Copy over the corresponding file in `shared_data.packages`.
+ shutil.copy(
+ shared_data.packages / package_link.filename,
+ pkg_subdir / package_link.filename,
+ )
+ # (3.3) Write a metadata file, if applicable.
+ if package_link.metadata != MetadataKind.NoFile:
+ with open(pkg_subdir / package_link.metadata_filename(), "wb") as f:
+ f.write(package_link.generate_metadata())
+
+ # (3.4) After collating all the download links and copying over the files,
+ # write an index.html with the generated download links for each
+ # copied file for this specific package name.
+ download_links_str = "\n".join(download_links)
+ pkg_index_content = f"""\
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta name="pypi:repository-version" content="1.0">
+ <title>Links for {pkg}</title>
+ </head>
+ <body>
+ <h1>Links for {pkg}</h1>
+{download_links_str}
+ </body>
+</html>"""
+ with open(pkg_subdir / "index.html", "w") as f:
+ f.write(pkg_index_content)
+
+ return index_html_subdir
+
+ return generate_html_index_for_packages
+
+
+@pytest.fixture(scope="function")
+def download_generated_html_index(
+ script: PipTestEnvironment,
+ html_index_for_packages: Callable[[Dict[str, List[Package]]], Path],
+ tmpdir: Path,
+) -> Callable[..., Tuple[TestPipResult, Path]]:
+ """Execute `pip download` against a generated PyPI index."""
+ download_dir = tmpdir / "download_dir"
+
+ def run_for_generated_index(
+ packages: Dict[str, List[Package]],
+ args: List[str],
+ allow_error: bool = False,
+ ) -> Tuple[TestPipResult, Path]:
+ """
+ Produce a PyPI directory structure pointing to the specified packages, then
+ execute `pip download -i ...` pointing to our generated index.
+ """
+ index_dir = html_index_for_packages(packages)
+ pip_args = [
+ "download",
+ "-d",
+ str(download_dir),
+ "-i",
+ path_to_url(str(index_dir)),
+ *args,
+ ]
+ result = script.pip(*pip_args, allow_error=allow_error)
+ return (result, download_dir)
+
+ return run_for_generated_index
+
+
+# The package database we generate for testing PEP 658 support.
+_simple_packages: Dict[str, List[Package]] = {
+ "simple": [
+ Package("simple", "1.0", "simple-1.0.tar.gz", MetadataKind.Sha256),
+ Package("simple", "2.0", "simple-2.0.tar.gz", MetadataKind.No),
+ # This will raise a hashing error.
+ Package("simple", "3.0", "simple-3.0.tar.gz", MetadataKind.WrongHash),
+ ],
+ "simple2": [
+ # Override the dependencies here in order to force pip to download
+ # simple-1.0.tar.gz as well.
+ Package(
+ "simple2",
+ "1.0",
+ "simple2-1.0.tar.gz",
+ MetadataKind.Unhashed,
+ ("simple==1.0",),
+ ),
+ # This will raise an error when pip attempts to fetch the metadata file.
+ Package("simple2", "2.0", "simple2-2.0.tar.gz", MetadataKind.NoFile),
+ ],
+ "colander": [
+ # Ensure we can read the dependencies from a metadata file within a wheel
+ # *without* PEP 658 metadata.
+ Package(
+ "colander", "0.9.9", "colander-0.9.9-py2.py3-none-any.whl", MetadataKind.No
+ ),
+ ],
+ "compilewheel": [
+ # Ensure we can override the dependencies of a wheel file by injecting PEP
+ # 658 metadata.
+ Package(
+ "compilewheel",
+ "1.0",
+ "compilewheel-1.0-py2.py3-none-any.whl",
+ MetadataKind.Unhashed,
+ ("simple==1.0",),
+ ),
+ ],
+ "has-script": [
+ # Ensure we check PEP 658 metadata hashing errors for wheel files.
+ Package(
+ "has-script",
+ "1.0",
+ "has.script-1.0-py2.py3-none-any.whl",
+ MetadataKind.WrongHash,
+ ),
+ ],
+ "translationstring": [
+ Package(
+ "translationstring", "1.1", "translationstring-1.1.tar.gz", MetadataKind.No
+ ),
+ ],
+ "priority": [
+ # Ensure we check for a missing metadata file for wheels.
+ Package(
+ "priority", "1.0", "priority-1.0-py2.py3-none-any.whl", MetadataKind.NoFile
+ ),
+ ],
+}
+
+
+@pytest.mark.parametrize(
+ "requirement_to_download, expected_outputs",
+ [
+ ("simple2==1.0", ["simple-1.0.tar.gz", "simple2-1.0.tar.gz"]),
+ ("simple==2.0", ["simple-2.0.tar.gz"]),
+ (
+ "colander",
+ ["colander-0.9.9-py2.py3-none-any.whl", "translationstring-1.1.tar.gz"],
+ ),
+ (
+ "compilewheel",
+ ["compilewheel-1.0-py2.py3-none-any.whl", "simple-1.0.tar.gz"],
+ ),
+ ],
+)
+def test_download_metadata(
+ download_generated_html_index: Callable[..., Tuple[TestPipResult, Path]],
+ requirement_to_download: str,
+ expected_outputs: List[str],
+) -> None:
+ """Verify that if a data-dist-info-metadata attribute is present, then it is used
+ instead of the actual dist's METADATA."""
+ _, download_dir = download_generated_html_index(
+ _simple_packages,
+ [requirement_to_download],
+ )
+ assert sorted(os.listdir(download_dir)) == expected_outputs
+
+
+@pytest.mark.parametrize(
+ "requirement_to_download, real_hash",
+ [
+ (
+ "simple==3.0",
+ "95e0f200b6302989bcf2cead9465cf229168295ea330ca30d1ffeab5c0fed996",
+ ),
+ (
+ "has-script",
+ "16ba92d7f6f992f6de5ecb7d58c914675cf21f57f8e674fb29dcb4f4c9507e5b",
+ ),
+ ],
+)
+def test_incorrect_metadata_hash(
+ download_generated_html_index: Callable[..., Tuple[TestPipResult, Path]],
+ requirement_to_download: str,
+ real_hash: str,
+) -> None:
+ """Verify that if a hash for data-dist-info-metadata is provided, it must match the
+ actual hash of the metadata file."""
+ result, _ = download_generated_html_index(
+ _simple_packages,
+ [requirement_to_download],
+ allow_error=True,
+ )
+ assert result.returncode != 0
+ expected_msg = f"""\
+ Expected sha256 WRONG-HASH
+ Got {real_hash}"""
+ assert expected_msg in result.stderr
+
+
+@pytest.mark.parametrize(
+ "requirement_to_download, expected_url",
+ [
+ ("simple2==2.0", "simple2-2.0.tar.gz.metadata"),
+ ("priority", "priority-1.0-py2.py3-none-any.whl.metadata"),
+ ],
+)
+def test_metadata_not_found(
+ download_generated_html_index: Callable[..., Tuple[TestPipResult, Path]],
+ requirement_to_download: str,
+ expected_url: str,
+) -> None:
+ """Verify that if a data-dist-info-metadata attribute is provided, that pip will
+ fetch the .metadata file at the location specified by PEP 658, and error
+ if unavailable."""
+ result, _ = download_generated_html_index(
+ _simple_packages,
+ [requirement_to_download],
+ allow_error=True,
+ )
+ assert result.returncode != 0
+ expected_re = re.escape(expected_url)
+ pattern = re.compile(
+ f"ERROR: 404 Client Error: FileNotFoundError for url:.*{expected_re}"
+ )
+ assert pattern.search(result.stderr), (pattern, result.stderr)
diff --git a/tests/functional/test_fast_deps.py b/tests/functional/test_fast_deps.py
index e82641986..0109db825 100644
--- a/tests/functional/test_fast_deps.py
+++ b/tests/functional/test_fast_deps.py
@@ -1,79 +1,103 @@
import fnmatch
import json
+import os
+import pathlib
from os.path import basename
+from typing import Iterable
from pip._vendor.packaging.utils import canonicalize_name
from pytest import mark
+from tests.lib import PipTestEnvironment, TestData, TestPipResult
-def pip(script, command, requirement):
+
+def pip(script: PipTestEnvironment, command: str, requirement: str) -> TestPipResult:
return script.pip(
- command, '--prefer-binary', '--no-cache-dir',
- '--use-feature=fast-deps', requirement,
+ command,
+ "--prefer-binary",
+ "--no-cache-dir",
+ "--use-feature=fast-deps",
+ requirement,
allow_stderr_warning=True,
)
-def assert_installed(script, names):
- list_output = json.loads(script.pip('list', '--format=json').stdout)
- installed = {canonicalize_name(item['name']) for item in list_output}
+def assert_installed(script: PipTestEnvironment, names: str) -> None:
+ list_output = json.loads(script.pip("list", "--format=json").stdout)
+ installed = {canonicalize_name(item["name"]) for item in list_output}
assert installed.issuperset(map(canonicalize_name, names))
@mark.network
-@mark.parametrize(('requirement', 'expected'), (
- ('Paste==3.4.2', ('Paste', 'six')),
- ('Paste[flup]==3.4.2', ('Paste', 'six', 'flup')),
-))
-def test_install_from_pypi(requirement, expected, script):
- pip(script, 'install', requirement)
+@mark.parametrize(
+ ("requirement", "expected"),
+ (
+ ("Paste==3.4.2", ("Paste", "six")),
+ ("Paste[flup]==3.4.2", ("Paste", "six", "flup")),
+ ),
+)
+def test_install_from_pypi(
+ requirement: str, expected: str, script: PipTestEnvironment
+) -> None:
+ pip(script, "install", requirement)
assert_installed(script, expected)
@mark.network
-@mark.parametrize(('requirement', 'expected'), (
- ('Paste==3.4.2', ('Paste-3.4.2-*.whl', 'six-*.whl')),
- ('Paste[flup]==3.4.2', ('Paste-3.4.2-*.whl', 'six-*.whl', 'flup-*')),
-))
-def test_download_from_pypi(requirement, expected, script):
- result = pip(script, 'download', requirement)
- created = list(map(basename, result.files_created))
+@mark.parametrize(
+ ("requirement", "expected"),
+ (
+ ("Paste==3.4.2", ("Paste-3.4.2-*.whl", "six-*.whl")),
+ ("Paste[flup]==3.4.2", ("Paste-3.4.2-*.whl", "six-*.whl", "flup-*")),
+ ),
+)
+def test_download_from_pypi(
+ requirement: str, expected: Iterable[str], script: PipTestEnvironment
+) -> None:
+ result = pip(script, "download", requirement)
+ created = [basename(f) for f in result.files_created]
assert all(fnmatch.filter(created, f) for f in expected)
@mark.network
-def test_build_wheel_with_deps(data, script):
- result = pip(script, 'wheel', data.packages/'requiresPaste')
- created = list(map(basename, result.files_created))
- assert fnmatch.filter(created, 'requiresPaste-3.1.4-*.whl')
- assert fnmatch.filter(created, 'Paste-3.4.2-*.whl')
- assert fnmatch.filter(created, 'six-*.whl')
+def test_build_wheel_with_deps(data: TestData, script: PipTestEnvironment) -> None:
+ result = pip(script, "wheel", os.fspath(data.packages / "requiresPaste"))
+ created = [basename(f) for f in result.files_created]
+ assert fnmatch.filter(created, "requirespaste-3.1.4-*.whl")
+ assert fnmatch.filter(created, "Paste-3.4.2-*.whl")
+ assert fnmatch.filter(created, "six-*.whl")
@mark.network
-def test_require_hash(script, tmp_path):
- reqs = tmp_path / 'requirements.txt'
+def test_require_hash(script: PipTestEnvironment, tmp_path: pathlib.Path) -> None:
+ reqs = tmp_path / "requirements.txt"
reqs.write_text(
- 'idna==2.10'
- ' --hash=sha256:'
- 'b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0'
- ' --hash=sha256:'
- 'b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6'
+ "idna==2.10"
+ " --hash=sha256:"
+ "b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
+ " --hash=sha256:"
+ "b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"
)
result = script.pip(
- 'download', '--use-feature=fast-deps', '-r', str(reqs),
+ "download",
+ "--use-feature=fast-deps",
+ "-r",
+ str(reqs),
allow_stderr_warning=True,
)
- created = list(map(basename, result.files_created))
- assert fnmatch.filter(created, 'idna-2.10*')
+ created = [basename(f) for f in result.files_created]
+ assert fnmatch.filter(created, "idna-2.10*")
@mark.network
-def test_hash_mismatch(script, tmp_path):
- reqs = tmp_path / 'requirements.txt'
- reqs.write_text('idna==2.10 --hash=sha256:irna')
+def test_hash_mismatch(script: PipTestEnvironment, tmp_path: pathlib.Path) -> None:
+ reqs = tmp_path / "requirements.txt"
+ reqs.write_text("idna==2.10 --hash=sha256:irna")
result = script.pip(
- 'download', '--use-feature=fast-deps', '-r', str(reqs),
+ "download",
+ "--use-feature=fast-deps",
+ "-r",
+ str(reqs),
expect_error=True,
)
- assert 'DO NOT MATCH THE HASHES' in result.stderr
+ assert "DO NOT MATCH THE HASHES" in result.stderr
diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py
index 6a0f4c8e5..535581121 100644
--- a/tests/functional/test_freeze.py
+++ b/tests/functional/test_freeze.py
@@ -3,11 +3,15 @@ import re
import sys
import textwrap
from doctest import ELLIPSIS, OutputChecker
+from pathlib import Path
import pytest
from pip._vendor.packaging.utils import canonicalize_name
+from pip._internal.models.direct_url import DirectUrl, DirInfo
from tests.lib import (
+ PipTestEnvironment,
+ TestData,
_create_test_package,
_create_test_package_with_srcdir,
_git_commit,
@@ -16,14 +20,15 @@ from tests.lib import (
need_bzr,
need_mercurial,
need_svn,
- path_to_url,
wheel,
)
+from tests.lib.direct_url import get_created_direct_url_path
+from tests.lib.venv import VirtualEnvironment
-distribute_re = re.compile('^distribute==[0-9.]+\n', re.MULTILINE)
+distribute_re = re.compile("^distribute==[0-9.]+\n", re.MULTILINE)
-def _check_output(result, expected):
+def _check_output(result: str, expected: str) -> None:
checker = OutputChecker()
actual = str(result)
@@ -35,23 +40,22 @@ def _check_output(result, expected):
# but then you have to remember to upcase <BLANKLINE>. The right
# thing to do in the end is probably to find out how to report
# the proper fully-cased package name in our error message.
- if sys.platform == 'win32':
- actual = actual.replace('initools', 'INITools')
+ if sys.platform == "win32":
+ actual = actual.replace("initools", "INITools")
# This allows our existing tests to work when run in a context
# with distribute installed.
- actual = distribute_re.sub('', actual)
+ actual = distribute_re.sub("", actual)
- def banner(msg):
- return f'\n========== {msg} ==========\n'
+ def banner(msg: str) -> str:
+ return f"\n========== {msg} ==========\n"
assert checker.check_output(expected, actual, ELLIPSIS), (
- banner('EXPECTED') + expected + banner('ACTUAL') + actual +
- banner(6 * '=')
+ banner("EXPECTED") + expected + banner("ACTUAL") + actual + banner(6 * "=")
)
-def test_basic_freeze(script):
+def test_basic_freeze(script: PipTestEnvironment) -> None:
"""
Some tests of freeze, first we have to install some stuff. Note that
the test is a little crude at the end because Python 2.5+ adds egg
@@ -60,31 +64,39 @@ def test_basic_freeze(script):
currently it is not).
"""
- script.scratch_path.joinpath("initools-req.txt").write_text(textwrap.dedent("""\
+ script.scratch_path.joinpath("initools-req.txt").write_text(
+ textwrap.dedent(
+ """\
simple==2.0
# and something else to test out:
simple2<=3.0
- """))
+ """
+ )
+ )
script.pip_install_local(
- '-r', script.scratch_path / 'initools-req.txt',
+ "-r",
+ script.scratch_path / "initools-req.txt",
)
- result = script.pip('freeze', expect_stderr=True)
- expected = textwrap.dedent("""\
+ result = script.pip("freeze", expect_stderr=True)
+ expected = textwrap.dedent(
+ """\
...simple==2.0
simple2==3.0...
- <BLANKLINE>""")
+ <BLANKLINE>"""
+ )
_check_output(result.stdout, expected)
-def test_freeze_with_pip(script):
+def test_freeze_with_pip(script: PipTestEnvironment) -> None:
"""Test pip shows itself"""
- result = script.pip('freeze', '--all')
- assert 'pip==' in result.stdout
+ result = script.pip("freeze", "--all")
+ assert "pip==" in result.stdout
-def test_exclude_and_normalization(script, tmpdir):
- req_path = wheel.make_wheel(
- name="Normalizable_Name", version="1.0").save_to_dir(tmpdir)
+def test_exclude_and_normalization(script: PipTestEnvironment, tmpdir: Path) -> None:
+ req_path = wheel.make_wheel(name="Normalizable_Name", version="1.0").save_to_dir(
+ tmpdir
+ )
script.pip("install", "--no-index", req_path)
result = script.pip("freeze")
assert "Normalizable_Name" in result.stdout
@@ -92,45 +104,54 @@ def test_exclude_and_normalization(script, tmpdir):
assert "Normalizable_Name" not in result.stdout
-def test_freeze_multiple_exclude_with_all(script, with_wheel):
- result = script.pip('freeze', '--all')
- assert 'pip==' in result.stdout
- assert 'wheel==' in result.stdout
- result = script.pip('freeze', '--all', '--exclude', 'pip', '--exclude', 'wheel')
- assert 'pip==' not in result.stdout
- assert 'wheel==' not in result.stdout
+@pytest.mark.usefixtures("with_wheel")
+def test_freeze_multiple_exclude_with_all(script: PipTestEnvironment) -> None:
+ result = script.pip("freeze", "--all")
+ assert "pip==" in result.stdout
+ assert "wheel==" in result.stdout
+ result = script.pip("freeze", "--all", "--exclude", "pip", "--exclude", "wheel")
+ assert "pip==" not in result.stdout
+ assert "wheel==" not in result.stdout
-def test_freeze_with_invalid_names(script):
+def test_freeze_with_invalid_names(script: PipTestEnvironment) -> None:
"""
Test that invalid names produce warnings and are passed over gracefully.
"""
- def fake_install(pkgname, dest):
+ def fake_install(pkgname: str, dest: str) -> None:
egg_info_path = os.path.join(
- dest, '{}-1.0-py{}.{}.egg-info'.format(
- pkgname.replace('-', '_'),
- sys.version_info[0],
- sys.version_info[1]
- )
+ dest,
+ "{}-1.0-py{}.{}.egg-info".format(
+ pkgname.replace("-", "_"), sys.version_info[0], sys.version_info[1]
+ ),
)
- with open(egg_info_path, 'w') as egg_info_file:
- egg_info_file.write(textwrap.dedent("""\
+ with open(egg_info_path, "w") as egg_info_file:
+ egg_info_file.write(
+ textwrap.dedent(
+ """\
Metadata-Version: 1.0
Name: {}
Version: 1.0
- """.format(pkgname)
- ))
+ """.format(
+ pkgname
+ )
+ )
+ )
- valid_pkgnames = ('middle-dash', 'middle_underscore', 'middle.dot')
+ valid_pkgnames = ("middle-dash", "middle_underscore", "middle.dot")
invalid_pkgnames = (
- '-leadingdash', '_leadingunderscore', '.leadingdot',
- 'trailingdash-', 'trailingunderscore_', 'trailingdot.'
+ "-leadingdash",
+ "_leadingunderscore",
+ ".leadingdot",
+ "trailingdash-",
+ "trailingunderscore_",
+ "trailingdot.",
)
for pkgname in valid_pkgnames + invalid_pkgnames:
- fake_install(pkgname, script.site_packages_path)
+ fake_install(pkgname, os.fspath(script.site_packages_path))
- result = script.pip('freeze', expect_stderr=True)
+ result = script.pip("freeze", expect_stderr=True)
# Check all valid names are in the output.
output_lines = {line.strip() for line in result.stdout.splitlines()}
@@ -149,62 +170,71 @@ def test_freeze_with_invalid_names(script):
@pytest.mark.git
-def test_freeze_editable_not_vcs(script, tmpdir):
+def test_freeze_editable_not_vcs(script: PipTestEnvironment) -> None:
"""
Test an editable install that is not version controlled.
"""
- pkg_path = _create_test_package(script)
+ pkg_path = _create_test_package(script.scratch_path)
# Rename the .git directory so the directory is no longer recognized
# as a VCS directory.
- os.rename(os.path.join(pkg_path, '.git'), os.path.join(pkg_path, '.bak'))
- script.pip('install', '-e', pkg_path)
- result = script.pip('freeze')
+ os.rename(os.path.join(pkg_path, ".git"), os.path.join(pkg_path, ".bak"))
+ script.pip("install", "-e", pkg_path)
+ result = script.pip("freeze")
# We need to apply os.path.normcase() to the path since that is what
# the freeze code does.
- expected = textwrap.dedent("""\
+ expected = textwrap.dedent(
+ """\
...# Editable install with no version control (version-pkg==0.1)
-e {}
- ...""".format(os.path.normcase(pkg_path)))
+ ...""".format(
+ os.path.normcase(pkg_path)
+ )
+ )
_check_output(result.stdout, expected)
@pytest.mark.git
-def test_freeze_editable_git_with_no_remote(script, tmpdir, deprecated_python):
+def test_freeze_editable_git_with_no_remote(
+ script: PipTestEnvironment, deprecated_python: bool
+) -> None:
"""
Test an editable Git install with no remote url.
"""
- pkg_path = _create_test_package(script)
- script.pip('install', '-e', pkg_path)
- result = script.pip('freeze')
+ pkg_path = _create_test_package(script.scratch_path)
+ script.pip("install", "-e", pkg_path)
+ result = script.pip("freeze")
if not deprecated_python:
- assert result.stderr == ''
+ assert result.stderr == ""
# We need to apply os.path.normcase() to the path since that is what
# the freeze code does.
- expected = textwrap.dedent("""\
+ expected = textwrap.dedent(
+ """\
...# Editable Git install with no remote (version-pkg==0.1)
-e {}
- ...""".format(os.path.normcase(pkg_path)))
+ ...""".format(
+ os.path.normcase(pkg_path)
+ )
+ )
_check_output(result.stdout, expected)
@need_svn
-def test_freeze_svn(script, tmpdir):
+def test_freeze_svn(script: PipTestEnvironment) -> None:
"""Test freezing a svn checkout"""
- checkout_path = _create_test_package(script, vcs='svn')
+ checkout_path = _create_test_package(script.scratch_path, vcs="svn")
# Install with develop
- script.run(
- 'python', 'setup.py', 'develop',
- cwd=checkout_path, expect_stderr=True
- )
- result = script.pip('freeze', expect_stderr=True)
- expected = textwrap.dedent("""\
+ script.run("python", "setup.py", "develop", cwd=checkout_path, expect_stderr=True)
+ result = script.pip("freeze", expect_stderr=True)
+ expected = textwrap.dedent(
+ """\
...-e svn+...#egg=version_pkg
- ...""")
+ ..."""
+ )
_check_output(result.stdout, expected)
@@ -215,24 +245,29 @@ def test_freeze_svn(script, tmpdir):
run=True,
strict=True,
)
-def test_freeze_exclude_editable(script, tmpdir):
+def test_freeze_exclude_editable(script: PipTestEnvironment) -> None:
"""
Test excluding editable from freezing list.
"""
# Returns path to a generated package called "version_pkg"
- pkg_version = _create_test_package(script)
+ pkg_version = _create_test_package(script.scratch_path)
result = script.run(
- 'git', 'clone', pkg_version, 'pip-test-package',
+ "git",
+ "clone",
+ os.fspath(pkg_version),
+ "pip-test-package",
expect_stderr=True,
)
- repo_dir = script.scratch_path / 'pip-test-package'
+ repo_dir = script.scratch_path / "pip-test-package"
result = script.run(
- 'python', 'setup.py', 'develop',
+ "python",
+ "setup.py",
+ "develop",
cwd=repo_dir,
expect_stderr=True,
)
- result = script.pip('freeze', '--exclude-editable', expect_stderr=True)
+ result = script.pip("freeze", "--exclude-editable", expect_stderr=True)
expected = textwrap.dedent(
"""
...-e git+...#egg=version_pkg
@@ -243,24 +278,29 @@ def test_freeze_exclude_editable(script, tmpdir):
@pytest.mark.git
-def test_freeze_git_clone(script, tmpdir):
+def test_freeze_git_clone(script: PipTestEnvironment) -> None:
"""
Test freezing a Git clone.
"""
# Returns path to a generated package called "version_pkg"
- pkg_version = _create_test_package(script)
+ pkg_version = _create_test_package(script.scratch_path)
result = script.run(
- 'git', 'clone', pkg_version, 'pip-test-package',
+ "git",
+ "clone",
+ os.fspath(pkg_version),
+ "pip-test-package",
expect_stderr=True,
)
- repo_dir = script.scratch_path / 'pip-test-package'
+ repo_dir = script.scratch_path / "pip-test-package"
result = script.run(
- 'python', 'setup.py', 'develop',
+ "python",
+ "setup.py",
+ "develop",
cwd=repo_dir,
expect_stderr=True,
)
- result = script.pip('freeze', expect_stderr=True)
+ result = script.pip("freeze", expect_stderr=True)
expected = textwrap.dedent(
"""
...-e git+...#egg=version_pkg
@@ -272,17 +312,20 @@ def test_freeze_git_clone(script, tmpdir):
# Check that slashes in branch or tag names are translated.
# See also issue #1083: https://github.com/pypa/pip/issues/1083
script.run(
- 'git', 'checkout', '-b', 'branch/name/with/slash',
+ "git",
+ "checkout",
+ "-b",
+ "branch/name/with/slash",
cwd=repo_dir,
expect_stderr=True,
)
# Create a new commit to ensure that the commit has only one branch
# or tag name associated to it (to avoid the non-determinism reported
# in issue #1867).
- (repo_dir / 'newfile').touch()
- script.run('git', 'add', 'newfile', cwd=repo_dir)
- _git_commit(script, repo_dir, message='...')
- result = script.pip('freeze', expect_stderr=True)
+ (repo_dir / "newfile").touch()
+ script.run("git", "add", "newfile", cwd=repo_dir)
+ _git_commit(script, repo_dir, message="...")
+ result = script.pip("freeze", expect_stderr=True)
expected = textwrap.dedent(
"""
...-e ...@...#egg=version_pkg
@@ -293,26 +336,31 @@ def test_freeze_git_clone(script, tmpdir):
@pytest.mark.git
-def test_freeze_git_clone_srcdir(script, tmpdir):
+def test_freeze_git_clone_srcdir(script: PipTestEnvironment) -> None:
"""
Test freezing a Git clone where setup.py is in a subdirectory
relative the repo root and the source code is in a subdirectory
relative to setup.py.
"""
# Returns path to a generated package called "version_pkg"
- pkg_version = _create_test_package_with_srcdir(script)
+ pkg_version = _create_test_package_with_srcdir(script.scratch_path)
result = script.run(
- 'git', 'clone', pkg_version, 'pip-test-package',
+ "git",
+ "clone",
+ os.fspath(pkg_version),
+ "pip-test-package",
expect_stderr=True,
)
- repo_dir = script.scratch_path / 'pip-test-package'
+ repo_dir = script.scratch_path / "pip-test-package"
result = script.run(
- 'python', 'setup.py', 'develop',
- cwd=repo_dir / 'subdir',
+ "python",
+ "setup.py",
+ "develop",
+ cwd=repo_dir / "subdir",
expect_stderr=True,
)
- result = script.pip('freeze', expect_stderr=True)
+ result = script.pip("freeze", expect_stderr=True)
expected = textwrap.dedent(
"""
...-e git+...#egg=version_pkg&subdirectory=subdir
@@ -323,24 +371,19 @@ def test_freeze_git_clone_srcdir(script, tmpdir):
@need_mercurial
-def test_freeze_mercurial_clone_srcdir(script, tmpdir):
+def test_freeze_mercurial_clone_srcdir(script: PipTestEnvironment) -> None:
"""
Test freezing a Mercurial clone where setup.py is in a subdirectory
relative to the repo root and the source code is in a subdirectory
relative to setup.py.
"""
# Returns path to a generated package called "version_pkg"
- pkg_version = _create_test_package_with_srcdir(script, vcs='hg')
+ pkg_version = _create_test_package_with_srcdir(script.scratch_path, vcs="hg")
- result = script.run(
- 'hg', 'clone', pkg_version, 'pip-test-package'
- )
- repo_dir = script.scratch_path / 'pip-test-package'
- result = script.run(
- 'python', 'setup.py', 'develop',
- cwd=repo_dir / 'subdir'
- )
- result = script.pip('freeze')
+ result = script.run("hg", "clone", os.fspath(pkg_version), "pip-test-package")
+ repo_dir = script.scratch_path / "pip-test-package"
+ result = script.run("python", "setup.py", "develop", cwd=repo_dir / "subdir")
+ result = script.pip("freeze")
expected = textwrap.dedent(
"""
...-e hg+...#egg=version_pkg&subdirectory=subdir
@@ -351,89 +394,113 @@ def test_freeze_mercurial_clone_srcdir(script, tmpdir):
@pytest.mark.git
-def test_freeze_git_remote(script, tmpdir):
+def test_freeze_git_remote(script: PipTestEnvironment) -> None:
"""
Test freezing a Git clone.
"""
# Returns path to a generated package called "version_pkg"
- pkg_version = _create_test_package(script)
+ pkg_version = _create_test_package(script.scratch_path)
result = script.run(
- 'git', 'clone', pkg_version, 'pip-test-package',
+ "git",
+ "clone",
+ os.fspath(pkg_version),
+ "pip-test-package",
expect_stderr=True,
)
- repo_dir = script.scratch_path / 'pip-test-package'
+ repo_dir = script.scratch_path / "pip-test-package"
result = script.run(
- 'python', 'setup.py', 'develop',
+ "python",
+ "setup.py",
+ "develop",
cwd=repo_dir,
expect_stderr=True,
)
origin_remote = pkg_version
# check frozen remote after clone
- result = script.pip('freeze', expect_stderr=True)
- expected = textwrap.dedent(
- """
+ result = script.pip("freeze", expect_stderr=True)
+ expected = (
+ textwrap.dedent(
+ """
...-e git+{remote}@...#egg=version_pkg
...
"""
- ).format(remote=path_to_url(origin_remote)).strip()
+ )
+ .format(remote=origin_remote.as_uri())
+ .strip()
+ )
_check_output(result.stdout, expected)
# check frozen remote when there is no remote named origin
- script.run('git', 'remote', 'rename', 'origin', 'other', cwd=repo_dir)
- result = script.pip('freeze', expect_stderr=True)
- expected = textwrap.dedent(
- """
+ script.run("git", "remote", "rename", "origin", "other", cwd=repo_dir)
+ result = script.pip("freeze", expect_stderr=True)
+ expected = (
+ textwrap.dedent(
+ """
...-e git+{remote}@...#egg=version_pkg
...
"""
- ).format(remote=path_to_url(origin_remote)).strip()
+ )
+ .format(remote=origin_remote.as_uri())
+ .strip()
+ )
_check_output(result.stdout, expected)
# When the remote is a local path, it must exist.
# If it doesn't, it gets flagged as invalid.
- other_remote = pkg_version + '-other'
- script.run('git', 'remote', 'set-url', 'other', other_remote, cwd=repo_dir)
- result = script.pip('freeze', expect_stderr=True)
- expected = os.path.normcase(textwrap.dedent(
- f"""
+ other_remote = f"{pkg_version}-other"
+ script.run("git", "remote", "set-url", "other", other_remote, cwd=repo_dir)
+ result = script.pip("freeze", expect_stderr=True)
+ expected = os.path.normcase(
+ textwrap.dedent(
+ f"""
...# Editable Git...(version-pkg...)...
# '{other_remote}'
-e {repo_dir}...
"""
- ).strip())
+ ).strip()
+ )
_check_output(os.path.normcase(result.stdout), expected)
# when there are more than one origin, priority is given to the
# remote named origin
- script.run('git', 'remote', 'add', 'origin', origin_remote, cwd=repo_dir)
- result = script.pip('freeze', expect_stderr=True)
- expected = textwrap.dedent(
- """
+ script.run("git", "remote", "add", "origin", os.fspath(origin_remote), cwd=repo_dir)
+ result = script.pip("freeze", expect_stderr=True)
+ expected = (
+ textwrap.dedent(
+ """
...-e git+{remote}@...#egg=version_pkg
...
"""
- ).format(remote=path_to_url(origin_remote)).strip()
+ )
+ .format(remote=origin_remote.as_uri())
+ .strip()
+ )
_check_output(result.stdout, expected)
@need_mercurial
-def test_freeze_mercurial_clone(script, tmpdir):
+def test_freeze_mercurial_clone(script: PipTestEnvironment) -> None:
"""
Test freezing a Mercurial clone.
"""
# Returns path to a generated package called "version_pkg"
- pkg_version = _create_test_package(script, vcs='hg')
+ pkg_version = _create_test_package(script.scratch_path, vcs="hg")
result = script.run(
- 'hg', 'clone', pkg_version, 'pip-test-package',
+ "hg",
+ "clone",
+ os.fspath(pkg_version),
+ "pip-test-package",
expect_stderr=True,
)
- repo_dir = script.scratch_path / 'pip-test-package'
+ repo_dir = script.scratch_path / "pip-test-package"
result = script.run(
- 'python', 'setup.py', 'develop',
+ "python",
+ "setup.py",
+ "develop",
cwd=repo_dir,
expect_stderr=True,
)
- result = script.pip('freeze', expect_stderr=True)
+ result = script.pip("freeze", expect_stderr=True)
expected = textwrap.dedent(
"""
...-e hg+...#egg=version_pkg
@@ -444,28 +511,30 @@ def test_freeze_mercurial_clone(script, tmpdir):
@need_bzr
-def test_freeze_bazaar_clone(script, tmpdir):
+def test_freeze_bazaar_clone(script: PipTestEnvironment) -> None:
"""
Test freezing a Bazaar clone.
"""
try:
- checkout_path = _create_test_package(script, vcs='bazaar')
+ checkout_path = _create_test_package(script.scratch_path, vcs="bazaar")
except OSError as e:
- pytest.fail(f'Invoking `bzr` failed: {e}')
+ pytest.fail(f"Invoking `bzr` failed: {e}")
+ result = script.run("bzr", "checkout", os.fspath(checkout_path), "bzr-package")
result = script.run(
- 'bzr', 'checkout', checkout_path, 'bzr-package'
- )
- result = script.run(
- 'python', 'setup.py', 'develop',
- cwd=script.scratch_path / 'bzr-package',
+ "python",
+ "setup.py",
+ "develop",
+ cwd=script.scratch_path / "bzr-package",
expect_stderr=True,
)
- result = script.pip('freeze', expect_stderr=True)
- expected = textwrap.dedent("""\
+ result = script.pip("freeze", expect_stderr=True)
+ expected = textwrap.dedent(
+ """\
...-e bzr+file://...@1#egg=version_pkg
- ...""")
+ ..."""
+ )
_check_output(result.stdout, expected)
@@ -475,11 +544,12 @@ def test_freeze_bazaar_clone(script, tmpdir):
"outer_vcs, inner_vcs",
[("hg", "git"), ("git", "hg")],
)
-def test_freeze_nested_vcs(script, outer_vcs, inner_vcs):
- """Test VCS can be correctly freezed when resides inside another VCS repo.
- """
+def test_freeze_nested_vcs(
+ script: PipTestEnvironment, outer_vcs: str, inner_vcs: str
+) -> None:
+ """Test VCS can be correctly freezed when resides inside another VCS repo."""
# Create Python package.
- pkg_path = _create_test_package(script, vcs=inner_vcs)
+ pkg_path = _create_test_package(script.scratch_path, vcs=inner_vcs)
# Create outer repo to clone into.
root_path = script.scratch_path.joinpath("test_freeze_nested_vcs")
@@ -491,7 +561,13 @@ def test_freeze_nested_vcs(script, outer_vcs, inner_vcs):
# Clone Python package into inner directory and install it.
src_path = root_path.joinpath("src")
src_path.mkdir()
- script.run(inner_vcs, "clone", pkg_path, src_path, expect_stderr=True)
+ script.run(
+ inner_vcs,
+ "clone",
+ os.fspath(pkg_path),
+ os.fspath(src_path),
+ expect_stderr=True,
+ )
script.pip("install", "-e", src_path, expect_stderr=True)
# Check the freeze output recognizes the inner VCS.
@@ -503,7 +579,8 @@ def test_freeze_nested_vcs(script, outer_vcs, inner_vcs):
# used by the test_freeze_with_requirement_* tests below
-_freeze_req_opts = textwrap.dedent("""\
+_freeze_req_opts = textwrap.dedent(
+ """\
# Unchanged requirements below this line
-r ignore.txt
--requirement ignore.txt
@@ -516,25 +593,30 @@ _freeze_req_opts = textwrap.dedent("""\
--find-links http://ignore
--index-url http://ignore
--use-feature 2020-resolver
-""")
+"""
+)
def test_freeze_with_requirement_option_file_url_egg_not_installed(
- script, deprecated_python):
+ script: PipTestEnvironment, deprecated_python: bool
+) -> None:
"""
Test "freeze -r requirements.txt" with a local file URL whose egg name
is not installed.
"""
- url = path_to_url('my-package.tar.gz') + '#egg=Does.Not-Exist'
- requirements_path = script.scratch_path.joinpath('requirements.txt')
- requirements_path.write_text(url + '\n')
+ url = "file:///my-package.tar.gz#egg=Does.Not-Exist"
+ requirements_path = script.scratch_path.joinpath("requirements.txt")
+ requirements_path.write_text(url + "\n")
result = script.pip(
- 'freeze', '--requirement', 'requirements.txt', expect_stderr=True,
+ "freeze",
+ "--requirement",
+ "requirements.txt",
+ expect_stderr=True,
)
expected_err = (
- 'WARNING: Requirement file [requirements.txt] contains {}, '
+ "WARNING: Requirement file [requirements.txt] contains {}, "
"but package 'Does.Not-Exist' is not installed\n"
).format(url)
if deprecated_python:
@@ -543,32 +625,46 @@ def test_freeze_with_requirement_option_file_url_egg_not_installed(
assert expected_err == result.stderr
-def test_freeze_with_requirement_option(script):
+def test_freeze_with_requirement_option(script: PipTestEnvironment) -> None:
"""
Test that new requirements are created correctly with --requirement hints
"""
- script.scratch_path.joinpath("hint1.txt").write_text(textwrap.dedent("""\
+ script.scratch_path.joinpath("hint1.txt").write_text(
+ textwrap.dedent(
+ """\
INITools==0.1
NoExist==4.2 # A comment that ensures end of line comments work.
simple==3.0; python_version > '1.0'
- """) + _freeze_req_opts)
- script.scratch_path.joinpath("hint2.txt").write_text(textwrap.dedent("""\
+ """
+ )
+ + _freeze_req_opts
+ )
+ script.scratch_path.joinpath("hint2.txt").write_text(
+ textwrap.dedent(
+ """\
iniTools==0.1
Noexist==4.2 # A comment that ensures end of line comments work.
Simple==3.0; python_version > '1.0'
- """) + _freeze_req_opts)
- result = script.pip_install_local('initools==0.2')
- result = script.pip_install_local('simple')
+ """
+ )
+ + _freeze_req_opts
+ )
+ result = script.pip_install_local("initools==0.2")
+ result = script.pip_install_local("simple")
result = script.pip(
- 'freeze', '--requirement', 'hint1.txt',
+ "freeze",
+ "--requirement",
+ "hint1.txt",
expect_stderr=True,
)
- expected = textwrap.dedent("""\
+ expected = textwrap.dedent(
+ """\
INITools==0.2
simple==3.0
- """)
+ """
+ )
expected += _freeze_req_opts
expected += "## The following requirements were added by pip freeze:..."
_check_output(result.stdout, expected)
@@ -577,7 +673,9 @@ def test_freeze_with_requirement_option(script):
"'NoExist' is not installed"
) in result.stderr
result = script.pip(
- 'freeze', '--requirement', 'hint2.txt',
+ "freeze",
+ "--requirement",
+ "hint2.txt",
expect_stderr=True,
)
_check_output(result.stdout, expected)
@@ -587,41 +685,61 @@ def test_freeze_with_requirement_option(script):
) in result.stderr
-def test_freeze_with_requirement_option_multiple(script):
+def test_freeze_with_requirement_option_multiple(script: PipTestEnvironment) -> None:
"""
Test that new requirements are created correctly with multiple
--requirement hints
"""
- script.scratch_path.joinpath('hint1.txt').write_text(textwrap.dedent("""\
+ script.scratch_path.joinpath("hint1.txt").write_text(
+ textwrap.dedent(
+ """\
INITools==0.1
NoExist==4.2
simple==3.0; python_version > '1.0'
- """) + _freeze_req_opts)
- script.scratch_path.joinpath('hint2.txt').write_text(textwrap.dedent("""\
+ """
+ )
+ + _freeze_req_opts
+ )
+ script.scratch_path.joinpath("hint2.txt").write_text(
+ textwrap.dedent(
+ """\
NoExist2==2.0
simple2==1.0
- """) + _freeze_req_opts)
- result = script.pip_install_local('initools==0.2')
- result = script.pip_install_local('simple')
- result = script.pip_install_local('simple2==1.0')
- result = script.pip_install_local('meta')
+ """
+ )
+ + _freeze_req_opts
+ )
+ result = script.pip_install_local("initools==0.2")
+ result = script.pip_install_local("simple")
+ result = script.pip_install_local("simple2==1.0")
+ result = script.pip_install_local("meta")
result = script.pip(
- 'freeze', '--requirement', 'hint1.txt', '--requirement', 'hint2.txt',
+ "freeze",
+ "--requirement",
+ "hint1.txt",
+ "--requirement",
+ "hint2.txt",
expect_stderr=True,
)
- expected = textwrap.dedent("""\
+ expected = textwrap.dedent(
+ """\
INITools==0.2
simple==1.0
- """)
+ """
+ )
expected += _freeze_req_opts
- expected += textwrap.dedent("""\
+ expected += textwrap.dedent(
+ """\
simple2==1.0
- """)
+ """
+ )
expected += "## The following requirements were added by pip freeze:"
- expected += '\n' + textwrap.dedent("""\
+ expected += "\n" + textwrap.dedent(
+ """\
...meta==1.0...
- """)
+ """
+ )
_check_output(result.stdout, expected)
assert (
"Requirement file [hint1.txt] contains NoExist==4.2, but package "
@@ -636,136 +754,184 @@ def test_freeze_with_requirement_option_multiple(script):
assert result.stdout.count("--index-url http://ignore") == 1
-def test_freeze_with_requirement_option_package_repeated_one_file(script):
+def test_freeze_with_requirement_option_package_repeated_one_file(
+ script: PipTestEnvironment,
+) -> None:
"""
Test freezing with single requirements file that contains a package
multiple times
"""
- script.scratch_path.joinpath('hint1.txt').write_text(textwrap.dedent("""\
+ script.scratch_path.joinpath("hint1.txt").write_text(
+ textwrap.dedent(
+ """\
simple2
simple2
NoExist
- """) + _freeze_req_opts)
- result = script.pip_install_local('simple2==1.0')
- result = script.pip_install_local('meta')
+ """
+ )
+ + _freeze_req_opts
+ )
+ result = script.pip_install_local("simple2==1.0")
+ result = script.pip_install_local("meta")
result = script.pip(
- 'freeze', '--requirement', 'hint1.txt',
+ "freeze",
+ "--requirement",
+ "hint1.txt",
expect_stderr=True,
)
- expected_out = textwrap.dedent("""\
+ expected_out = textwrap.dedent(
+ """\
simple2==1.0
- """)
+ """
+ )
expected_out += _freeze_req_opts
expected_out += "## The following requirements were added by pip freeze:"
- expected_out += '\n' + textwrap.dedent("""\
+ expected_out += "\n" + textwrap.dedent(
+ """\
...meta==1.0...
- """)
+ """
+ )
_check_output(result.stdout, expected_out)
- err1 = ("Requirement file [hint1.txt] contains NoExist, "
- "but package 'NoExist' is not installed\n")
+ err1 = (
+ "Requirement file [hint1.txt] contains NoExist, "
+ "but package 'NoExist' is not installed\n"
+ )
err2 = "Requirement simple2 included multiple times [hint1.txt]\n"
assert err1 in result.stderr
assert err2 in result.stderr
# there shouldn't be any other 'is not installed' warnings
- assert result.stderr.count('is not installed') == 1
+ assert result.stderr.count("is not installed") == 1
-def test_freeze_with_requirement_option_package_repeated_multi_file(script):
+def test_freeze_with_requirement_option_package_repeated_multi_file(
+ script: PipTestEnvironment,
+) -> None:
"""
Test freezing with multiple requirements file that contain a package
"""
- script.scratch_path.joinpath('hint1.txt').write_text(textwrap.dedent("""\
+ script.scratch_path.joinpath("hint1.txt").write_text(
+ textwrap.dedent(
+ """\
simple
- """) + _freeze_req_opts)
- script.scratch_path.joinpath('hint2.txt').write_text(textwrap.dedent("""\
+ """
+ )
+ + _freeze_req_opts
+ )
+ script.scratch_path.joinpath("hint2.txt").write_text(
+ textwrap.dedent(
+ """\
simple
NoExist
- """) + _freeze_req_opts)
- result = script.pip_install_local('simple==1.0')
- result = script.pip_install_local('meta')
+ """
+ )
+ + _freeze_req_opts
+ )
+ result = script.pip_install_local("simple==1.0")
+ result = script.pip_install_local("meta")
result = script.pip(
- 'freeze', '--requirement', 'hint1.txt',
- '--requirement', 'hint2.txt',
+ "freeze",
+ "--requirement",
+ "hint1.txt",
+ "--requirement",
+ "hint2.txt",
expect_stderr=True,
)
- expected_out = textwrap.dedent("""\
+ expected_out = textwrap.dedent(
+ """\
simple==1.0
- """)
+ """
+ )
expected_out += _freeze_req_opts
expected_out += "## The following requirements were added by pip freeze:"
- expected_out += '\n' + textwrap.dedent("""\
+ expected_out += "\n" + textwrap.dedent(
+ """\
...meta==1.0...
- """)
+ """
+ )
_check_output(result.stdout, expected_out)
- err1 = ("Requirement file [hint2.txt] contains NoExist, but package "
- "'NoExist' is not installed\n")
- err2 = ("Requirement simple included multiple times "
- "[hint1.txt, hint2.txt]\n")
+ err1 = (
+ "Requirement file [hint2.txt] contains NoExist, but package "
+ "'NoExist' is not installed\n"
+ )
+ err2 = "Requirement simple included multiple times [hint1.txt, hint2.txt]\n"
assert err1 in result.stderr
assert err2 in result.stderr
# there shouldn't be any other 'is not installed' warnings
- assert result.stderr.count('is not installed') == 1
+ assert result.stderr.count("is not installed") == 1
@pytest.mark.network
@pytest.mark.incompatible_with_test_venv
-def test_freeze_user(script, virtualenv, data):
+def test_freeze_user(
+ script: PipTestEnvironment, virtualenv: VirtualEnvironment, data: TestData
+) -> None:
"""
Testing freeze with --user, first we have to install some stuff.
"""
- script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
- script.pip_install_local('--find-links', data.find_links,
- '--user', 'simple==2.0')
- script.pip_install_local('--find-links', data.find_links,
- 'simple2==3.0')
- result = script.pip('freeze', '--user', expect_stderr=True)
- expected = textwrap.dedent("""\
+ script.pip("download", "setuptools", "wheel", "-d", data.packages)
+ script.pip_install_local("--find-links", data.find_links, "--user", "simple==2.0")
+ script.pip_install_local("--find-links", data.find_links, "simple2==3.0")
+ result = script.pip("freeze", "--user", expect_stderr=True)
+ expected = textwrap.dedent(
+ """\
simple==2.0
- <BLANKLINE>""")
+ <BLANKLINE>"""
+ )
_check_output(result.stdout, expected)
- assert 'simple2' not in result.stdout
+ assert "simple2" not in result.stdout
@pytest.mark.network
-def test_freeze_path(tmpdir, script, data):
+def test_freeze_path(tmpdir: Path, script: PipTestEnvironment, data: TestData) -> None:
"""
Test freeze with --path.
"""
- script.pip('install', '--find-links', data.find_links,
- '--target', tmpdir, 'simple==2.0')
- result = script.pip('freeze', '--path', tmpdir)
- expected = textwrap.dedent("""\
+ script.pip(
+ "install", "--find-links", data.find_links, "--target", tmpdir, "simple==2.0"
+ )
+ result = script.pip("freeze", "--path", tmpdir)
+ expected = textwrap.dedent(
+ """\
simple==2.0
- <BLANKLINE>""")
+ <BLANKLINE>"""
+ )
_check_output(result.stdout, expected)
@pytest.mark.network
@pytest.mark.incompatible_with_test_venv
-def test_freeze_path_exclude_user(tmpdir, script, data):
+def test_freeze_path_exclude_user(
+ tmpdir: Path, script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test freeze with --path and make sure packages from --user are not picked
up.
"""
- script.pip_install_local('--find-links', data.find_links,
- '--user', 'simple2')
- script.pip('install', '--find-links', data.find_links,
- '--target', tmpdir, 'simple==1.0')
- result = script.pip('freeze', '--user')
- expected = textwrap.dedent("""\
+ script.pip_install_local("--find-links", data.find_links, "--user", "simple2")
+ script.pip(
+ "install", "--find-links", data.find_links, "--target", tmpdir, "simple==1.0"
+ )
+ result = script.pip("freeze", "--user")
+ expected = textwrap.dedent(
+ """\
simple2==3.0
- <BLANKLINE>""")
+ <BLANKLINE>"""
+ )
_check_output(result.stdout, expected)
- result = script.pip('freeze', '--path', tmpdir)
- expected = textwrap.dedent("""\
+ result = script.pip("freeze", "--path", tmpdir)
+ expected = textwrap.dedent(
+ """\
simple==1.0
- <BLANKLINE>""")
+ <BLANKLINE>"""
+ )
_check_output(result.stdout, expected)
@pytest.mark.network
-def test_freeze_path_multiple(tmpdir, script, data):
+def test_freeze_path_multiple(
+ tmpdir: Path, script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test freeze with multiple --path arguments.
"""
@@ -773,63 +939,88 @@ def test_freeze_path_multiple(tmpdir, script, data):
os.mkdir(path1)
path2 = tmpdir / "path2"
os.mkdir(path2)
- script.pip('install', '--find-links', data.find_links,
- '--target', path1, 'simple==2.0')
- script.pip('install', '--find-links', data.find_links,
- '--target', path2, 'simple2==3.0')
- result = script.pip('freeze', '--path', path1)
- expected = textwrap.dedent("""\
+ script.pip(
+ "install", "--find-links", data.find_links, "--target", path1, "simple==2.0"
+ )
+ script.pip(
+ "install", "--find-links", data.find_links, "--target", path2, "simple2==3.0"
+ )
+ result = script.pip("freeze", "--path", path1)
+ expected = textwrap.dedent(
+ """\
simple==2.0
- <BLANKLINE>""")
+ <BLANKLINE>"""
+ )
_check_output(result.stdout, expected)
- result = script.pip('freeze', '--path', path1, '--path', path2)
- expected = textwrap.dedent("""\
+ result = script.pip("freeze", "--path", path1, "--path", path2)
+ expected = textwrap.dedent(
+ """\
simple==2.0
simple2==3.0
- <BLANKLINE>""")
+ <BLANKLINE>"""
+ )
_check_output(result.stdout, expected)
-def test_freeze_direct_url_archive(script, shared_data, with_wheel):
- req = "simple @ " + path_to_url(shared_data.packages / "simple-2.0.tar.gz")
- assert req.startswith("simple @ file://")
+@pytest.mark.usefixtures("with_wheel")
+def test_freeze_direct_url_archive(
+ script: PipTestEnvironment, shared_data: TestData
+) -> None:
+ req = "simple @ " + shared_data.packages.joinpath("simple-2.0.tar.gz").as_uri()
script.pip("install", req)
result = script.pip("freeze")
assert req in result.stdout
-def test_freeze_skip_work_dir_pkg(script):
+def test_freeze_skip_work_dir_pkg(script: PipTestEnvironment) -> None:
"""
Test that freeze should not include package
present in working directory
"""
# Create a test package and create .egg-info dir
- pkg_path = create_test_package_with_setup(
- script, name='simple', version='1.0')
- script.run('python', 'setup.py', 'egg_info',
- expect_stderr=True, cwd=pkg_path)
+ pkg_path = create_test_package_with_setup(script, name="simple", version="1.0")
+ script.run("python", "setup.py", "egg_info", expect_stderr=True, cwd=pkg_path)
# Freeze should not include package simple when run from package directory
- result = script.pip('freeze', cwd=pkg_path)
- assert 'simple' not in result.stdout
+ result = script.pip("freeze", cwd=pkg_path)
+ assert "simple" not in result.stdout
-def test_freeze_include_work_dir_pkg(script):
+def test_freeze_include_work_dir_pkg(script: PipTestEnvironment) -> None:
"""
Test that freeze should include package in working directory
if working directory is added in PYTHONPATH
"""
# Create a test package and create .egg-info dir
- pkg_path = create_test_package_with_setup(
- script, name='simple', version='1.0')
- script.run('python', 'setup.py', 'egg_info',
- expect_stderr=True, cwd=pkg_path)
+ pkg_path = create_test_package_with_setup(script, name="simple", version="1.0")
+ script.run("python", "setup.py", "egg_info", expect_stderr=True, cwd=pkg_path)
- script.environ.update({'PYTHONPATH': pkg_path})
+ script.environ.update({"PYTHONPATH": pkg_path})
# Freeze should include package simple when run from package directory,
# when package directory is in PYTHONPATH
- result = script.pip('freeze', cwd=pkg_path)
- assert 'simple==1.0' in result.stdout
+ result = script.pip("freeze", cwd=pkg_path)
+ assert "simple==1.0" in result.stdout
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_freeze_pep610_editable(script: PipTestEnvironment) -> None:
+ """
+ Test that a package installed with a direct_url.json with editable=true
+ is correctly frozeon as editable.
+ """
+ pkg_path = _create_test_package(script.scratch_path, name="testpkg")
+ result = script.pip("install", pkg_path)
+ direct_url_path = get_created_direct_url_path(result, "testpkg")
+ assert direct_url_path
+ # patch direct_url.json to simulate an editable install
+ with open(direct_url_path) as f:
+ direct_url = DirectUrl.from_json(f.read())
+ assert isinstance(direct_url.info, DirInfo)
+ direct_url.info.editable = True
+ with open(direct_url_path, "w") as f:
+ f.write(direct_url.to_json())
+ result = script.pip("freeze")
+ assert "# Editable Git install with no remote (testpkg==0.1)" in result.stdout
diff --git a/tests/functional/test_hash.py b/tests/functional/test_hash.py
index 5d7bd975e..0422f73ff 100644
--- a/tests/functional/test_hash.py
+++ b/tests/functional/test_hash.py
@@ -1,32 +1,40 @@
"""Tests for the ``pip hash`` command"""
+from pathlib import Path
+from tests.lib import PipTestEnvironment
-def test_basic_hash(script, tmpdir):
+
+def test_basic_hash(script: PipTestEnvironment, tmpdir: Path) -> None:
"""Run 'pip hash' through its default behavior."""
- expected = ('--hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425'
- 'e73043362938b9824')
- result = script.pip('hash', _hello_file(tmpdir))
+ expected = (
+ "--hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425"
+ "e73043362938b9824"
+ )
+ result = script.pip("hash", _hello_file(tmpdir))
assert expected in str(result)
-def test_good_algo_option(script, tmpdir):
+def test_good_algo_option(script: PipTestEnvironment, tmpdir: Path) -> None:
"""Make sure the -a option works."""
- expected = ('--hash=sha512:9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caad'
- 'ae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e'
- '5c3adef46f73bcdec043')
- result = script.pip('hash', '-a', 'sha512', _hello_file(tmpdir))
+ expected = (
+ "--hash=sha512:9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caad"
+ "ae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e"
+ "5c3adef46f73bcdec043"
+ )
+ result = script.pip("hash", "-a", "sha512", _hello_file(tmpdir))
assert expected in str(result)
-def test_bad_algo_option(script, tmpdir):
+def test_bad_algo_option(script: PipTestEnvironment, tmpdir: Path) -> None:
"""Make sure the -a option raises an error when given a bad operand."""
- result = script.pip('hash', '-a', 'invalidname', _hello_file(tmpdir),
- expect_error=True)
+ result = script.pip(
+ "hash", "-a", "invalidname", _hello_file(tmpdir), expect_error=True
+ )
assert "invalid choice: 'invalidname'" in str(result)
-def _hello_file(tmpdir):
+def _hello_file(tmpdir: Path) -> Path:
"""Return a temp file to hash containing "hello"."""
- file = tmpdir / 'hashable'
- file.write_text('hello')
+ file = tmpdir / "hashable"
+ file.write_text("hello")
return file
diff --git a/tests/functional/test_help.py b/tests/functional/test_help.py
index 81491662f..dba41af5f 100644
--- a/tests/functional/test_help.py
+++ b/tests/functional/test_help.py
@@ -5,104 +5,114 @@ import pytest
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.commands import commands_dict, create_command
from pip._internal.exceptions import CommandError
+from tests.conftest import InMemoryPip
+from tests.lib import PipTestEnvironment
-def test_run_method_should_return_success_when_finds_command_name():
+def test_run_method_should_return_success_when_finds_command_name() -> None:
"""
Test HelpCommand.run for existing command
"""
options_mock = Mock()
- args = ('freeze',)
- help_cmd = create_command('help')
+ args = ["freeze"]
+ help_cmd = create_command("help")
status = help_cmd.run(options_mock, args)
assert status == SUCCESS
-def test_run_method_should_return_success_when_command_name_not_specified():
+def test_run_method_should_return_success_when_command_name_not_specified() -> None:
"""
Test HelpCommand.run when there are no args
"""
options_mock = Mock()
- args = ()
- help_cmd = create_command('help')
- status = help_cmd.run(options_mock, args)
+ help_cmd = create_command("help")
+ status = help_cmd.run(options_mock, [])
assert status == SUCCESS
-def test_run_method_should_raise_command_error_when_command_does_not_exist():
+def test_run_method_should_raise_command_error_when_command_does_not_exist() -> None:
"""
Test HelpCommand.run for non-existing command
"""
options_mock = Mock()
- args = ('mycommand',)
- help_cmd = create_command('help')
+ args = ["mycommand"]
+ help_cmd = create_command("help")
with pytest.raises(CommandError):
help_cmd.run(options_mock, args)
-def test_help_command_should_exit_status_ok_when_command_exists(script):
+def test_help_command_should_exit_status_ok_when_command_exists(
+ script: PipTestEnvironment,
+) -> None:
"""
Test `help` command for existing command
"""
- result = script.pip('help', 'freeze')
+ result = script.pip("help", "freeze")
assert result.returncode == SUCCESS
-def test_help_command_should_exit_status_ok_when_no_cmd_is_specified(script):
+def test_help_command_should_exit_status_ok_when_no_cmd_is_specified(
+ script: PipTestEnvironment,
+) -> None:
"""
Test `help` command for no command
"""
- result = script.pip('help')
+ result = script.pip("help")
assert result.returncode == SUCCESS
-def test_help_command_should_exit_status_error_when_cmd_does_not_exist(script):
+def test_help_command_should_exit_status_error_when_cmd_does_not_exist(
+ script: PipTestEnvironment,
+) -> None:
"""
Test `help` command for non-existing command
"""
- result = script.pip('help', 'mycommand', expect_error=True)
+ result = script.pip("help", "mycommand", expect_error=True)
assert result.returncode == ERROR
-def test_help_command_redact_auth_from_url(script):
+def test_help_command_redact_auth_from_url(script: PipTestEnvironment) -> None:
"""
Test `help` on various subcommands redact auth from url
"""
- script.environ['PIP_INDEX_URL'] = 'https://user:secret@example.com'
- result = script.pip('install', '--help')
+ script.environ["PIP_INDEX_URL"] = "https://user:secret@example.com"
+ result = script.pip("install", "--help")
assert result.returncode == SUCCESS
- assert 'secret' not in result.stdout
+ assert "secret" not in result.stdout
-def test_help_command_redact_auth_from_url_with_extra_index_url(script):
+def test_help_command_redact_auth_from_url_with_extra_index_url(
+ script: PipTestEnvironment,
+) -> None:
"""
Test `help` on various subcommands redact auth from url with extra index url
"""
- script.environ['PIP_INDEX_URL'] = 'https://user:secret@example.com'
- script.environ['PIP_EXTRA_INDEX_URL'] = 'https://user:secret@example2.com'
- result = script.pip('install', '--help')
+ script.environ["PIP_INDEX_URL"] = "https://user:secret@example.com"
+ script.environ["PIP_EXTRA_INDEX_URL"] = "https://user:secret@example2.com"
+ result = script.pip("install", "--help")
assert result.returncode == SUCCESS
- assert 'secret' not in result.stdout
+ assert "secret" not in result.stdout
-def test_help_commands_equally_functional(in_memory_pip):
+def test_help_commands_equally_functional(in_memory_pip: InMemoryPip) -> None:
"""
Test if `pip help` and 'pip --help' behave the same way.
"""
- results = list(map(in_memory_pip.pip, ('help', '--help')))
+ results = list(map(in_memory_pip.pip, ("help", "--help")))
results.append(in_memory_pip.pip())
out = map(lambda x: x.stdout, results)
ret = map(lambda x: x.returncode, results)
msg = '"pip --help" != "pip help" != "pip"'
- assert len(set(out)) == 1, 'output of: ' + msg
- assert sum(ret) == 0, 'exit codes of: ' + msg
+ assert len(set(out)) == 1, "output of: " + msg
+ assert sum(ret) == 0, "exit codes of: " + msg
assert all(len(o) > 0 for o in out)
for name in commands_dict:
assert (
- in_memory_pip.pip('help', name).stdout ==
- in_memory_pip.pip(name, '--help').stdout != ""
+ in_memory_pip.pip("help", name).stdout
+ == in_memory_pip.pip(name, "--help").stdout
+ != ""
)
diff --git a/tests/functional/test_index.py b/tests/functional/test_index.py
index 004e672a5..43b8f09c3 100644
--- a/tests/functional/test_index.py
+++ b/tests/functional/test_index.py
@@ -2,59 +2,59 @@ import pytest
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.commands import create_command
+from tests.lib import PipTestEnvironment
@pytest.mark.network
-def test_list_all_versions_basic_search(script):
+def test_list_all_versions_basic_search(script: PipTestEnvironment) -> None:
"""
End to end test of index versions command.
"""
- output = script.pip('index', 'versions', 'pip', allow_stderr_warning=True)
- assert 'Available versions:' in output.stdout
+ output = script.pip("index", "versions", "pip", allow_stderr_warning=True)
+ assert "Available versions:" in output.stdout
assert (
- '20.2.3, 20.2.2, 20.2.1, 20.2, 20.1.1, 20.1, 20.0.2'
- ', 20.0.1, 19.3.1, 19.3, 19.2.3, 19.2.2, 19.2.1, 19.2, 19.1.1'
- ', 19.1, 19.0.3, 19.0.2, 19.0.1, 19.0, 18.1, 18.0, 10.0.1, 10.0.0, '
- '9.0.3, 9.0.2, 9.0.1, 9.0.0, 8.1.2, 8.1.1, '
- '8.1.0, 8.0.3, 8.0.2, 8.0.1, 8.0.0, 7.1.2, 7.1.1, 7.1.0, 7.0.3, '
- '7.0.2, 7.0.1, 7.0.0, 6.1.1, 6.1.0, 6.0.8, 6.0.7, 6.0.6, 6.0.5, '
- '6.0.4, 6.0.3, 6.0.2, 6.0.1, 6.0, 1.5.6, 1.5.5, 1.5.4, 1.5.3, '
- '1.5.2, 1.5.1, 1.5, 1.4.1, 1.4, 1.3.1, 1.3, 1.2.1, 1.2, 1.1, 1.0.2,'
- ' 1.0.1, 1.0, 0.8.3, 0.8.2, 0.8.1, 0.8, 0.7.2, 0.7.1, 0.7, 0.6.3, '
- '0.6.2, 0.6.1, 0.6, 0.5.1, 0.5, 0.4, 0.3.1, '
- '0.3, 0.2.1, 0.2' in output.stdout
+ "20.2.3, 20.2.2, 20.2.1, 20.2, 20.1.1, 20.1, 20.0.2"
+ ", 20.0.1, 19.3.1, 19.3, 19.2.3, 19.2.2, 19.2.1, 19.2, 19.1.1"
+ ", 19.1, 19.0.3, 19.0.2, 19.0.1, 19.0, 18.1, 18.0, 10.0.1, 10.0.0, "
+ "9.0.3, 9.0.2, 9.0.1, 9.0.0, 8.1.2, 8.1.1, "
+ "8.1.0, 8.0.3, 8.0.2, 8.0.1, 8.0.0, 7.1.2, 7.1.1, 7.1.0, 7.0.3, "
+ "7.0.2, 7.0.1, 7.0.0, 6.1.1, 6.1.0, 6.0.8, 6.0.7, 6.0.6, 6.0.5, "
+ "6.0.4, 6.0.3, 6.0.2, 6.0.1, 6.0, 1.5.6, 1.5.5, 1.5.4, 1.5.3, "
+ "1.5.2, 1.5.1, 1.5, 1.4.1, 1.4, 1.3.1, 1.3, 1.2.1, 1.2, 1.1, 1.0.2,"
+ " 1.0.1, 1.0, 0.8.3, 0.8.2, 0.8.1, 0.8, 0.7.2, 0.7.1, 0.7, 0.6.3, "
+ "0.6.2, 0.6.1, 0.6, 0.5.1, 0.5, 0.4, 0.3.1, "
+ "0.3, 0.2.1, 0.2" in output.stdout
)
@pytest.mark.network
-def test_list_all_versions_search_with_pre(script):
+def test_list_all_versions_search_with_pre(script: PipTestEnvironment) -> None:
"""
See that adding the --pre flag adds pre-releases
"""
- output = script.pip(
- 'index', 'versions', 'pip', '--pre', allow_stderr_warning=True)
- assert 'Available versions:' in output.stdout
+ output = script.pip("index", "versions", "pip", "--pre", allow_stderr_warning=True)
+ assert "Available versions:" in output.stdout
assert (
- '20.2.3, 20.2.2, 20.2.1, 20.2, 20.2b1, 20.1.1, 20.1, 20.1b1, 20.0.2'
- ', 20.0.1, 19.3.1, 19.3, 19.2.3, 19.2.2, 19.2.1, 19.2, 19.1.1'
- ', 19.1, 19.0.3, 19.0.2, 19.0.1, 19.0, 18.1, 18.0, 10.0.1, 10.0.0, '
- '10.0.0b2, 10.0.0b1, 9.0.3, 9.0.2, 9.0.1, 9.0.0, 8.1.2, 8.1.1, '
- '8.1.0, 8.0.3, 8.0.2, 8.0.1, 8.0.0, 7.1.2, 7.1.1, 7.1.0, 7.0.3, '
- '7.0.2, 7.0.1, 7.0.0, 6.1.1, 6.1.0, 6.0.8, 6.0.7, 6.0.6, 6.0.5, '
- '6.0.4, 6.0.3, 6.0.2, 6.0.1, 6.0, 1.5.6, 1.5.5, 1.5.4, 1.5.3, '
- '1.5.2, 1.5.1, 1.5, 1.4.1, 1.4, 1.3.1, 1.3, 1.2.1, 1.2, 1.1, 1.0.2,'
- ' 1.0.1, 1.0, 0.8.3, 0.8.2, 0.8.1, 0.8, 0.7.2, 0.7.1, 0.7, 0.6.3, '
- '0.6.2, 0.6.1, 0.6, 0.5.1, 0.5, 0.4, 0.3.1, '
- '0.3, 0.2.1, 0.2' in output.stdout
+ "20.2.3, 20.2.2, 20.2.1, 20.2, 20.2b1, 20.1.1, 20.1, 20.1b1, 20.0.2"
+ ", 20.0.1, 19.3.1, 19.3, 19.2.3, 19.2.2, 19.2.1, 19.2, 19.1.1"
+ ", 19.1, 19.0.3, 19.0.2, 19.0.1, 19.0, 18.1, 18.0, 10.0.1, 10.0.0, "
+ "10.0.0b2, 10.0.0b1, 9.0.3, 9.0.2, 9.0.1, 9.0.0, 8.1.2, 8.1.1, "
+ "8.1.0, 8.0.3, 8.0.2, 8.0.1, 8.0.0, 7.1.2, 7.1.1, 7.1.0, 7.0.3, "
+ "7.0.2, 7.0.1, 7.0.0, 6.1.1, 6.1.0, 6.0.8, 6.0.7, 6.0.6, 6.0.5, "
+ "6.0.4, 6.0.3, 6.0.2, 6.0.1, 6.0, 1.5.6, 1.5.5, 1.5.4, 1.5.3, "
+ "1.5.2, 1.5.1, 1.5, 1.4.1, 1.4, 1.3.1, 1.3, 1.2.1, 1.2, 1.1, 1.0.2,"
+ " 1.0.1, 1.0, 0.8.3, 0.8.2, 0.8.1, 0.8, 0.7.2, 0.7.1, 0.7, 0.6.3, "
+ "0.6.2, 0.6.1, 0.6, 0.5.1, 0.5, 0.4, 0.3.1, "
+ "0.3, 0.2.1, 0.2" in output.stdout
)
@pytest.mark.network
-def test_list_all_versions_returns_no_matches_found_when_name_not_exact():
+def test_list_all_versions_returns_no_matches_found_when_name_not_exact() -> None:
"""
Test that non exact name do not match
"""
- command = create_command('index')
+ command = create_command("index")
cmdline = "versions pand"
with command.main_context():
options, args = command.parse_args(cmdline.split())
@@ -63,11 +63,11 @@ def test_list_all_versions_returns_no_matches_found_when_name_not_exact():
@pytest.mark.network
-def test_list_all_versions_returns_matches_found_when_name_is_exact():
+def test_list_all_versions_returns_matches_found_when_name_is_exact() -> None:
"""
Test that exact name matches
"""
- command = create_command('index')
+ command = create_command("index")
cmdline = "versions pandas"
with command.main_context():
options, args = command.parse_args(cmdline.split())
diff --git a/tests/functional/test_inspect.py b/tests/functional/test_inspect.py
new file mode 100644
index 000000000..464bdbaa1
--- /dev/null
+++ b/tests/functional/test_inspect.py
@@ -0,0 +1,45 @@
+import json
+
+import pytest
+
+from tests.conftest import ScriptFactory
+from tests.lib import PipTestEnvironment, TestData
+
+
+@pytest.fixture(scope="session")
+def simple_script(
+ tmpdir_factory: pytest.TempPathFactory,
+ script_factory: ScriptFactory,
+ shared_data: TestData,
+) -> PipTestEnvironment:
+ tmpdir = tmpdir_factory.mktemp("pip_test_package")
+ script = script_factory(tmpdir.joinpath("workspace"))
+ script.pip(
+ "install",
+ "-f",
+ shared_data.find_links,
+ "--no-index",
+ "simplewheel==1.0",
+ )
+ return script
+
+
+def test_inspect_basic(simple_script: PipTestEnvironment) -> None:
+ """
+ Test default behavior of inspect command.
+ """
+ result = simple_script.pip("inspect", allow_stderr_warning=True)
+ report = json.loads(result.stdout)
+ installed = report["installed"]
+ assert len(installed) == 4
+ installed_by_name = {i["metadata"]["name"]: i for i in installed}
+ assert installed_by_name.keys() == {
+ "pip",
+ "setuptools",
+ "coverage",
+ "simplewheel",
+ }
+ assert installed_by_name["simplewheel"]["metadata"]["version"] == "1.0"
+ assert installed_by_name["simplewheel"]["requested"] is True
+ assert installed_by_name["simplewheel"]["installer"] == "pip"
+ assert "environment" in report
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index fd837c9b8..f4f8d4efb 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -1,19 +1,24 @@
import distutils
-import glob
import os
import re
-import shutil
import ssl
import sys
import textwrap
from os.path import curdir, join, pardir
+from pathlib import Path
+from typing import Dict, List, Tuple
import pytest
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.models.index import PyPI, TestPyPI
+from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX
from pip._internal.utils.misc import rmtree
+from tests.conftest import CertFactory
from tests.lib import (
+ PipTestEnvironment,
+ ResolverVariant,
+ TestData,
_create_svn_repo,
_create_test_package,
create_basic_wheel_for_package,
@@ -21,13 +26,10 @@ from tests.lib import (
need_bzr,
need_mercurial,
need_svn,
- path_to_url,
pyversion,
requirements_file,
)
-from tests.lib.filesystem import make_socket_file
from tests.lib.local_repos import local_checkout
-from tests.lib.path import Path
from tests.lib.server import (
file_response,
make_mock_server,
@@ -36,82 +38,131 @@ from tests.lib.server import (
)
-@pytest.mark.parametrize('command', ('install', 'wheel'))
-@pytest.mark.parametrize('variant', ('missing_setuptools', 'bad_setuptools'))
-def test_pep518_uses_build_env(script, data, common_wheels, command, variant):
- if variant == 'missing_setuptools':
+@pytest.mark.parametrize("command", ("install", "wheel"))
+@pytest.mark.parametrize("variant", ("missing_setuptools", "bad_setuptools"))
+def test_pep518_uses_build_env(
+ script: PipTestEnvironment,
+ data: TestData,
+ common_wheels: Path,
+ command: str,
+ variant: str,
+) -> None:
+ if variant == "missing_setuptools":
script.pip("uninstall", "-y", "setuptools")
- elif variant == 'bad_setuptools':
+ elif variant == "bad_setuptools":
setuptools_mod = script.site_packages_path.joinpath("setuptools.py")
- with open(setuptools_mod, 'a') as f:
+ with open(setuptools_mod, "a") as f:
f.write('\nraise ImportError("toto")')
else:
raise ValueError(variant)
script.pip(
- command, '--no-index', '-f', common_wheels, '-f', data.packages,
+ command,
+ "--no-index",
+ "-f",
+ common_wheels,
+ "-f",
+ data.packages,
data.src.joinpath("pep518-3.0"),
)
def test_pep518_build_env_uses_same_pip(
- script, data, pip_src, common_wheels, deprecated_python):
+ script: PipTestEnvironment,
+ data: TestData,
+ pip_src: Path,
+ common_wheels: Path,
+ deprecated_python: bool,
+) -> None:
"""Ensure the subprocess call to pip for installing the
build dependencies is using the same version of pip.
"""
- with open(script.scratch_path / 'pip.py', 'w') as fp:
- fp.write('raise ImportError')
+ with open(script.scratch_path / "pip.py", "w") as fp:
+ fp.write("raise ImportError")
script.run(
- 'python', pip_src / 'src/pip', 'install', '--no-index',
- '-f', common_wheels, '-f', data.packages,
- data.src.joinpath("pep518-3.0"),
+ "python",
+ os.fspath(pip_src / "src/pip"),
+ "install",
+ "--no-index",
+ "-f",
+ os.fspath(common_wheels),
+ "-f",
+ os.fspath(data.packages),
+ os.fspath(data.src.joinpath("pep518-3.0")),
expect_stderr=deprecated_python,
)
-def test_pep518_refuses_conflicting_requires(script, data):
- create_basic_wheel_for_package(script, 'setuptools', '1.0')
- create_basic_wheel_for_package(script, 'wheel', '1.0')
+def test_pep518_refuses_conflicting_requires(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ create_basic_wheel_for_package(script, "setuptools", "1.0")
+ create_basic_wheel_for_package(script, "wheel", "1.0")
project_dir = data.src.joinpath("pep518_conflicting_requires")
- result = script.pip_install_local('-f', script.scratch_path,
- project_dir, expect_error=True)
+ result = script.pip_install_local(
+ "-f", script.scratch_path, project_dir, expect_error=True
+ )
assert (
- result.returncode != 0 and (
- 'Some build dependencies for {url} conflict '
- 'with PEP 517/518 supported '
- 'requirements: setuptools==1.0 is incompatible with '
- 'setuptools>=40.8.0.'
- .format(url=path_to_url(project_dir))) in result.stderr
+ result.returncode != 0
+ and (
+ "Some build dependencies for {url} conflict "
+ "with PEP 517/518 supported "
+ "requirements: setuptools==1.0 is incompatible with "
+ "setuptools>=40.8.0.".format(url=project_dir.as_uri())
+ )
+ in result.stderr
), str(result)
-def test_pep518_refuses_invalid_requires(script, data, common_wheels):
+def test_pep518_refuses_invalid_requires(
+ script: PipTestEnvironment, data: TestData, common_wheels: Path
+) -> None:
result = script.pip(
- 'install', '-f', common_wheels,
+ "install",
+ "-f",
+ common_wheels,
data.src.joinpath("pep518_invalid_requires"),
- expect_error=True
+ expect_error=True,
)
assert result.returncode == 1
- assert "does not comply with PEP 518" in result.stderr
+
+ # Ensure the relevant things are mentioned.
+ assert "PEP 518" in result.stderr
+ assert "not a list of strings" in result.stderr
+ assert "build-system.requires" in result.stderr
+ assert "pyproject.toml" in result.stderr
-def test_pep518_refuses_invalid_build_system(script, data, common_wheels):
+def test_pep518_refuses_invalid_build_system(
+ script: PipTestEnvironment, data: TestData, common_wheels: Path
+) -> None:
result = script.pip(
- 'install', '-f', common_wheels,
+ "install",
+ "-f",
+ common_wheels,
data.src.joinpath("pep518_invalid_build_system"),
- expect_error=True
+ expect_error=True,
)
assert result.returncode == 1
- assert "does not comply with PEP 518" in result.stderr
+
+ # Ensure the relevant things are mentioned.
+ assert "PEP 518" in result.stderr
+ assert "mandatory `requires` key" in result.stderr
+ assert "[build-system] table" in result.stderr
+ assert "pyproject.toml" in result.stderr
-def test_pep518_allows_missing_requires(script, data, common_wheels):
+def test_pep518_allows_missing_requires(
+ script: PipTestEnvironment, data: TestData, common_wheels: Path
+) -> None:
result = script.pip(
- 'install', '-f', common_wheels,
+ "install",
+ "-f",
+ common_wheels,
data.src.joinpath("pep518_missing_requires"),
- expect_stderr=True
+ expect_stderr=True,
)
# Make sure we don't warn when this occurs.
- assert "does not comply with PEP 518" not in result.stderr
+ assert "PEP 518" not in result.stderr
# We want it to go through isolation for now.
assert "Installing build dependencies" in result.stdout, result.stdout
@@ -121,7 +172,9 @@ def test_pep518_allows_missing_requires(script, data, common_wheels):
@pytest.mark.incompatible_with_test_venv
-def test_pep518_with_user_pip(script, pip_src, data, common_wheels):
+def test_pep518_with_user_pip(
+ script: PipTestEnvironment, pip_src: Path, data: TestData, common_wheels: Path
+) -> None:
"""
Check that build dependencies are installed into the build
environment without using build isolation for the pip invocation.
@@ -131,106 +184,148 @@ def test_pep518_with_user_pip(script, pip_src, data, common_wheels):
non-isolated environment, and break pip in the system site-packages,
so that isolated uses of pip will fail.
"""
- script.pip("install", "--ignore-installed",
- "-f", common_wheels, "--user", pip_src)
- system_pip_dir = script.site_packages_path / 'pip'
+ script.pip(
+ "install",
+ "--ignore-installed",
+ "-f",
+ common_wheels,
+ "--user",
+ pip_src,
+ # WARNING: The scripts pip, pip3, ... are installed in ... which is not on PATH
+ allow_stderr_warning=True,
+ )
+ system_pip_dir = script.site_packages_path / "pip"
assert not system_pip_dir.exists()
system_pip_dir.mkdir()
- with open(system_pip_dir / '__init__.py', 'w') as fp:
- fp.write('raise ImportError\n')
+ with open(system_pip_dir / "__init__.py", "w") as fp:
+ fp.write("raise ImportError\n")
script.pip(
- 'wheel', '--no-index', '-f', common_wheels, '-f', data.packages,
+ "wheel",
+ "--no-index",
+ "-f",
+ common_wheels,
+ "-f",
+ data.packages,
data.src.joinpath("pep518-3.0"),
)
-def test_pep518_with_extra_and_markers(script, data, common_wheels):
+def test_pep518_with_extra_and_markers(
+ script: PipTestEnvironment, data: TestData, common_wheels: Path
+) -> None:
script.pip(
- 'wheel', '--no-index',
- '-f', common_wheels,
- '-f', data.find_links,
+ "wheel",
+ "--no-index",
+ "-f",
+ common_wheels,
+ "-f",
+ data.find_links,
data.src.joinpath("pep518_with_extra_and_markers-1.0"),
)
-def test_pep518_with_namespace_package(script, data, common_wheels):
+def test_pep518_with_namespace_package(
+ script: PipTestEnvironment, data: TestData, common_wheels: Path
+) -> None:
script.pip(
- 'wheel', '--no-index',
- '-f', common_wheels,
- '-f', data.find_links,
+ "wheel",
+ "--no-index",
+ "-f",
+ common_wheels,
+ "-f",
+ data.find_links,
data.src.joinpath("pep518_with_namespace_package-1.0"),
use_module=True,
)
-@pytest.mark.parametrize('command', ('install', 'wheel'))
-@pytest.mark.parametrize('package', ('pep518_forkbomb',
- 'pep518_twin_forkbombs_first',
- 'pep518_twin_forkbombs_second'))
-def test_pep518_forkbombs(script, data, common_wheels, command, package):
- package_source = next(data.packages.glob(package + '-[0-9]*.tar.gz'))
+@pytest.mark.parametrize("command", ("install", "wheel"))
+@pytest.mark.parametrize(
+ "package",
+ ("pep518_forkbomb", "pep518_twin_forkbombs_first", "pep518_twin_forkbombs_second"),
+)
+def test_pep518_forkbombs(
+ script: PipTestEnvironment,
+ data: TestData,
+ common_wheels: Path,
+ command: str,
+ package: str,
+) -> None:
+ package_source = next(data.packages.glob(package + "-[0-9]*.tar.gz"))
result = script.pip(
- command, '--no-index', '-v',
- '-f', common_wheels,
- '-f', data.find_links,
+ command,
+ "--no-index",
+ "-v",
+ "-f",
+ common_wheels,
+ "-f",
+ data.find_links,
package,
expect_error=True,
)
- assert '{1} is already being built: {0} from {1}'.format(
- package, path_to_url(package_source),
- ) in result.stderr, str(result)
+ assert (
+ "{1} is already being built: {0} from {1}".format(
+ package,
+ package_source.as_uri(),
+ )
+ in result.stderr
+ ), str(result)
@pytest.mark.network
+@pytest.mark.usefixtures("with_wheel")
def test_pip_second_command_line_interface_works(
- script, pip_src, data, common_wheels, deprecated_python, with_wheel
-):
+ script: PipTestEnvironment,
+ pip_src: Path,
+ data: TestData,
+ common_wheels: Path,
+ deprecated_python: bool,
+) -> None:
"""
Check if ``pip<PYVERSION>`` commands behaves equally
"""
# Re-install pip so we get the launchers.
- script.pip_install_local('-f', common_wheels, pip_src)
- args = [f'pip{pyversion}']
- args.extend(['install', 'INITools==0.2'])
- args.extend(['-f', data.packages])
+ script.pip_install_local("-f", common_wheels, pip_src)
+ args = [f"pip{pyversion}"]
+ args.extend(["install", "INITools==0.2"])
+ args.extend(["-f", os.fspath(data.packages)])
result = script.run(*args)
- dist_info_folder = (
- script.site_packages /
- 'INITools-0.2.dist-info'
- )
- initools_folder = script.site_packages / 'initools'
+ dist_info_folder = script.site_packages / "INITools-0.2.dist-info"
+ initools_folder = script.site_packages / "initools"
result.did_create(dist_info_folder)
result.did_create(initools_folder)
-def test_install_exit_status_code_when_no_requirements(script):
+def test_install_exit_status_code_when_no_requirements(
+ script: PipTestEnvironment,
+) -> None:
"""
Test install exit status code when no requirements specified
"""
- result = script.pip('install', expect_error=True)
+ result = script.pip("install", expect_error=True)
assert "You must give at least one requirement to install" in result.stderr
assert result.returncode == ERROR
-def test_install_exit_status_code_when_blank_requirements_file(script):
+def test_install_exit_status_code_when_blank_requirements_file(
+ script: PipTestEnvironment,
+) -> None:
"""
Test install exit status code when blank requirements file specified
"""
script.scratch_path.joinpath("blank.txt").write_text("\n")
- script.pip('install', '-r', 'blank.txt')
+ script.pip("install", "-r", "blank.txt")
@pytest.mark.network
-def test_basic_install_from_pypi(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_basic_install_from_pypi(script: PipTestEnvironment) -> None:
"""
Test installing a package from PyPI.
"""
- result = script.pip('install', 'INITools==0.2')
- dist_info_folder = (
- script.site_packages /
- 'INITools-0.2.dist-info'
- )
- initools_folder = script.site_packages / 'initools'
+ result = script.pip("install", "INITools==0.2")
+ dist_info_folder = script.site_packages / "INITools-0.2.dist-info"
+ initools_folder = script.site_packages / "initools"
result.did_create(dist_info_folder)
result.did_create(initools_folder)
@@ -246,54 +341,51 @@ def test_basic_install_from_pypi(script, with_wheel):
assert "https://" not in result.stdout
-def test_basic_editable_install(script):
+def test_basic_editable_install(script: PipTestEnvironment) -> None:
"""
Test editable installation.
"""
- result = script.pip('install', '-e', 'INITools==0.2', expect_error=True)
- assert (
- "INITools==0.2 is not a valid editable requirement"
- in result.stderr
- )
+ result = script.pip("install", "-e", "INITools==0.2", expect_error=True)
+ assert "INITools==0.2 is not a valid editable requirement" in result.stderr
assert not result.files_created
@need_svn
-def test_basic_install_editable_from_svn(script):
+def test_basic_install_editable_from_svn(script: PipTestEnvironment) -> None:
"""
Test checking out from svn.
"""
- checkout_path = _create_test_package(script)
- repo_url = _create_svn_repo(script, checkout_path)
- result = script.pip(
- 'install',
- '-e', 'svn+' + repo_url + '#egg=version-pkg'
- )
- result.assert_installed('version-pkg', with_files=['.svn'])
+ checkout_path = _create_test_package(script.scratch_path)
+ repo_url = _create_svn_repo(script.scratch_path, checkout_path)
+ result = script.pip("install", "-e", "svn+" + repo_url + "#egg=version-pkg")
+ result.assert_installed("version-pkg", with_files=[".svn"])
-def _test_install_editable_from_git(script, tmpdir):
+def _test_install_editable_from_git(script: PipTestEnvironment) -> None:
"""Test cloning from Git."""
- pkg_path = _create_test_package(script, name='testpackage', vcs='git')
+ pkg_path = _create_test_package(script.scratch_path, name="testpackage", vcs="git")
args = [
- 'install', '-e',
- 'git+{url}#egg=testpackage'.format(url=path_to_url(pkg_path)),
+ "install",
+ "-e",
+ f"git+{pkg_path.as_uri()}#egg=testpackage",
]
result = script.pip(*args)
- result.assert_installed('testpackage', with_files=['.git'])
+ result.assert_installed("testpackage", with_files=[".git"])
-def test_basic_install_editable_from_git(script, tmpdir):
- _test_install_editable_from_git(script, tmpdir)
+def test_basic_install_editable_from_git(script: PipTestEnvironment) -> None:
+ _test_install_editable_from_git(script)
-def test_install_editable_from_git_autobuild_wheel(
- script, tmpdir, with_wheel):
- _test_install_editable_from_git(script, tmpdir)
+@pytest.mark.usefixtures("with_wheel")
+def test_install_editable_from_git_autobuild_wheel(script: PipTestEnvironment) -> None:
+ _test_install_editable_from_git(script)
@pytest.mark.network
-def test_install_editable_uninstalls_existing(data, script, tmpdir):
+def test_install_editable_uninstalls_existing(
+ data: TestData, script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""
Test that installing an editable uninstalls a previously installed
non-editable version.
@@ -302,103 +394,120 @@ def test_install_editable_uninstalls_existing(data, script, tmpdir):
"""
to_install = data.packages.joinpath("pip-test-package-0.1.tar.gz")
result = script.pip_install_local(to_install)
- assert 'Successfully installed pip-test-package' in result.stdout
- result.assert_installed('piptestpackage', editable=False)
+ assert "Successfully installed pip-test-package" in result.stdout
+ result.assert_installed("piptestpackage", editable=False)
result = script.pip(
- 'install', '-e',
- '{dir}#egg=pip-test-package'.format(
+ "install",
+ "-e",
+ "{dir}#egg=pip-test-package".format(
dir=local_checkout(
- 'git+https://github.com/pypa/pip-test-package.git', tmpdir,
- )),
+ "git+https://github.com/pypa/pip-test-package.git",
+ tmpdir,
+ )
+ ),
)
- result.assert_installed('pip-test-package', with_files=['.git'])
- assert 'Found existing installation: pip-test-package 0.1' in result.stdout
- assert 'Uninstalling pip-test-package-' in result.stdout
- assert 'Successfully uninstalled pip-test-package' in result.stdout
+ result.assert_installed("pip-test-package", with_files=[".git"])
+ assert "Found existing installation: pip-test-package 0.1" in result.stdout
+ assert "Uninstalling pip-test-package-" in result.stdout
+ assert "Successfully uninstalled pip-test-package" in result.stdout
-def test_install_editable_uninstalls_existing_from_path(script, data):
+def test_install_editable_uninstalls_existing_from_path(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test that installing an editable uninstalls a previously installed
non-editable version from path
"""
- to_install = data.src.joinpath('simplewheel-1.0')
+ to_install = data.src.joinpath("simplewheel-1.0")
result = script.pip_install_local(to_install)
- assert 'Successfully installed simplewheel' in result.stdout
- simple_folder = script.site_packages / 'simplewheel'
- result.assert_installed('simplewheel', editable=False)
+ assert "Successfully installed simplewheel" in result.stdout
+ simple_folder = script.site_packages / "simplewheel"
+ result.assert_installed("simplewheel", editable=False)
result.did_create(simple_folder)
result = script.pip(
- 'install', '-e',
+ "install",
+ "-e",
to_install,
)
- install_path = script.site_packages / 'simplewheel.egg-link'
+ install_path = script.site_packages / "simplewheel.egg-link"
result.did_create(install_path)
- assert 'Found existing installation: simplewheel 1.0' in result.stdout
- assert 'Uninstalling simplewheel-' in result.stdout
- assert 'Successfully uninstalled simplewheel' in result.stdout
+ assert "Found existing installation: simplewheel 1.0" in result.stdout
+ assert "Uninstalling simplewheel-" in result.stdout
+ assert "Successfully uninstalled simplewheel" in result.stdout
assert simple_folder in result.files_deleted, str(result.stdout)
@need_mercurial
-def test_basic_install_editable_from_hg(script, tmpdir):
+def test_basic_install_editable_from_hg(script: PipTestEnvironment) -> None:
"""Test cloning and hg+file install from Mercurial."""
- pkg_path = _create_test_package(script, name='testpackage', vcs='hg')
- url = 'hg+{}#egg=testpackage'.format(path_to_url(pkg_path))
- assert url.startswith('hg+file')
- args = ['install', '-e', url]
+ pkg_path = _create_test_package(script.scratch_path, name="testpackage", vcs="hg")
+ url = f"hg+{pkg_path.as_uri()}#egg=testpackage"
+ assert url.startswith("hg+file")
+ args = ["install", "-e", url]
result = script.pip(*args)
- result.assert_installed('testpackage', with_files=['.hg'])
+ result.assert_installed("testpackage", with_files=[".hg"])
@need_mercurial
-def test_vcs_url_final_slash_normalization(script, tmpdir):
+def test_vcs_url_final_slash_normalization(script: PipTestEnvironment) -> None:
"""
Test that presence or absence of final slash in VCS URL is normalized.
"""
- pkg_path = _create_test_package(script, name='testpackage', vcs='hg')
+ pkg_path = _create_test_package(script.scratch_path, name="testpackage", vcs="hg")
args = [
- 'install',
- '-e', 'hg+{url}/#egg=testpackage'.format(url=path_to_url(pkg_path))]
+ "install",
+ "-e",
+ f"hg+{pkg_path.as_uri()}/#egg=testpackage",
+ ]
result = script.pip(*args)
- result.assert_installed('testpackage', with_files=['.hg'])
+ result.assert_installed("testpackage", with_files=[".hg"])
@need_bzr
-def test_install_editable_from_bazaar(script, tmpdir):
+def test_install_editable_from_bazaar(script: PipTestEnvironment) -> None:
"""Test checking out from Bazaar."""
- pkg_path = _create_test_package(script, name='testpackage', vcs='bazaar')
+ pkg_path = _create_test_package(
+ script.scratch_path, name="testpackage", vcs="bazaar"
+ )
args = [
- 'install',
- '-e', 'bzr+{url}/#egg=testpackage'.format(url=path_to_url(pkg_path))]
+ "install",
+ "-e",
+ f"bzr+{pkg_path.as_uri()}/#egg=testpackage",
+ ]
result = script.pip(*args)
- result.assert_installed('testpackage', with_files=['.bzr'])
+ result.assert_installed("testpackage", with_files=[".bzr"])
@pytest.mark.network
@need_bzr
-def test_vcs_url_urlquote_normalization(script, tmpdir):
+def test_vcs_url_urlquote_normalization(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""
Test that urlquoted characters are normalized for repo URL comparison.
"""
script.pip(
- 'install', '-e',
- '{url}/#egg=django-wikiapp'.format(
+ "install",
+ "-e",
+ "{url}/#egg=django-wikiapp".format(
url=local_checkout(
- 'bzr+http://bazaar.launchpad.net/'
- '%7Edjango-wikiapp/django-wikiapp'
- '/release-0.1',
+ "bzr+http://bazaar.launchpad.net/"
+ "%7Edjango-wikiapp/django-wikiapp"
+ "/release-0.1",
tmpdir,
- )),
+ )
+ ),
)
@pytest.mark.parametrize("resolver", ["", "--use-deprecated=legacy-resolver"])
+@pytest.mark.usefixtures("with_wheel")
def test_basic_install_from_local_directory(
- script, data, resolver, with_wheel
-):
+ script: PipTestEnvironment, data: TestData, resolver: str
+) -> None:
"""
Test installing from a local directory.
"""
@@ -406,69 +515,61 @@ def test_basic_install_from_local_directory(
if resolver:
args.append(resolver)
to_install = data.packages.joinpath("FSPkg")
- args.append(to_install)
+ args.append(os.fspath(to_install))
result = script.pip(*args)
- fspkg_folder = script.site_packages / 'fspkg'
- dist_info_folder = (
- script.site_packages /
- 'FSPkg-0.1.dev0.dist-info'
- )
+ fspkg_folder = script.site_packages / "fspkg"
+ dist_info_folder = script.site_packages / "FSPkg-0.1.dev0.dist-info"
result.did_create(fspkg_folder)
result.did_create(dist_info_folder)
-@pytest.mark.parametrize("test_type,editable", [
- ("rel_path", False),
- ("rel_path", True),
- ("rel_url", False),
- ("rel_url", True),
- ("embedded_rel_path", False),
- ("embedded_rel_path", True),
-])
+@pytest.mark.parametrize(
+ "test_type,editable",
+ [
+ ("rel_path", False),
+ ("rel_path", True),
+ ("rel_url", False),
+ ("rel_url", True),
+ ("embedded_rel_path", False),
+ ("embedded_rel_path", True),
+ ],
+)
+@pytest.mark.usefixtures("with_wheel")
def test_basic_install_relative_directory(
- script, data, test_type, editable, with_wheel
-):
+ script: PipTestEnvironment, data: TestData, test_type: str, editable: bool
+) -> None:
"""
Test installing a requirement using a relative path.
"""
- dist_info_folder = (
- script.site_packages /
- 'FSPkg-0.1.dev0.dist-info'
- )
- egg_link_file = (
- script.site_packages / 'FSPkg.egg-link'
- )
- package_folder = script.site_packages / 'fspkg'
+ dist_info_folder = script.site_packages / "FSPkg-0.1.dev0.dist-info"
+ egg_link_file = script.site_packages / "FSPkg.egg-link"
+ package_folder = script.site_packages / "fspkg"
# Compute relative install path to FSPkg from scratch path.
full_rel_path = Path(
- os.path.relpath(data.packages.joinpath('FSPkg'), script.scratch_path)
- )
- full_rel_url = (
- 'file:' + full_rel_path.replace(os.path.sep, '/') + '#egg=FSPkg'
+ os.path.relpath(data.packages.joinpath("FSPkg"), script.scratch_path)
)
+ full_rel_url = f"file:{full_rel_path.as_posix()}#egg=FSPkg"
embedded_rel_path = script.scratch_path.joinpath(full_rel_path)
req_path = {
- "rel_path": full_rel_path,
+ "rel_path": os.fspath(full_rel_path),
"rel_url": full_rel_url,
- "embedded_rel_path": embedded_rel_path,
+ "embedded_rel_path": os.fspath(embedded_rel_path),
}[test_type]
# Install as either editable or not.
if not editable:
- result = script.pip('install', req_path,
- cwd=script.scratch_path)
+ result = script.pip("install", req_path, cwd=script.scratch_path)
result.did_create(dist_info_folder)
result.did_create(package_folder)
else:
# Editable install.
- result = script.pip('install', '-e' + req_path,
- cwd=script.scratch_path)
+ result = script.pip("install", "-e", req_path, cwd=script.scratch_path)
result.did_create(egg_link_file)
-def test_install_quiet(script, data):
+def test_install_quiet(script: PipTestEnvironment, data: TestData) -> None:
"""
Test that install -q is actually quiet.
"""
@@ -477,12 +578,14 @@ def test_install_quiet(script, data):
# https://github.com/pypa/pip/issues/3418
# https://github.com/docker-library/python/issues/83
to_install = data.packages.joinpath("FSPkg")
- result = script.pip('install', '-qqq', to_install)
+ result = script.pip("install", "-qqq", to_install)
assert result.stdout == ""
assert result.stderr == ""
-def test_hashed_install_success(script, data, tmpdir):
+def test_hashed_install_success(
+ script: PipTestEnvironment, data: TestData, tmpdir: Path
+) -> None:
"""
Test that installing various sorts of requirements with correct hashes
works.
@@ -491,18 +594,18 @@ def test_hashed_install_success(script, data, tmpdir):
scenes).
"""
- file_url = path_to_url(
- (data.packages / 'simple-1.0.tar.gz').resolve())
+ file_url = data.packages.joinpath("simple-1.0.tar.gz").resolve().as_uri()
with requirements_file(
- 'simple2==1.0 --hash=sha256:9336af72ca661e6336eb87bc7de3e8844d853e'
- '3848c2b9bbd2e8bf01db88c2c7\n'
- '{simple} --hash=sha256:393043e672415891885c9a2a0929b1af95fb866d6c'
- 'a016b42d2e6ce53619b653'.format(simple=file_url),
- tmpdir) as reqs_file:
- script.pip_install_local('-r', reqs_file.resolve())
+ "simple2==1.0 --hash=sha256:9336af72ca661e6336eb87bc7de3e8844d853e"
+ "3848c2b9bbd2e8bf01db88c2c7\n"
+ "{simple} --hash=sha256:393043e672415891885c9a2a0929b1af95fb866d6c"
+ "a016b42d2e6ce53619b653".format(simple=file_url),
+ tmpdir,
+ ) as reqs_file:
+ script.pip_install_local("-r", reqs_file.resolve())
-def test_hashed_install_failure(script, tmpdir):
+def test_hashed_install_failure(script: PipTestEnvironment, tmpdir: Path) -> None:
"""Test that wrong hashes stop installation.
This makes sure prepare_files() is called in the course of installation
@@ -510,24 +613,24 @@ def test_hashed_install_failure(script, tmpdir):
kinds of hashes are in test_req.py.
"""
- with requirements_file('simple2==1.0 --hash=sha256:9336af72ca661e6336eb87b'
- 'c7de3e8844d853e3848c2b9bbd2e8bf01db88c2c\n',
- tmpdir) as reqs_file:
- result = script.pip_install_local('-r',
- reqs_file.resolve(),
- expect_error=True)
+ with requirements_file(
+ "simple2==1.0 --hash=sha256:9336af72ca661e6336eb87b"
+ "c7de3e8844d853e3848c2b9bbd2e8bf01db88c2c\n",
+ tmpdir,
+ ) as reqs_file:
+ result = script.pip_install_local("-r", reqs_file.resolve(), expect_error=True)
assert len(result.files_created) == 0
-def assert_re_match(pattern, text):
- assert re.search(pattern, text), (
- f"Could not find {pattern!r} in {text!r}"
- )
+def assert_re_match(pattern: str, text: str) -> None:
+ assert re.search(pattern, text), f"Could not find {pattern!r} in {text!r}"
@pytest.mark.network
@pytest.mark.skip("Fails on new resolver")
-def test_hashed_install_failure_later_flag(script, tmpdir):
+def test_hashed_install_failure_later_flag(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
with requirements_file(
"blessings==1.0\n"
"tracefront==0.1 --hash=sha256:somehash\n"
@@ -537,147 +640,169 @@ def test_hashed_install_failure_later_flag(script, tmpdir):
"packages/source/p/peep/peep-3.1.1.tar.gz\n",
tmpdir,
) as reqs_file:
- result = script.pip(
- "install", "-r", reqs_file.resolve(), expect_error=True
- )
+ result = script.pip("install", "-r", reqs_file.resolve(), expect_error=True)
assert_re_match(
- r'Hashes are required in --require-hashes mode, but they are '
- r'missing .*\n'
- r' https://files\.pythonhosted\.org/packages/source/p/peep/peep'
- r'-3\.1\.1\.tar\.gz --hash=sha256:[0-9a-f]+\n'
- r' blessings==1.0 --hash=sha256:[0-9a-f]+\n'
- r'THESE PACKAGES DO NOT MATCH THE HASHES.*\n'
- r' tracefront==0.1 .*:\n'
- r' Expected sha256 somehash\n'
- r' Got [0-9a-f]+',
+ r"Hashes are required in --require-hashes mode, but they are "
+ r"missing .*\n"
+ r" https://files\.pythonhosted\.org/packages/source/p/peep/peep"
+ r"-3\.1\.1\.tar\.gz --hash=sha256:[0-9a-f]+\n"
+ r" blessings==1.0 --hash=sha256:[0-9a-f]+\n"
+ r"THESE PACKAGES DO NOT MATCH THE HASHES.*\n"
+ r" tracefront==0.1 .*:\n"
+ r" Expected sha256 somehash\n"
+ r" Got [0-9a-f]+",
result.stderr,
)
-def test_install_from_local_directory_with_symlinks_to_directories(
- script, data, with_wheel
-):
- """
- Test installing from a local directory containing symlinks to directories.
- """
- to_install = data.packages.joinpath("symlinks")
- result = script.pip('install', to_install)
- pkg_folder = script.site_packages / 'symlinks'
- dist_info_folder = (
- script.site_packages /
- 'symlinks-0.1.dev0.dist-info'
- )
- result.did_create(pkg_folder)
- result.did_create(dist_info_folder)
-
-
+@pytest.mark.usefixtures("with_wheel")
def test_install_from_local_directory_with_in_tree_build(
- script, data, with_wheel
-):
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
- Test installing from a local directory with --use-feature=in-tree-build.
+ Test installing from a local directory with default in tree build.
"""
to_install = data.packages.joinpath("FSPkg")
- args = ["install", "--use-feature=in-tree-build", to_install]
in_tree_build_dir = to_install / "build"
assert not in_tree_build_dir.exists()
- result = script.pip(*args)
- fspkg_folder = script.site_packages / 'fspkg'
- dist_info_folder = (
- script.site_packages /
- 'FSPkg-0.1.dev0.dist-info'
- )
+ result = script.pip("install", to_install)
+ fspkg_folder = script.site_packages / "fspkg"
+ dist_info_folder = script.site_packages / "FSPkg-0.1.dev0.dist-info"
result.did_create(fspkg_folder)
result.did_create(dist_info_folder)
assert in_tree_build_dir.exists()
-@pytest.mark.skipif("sys.platform == 'win32'")
-def test_install_from_local_directory_with_socket_file(
- script, data, tmpdir, with_wheel
-):
- """
- Test installing from a local directory containing a socket file.
- """
- dist_info_folder = (
- script.site_packages /
- "FSPkg-0.1.dev0.dist-info"
- )
- package_folder = script.site_packages / "fspkg"
- to_copy = data.packages.joinpath("FSPkg")
- to_install = tmpdir.joinpath("src")
-
- shutil.copytree(to_copy, to_install)
- # Socket file, should be ignored.
- socket_file_path = os.path.join(to_install, "example")
- make_socket_file(socket_file_path)
-
- result = script.pip("install", "--verbose", to_install)
- result.did_create(package_folder)
- result.did_create(dist_info_folder)
- assert str(socket_file_path) in result.stderr
-
-
-def test_install_from_local_directory_with_no_setup_py(script, data):
+def test_install_from_local_directory_with_no_setup_py(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test installing from a local directory with no 'setup.py'.
"""
- result = script.pip('install', data.root, expect_error=True)
+ result = script.pip("install", data.root, expect_error=True)
assert not result.files_created
- assert "is not installable." in result.stderr
assert "Neither 'setup.py' nor 'pyproject.toml' found." in result.stderr
def test_editable_install__local_dir_no_setup_py(
- script, data, deprecated_python):
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test installing in editable mode from a local directory with no setup.py.
"""
- result = script.pip('install', '-e', data.root, expect_error=True)
+ result = script.pip("install", "-e", data.root, expect_error=True)
assert not result.files_created
-
- msg = result.stderr
- if deprecated_python:
- assert 'File "setup.py" or "setup.cfg" not found. ' in msg
- else:
- assert msg.startswith('ERROR: File "setup.py" or "setup.cfg" not found. ')
- assert 'pyproject.toml' not in msg
+ assert (
+ "does not appear to be a Python project: "
+ "neither 'setup.py' nor 'pyproject.toml' found" in result.stderr
+ )
+@pytest.mark.network
def test_editable_install__local_dir_no_setup_py_with_pyproject(
- script, deprecated_python):
+ script: PipTestEnvironment,
+) -> None:
"""
Test installing in editable mode from a local directory with no setup.py
- but that does have pyproject.toml.
+ but that does have pyproject.toml with a build backend that does not support
+ the build_editable hook.
"""
- local_dir = script.scratch_path.joinpath('temp')
+ local_dir = script.scratch_path.joinpath("temp")
local_dir.mkdir()
- pyproject_path = local_dir.joinpath('pyproject.toml')
- pyproject_path.write_text('')
+ pyproject_path = local_dir.joinpath("pyproject.toml")
+ pyproject_path.write_text(
+ textwrap.dedent(
+ """
+ [build-system]
+ requires = ["setuptools<64"]
+ build-backend = "setuptools.build_meta"
+ """
+ )
+ )
- result = script.pip('install', '-e', local_dir, expect_error=True)
+ result = script.pip("install", "-e", local_dir, expect_error=True)
assert not result.files_created
msg = result.stderr
- if deprecated_python:
- assert 'File "setup.py" or "setup.cfg" not found. ' in msg
- else:
- assert msg.startswith('ERROR: File "setup.py" or "setup.cfg" not found. ')
- assert 'A "pyproject.toml" file was found' in msg
+ assert "has a 'pyproject.toml'" in msg
+ assert "does not have a 'setup.py' nor a 'setup.cfg'" in msg
+ assert "cannot be installed in editable mode" in msg
+
+
+def test_editable_install__local_dir_setup_requires_with_pyproject(
+ script: PipTestEnvironment, shared_data: TestData
+) -> None:
+ """
+ Test installing in editable mode from a local directory with a setup.py
+ that has setup_requires and a pyproject.toml.
+
+ https://github.com/pypa/pip/issues/10573
+ """
+ local_dir = script.scratch_path.joinpath("temp")
+ local_dir.mkdir()
+ pyproject_path = local_dir.joinpath("pyproject.toml")
+ pyproject_path.write_text("")
+ setup_py_path = local_dir.joinpath("setup.py")
+ setup_py_path.write_text(
+ "from setuptools import setup\n"
+ "setup(name='dummy', setup_requires=['simplewheel'])\n"
+ )
+
+ script.pip("install", "--find-links", shared_data.find_links, "-e", local_dir)
+
+
+def test_install_pre__setup_requires_with_pyproject(
+ script: PipTestEnvironment, shared_data: TestData, common_wheels: Path
+) -> None:
+ """
+ Test installing with a pre-release build dependency declared in both
+ setup.py and pyproject.toml.
+
+ https://github.com/pypa/pip/issues/10573
+ """
+ depends_package = "prerelease_dependency"
+ depends_path = create_basic_wheel_for_package(script, depends_package, "1.0.0a1")
+
+ local_dir = script.scratch_path.joinpath("temp")
+ local_dir.mkdir()
+ pyproject_path = local_dir.joinpath("pyproject.toml")
+ pyproject_path.write_text(
+ "[build-system]\n"
+ f'requires = ["setuptools", "wheel", "{depends_package}"]\n'
+ 'build-backend = "setuptools.build_meta"\n'
+ )
+ setup_py_path = local_dir.joinpath("setup.py")
+ setup_py_path.write_text(
+ "from setuptools import setup\n"
+ f"setup(name='dummy', setup_requires=['{depends_package}'])\n"
+ )
+
+ script.pip(
+ "install",
+ "--pre",
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ common_wheels,
+ "--find-links",
+ depends_path.parent,
+ local_dir,
+ )
@pytest.mark.network
-def test_upgrade_argparse_shadowed(script):
+def test_upgrade_argparse_shadowed(script: PipTestEnvironment) -> None:
# If argparse is installed - even if shadowed for imported - we support
# upgrading it and properly remove the older versions files.
- script.pip('install', 'argparse==1.3')
- result = script.pip('install', 'argparse>=1.4')
+ script.pip("install", "argparse==1.3")
+ result = script.pip("install", "argparse>=1.4")
assert "Not uninstalling argparse" not in result.stdout
-def test_install_curdir(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_curdir(script: PipTestEnvironment, data: TestData) -> None:
"""
Test installing current directory ('.').
"""
@@ -686,68 +811,69 @@ def test_install_curdir(script, data, with_wheel):
egg_info = join(run_from, "FSPkg.egg-info")
if os.path.isdir(egg_info):
rmtree(egg_info)
- result = script.pip('install', curdir, cwd=run_from)
- fspkg_folder = script.site_packages / 'fspkg'
- dist_info_folder = (
- script.site_packages /
- 'FSPkg-0.1.dev0.dist-info'
- )
+ result = script.pip("install", curdir, cwd=run_from)
+ fspkg_folder = script.site_packages / "fspkg"
+ dist_info_folder = script.site_packages / "FSPkg-0.1.dev0.dist-info"
result.did_create(fspkg_folder)
result.did_create(dist_info_folder)
-def test_install_pardir(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_pardir(script: PipTestEnvironment, data: TestData) -> None:
"""
Test installing parent directory ('..').
"""
run_from = data.packages.joinpath("FSPkg", "fspkg")
- result = script.pip('install', pardir, cwd=run_from)
- fspkg_folder = script.site_packages / 'fspkg'
- dist_info_folder = (
- script.site_packages /
- 'FSPkg-0.1.dev0.dist-info'
- )
+ result = script.pip("install", pardir, cwd=run_from)
+ fspkg_folder = script.site_packages / "fspkg"
+ dist_info_folder = script.site_packages / "FSPkg-0.1.dev0.dist-info"
result.did_create(fspkg_folder)
result.did_create(dist_info_folder)
@pytest.mark.network
-def test_install_global_option(script):
+def test_install_global_option(script: PipTestEnvironment) -> None:
"""
Test using global distutils options.
(In particular those that disable the actual install action)
"""
result = script.pip(
- 'install', '--global-option=--version', "INITools==0.1",
- expect_stderr=True)
- assert 'INITools==0.1\n' in result.stdout
+ "install", "--global-option=--version", "INITools==0.1", expect_stderr=True
+ )
+ assert "INITools==0.1\n" in result.stdout
assert not result.files_created
-def test_install_with_hacked_egg_info(script, data):
+def test_install_with_hacked_egg_info(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
test installing a package which defines its own egg_info class
"""
run_from = data.packages.joinpath("HackedEggInfo")
- result = script.pip('install', '.', cwd=run_from)
- assert 'Successfully installed hackedegginfo-0.0.0\n' in result.stdout
+ result = script.pip("install", ".", cwd=run_from)
+ assert "Successfully installed hackedegginfo-0.0.0\n" in result.stdout
@pytest.mark.network
-def test_install_using_install_option_and_editable(script, tmpdir):
+def test_install_using_install_option_and_editable(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""
Test installing a tool using -e and --install-option
"""
- folder = 'script_folder'
+ folder = "script_folder"
script.scratch_path.joinpath(folder).mkdir()
- url = local_checkout('git+git://github.com/pypa/pip-test-package', tmpdir)
+ url = local_checkout("git+https://github.com/pypa/pip-test-package", tmpdir)
result = script.pip(
- 'install', '-e', f'{url}#egg=pip-test-package',
- f'--install-option=--script-dir={folder}',
- expect_stderr=True)
+ "install",
+ "-e",
+ f"{url}#egg=pip-test-package",
+ f"--install-option=--script-dir={folder}",
+ expect_stderr=True,
+ )
script_file = (
- script.venv / 'src' / 'pip-test-package' /
- folder / 'pip-test-package' + script.exe
+ script.venv / "src/pip-test-package" / folder / f"pip-test-package{script.exe}"
)
result.did_create(script_file)
@@ -755,190 +881,207 @@ def test_install_using_install_option_and_editable(script, tmpdir):
@pytest.mark.xfail
@pytest.mark.network
@need_mercurial
-def test_install_global_option_using_editable(script, tmpdir):
+def test_install_global_option_using_editable(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""
Test using global distutils options, but in an editable installation
"""
- url = 'hg+http://bitbucket.org/runeh/anyjson'
+ url = "hg+http://bitbucket.org/runeh/anyjson"
result = script.pip(
- 'install', '--global-option=--version', '-e',
- '{url}@0.2.5#egg=anyjson'.format(url=local_checkout(url, tmpdir)),
- expect_stderr=True)
- assert 'Successfully installed anyjson' in result.stdout
+ "install",
+ "--global-option=--version",
+ "-e",
+ f"{local_checkout(url, tmpdir)}@0.2.5#egg=anyjson",
+ expect_stderr=True,
+ )
+ assert "Successfully installed anyjson" in result.stdout
@pytest.mark.network
-def test_install_package_with_same_name_in_curdir(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_package_with_same_name_in_curdir(script: PipTestEnvironment) -> None:
"""
Test installing a package with the same name of a local folder
"""
script.scratch_path.joinpath("mock==0.6").mkdir()
- result = script.pip('install', 'mock==0.6')
- dist_info_folder = (
- script.site_packages /
- 'mock-0.6.0.dist-info'
- )
+ result = script.pip("install", "mock==0.6")
+ dist_info_folder = script.site_packages / "mock-0.6.0.dist-info"
result.did_create(dist_info_folder)
-mock100_setup_py = textwrap.dedent('''\
+mock100_setup_py = textwrap.dedent(
+ """\
from setuptools import setup
setup(name='mock',
- version='100.1')''')
+ version='100.1')"""
+)
-def test_install_folder_using_dot_slash(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_folder_using_dot_slash(script: PipTestEnvironment) -> None:
"""
Test installing a folder using pip install ./foldername
"""
script.scratch_path.joinpath("mock").mkdir()
- pkg_path = script.scratch_path / 'mock'
+ pkg_path = script.scratch_path / "mock"
pkg_path.joinpath("setup.py").write_text(mock100_setup_py)
- result = script.pip('install', './mock')
- dist_info_folder = (
- script.site_packages /
- 'mock-100.1.dist-info'
- )
+ result = script.pip("install", "./mock")
+ dist_info_folder = script.site_packages / "mock-100.1.dist-info"
result.did_create(dist_info_folder)
-def test_install_folder_using_slash_in_the_end(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_folder_using_slash_in_the_end(script: PipTestEnvironment) -> None:
r"""
Test installing a folder using pip install foldername/ or foldername\
"""
script.scratch_path.joinpath("mock").mkdir()
- pkg_path = script.scratch_path / 'mock'
+ pkg_path = script.scratch_path / "mock"
pkg_path.joinpath("setup.py").write_text(mock100_setup_py)
- result = script.pip('install', 'mock' + os.path.sep)
- dist_info_folder = script.site_packages / 'mock-100.1.dist-info'
+ result = script.pip("install", "mock" + os.path.sep)
+ dist_info_folder = script.site_packages / "mock-100.1.dist-info"
result.did_create(dist_info_folder)
-def test_install_folder_using_relative_path(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_folder_using_relative_path(script: PipTestEnvironment) -> None:
"""
Test installing a folder using pip install folder1/folder2
"""
script.scratch_path.joinpath("initools").mkdir()
script.scratch_path.joinpath("initools", "mock").mkdir()
- pkg_path = script.scratch_path / 'initools' / 'mock'
+ pkg_path = script.scratch_path / "initools" / "mock"
pkg_path.joinpath("setup.py").write_text(mock100_setup_py)
- result = script.pip('install', Path('initools') / 'mock')
- dist_info_folder = script.site_packages / 'mock-100.1.dist-info'
+ result = script.pip("install", Path("initools") / "mock")
+ dist_info_folder = script.site_packages / "mock-100.1.dist-info"
result.did_create(dist_info_folder)
@pytest.mark.network
-def test_install_package_which_contains_dev_in_name(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_package_which_contains_dev_in_name(script: PipTestEnvironment) -> None:
"""
Test installing package from PyPI which contains 'dev' in name
"""
- result = script.pip('install', 'django-devserver==0.0.4')
- devserver_folder = script.site_packages / 'devserver'
- dist_info_folder = (
- script.site_packages /
- 'django_devserver-0.0.4.dist-info'
- )
+ result = script.pip("install", "django-devserver==0.0.4")
+ devserver_folder = script.site_packages / "devserver"
+ dist_info_folder = script.site_packages / "django_devserver-0.0.4.dist-info"
result.did_create(devserver_folder)
result.did_create(dist_info_folder)
-def test_install_package_with_target(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_package_with_target(script: PipTestEnvironment) -> None:
"""
Test installing a package using pip install --target
"""
- target_dir = script.scratch_path / 'target'
- result = script.pip_install_local('-t', target_dir, "simple==1.0")
- result.did_create(Path('scratch') / 'target' / 'simple')
+ target_dir = script.scratch_path / "target"
+ result = script.pip_install_local("-t", target_dir, "simple==1.0")
+ result.did_create(Path("scratch") / "target" / "simple")
# Test repeated call without --upgrade, no files should have changed
result = script.pip_install_local(
- '-t', target_dir, "simple==1.0", expect_stderr=True,
+ "-t",
+ target_dir,
+ "simple==1.0",
+ expect_stderr=True,
)
- result.did_not_update(Path('scratch') / 'target' / 'simple')
+ result.did_not_update(Path("scratch") / "target" / "simple")
# Test upgrade call, check that new version is installed
- result = script.pip_install_local('--upgrade', '-t',
- target_dir, "simple==2.0")
- result.did_update(Path('scratch') / 'target' / 'simple')
- dist_info_folder = (
- Path('scratch') / 'target' /
- 'simple-2.0.dist-info'
- )
+ result = script.pip_install_local("--upgrade", "-t", target_dir, "simple==2.0")
+ result.did_update(Path("scratch") / "target" / "simple")
+ dist_info_folder = Path("scratch") / "target" / "simple-2.0.dist-info"
result.did_create(dist_info_folder)
# Test install and upgrade of single-module package
- result = script.pip_install_local('-t', target_dir, 'singlemodule==0.0.0')
- singlemodule_py = Path('scratch') / 'target' / 'singlemodule.py'
+ result = script.pip_install_local("-t", target_dir, "singlemodule==0.0.0")
+ singlemodule_py = Path("scratch") / "target" / "singlemodule.py"
result.did_create(singlemodule_py)
- result = script.pip_install_local('-t', target_dir, 'singlemodule==0.0.1',
- '--upgrade')
+ result = script.pip_install_local(
+ "-t", target_dir, "singlemodule==0.0.1", "--upgrade"
+ )
result.did_update(singlemodule_py)
-@pytest.mark.parametrize("target_option", ['--target', '-t'])
-def test_install_package_to_usersite_with_target_must_fail(script,
- target_option):
+@pytest.mark.parametrize("target_option", ["--target", "-t"])
+def test_install_package_to_usersite_with_target_must_fail(
+ script: PipTestEnvironment, target_option: str
+) -> None:
"""
Test that installing package to usersite with target
must raise error
"""
- target_dir = script.scratch_path / 'target'
+ target_dir = script.scratch_path / "target"
result = script.pip_install_local(
- '--user', target_option, target_dir, "simple==1.0", expect_error=True
- )
- assert "Can not combine '--user' and '--target'" in result.stderr, (
- str(result)
+ "--user", target_option, target_dir, "simple==1.0", expect_error=True
)
+ assert "Can not combine '--user' and '--target'" in result.stderr, str(result)
-def test_install_nonlocal_compatible_wheel(script, data):
- target_dir = script.scratch_path / 'target'
+def test_install_nonlocal_compatible_wheel(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ target_dir = script.scratch_path / "target"
# Test install with --target
result = script.pip(
- 'install',
- '-t', target_dir,
- '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--python', '3',
- '--platform', 'fakeplat',
- '--abi', 'fakeabi',
- 'simplewheel',
+ "install",
+ "-t",
+ target_dir,
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--python",
+ "3",
+ "--platform",
+ "fakeplat",
+ "--abi",
+ "fakeabi",
+ "simplewheel",
)
assert result.returncode == SUCCESS
- distinfo = Path('scratch') / 'target' / 'simplewheel-2.0-1.dist-info'
+ distinfo = Path("scratch") / "target" / "simplewheel-2.0-1.dist-info"
result.did_create(distinfo)
# Test install without --target
result = script.pip(
- 'install',
- '--no-index', '--find-links', data.find_links,
- '--only-binary=:all:',
- '--python', '3',
- '--platform', 'fakeplat',
- '--abi', 'fakeabi',
- 'simplewheel',
- expect_error=True
+ "install",
+ "--no-index",
+ "--find-links",
+ data.find_links,
+ "--only-binary=:all:",
+ "--python",
+ "3",
+ "--platform",
+ "fakeplat",
+ "--abi",
+ "fakeabi",
+ "simplewheel",
+ expect_error=True,
)
assert result.returncode == ERROR
def test_install_nonlocal_compatible_wheel_path(
- script,
- data,
- resolver_variant,
-):
- target_dir = script.scratch_path / 'target'
+ script: PipTestEnvironment,
+ data: TestData,
+ resolver_variant: ResolverVariant,
+) -> None:
+ target_dir = script.scratch_path / "target"
# Test a full path requirement
result = script.pip(
- 'install',
- '-t', target_dir,
- '--no-index',
- '--only-binary=:all:',
- Path(data.packages) / 'simplewheel-2.0-py3-fakeabi-fakeplat.whl',
+ "install",
+ "-t",
+ target_dir,
+ "--no-index",
+ "--only-binary=:all:",
+ Path(data.packages) / "simplewheel-2.0-py3-fakeabi-fakeplat.whl",
expect_error=(resolver_variant == "2020-resolver"),
)
if resolver_variant == "2020-resolver":
@@ -946,30 +1089,35 @@ def test_install_nonlocal_compatible_wheel_path(
else:
assert result.returncode == SUCCESS
- distinfo = Path('scratch') / 'target' / 'simplewheel-2.0.dist-info'
+ distinfo = Path("scratch") / "target" / "simplewheel-2.0.dist-info"
result.did_create(distinfo)
# Test a full path requirement (without --target)
result = script.pip(
- 'install',
- '--no-index',
- '--only-binary=:all:',
- Path(data.packages) / 'simplewheel-2.0-py3-fakeabi-fakeplat.whl',
- expect_error=True
+ "install",
+ "--no-index",
+ "--only-binary=:all:",
+ Path(data.packages) / "simplewheel-2.0-py3-fakeabi-fakeplat.whl",
+ expect_error=True,
)
assert result.returncode == ERROR
-@pytest.mark.parametrize('opt', ('--target', '--prefix'))
-def test_install_with_target_or_prefix_and_scripts_no_warning(opt, script, with_wheel):
+@pytest.mark.parametrize("opt", ("--target", "--prefix"))
+@pytest.mark.usefixtures("with_wheel")
+def test_install_with_target_or_prefix_and_scripts_no_warning(
+ opt: str, script: PipTestEnvironment
+) -> None:
"""
Test that installing with --target does not trigger the "script not
in PATH" warning (issue #5201)
"""
- target_dir = script.scratch_path / 'target'
- pkga_path = script.scratch_path / 'pkga'
+ target_dir = script.scratch_path / "target"
+ pkga_path = script.scratch_path / "pkga"
pkga_path.mkdir()
- pkga_path.joinpath("setup.py").write_text(textwrap.dedent("""
+ pkga_path.joinpath("setup.py").write_text(
+ textwrap.dedent(
+ """
from setuptools import setup
setup(name='pkga',
version='0.1',
@@ -978,36 +1126,45 @@ def test_install_with_target_or_prefix_and_scripts_no_warning(opt, script, with_
'console_scripts': ['pkga=pkga:main']
}
)
- """))
- pkga_path.joinpath("pkga.py").write_text(textwrap.dedent("""
+ """
+ )
+ )
+ pkga_path.joinpath("pkga.py").write_text(
+ textwrap.dedent(
+ """
def main(): pass
- """))
- result = script.pip('install', opt, target_dir, pkga_path)
+ """
+ )
+ )
+ result = script.pip("install", opt, target_dir, pkga_path)
# This assertion isn't actually needed, if we get the script warning
# the script.pip() call will fail with "stderr not expected". But we
# leave the assertion to make the intention of the code clearer.
assert "--no-warn-script-location" not in result.stderr, str(result)
-def test_install_package_with_root(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_package_with_root(script: PipTestEnvironment, data: TestData) -> None:
"""
Test installing a package using pip install --root
"""
- root_dir = script.scratch_path / 'root'
+ root_dir = script.scratch_path / "root"
result = script.pip(
- 'install', '--root', root_dir, '-f', data.find_links, '--no-index',
- 'simple==1.0',
+ "install",
+ "--root",
+ root_dir,
+ "-f",
+ data.find_links,
+ "--no-index",
+ "simple==1.0",
)
- normal_install_path = (
- script.base_path / script.site_packages /
- 'simple-1.0.dist-info'
+ normal_install_path = os.fspath(
+ script.base_path / script.site_packages / "simple-1.0.dist-info"
)
# use distutils to change the root exactly how the --root option does it
from distutils.util import change_root
- root_path = change_root(
- os.path.join(script.scratch, 'root'),
- normal_install_path
- )
+
+ root_path = change_root(os.path.join(script.scratch, "root"), normal_install_path)
result.did_create(root_path)
# Should show find-links location in output
@@ -1015,28 +1172,39 @@ def test_install_package_with_root(script, data, with_wheel):
assert "Looking in links: " in result.stdout
-def test_install_package_with_prefix(script, data):
+def test_install_package_with_prefix(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test installing a package using pip install --prefix
"""
- prefix_path = script.scratch_path / 'prefix'
+ prefix_path = script.scratch_path / "prefix"
result = script.pip(
- 'install', '--prefix', prefix_path, '-f', data.find_links,
- '--no-binary', 'simple', '--no-index', 'simple==1.0',
- )
-
- rel_prefix_path = script.scratch / 'prefix'
- install_path = (
- distutils.sysconfig.get_python_lib(prefix=rel_prefix_path) /
+ "install",
+ "--prefix",
+ prefix_path,
+ "-f",
+ data.find_links,
+ "--no-binary",
+ "simple",
+ "--no-index",
+ "simple==1.0",
+ )
+
+ rel_prefix_path = script.scratch / "prefix"
+ install_path = join(
+ distutils.sysconfig.get_python_lib(prefix=rel_prefix_path),
# we still test for egg-info because no-binary implies setup.py install
- f'simple-1.0-py{pyversion}.egg-info'
+ f"simple-1.0-py{pyversion}.egg-info",
)
result.did_create(install_path)
-def _test_install_editable_with_prefix(script, files):
+def _test_install_editable_with_prefix(
+ script: PipTestEnvironment, files: Dict[str, str]
+) -> None:
# make a dummy project
- pkga_path = script.scratch_path / 'pkga'
+ pkga_path = script.scratch_path / "pkga"
pkga_path.mkdir()
for fn, contents in files.items():
@@ -1044,9 +1212,10 @@ def _test_install_editable_with_prefix(script, files):
if hasattr(sys, "pypy_version_info"):
site_packages = os.path.join(
- 'prefix', 'lib', f'python{pyversion}', 'site-packages')
+ "prefix", "lib", f"python{pyversion}", "site-packages"
+ )
else:
- site_packages = distutils.sysconfig.get_python_lib(prefix='prefix')
+ site_packages = distutils.sysconfig.get_python_lib(prefix="prefix")
# make sure target path is in PYTHONPATH
pythonpath = script.scratch_path / site_packages
@@ -1054,38 +1223,39 @@ def _test_install_editable_with_prefix(script, files):
script.environ["PYTHONPATH"] = pythonpath
# install pkga package into the absolute prefix directory
- prefix_path = script.scratch_path / 'prefix'
- result = script.pip(
- 'install', '--editable', pkga_path, '--prefix', prefix_path)
+ prefix_path = script.scratch_path / "prefix"
+ result = script.pip("install", "--editable", pkga_path, "--prefix", prefix_path)
# assert pkga is installed at correct location
- install_path = script.scratch / site_packages / 'pkga.egg-link'
+ install_path = script.scratch / site_packages / "pkga.egg-link"
result.did_create(install_path)
@pytest.mark.network
-def test_install_editable_with_target(script):
- pkg_path = script.scratch_path / 'pkg'
+def test_install_editable_with_target(script: PipTestEnvironment) -> None:
+ pkg_path = script.scratch_path / "pkg"
pkg_path.mkdir()
- pkg_path.joinpath("setup.py").write_text(textwrap.dedent("""
+ pkg_path.joinpath("setup.py").write_text(
+ textwrap.dedent(
+ """
from setuptools import setup
setup(
name='pkg',
install_requires=['watching_testrunner']
)
- """))
+ """
+ )
+ )
- target = script.scratch_path / 'target'
+ target = script.scratch_path / "target"
target.mkdir()
- result = script.pip(
- 'install', '--editable', pkg_path, '--target', target
- )
+ result = script.pip("install", "--editable", pkg_path, "--target", target)
- result.did_create(script.scratch / 'target' / 'pkg.egg-link')
- result.did_create(script.scratch / 'target' / 'watching_testrunner.py')
+ result.did_create(script.scratch / "target" / "pkg.egg-link")
+ result.did_create(script.scratch / "target" / "watching_testrunner.py")
-def test_install_editable_with_prefix_setup_py(script):
+def test_install_editable_with_prefix_setup_py(script: PipTestEnvironment) -> None:
setup_py = """
from setuptools import setup
setup(name='pkga', version='0.1')
@@ -1093,13 +1263,14 @@ setup(name='pkga', version='0.1')
_test_install_editable_with_prefix(script, {"setup.py": setup_py})
-def test_install_editable_with_prefix_setup_cfg(script):
+@pytest.mark.network
+def test_install_editable_with_prefix_setup_cfg(script: PipTestEnvironment) -> None:
setup_cfg = """[metadata]
name = pkga
version = 0.1
"""
pyproject_toml = """[build-system]
-requires = ["setuptools", "wheel"]
+requires = ["setuptools<64", "wheel"]
build-backend = "setuptools.build_meta"
"""
_test_install_editable_with_prefix(
@@ -1107,22 +1278,31 @@ build-backend = "setuptools.build_meta"
)
-def test_install_package_conflict_prefix_and_user(script, data):
+def test_install_package_conflict_prefix_and_user(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test installing a package using pip install --prefix --user errors out
"""
- prefix_path = script.scratch_path / 'prefix'
+ prefix_path = script.scratch_path / "prefix"
result = script.pip(
- 'install', '-f', data.find_links, '--no-index', '--user',
- '--prefix', prefix_path, 'simple==1.0',
- expect_error=True, quiet=True,
- )
- assert (
- "Can not combine '--user' and '--prefix'" in result.stderr
+ "install",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "--user",
+ "--prefix",
+ prefix_path,
+ "simple==1.0",
+ expect_error=True,
+ quiet=True,
)
+ assert "Can not combine '--user' and '--prefix'" in result.stderr
-def test_install_package_that_emits_unicode(script, data):
+def test_install_package_that_emits_unicode(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Install a package with a setup.py that emits UTF-8 output and then fails.
@@ -1130,28 +1310,39 @@ def test_install_package_that_emits_unicode(script, data):
"""
to_install = data.packages.joinpath("BrokenEmitsUTF8")
result = script.pip(
- 'install', to_install, expect_error=True, expect_temp=True, quiet=True,
+ "install",
+ to_install,
+ expect_error=True,
+ expect_temp=True,
+ quiet=True,
)
assert (
- 'FakeError: this package designed to fail on install' in result.stderr
- ), f'stderr: {result.stderr}'
- assert 'UnicodeDecodeError' not in result.stderr
- assert 'UnicodeDecodeError' not in result.stdout
+ "FakeError: this package designed to fail on install" in result.stderr
+ ), f"stderr: {result.stderr}"
+ assert "UnicodeDecodeError" not in result.stderr
+ assert "UnicodeDecodeError" not in result.stdout
-def test_install_package_with_utf8_setup(script, data):
+def test_install_package_with_utf8_setup(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""Install a package with a setup.py that declares a utf-8 encoding."""
to_install = data.packages.joinpath("SetupPyUTF8")
- script.pip('install', to_install)
+ script.pip("install", to_install)
-def test_install_package_with_latin1_setup(script, data):
+def test_install_package_with_latin1_setup(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""Install a package with a setup.py that declares a latin-1 encoding."""
to_install = data.packages.joinpath("SetupPyLatin1")
- script.pip('install', to_install)
+ script.pip("install", to_install)
-def test_url_req_case_mismatch_no_index(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_url_req_case_mismatch_no_index(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
tar ball url requirements (with no egg fragment), that happen to have upper
case project names, should be considered equal to later requirements that
@@ -1160,21 +1351,22 @@ def test_url_req_case_mismatch_no_index(script, data, with_wheel):
tests/data/packages contains Upper-1.0.tar.gz and Upper-2.0.tar.gz
'requiresupper' has install_requires = ['upper']
"""
- Upper = '/'.join((data.find_links, 'Upper-1.0.tar.gz'))
+ Upper = "/".join((data.find_links, "Upper-1.0.tar.gz"))
result = script.pip(
- 'install', '--no-index', '-f', data.find_links, Upper, 'requiresupper'
+ "install", "--no-index", "-f", data.find_links, Upper, "requiresupper"
)
# only Upper-1.0.tar.gz should get installed.
- dist_info_folder = script.site_packages / \
- 'Upper-1.0.dist-info'
+ dist_info_folder = script.site_packages / "Upper-1.0.dist-info"
result.did_create(dist_info_folder)
- dist_info_folder = script.site_packages / \
- 'Upper-2.0.dist-info'
+ dist_info_folder = script.site_packages / "Upper-2.0.dist-info"
result.did_not_create(dist_info_folder)
-def test_url_req_case_mismatch_file_index(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_url_req_case_mismatch_file_index(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
tar ball url requirements (with no egg fragment), that happen to have upper
case project names, should be considered equal to later requirements that
@@ -1189,56 +1381,63 @@ def test_url_req_case_mismatch_file_index(script, data, with_wheel):
set of packages as it requires a prepared index.html file and
subdirectory-per-package structure.
"""
- Dinner = '/'.join((data.find_links3, 'dinner', 'Dinner-1.0.tar.gz'))
+ Dinner = "/".join((data.find_links3, "dinner", "Dinner-1.0.tar.gz"))
result = script.pip(
- 'install', '--index-url', data.find_links3, Dinner, 'requiredinner'
+ "install", "--index-url", data.find_links3, Dinner, "requiredinner"
)
# only Upper-1.0.tar.gz should get installed.
- dist_info_folder = script.site_packages / \
- 'Dinner-1.0.dist-info'
+ dist_info_folder = script.site_packages / "Dinner-1.0.dist-info"
result.did_create(dist_info_folder)
- dist_info_folder = script.site_packages / \
- 'Dinner-2.0.dist-info'
+ dist_info_folder = script.site_packages / "Dinner-2.0.dist-info"
result.did_not_create(dist_info_folder)
-def test_url_incorrect_case_no_index(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_url_incorrect_case_no_index(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Same as test_url_req_case_mismatch_no_index, except testing for the case
where the incorrect case is given in the name of the package to install
rather than in a requirements file.
"""
result = script.pip(
- 'install', '--no-index', '-f', data.find_links, "upper",
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "upper",
)
# only Upper-2.0.tar.gz should get installed.
- dist_info_folder = script.site_packages / \
- 'Upper-1.0.dist-info'
+ dist_info_folder = script.site_packages / "Upper-1.0.dist-info"
result.did_not_create(dist_info_folder)
- dist_info_folder = script.site_packages / \
- 'Upper-2.0.dist-info'
+ dist_info_folder = script.site_packages / "Upper-2.0.dist-info"
result.did_create(dist_info_folder)
-def test_url_incorrect_case_file_index(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_url_incorrect_case_file_index(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Same as test_url_req_case_mismatch_file_index, except testing for the case
where the incorrect case is given in the name of the package to install
rather than in a requirements file.
"""
result = script.pip(
- 'install', '--index-url', data.find_links3, "dinner",
+ "install",
+ "--index-url",
+ data.find_links3,
+ "dinner",
expect_stderr=True,
)
# only Upper-2.0.tar.gz should get installed.
- dist_info_folder = script.site_packages / \
- 'Dinner-1.0.dist-info'
+ dist_info_folder = script.site_packages / "Dinner-1.0.dist-info"
result.did_not_create(dist_info_folder)
- dist_info_folder = script.site_packages / \
- 'Dinner-2.0.dist-info'
+ dist_info_folder = script.site_packages / "Dinner-2.0.dist-info"
result.did_create(dist_info_folder)
# Should show index-url location in output
@@ -1247,7 +1446,7 @@ def test_url_incorrect_case_file_index(script, data, with_wheel):
@pytest.mark.network
-def test_compiles_pyc(script):
+def test_compiles_pyc(script: PipTestEnvironment) -> None:
"""
Test installing with --compile on
"""
@@ -1258,17 +1457,14 @@ def test_compiles_pyc(script):
# any of them
exists = [
os.path.exists(script.site_packages_path / "initools/__init__.pyc"),
+ *script.site_packages_path.glob("initools/__pycache__/__init__*.pyc"),
]
- exists += glob.glob(
- script.site_packages_path / "initools/__pycache__/__init__*.pyc"
- )
-
assert any(exists)
@pytest.mark.network
-def test_no_compiles_pyc(script):
+def test_no_compiles_pyc(script: PipTestEnvironment) -> None:
"""
Test installing from wheel with --compile on
"""
@@ -1279,42 +1475,51 @@ def test_no_compiles_pyc(script):
# any of them
exists = [
os.path.exists(script.site_packages_path / "initools/__init__.pyc"),
+ *script.site_packages_path.glob("initools/__pycache__/__init__*.pyc"),
]
- exists += glob.glob(
- script.site_packages_path / "initools/__pycache__/__init__*.pyc"
- )
-
assert not any(exists)
-def test_install_upgrade_editable_depending_on_other_editable(script):
+def test_install_upgrade_editable_depending_on_other_editable(
+ script: PipTestEnvironment,
+) -> None:
script.scratch_path.joinpath("pkga").mkdir()
- pkga_path = script.scratch_path / 'pkga'
- pkga_path.joinpath("setup.py").write_text(textwrap.dedent("""
+ pkga_path = script.scratch_path / "pkga"
+ pkga_path.joinpath("setup.py").write_text(
+ textwrap.dedent(
+ """
from setuptools import setup
setup(name='pkga',
version='0.1')
- """))
- script.pip('install', '--editable', pkga_path)
- result = script.pip('list', '--format=freeze')
+ """
+ )
+ )
+ script.pip("install", "--editable", pkga_path)
+ result = script.pip("list", "--format=freeze")
assert "pkga==0.1" in result.stdout
script.scratch_path.joinpath("pkgb").mkdir()
- pkgb_path = script.scratch_path / 'pkgb'
- pkgb_path.joinpath("setup.py").write_text(textwrap.dedent("""
+ pkgb_path = script.scratch_path / "pkgb"
+ pkgb_path.joinpath("setup.py").write_text(
+ textwrap.dedent(
+ """
from setuptools import setup
setup(name='pkgb',
version='0.1',
install_requires=['pkga'])
- """))
- script.pip('install', '--upgrade', '--editable', pkgb_path, '--no-index')
- result = script.pip('list', '--format=freeze')
+ """
+ )
+ )
+ script.pip("install", "--upgrade", "--editable", pkgb_path, "--no-index")
+ result = script.pip("list", "--format=freeze")
assert "pkgb==0.1" in result.stdout
-def test_install_subprocess_output_handling(script, data):
- args = ['install', data.src.joinpath('chattymodule')]
+def test_install_subprocess_output_handling(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ args = ["install", os.fspath(data.src.joinpath("chattymodule"))]
# Regular install should not show output from the chatty setup.py
result = script.pip(*args)
@@ -1330,74 +1535,88 @@ def test_install_subprocess_output_handling(script, data):
# If the install fails, then we *should* show the output... but only once,
# even if --verbose is given.
- result = script.pip(*(args + ["--global-option=--fail"]),
- expect_error=True)
+ result = script.pip(*(args + ["--global-option=--fail"]), expect_error=True)
assert 1 == result.stderr.count("I DIE, I DIE")
- result = script.pip(*(args + ["--global-option=--fail", "--verbose"]),
- expect_error=True)
+ result = script.pip(
+ *(args + ["--global-option=--fail", "--verbose"]), expect_error=True
+ )
assert 1 == result.stderr.count("I DIE, I DIE")
-def test_install_log(script, data, tmpdir):
+def test_install_log(script: PipTestEnvironment, data: TestData, tmpdir: Path) -> None:
# test that verbose logs go to "--log" file
f = tmpdir.joinpath("log.txt")
- args = [f'--log={f}',
- 'install', data.src.joinpath('chattymodule')]
- result = script.pip(*args)
+ result = script.pip(f"--log={f}", "install", data.src.joinpath("chattymodule"))
assert 0 == result.stdout.count("HELLO FROM CHATTYMODULE")
with open(f) as fp:
# one from egg_info, one from install
assert 2 == fp.read().count("HELLO FROM CHATTYMODULE")
-def test_install_topological_sort(script, data):
- args = ['install', 'TopoRequires4', '--no-index', '-f', data.packages]
- res = str(script.pip(*args))
- order1 = 'TopoRequires, TopoRequires2, TopoRequires3, TopoRequires4'
- order2 = 'TopoRequires, TopoRequires3, TopoRequires2, TopoRequires4'
+def test_install_topological_sort(script: PipTestEnvironment, data: TestData) -> None:
+ res = str(script.pip("install", "TopoRequires4", "--no-index", "-f", data.packages))
+ order1 = "TopoRequires, TopoRequires2, TopoRequires3, TopoRequires4"
+ order2 = "TopoRequires, TopoRequires3, TopoRequires2, TopoRequires4"
assert order1 in res or order2 in res, res
-def test_install_wheel_broken(script, with_wheel):
- res = script.pip_install_local('wheelbroken', expect_stderr=True)
+@pytest.mark.usefixtures("with_wheel")
+def test_install_wheel_broken(script: PipTestEnvironment) -> None:
+ res = script.pip_install_local("wheelbroken", allow_stderr_error=True)
+ assert "ERROR: Failed building wheel for wheelbroken" in res.stderr
+ # Fallback to setup.py install (https://github.com/pypa/pip/issues/8368)
assert "Successfully installed wheelbroken-0.1" in str(res), str(res)
-def test_cleanup_after_failed_wheel(script, with_wheel):
- res = script.pip_install_local('wheelbrokenafter', expect_stderr=True)
+@pytest.mark.usefixtures("with_wheel")
+def test_cleanup_after_failed_wheel(script: PipTestEnvironment) -> None:
+ res = script.pip_install_local("wheelbrokenafter", allow_stderr_error=True)
+ assert "ERROR: Failed building wheel for wheelbrokenafter" in res.stderr
# One of the effects of not cleaning up is broken scripts:
script_py = script.bin_path / "script.py"
assert script_py.exists(), script_py
with open(script_py) as f:
shebang = f.readline().strip()
- assert shebang != '#!python', shebang
+ assert shebang != "#!python", shebang
# OK, assert that we *said* we were cleaning up:
# /!\ if in need to change this, also change test_pep517_no_legacy_cleanup
assert "Running setup.py clean for wheelbrokenafter" in str(res), str(res)
-def test_install_builds_wheels(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_builds_wheels(script: PipTestEnvironment, data: TestData) -> None:
# We need to use a subprocess to get the right value on Windows.
- res = script.run('python', '-c', (
- 'from pip._internal.utils import appdirs; '
- 'print(appdirs.user_cache_dir("pip"))'
- ))
- wheels_cache = os.path.join(res.stdout.rstrip('\n'), 'wheels')
+ res = script.run(
+ "python",
+ "-c",
+ (
+ "from pip._internal.utils import appdirs; "
+ 'print(appdirs.user_cache_dir("pip"))'
+ ),
+ )
+ wheels_cache = os.path.join(res.stdout.rstrip("\n"), "wheels")
# NB This incidentally tests a local tree + tarball inputs
# see test_install_editable_from_git_autobuild_wheel for editable
# vcs coverage.
- to_install = data.packages.joinpath('requires_wheelbroken_upper')
+ to_install = data.packages.joinpath("requires_wheelbroken_upper")
res = script.pip(
- 'install', '--no-index', '-f', data.find_links,
- to_install, expect_stderr=True)
- expected = ("Successfully installed requires-wheelbroken-upper-0"
- " upper-2.0 wheelbroken-0.1")
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ to_install,
+ allow_stderr_error=True, # error building wheelbroken
+ )
+ expected = (
+ "Successfully installed requires-wheelbroken-upper-0"
+ " upper-2.0 wheelbroken-0.1"
+ )
# Must have installed it all
assert expected in str(res), str(res)
- wheels = []
+ wheels: List[str] = []
for _, _, files in os.walk(wheels_cache):
- wheels.extend(files)
+ wheels.extend(f for f in files if f.endswith(".whl"))
# and built wheels for upper and wheelbroken
assert "Building wheel for upper" in str(res), str(res)
assert "Building wheel for wheelb" in str(res), str(res)
@@ -1418,13 +1637,24 @@ def test_install_builds_wheels(script, data, with_wheel):
]
-def test_install_no_binary_disables_building_wheels(script, data, with_wheel):
- to_install = data.packages.joinpath('requires_wheelbroken_upper')
+@pytest.mark.usefixtures("with_wheel")
+def test_install_no_binary_disables_building_wheels(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ to_install = data.packages.joinpath("requires_wheelbroken_upper")
res = script.pip(
- 'install', '--no-index', '--no-binary=upper', '-f', data.find_links,
- to_install, expect_stderr=True)
- expected = ("Successfully installed requires-wheelbroken-upper-0"
- " upper-2.0 wheelbroken-0.1")
+ "install",
+ "--no-index",
+ "--no-binary=upper",
+ "-f",
+ data.find_links,
+ to_install,
+ allow_stderr_error=True, # error building wheelbroken
+ )
+ expected = (
+ "Successfully installed requires-wheelbroken-upper-0"
+ " upper-2.0 wheelbroken-0.1"
+ )
# Must have installed it all
assert expected in str(res), str(res)
# and built wheels for wheelbroken only
@@ -1441,12 +1671,13 @@ def test_install_no_binary_disables_building_wheels(script, data, with_wheel):
@pytest.mark.network
-def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel):
- to_install = data.packages.joinpath('pep517_setup_and_pyproject')
- res = script.pip(
- 'install', '--no-binary=:all:', '-f', data.find_links, to_install
- )
- expected = ("Successfully installed pep517-setup-and-pyproject")
+@pytest.mark.usefixtures("with_wheel")
+def test_install_no_binary_builds_pep_517_wheel(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ to_install = data.packages.joinpath("pep517_setup_and_pyproject")
+ res = script.pip("install", "--no-binary=:all:", "-f", data.find_links, to_install)
+ expected = "Successfully installed pep517-setup-and-pyproject"
# Must have installed the package
assert expected in str(res), str(res)
@@ -1455,13 +1686,13 @@ def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel):
@pytest.mark.network
+@pytest.mark.usefixtures("with_wheel")
def test_install_no_binary_uses_local_backend(
- script, data, with_wheel, tmpdir):
- to_install = data.packages.joinpath('pep517_wrapper_buildsys')
- script.environ['PIP_TEST_MARKER_FILE'] = marker = str(tmpdir / 'marker')
- res = script.pip(
- 'install', '--no-binary=:all:', '-f', data.find_links, to_install
- )
+ script: PipTestEnvironment, data: TestData, tmpdir: Path
+) -> None:
+ to_install = data.packages.joinpath("pep517_wrapper_buildsys")
+ script.environ["PIP_TEST_MARKER_FILE"] = marker = str(tmpdir / "marker")
+ res = script.pip("install", "--no-binary=:all:", "-f", data.find_links, to_install)
expected = "Successfully installed pep517-wrapper-buildsys"
# Must have installed the package
assert expected in str(res), str(res)
@@ -1469,15 +1700,22 @@ def test_install_no_binary_uses_local_backend(
assert os.path.isfile(marker), "Local PEP 517 backend not used"
-def test_install_no_binary_disables_cached_wheels(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_no_binary_disables_cached_wheels(
+ script: PipTestEnvironment, data: TestData
+) -> None:
# Seed the cache
- script.pip(
- 'install', '--no-index', '-f', data.find_links,
- 'upper')
- script.pip('uninstall', 'upper', '-y')
+ script.pip("install", "--no-index", "-f", data.find_links, "upper")
+ script.pip("uninstall", "upper", "-y")
res = script.pip(
- 'install', '--no-index', '--no-binary=:all:', '-f', data.find_links,
- 'upper', expect_stderr=True)
+ "install",
+ "--no-index",
+ "--no-binary=:all:",
+ "-f",
+ data.find_links,
+ "upper",
+ expect_stderr=True,
+ )
assert "Successfully installed upper-2.0" in str(res), str(res)
# No wheel building for upper, which was blacklisted
assert "Building wheel for upper" not in str(res), str(res)
@@ -1485,173 +1723,210 @@ def test_install_no_binary_disables_cached_wheels(script, data, with_wheel):
assert "Running setup.py install for upper" in str(res), str(res)
-def test_install_editable_with_wrong_egg_name(script, resolver_variant):
+def test_install_editable_with_wrong_egg_name(
+ script: PipTestEnvironment, resolver_variant: ResolverVariant
+) -> None:
script.scratch_path.joinpath("pkga").mkdir()
- pkga_path = script.scratch_path / 'pkga'
- pkga_path.joinpath("setup.py").write_text(textwrap.dedent("""
+ pkga_path = script.scratch_path / "pkga"
+ pkga_path.joinpath("setup.py").write_text(
+ textwrap.dedent(
+ """
from setuptools import setup
setup(name='pkga',
version='0.1')
- """))
+ """
+ )
+ )
result = script.pip(
- 'install', '--editable',
- f'file://{pkga_path}#egg=pkgb',
+ "install",
+ "--editable",
+ f"file://{pkga_path}#egg=pkgb",
expect_error=(resolver_variant == "2020-resolver"),
)
- assert ("Generating metadata for package pkgb produced metadata "
- "for project name pkga. Fix your #egg=pkgb "
- "fragments.") in result.stderr
+ assert (
+ "Generating metadata for package pkgb produced metadata "
+ "for project name pkga. Fix your #egg=pkgb "
+ "fragments."
+ ) in result.stderr
if resolver_variant == "2020-resolver":
- assert "has inconsistent" in result.stderr, str(result)
+ assert "has inconsistent" in result.stdout, str(result)
else:
assert "Successfully installed pkga" in str(result), str(result)
-def test_install_tar_xz(script, data):
+def test_install_tar_xz(script: PipTestEnvironment, data: TestData) -> None:
try:
import lzma # noqa
except ImportError:
pytest.skip("No lzma support")
- res = script.pip('install', data.packages / 'singlemodule-0.0.1.tar.xz')
+ res = script.pip("install", data.packages / "singlemodule-0.0.1.tar.xz")
assert "Successfully installed singlemodule-0.0.1" in res.stdout, res
-def test_install_tar_lzma(script, data):
+def test_install_tar_lzma(script: PipTestEnvironment, data: TestData) -> None:
try:
import lzma # noqa
except ImportError:
pytest.skip("No lzma support")
- res = script.pip('install', data.packages / 'singlemodule-0.0.1.tar.lzma')
+ res = script.pip("install", data.packages / "singlemodule-0.0.1.tar.lzma")
assert "Successfully installed singlemodule-0.0.1" in res.stdout, res
-def test_double_install(script):
+def test_double_install(script: PipTestEnvironment) -> None:
"""
Test double install passing with two same version requirements
"""
- result = script.pip('install', 'pip', 'pip')
+ result = script.pip("install", "pip", "pip")
msg = "Double requirement given: pip (already in pip, name='pip')"
assert msg not in result.stderr
-def test_double_install_fail(script, resolver_variant):
+def test_double_install_fail(
+ script: PipTestEnvironment, resolver_variant: ResolverVariant
+) -> None:
"""
Test double install failing with two different version requirements
"""
result = script.pip(
- 'install',
- 'pip==7.*',
- 'pip==7.1.2',
+ "install",
+ "pip==7.*",
+ "pip==7.1.2",
# The new resolver is perfectly capable of handling this
expect_error=(resolver_variant == "legacy"),
)
if resolver_variant == "legacy":
- msg = ("Double requirement given: pip==7.1.2 (already in pip==7.*, "
- "name='pip')")
+ msg = "Double requirement given: pip==7.1.2 (already in pip==7.*, name='pip')"
assert msg in result.stderr
-def _get_expected_error_text():
- return (
- "Package 'pkga' requires a different Python: {} not in '<1.0'"
- ).format('.'.join(map(str, sys.version_info[:3])))
+def _get_expected_error_text() -> str:
+ return ("Package 'pkga' requires a different Python: {} not in '<1.0'").format(
+ ".".join(map(str, sys.version_info[:3]))
+ )
-def test_install_incompatible_python_requires(script):
+def test_install_incompatible_python_requires(script: PipTestEnvironment) -> None:
script.scratch_path.joinpath("pkga").mkdir()
- pkga_path = script.scratch_path / 'pkga'
- pkga_path.joinpath("setup.py").write_text(textwrap.dedent("""
+ pkga_path = script.scratch_path / "pkga"
+ pkga_path.joinpath("setup.py").write_text(
+ textwrap.dedent(
+ """
from setuptools import setup
setup(name='pkga',
python_requires='<1.0',
version='0.1')
- """))
- result = script.pip('install', pkga_path, expect_error=True)
+ """
+ )
+ )
+ result = script.pip("install", pkga_path, expect_error=True)
assert _get_expected_error_text() in result.stderr, str(result)
-def test_install_incompatible_python_requires_editable(script):
+def test_install_incompatible_python_requires_editable(
+ script: PipTestEnvironment,
+) -> None:
script.scratch_path.joinpath("pkga").mkdir()
- pkga_path = script.scratch_path / 'pkga'
- pkga_path.joinpath("setup.py").write_text(textwrap.dedent("""
+ pkga_path = script.scratch_path / "pkga"
+ pkga_path.joinpath("setup.py").write_text(
+ textwrap.dedent(
+ """
from setuptools import setup
setup(name='pkga',
python_requires='<1.0',
version='0.1')
- """))
- result = script.pip(
- 'install',
- f'--editable={pkga_path}',
- expect_error=True)
+ """
+ )
+ )
+ result = script.pip("install", f"--editable={pkga_path}", expect_error=True)
assert _get_expected_error_text() in result.stderr, str(result)
-def test_install_incompatible_python_requires_wheel(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_incompatible_python_requires_wheel(script: PipTestEnvironment) -> None:
script.scratch_path.joinpath("pkga").mkdir()
- pkga_path = script.scratch_path / 'pkga'
- pkga_path.joinpath("setup.py").write_text(textwrap.dedent("""
+ pkga_path = script.scratch_path / "pkga"
+ pkga_path.joinpath("setup.py").write_text(
+ textwrap.dedent(
+ """
from setuptools import setup
setup(name='pkga',
python_requires='<1.0',
version='0.1')
- """))
+ """
+ )
+ )
script.run(
- 'python', 'setup.py', 'bdist_wheel', '--universal',
+ "python",
+ "setup.py",
+ "bdist_wheel",
+ "--universal",
cwd=pkga_path,
)
- result = script.pip('install', './pkga/dist/pkga-0.1-py2.py3-none-any.whl',
- expect_error=True)
+ result = script.pip(
+ "install", "./pkga/dist/pkga-0.1-py2.py3-none-any.whl", expect_error=True
+ )
assert _get_expected_error_text() in result.stderr, str(result)
-def test_install_compatible_python_requires(script):
+def test_install_compatible_python_requires(script: PipTestEnvironment) -> None:
script.scratch_path.joinpath("pkga").mkdir()
- pkga_path = script.scratch_path / 'pkga'
- pkga_path.joinpath("setup.py").write_text(textwrap.dedent("""
+ pkga_path = script.scratch_path / "pkga"
+ pkga_path.joinpath("setup.py").write_text(
+ textwrap.dedent(
+ """
from setuptools import setup
setup(name='pkga',
python_requires='>1.0',
version='0.1')
- """))
- res = script.pip('install', pkga_path)
+ """
+ )
+ )
+ res = script.pip("install", pkga_path)
assert "Successfully installed pkga-0.1" in res.stdout, res
@pytest.mark.network
-def test_install_pep508_with_url(script):
+def test_install_pep508_with_url(script: PipTestEnvironment) -> None:
res = script.pip(
- 'install', '--no-index',
- 'packaging@https://files.pythonhosted.org/packages/2f/2b/'
- 'c681de3e1dbcd469537aefb15186b800209aa1f299d933d23b48d85c9d56/'
- 'packaging-15.3-py2.py3-none-any.whl#sha256='
- 'ce1a869fe039fbf7e217df36c4653d1dbe657778b2d41709593a0003584405f4'
+ "install",
+ "--no-index",
+ "packaging@https://files.pythonhosted.org/packages/2f/2b/"
+ "c681de3e1dbcd469537aefb15186b800209aa1f299d933d23b48d85c9d56/"
+ "packaging-15.3-py2.py3-none-any.whl#sha256="
+ "ce1a869fe039fbf7e217df36c4653d1dbe657778b2d41709593a0003584405f4",
)
assert "Successfully installed packaging-15.3" in str(res), str(res)
@pytest.mark.network
-def test_install_pep508_with_url_in_install_requires(script):
+def test_install_pep508_with_url_in_install_requires(
+ script: PipTestEnvironment,
+) -> None:
pkga_path = create_test_package_with_setup(
- script, name='pkga', version='1.0',
+ script,
+ name="pkga",
+ version="1.0",
install_requires=[
- 'packaging@https://files.pythonhosted.org/packages/2f/2b/'
- 'c681de3e1dbcd469537aefb15186b800209aa1f299d933d23b48d85c9d56/'
- 'packaging-15.3-py2.py3-none-any.whl#sha256='
- 'ce1a869fe039fbf7e217df36c4653d1dbe657778b2d41709593a0003584405f4'
+ "packaging@https://files.pythonhosted.org/packages/2f/2b/"
+ "c681de3e1dbcd469537aefb15186b800209aa1f299d933d23b48d85c9d56/"
+ "packaging-15.3-py2.py3-none-any.whl#sha256="
+ "ce1a869fe039fbf7e217df36c4653d1dbe657778b2d41709593a0003584405f4"
],
)
- res = script.pip('install', pkga_path)
+ res = script.pip("install", pkga_path)
assert "Successfully installed packaging-15.3" in str(res), str(res)
@pytest.mark.network
-@pytest.mark.parametrize('index', (PyPI.simple_url, TestPyPI.simple_url))
-def test_install_from_test_pypi_with_ext_url_dep_is_blocked(script, index):
+@pytest.mark.parametrize("index", (PyPI.simple_url, TestPyPI.simple_url))
+def test_install_from_test_pypi_with_ext_url_dep_is_blocked(
+ script: PipTestEnvironment, index: str
+) -> None:
res = script.pip(
- 'install',
- '--index-url',
+ "install",
+ "--index-url",
index,
- 'pep-508-url-deps',
+ "pep-508-url-deps",
expect_error=True,
)
error_message = (
@@ -1670,106 +1945,125 @@ def test_install_from_test_pypi_with_ext_url_dep_is_blocked(script, index):
@pytest.mark.xfail(
reason="No longer possible to trigger the warning with either --prefix or --target"
)
-def test_installing_scripts_outside_path_prints_warning(script):
- result = script.pip_install_local(
- "--prefix", script.scratch_path, "script_wheel1"
- )
- assert "Successfully installed script-wheel1" in result.stdout, str(result)
+def test_installing_scripts_outside_path_prints_warning(
+ script: PipTestEnvironment,
+) -> None:
+ result = script.pip_install_local("--prefix", script.scratch_path, "script_wheel1")
+ assert "Successfully installed script_wheel1" in result.stdout, str(result)
assert "--no-warn-script-location" in result.stderr
-def test_installing_scripts_outside_path_can_suppress_warning(script):
+def test_installing_scripts_outside_path_can_suppress_warning(
+ script: PipTestEnvironment,
+) -> None:
result = script.pip_install_local(
- "--prefix", script.scratch_path, "--no-warn-script-location",
- "script_wheel1"
+ "--prefix", script.scratch_path, "--no-warn-script-location", "script_wheel1"
)
- assert "Successfully installed script-wheel1" in result.stdout, str(result)
+ assert "Successfully installed script_wheel1" in result.stdout, str(result)
assert "--no-warn-script-location" not in result.stderr
-def test_installing_scripts_on_path_does_not_print_warning(script):
+def test_installing_scripts_on_path_does_not_print_warning(
+ script: PipTestEnvironment,
+) -> None:
result = script.pip_install_local("script_wheel1")
- assert "Successfully installed script-wheel1" in result.stdout, str(result)
+ assert "Successfully installed script_wheel1" in result.stdout, str(result)
assert "--no-warn-script-location" not in result.stderr
-def test_installed_files_recorded_in_deterministic_order(script, data):
+def test_installed_files_recorded_in_deterministic_order(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Ensure that we record the files installed by a package in a deterministic
order, to make installs reproducible.
"""
to_install = data.packages.joinpath("FSPkg")
- result = script.pip('install', to_install)
- fspkg_folder = script.site_packages / 'fspkg'
- egg_info = f'FSPkg-0.1.dev0-py{pyversion}.egg-info'
- installed_files_path = (
- script.site_packages / egg_info / 'installed-files.txt'
- )
+ result = script.pip("install", to_install)
+ fspkg_folder = script.site_packages / "fspkg"
+ egg_info = f"FSPkg-0.1.dev0-py{pyversion}.egg-info"
+ installed_files_path = script.site_packages / egg_info / "installed-files.txt"
result.did_create(fspkg_folder)
result.did_create(installed_files_path)
installed_files_path = result.files_created[installed_files_path].full
installed_files_lines = [
- p for p in Path(installed_files_path).read_text().split('\n') if p
+ p for p in Path(installed_files_path).read_text().split("\n") if p
]
assert installed_files_lines == sorted(installed_files_lines)
-def test_install_conflict_results_in_warning(script, data):
+def test_install_conflict_results_in_warning(
+ script: PipTestEnvironment, data: TestData
+) -> None:
pkgA_path = create_test_package_with_setup(
script,
- name='pkgA', version='1.0', install_requires=['pkgb == 1.0'],
+ name="pkgA",
+ version="1.0",
+ install_requires=["pkgb == 1.0"],
)
pkgB_path = create_test_package_with_setup(
script,
- name='pkgB', version='2.0',
+ name="pkgB",
+ version="2.0",
)
# Install pkgA without its dependency
- result1 = script.pip('install', '--no-index', pkgA_path, '--no-deps')
+ result1 = script.pip("install", "--no-index", pkgA_path, "--no-deps")
assert "Successfully installed pkgA-1.0" in result1.stdout, str(result1)
# Then install an incorrect version of the dependency
result2 = script.pip(
- 'install', '--no-index', pkgB_path, allow_stderr_error=True,
+ "install",
+ "--no-index",
+ pkgB_path,
+ allow_stderr_error=True,
)
assert "pkga 1.0 requires pkgb==1.0" in result2.stderr, str(result2)
assert "Successfully installed pkgB-2.0" in result2.stdout, str(result2)
-def test_install_conflict_warning_can_be_suppressed(script, data):
+def test_install_conflict_warning_can_be_suppressed(
+ script: PipTestEnvironment, data: TestData
+) -> None:
pkgA_path = create_test_package_with_setup(
script,
- name='pkgA', version='1.0', install_requires=['pkgb == 1.0'],
+ name="pkgA",
+ version="1.0",
+ install_requires=["pkgb == 1.0"],
)
pkgB_path = create_test_package_with_setup(
script,
- name='pkgB', version='2.0',
+ name="pkgB",
+ version="2.0",
)
# Install pkgA without its dependency
- result1 = script.pip('install', '--no-index', pkgA_path, '--no-deps')
+ result1 = script.pip("install", "--no-index", pkgA_path, "--no-deps")
assert "Successfully installed pkgA-1.0" in result1.stdout, str(result1)
# Then install an incorrect version of the dependency; suppressing warning
- result2 = script.pip(
- 'install', '--no-index', pkgB_path, '--no-warn-conflicts'
- )
+ result2 = script.pip("install", "--no-index", pkgB_path, "--no-warn-conflicts")
assert "Successfully installed pkgB-2.0" in result2.stdout, str(result2)
-def test_target_install_ignores_distutils_config_install_prefix(script):
- prefix = script.scratch_path / 'prefix'
- distutils_config = Path(os.path.expanduser('~'),
- 'pydistutils.cfg' if sys.platform == 'win32'
- else '.pydistutils.cfg')
- distutils_config.write_text(textwrap.dedent(
- f'''
+def test_target_install_ignores_distutils_config_install_prefix(
+ script: PipTestEnvironment,
+) -> None:
+ prefix = script.scratch_path / "prefix"
+ distutils_config = Path.home().joinpath(
+ "pydistutils.cfg" if sys.platform == "win32" else ".pydistutils.cfg",
+ )
+ distutils_config.write_text(
+ textwrap.dedent(
+ f"""
[install]
prefix={prefix}
- '''))
- target = script.scratch_path / 'target'
- result = script.pip_install_local('simplewheel', '-t', target)
+ """
+ )
+ )
+ target = script.scratch_path / "target"
+ result = script.pip_install_local("simplewheel", "-t", target)
assert "Successfully installed simplewheel" in result.stdout
@@ -1780,78 +2074,94 @@ def test_target_install_ignores_distutils_config_install_prefix(script):
@pytest.mark.incompatible_with_test_venv
-def test_user_config_accepted(script):
+def test_user_config_accepted(script: PipTestEnvironment) -> None:
# user set in the config file is parsed as 0/1 instead of True/False.
# Check that this doesn't cause a problem.
- config_file = script.scratch_path / 'pip.conf'
- script.environ['PIP_CONFIG_FILE'] = str(config_file)
+ config_file = script.scratch_path / "pip.conf"
+ script.environ["PIP_CONFIG_FILE"] = str(config_file)
config_file.write_text("[install]\nuser = true")
- result = script.pip_install_local('simplewheel')
+ result = script.pip_install_local("simplewheel")
assert "Successfully installed simplewheel" in result.stdout
relative_user = os.path.relpath(script.user_site_path, script.base_path)
- result.did_create(join(relative_user, 'simplewheel'))
+ result.did_create(join(relative_user, "simplewheel"))
@pytest.mark.parametrize(
- 'install_args, expected_message', [
- ([], 'Requirement already satisfied: pip'),
- (['--upgrade'], 'Requirement already {}: pip in'),
- ]
+ "install_args, expected_message",
+ [
+ ([], "Requirement already satisfied: pip"),
+ (["--upgrade"], "Requirement already {}: pip in"),
+ ],
)
@pytest.mark.parametrize("use_module", [True, False])
def test_install_pip_does_not_modify_pip_when_satisfied(
- script, install_args, expected_message, use_module, resolver_variant):
+ script: PipTestEnvironment,
+ install_args: List[str],
+ expected_message: str,
+ use_module: bool,
+ resolver_variant: ResolverVariant,
+) -> None:
"""
Test it doesn't upgrade the pip if it already satisfies the requirement.
"""
variation = "satisfied" if resolver_variant else "up-to-date"
expected_message = expected_message.format(variation)
- result = script.pip_install_local(
- 'pip', *install_args, use_module=use_module
- )
+ result = script.pip_install_local("pip", *install_args, use_module=use_module)
assert expected_message in result.stdout, str(result)
-def test_ignore_yanked_file(script, data):
+def test_ignore_yanked_file(script: PipTestEnvironment, data: TestData) -> None:
"""
Test ignore a "yanked" file.
"""
result = script.pip(
- 'install', 'simple',
- '--index-url', data.index_url('yanked'),
+ "install",
+ "simple",
+ "--index-url",
+ data.index_url("yanked"),
)
# Make sure a "yanked" release is ignored
- assert 'Successfully installed simple-2.0\n' in result.stdout, str(result)
+ assert "Successfully installed simple-2.0\n" in result.stdout, str(result)
-def test_invalid_index_url_argument(script, shared_data):
+def test_invalid_index_url_argument(
+ script: PipTestEnvironment, shared_data: TestData
+) -> None:
"""
Test the behaviour of an invalid --index-url argument
"""
- result = script.pip('install', '--index-url', '--user',
- shared_data.find_links3, "Dinner",
- expect_error=True)
+ result = script.pip(
+ "install",
+ "--index-url",
+ "--user",
+ shared_data.find_links3,
+ "Dinner",
+ expect_error=True,
+ )
- assert ('WARNING: The index url "--user" seems invalid, '
- 'please provide a scheme.') in result.stderr, str(result)
+ assert (
+ 'WARNING: The index url "--user" seems invalid, please provide a scheme.'
+ ) in result.stderr, str(result)
-def test_valid_index_url_argument(script, shared_data):
+def test_valid_index_url_argument(
+ script: PipTestEnvironment, shared_data: TestData
+) -> None:
"""
Test the behaviour of an valid --index-url argument
"""
- result = script.pip('install', '--index-url',
- shared_data.find_links3,
- "Dinner")
+ result = script.pip("install", "--index-url", shared_data.find_links3, "Dinner")
- assert 'Successfully installed Dinner' in result.stdout, str(result)
+ assert "Successfully installed Dinner" in result.stdout, str(result)
-def test_install_yanked_file_and_print_warning(script, data):
+def test_install_yanked_file_and_print_warning(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test install a "yanked" file and print a warning.
@@ -1859,21 +2169,51 @@ def test_install_yanked_file_and_print_warning(script, data):
matches a version specifier that "pins" to an exact version (PEP 592).
"""
result = script.pip(
- 'install', 'simple==3.0',
- '--index-url', data.index_url('yanked'),
+ "install",
+ "simple==3.0",
+ "--index-url",
+ data.index_url("yanked"),
expect_stderr=True,
)
- expected_warning = 'Reason for being yanked: test reason message'
+ expected_warning = "Reason for being yanked: test reason message"
assert expected_warning in result.stderr, str(result)
# Make sure a "yanked" release is installed
- assert 'Successfully installed simple-3.0\n' in result.stdout, str(result)
+ assert "Successfully installed simple-3.0\n" in result.stdout, str(result)
+
+
+def test_error_all_yanked_files_and_no_pin(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ """
+ Test raising an error if there are only "yanked" files available and no pin
+ """
+ result = script.pip(
+ "install",
+ "simple",
+ "--index-url",
+ data.index_url("yanked_all"),
+ expect_error=True,
+ )
+ # Make sure an error is raised
+ assert (
+ result.returncode == 1
+ and "ERROR: No matching distribution found for simple\n" in result.stderr
+ ), str(result)
-@pytest.mark.parametrize("install_args", [
- (),
- ("--trusted-host", "localhost"),
-])
-def test_install_sends_client_cert(install_args, script, cert_factory, data):
+@pytest.mark.parametrize(
+ "install_args",
+ [
+ (),
+ ("--trusted-host", "localhost"),
+ ],
+)
+def test_install_sends_client_cert(
+ install_args: Tuple[str, ...],
+ script: PipTestEnvironment,
+ cert_factory: CertFactory,
+ data: TestData,
+) -> None:
cert_path = cert_factory()
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
ctx.load_cert_chain(cert_path, cert_path)
@@ -1882,10 +2222,12 @@ def test_install_sends_client_cert(install_args, script, cert_factory, data):
server = make_mock_server(ssl_context=ctx)
server.mock.side_effect = [
- package_page({
- "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz",
- }),
- file_response(str(data.packages / "simple-3.0.tar.gz")),
+ package_page(
+ {
+ "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz",
+ }
+ ),
+ file_response(data.packages / "simple-3.0.tar.gz"),
]
url = f"https://{server.host}:{server.port}/simple"
@@ -1907,7 +2249,7 @@ def test_install_sends_client_cert(install_args, script, cert_factory, data):
assert environ["SSL_CLIENT_CERT"]
-def test_install_skip_work_dir_pkg(script, data):
+def test_install_skip_work_dir_pkg(script: PipTestEnvironment, data: TestData) -> None:
"""
Test that install of a package in working directory
should pass on the second attempt after an install
@@ -1915,26 +2257,32 @@ def test_install_skip_work_dir_pkg(script, data):
"""
# Create a test package, install it and then uninstall it
- pkg_path = create_test_package_with_setup(
- script, name='simple', version='1.0')
- script.pip('install', '-e', '.',
- expect_stderr=True, cwd=pkg_path)
+ pkg_path = create_test_package_with_setup(script, name="simple", version="1.0")
+ script.pip("install", "-e", ".", expect_stderr=True, cwd=pkg_path)
- script.pip('uninstall', 'simple', '-y')
+ script.pip("uninstall", "simple", "-y")
# Running the install command again from the working directory
# will install the package as it was uninstalled earlier
- result = script.pip('install', '--find-links',
- data.find_links, 'simple',
- expect_stderr=True, cwd=pkg_path)
+ result = script.pip(
+ "install",
+ "--find-links",
+ data.find_links,
+ "simple",
+ expect_stderr=True,
+ cwd=pkg_path,
+ )
- assert 'Requirement already satisfied: simple' not in result.stdout
- assert 'Successfully installed simple' in result.stdout
+ assert "Requirement already satisfied: simple" not in result.stdout
+ assert "Successfully installed simple" in result.stdout
-@pytest.mark.parametrize('package_name', ('simple-package', 'simple_package',
- 'simple.package'))
-def test_install_verify_package_name_normalization(script, package_name):
+@pytest.mark.parametrize(
+ "package_name", ("simple-package", "simple_package", "simple.package")
+)
+def test_install_verify_package_name_normalization(
+ script: PipTestEnvironment, package_name: str
+) -> None:
"""
Test that install of a package again using a name which
@@ -1942,18 +2290,57 @@ def test_install_verify_package_name_normalization(script, package_name):
since the package is already installed
"""
pkg_path = create_test_package_with_setup(
- script, name='simple-package', version='1.0')
- result = script.pip('install', '-e', '.',
- expect_stderr=True, cwd=pkg_path)
- assert 'Successfully installed simple-package' in result.stdout
+ script, name="simple-package", version="1.0"
+ )
+ result = script.pip("install", "-e", ".", expect_stderr=True, cwd=pkg_path)
+ assert "Successfully installed simple-package" in result.stdout
- result = script.pip('install', package_name)
- assert 'Requirement already satisfied: {}'.format(
- package_name) in result.stdout
+ result = script.pip("install", package_name)
+ assert "Requirement already satisfied: {}".format(package_name) in result.stdout
-def test_install_logs_pip_version_in_debug(script, shared_data):
- fake_package = shared_data.packages / 'simple-2.0.tar.gz'
- result = script.pip('install', '-v', fake_package)
+def test_install_logs_pip_version_in_debug(
+ script: PipTestEnvironment, shared_data: TestData
+) -> None:
+ fake_package = shared_data.packages / "simple-2.0.tar.gz"
+ result = script.pip("install", "-v", fake_package)
pattern = "Using pip .* from .*"
assert_re_match(pattern, result.stdout)
+
+
+def test_install_dry_run(script: PipTestEnvironment, data: TestData) -> None:
+ """Test that pip install --dry-run logs what it would install."""
+ result = script.pip(
+ "install", "--dry-run", "--find-links", data.find_links, "simple"
+ )
+ assert "Would install simple-3.0" in result.stdout
+ assert "Successfully installed" not in result.stdout
+
+
+def test_install_8559_missing_wheel_package(
+ script: PipTestEnvironment, shared_data: TestData
+) -> None:
+ result = script.pip(
+ "install",
+ "--find-links",
+ shared_data.find_links,
+ "simple",
+ allow_stderr_warning=True,
+ )
+ assert DEPRECATION_MSG_PREFIX in result.stderr
+ assert "'wheel' package is not installed" in result.stderr
+ assert "using the legacy 'setup.py install' method" in result.stderr
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_install_8559_wheel_package_present(
+ script: PipTestEnvironment, shared_data: TestData
+) -> None:
+ result = script.pip(
+ "install",
+ "--find-links",
+ shared_data.find_links,
+ "simple",
+ allow_stderr_warning=False,
+ )
+ assert DEPRECATION_MSG_PREFIX not in result.stderr
diff --git a/tests/functional/test_install_check.py b/tests/functional/test_install_check.py
index 56ac7daf6..8a8a7c93a 100644
--- a/tests/functional/test_install_check.py
+++ b/tests/functional/test_install_check.py
@@ -1,38 +1,42 @@
-from tests.lib import create_test_package_with_setup
+from typing import Iterable
+from tests.lib import PipTestEnvironment, create_test_package_with_setup
-def assert_contains_expected_lines(string, expected_lines):
+
+def assert_contains_expected_lines(string: str, expected_lines: Iterable[str]) -> None:
for expected_line in expected_lines:
- assert (expected_line + '\n') in string
+ assert (expected_line + "\n") in string
-def test_check_install_canonicalization(script):
+def test_check_install_canonicalization(script: PipTestEnvironment) -> None:
pkga_path = create_test_package_with_setup(
script,
- name='pkgA',
- version='1.0',
- install_requires=['normal-missing', 'SPECIAL.missing'],
+ name="pkgA",
+ version="1.0",
+ install_requires=["normal-missing", "SPECIAL.missing"],
)
normal_path = create_test_package_with_setup(
script,
- name='normal-missing', version='0.1',
+ name="normal-missing",
+ version="0.1",
)
special_path = create_test_package_with_setup(
script,
- name='SPECIAL.missing', version='0.1',
+ name="SPECIAL.missing",
+ version="0.1",
)
# Let's install pkgA without its dependency
- result = script.pip('install', '--no-index', pkga_path, '--no-deps')
+ result = script.pip("install", "--no-index", pkga_path, "--no-deps")
assert "Successfully installed pkgA-1.0" in result.stdout, str(result)
# Install the first missing dependency. Only an error for the
# second dependency should remain.
result = script.pip(
- 'install',
- '--no-index',
+ "install",
+ "--no-index",
normal_path,
- '--quiet',
+ "--quiet",
allow_stderr_error=True,
)
expected_lines = [
@@ -46,13 +50,16 @@ def test_check_install_canonicalization(script):
# during the installation. This is special as the package name requires
# name normalization (as in https://github.com/pypa/pip/issues/5134)
result = script.pip(
- 'install', '--no-index', special_path, '--quiet',
+ "install",
+ "--no-index",
+ special_path,
+ "--quiet",
)
assert "requires" not in result.stderr
assert result.returncode == 0
# Double check that all errors are resolved in the end
- result = script.pip('check')
+ result = script.pip("check")
expected_lines = [
"No broken requirements found.",
]
@@ -60,46 +67,57 @@ def test_check_install_canonicalization(script):
assert result.returncode == 0
-def test_check_install_does_not_warn_for_out_of_graph_issues(script):
+def test_check_install_does_not_warn_for_out_of_graph_issues(
+ script: PipTestEnvironment,
+) -> None:
pkg_broken_path = create_test_package_with_setup(
script,
- name='broken',
- version='1.0',
- install_requires=['missing', 'conflict < 1.0'],
+ name="broken",
+ version="1.0",
+ install_requires=["missing", "conflict < 1.0"],
)
pkg_unrelated_path = create_test_package_with_setup(
script,
- name='unrelated',
- version='1.0',
+ name="unrelated",
+ version="1.0",
)
pkg_conflict_path = create_test_package_with_setup(
script,
- name='conflict',
- version='1.0',
+ name="conflict",
+ version="1.0",
)
# Install a package without it's dependencies
- result = script.pip('install', '--no-index', pkg_broken_path, '--no-deps')
+ result = script.pip("install", "--no-index", pkg_broken_path, "--no-deps")
assert "requires" not in result.stderr
# Install conflict package
result = script.pip(
- 'install', '--no-index', pkg_conflict_path, allow_stderr_error=True,
+ "install",
+ "--no-index",
+ pkg_conflict_path,
+ allow_stderr_error=True,
+ )
+ assert_contains_expected_lines(
+ result.stderr,
+ [
+ "broken 1.0 requires missing, which is not installed.",
+ "broken 1.0 requires conflict<1.0, "
+ "but you have conflict 1.0 which is incompatible.",
+ ],
)
- assert_contains_expected_lines(result.stderr, [
- "broken 1.0 requires missing, which is not installed.",
- "broken 1.0 requires conflict<1.0, "
- "but you have conflict 1.0 which is incompatible."
- ])
# Install unrelated package
result = script.pip(
- 'install', '--no-index', pkg_unrelated_path, '--quiet',
+ "install",
+ "--no-index",
+ pkg_unrelated_path,
+ "--quiet",
)
# should not warn about broken's deps when installing unrelated package
assert "requires" not in result.stderr
- result = script.pip('check', expect_error=True)
+ result = script.pip("check", expect_error=True)
expected_lines = [
"broken 1.0 requires missing, which is not installed.",
"broken 1.0 has requirement conflict<1.0, but you have conflict 1.0.",
diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py
index 7b64ed4b5..c0ea5a425 100644
--- a/tests/functional/test_install_cleanup.py
+++ b/tests/functional/test_install_cleanup.py
@@ -2,19 +2,26 @@ from os.path import exists
import pytest
+from tests.lib import PipTestEnvironment, TestData
+
@pytest.mark.network
-@pytest.mark.xfail(
- reason="The --build option was removed"
-)
-def test_no_clean_option_blocks_cleaning_after_install(script, data):
+@pytest.mark.xfail(reason="The --build option was removed")
+def test_no_clean_option_blocks_cleaning_after_install(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test --no-clean option blocks cleaning after install
"""
- build = script.base_path / 'pip-build'
+ build = script.base_path / "pip-build"
script.pip(
- 'install', '--no-clean', '--no-index', '--build', build,
- f'--find-links={data.find_links}', 'simple',
+ "install",
+ "--no-clean",
+ "--no-index",
+ "--build",
+ build,
+ f"--find-links={data.find_links}",
+ "simple",
expect_temp=True,
# TODO: allow_stderr_warning is used for the --build deprecation,
# remove it when removing support for --build
@@ -24,14 +31,12 @@ def test_no_clean_option_blocks_cleaning_after_install(script, data):
@pytest.mark.network
-def test_pep517_no_legacy_cleanup(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_pep517_no_legacy_cleanup(script: PipTestEnvironment, data: TestData) -> None:
"""Test a PEP 517 failed build does not attempt a legacy cleanup"""
- to_install = data.packages.joinpath('pep517_wrapper_buildsys')
+ to_install = data.packages.joinpath("pep517_wrapper_buildsys")
script.environ["PIP_TEST_FAIL_BUILD_WHEEL"] = "1"
- res = script.pip(
- 'install', '-f', data.find_links, to_install,
- expect_error=True
- )
+ res = script.pip("install", "-f", data.find_links, to_install, expect_error=True)
# Must not have built the package
expected = "Failed building wheel for pep517-wrapper-buildsys"
assert expected in str(res)
diff --git a/tests/functional/test_install_compat.py b/tests/functional/test_install_compat.py
index 44b9b290e..4b6b46b02 100644
--- a/tests/functional/test_install_compat.py
+++ b/tests/functional/test_install_compat.py
@@ -7,11 +7,11 @@ import os
import pytest
from tests.lib import pyversion # noqa: F401
-from tests.lib import assert_all_changes
+from tests.lib import PipTestEnvironment, TestData, assert_all_changes
@pytest.mark.network
-def test_debian_egg_name_workaround(script):
+def test_debian_egg_name_workaround(script: PipTestEnvironment) -> None:
"""
We can uninstall packages installed with the pyversion removed from the
egg-info metadata directory name.
@@ -22,27 +22,21 @@ def test_debian_egg_name_workaround(script):
https://bitbucket.org/ianb/pip/issue/104/pip-uninstall-on-ubuntu-linux
"""
- result = script.pip('install', 'INITools==0.2')
+ result = script.pip("install", "INITools==0.2")
egg_info = os.path.join(
- script.site_packages,
- f"INITools-0.2-py{pyversion}.egg-info")
+ script.site_packages, f"INITools-0.2-py{pyversion}.egg-info"
+ )
# Debian only removes pyversion for global installs, not inside a venv
# so even if this test runs on a Debian/Ubuntu system with broken
# setuptools, since our test runs inside a venv we'll still have the normal
# .egg-info
- result.did_create(
- egg_info,
- message=f"Couldn't find {egg_info}"
- )
+ result.did_create(egg_info, message=f"Couldn't find {egg_info}")
# The Debian no-pyversion version of the .egg-info
mangled = os.path.join(script.site_packages, "INITools-0.2.egg-info")
- result.did_not_create(
- mangled,
- message=f"Found unexpected {mangled}"
- )
+ result.did_not_create(mangled, message=f"Found unexpected {mangled}")
# Simulate a Debian install by copying the .egg-info to their name for it
full_egg_info = os.path.join(script.base_path, egg_info)
@@ -53,14 +47,16 @@ def test_debian_egg_name_workaround(script):
# Try the uninstall and verify that everything is removed.
result2 = script.pip("uninstall", "INITools", "-y")
- assert_all_changes(result, result2, [script.venv / 'build', 'cache'])
+ assert_all_changes(result, result2, [script.venv / "build", "cache"])
-def test_setup_py_with_dos_line_endings(script, data):
+def test_setup_py_with_dos_line_endings(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
It doesn't choke on a setup.py file that uses DOS line endings (\\r\\n).
Refs https://github.com/pypa/pip/issues/237
"""
to_install = data.packages.joinpath("LineEndings")
- script.pip('install', to_install)
+ script.pip("install", to_install)
diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py
index 59aec65ff..66043fa1f 100644
--- a/tests/functional/test_install_config.py
+++ b/tests/functional/test_install_config.py
@@ -5,6 +5,8 @@ import textwrap
import pytest
+from tests.conftest import CertFactory, MockServer
+from tests.lib import PipTestEnvironment, TestData
from tests.lib.server import (
authorization_response,
file_response,
@@ -12,36 +14,41 @@ from tests.lib.server import (
package_page,
server_running,
)
+from tests.lib.venv import VirtualEnvironment
+TEST_PYPI_INITOOLS = "https://test.pypi.org/simple/initools/"
-def test_options_from_env_vars(script):
+
+def test_options_from_env_vars(script: PipTestEnvironment) -> None:
"""
Test if ConfigOptionParser reads env vars (e.g. not using PyPI here)
"""
- script.environ['PIP_NO_INDEX'] = '1'
- result = script.pip('install', '-vvv', 'INITools', expect_error=True)
+ script.environ["PIP_NO_INDEX"] = "1"
+ result = script.pip("install", "-vvv", "INITools", expect_error=True)
assert "Ignoring indexes:" in result.stdout, str(result)
msg = "DistributionNotFound: No matching distribution found for INITools"
- # Case insensitive as the new resolver canonicalises the project name
+ # Case insensitive as the new resolver canonicalizes the project name
assert msg.lower() in result.stdout.lower(), str(result)
-def test_command_line_options_override_env_vars(script, virtualenv):
+def test_command_line_options_override_env_vars(
+ script: PipTestEnvironment, virtualenv: VirtualEnvironment
+) -> None:
"""
Test that command line options override environmental variables.
"""
- script.environ['PIP_INDEX_URL'] = 'https://example.com/simple/'
- result = script.pip('install', '-vvv', 'INITools', expect_error=True)
- assert (
- "Getting page https://example.com/simple/initools"
- in result.stdout
- )
+ script.environ["PIP_INDEX_URL"] = "https://example.com/simple/"
+ result = script.pip("install", "-vvv", "INITools", expect_error=True)
+ assert "Getting page https://example.com/simple/initools" in result.stdout
virtualenv.clear()
result = script.pip(
- 'install', '-vvv', '--index-url', 'https://download.zope.org/ppix',
- 'INITools',
+ "install",
+ "-vvv",
+ "--index-url",
+ "https://download.zope.org/ppix",
+ "INITools",
expect_error=True,
)
assert "example.com" not in result.stdout
@@ -49,42 +56,53 @@ def test_command_line_options_override_env_vars(script, virtualenv):
@pytest.mark.network
-def test_env_vars_override_config_file(script, virtualenv):
+def test_env_vars_override_config_file(
+ script: PipTestEnvironment, virtualenv: VirtualEnvironment
+) -> None:
"""
Test that environmental variables override settings in config files.
"""
config_file = script.scratch_path / "test-pip.cfg"
# set this to make pip load it
- script.environ['PIP_CONFIG_FILE'] = str(config_file)
+ script.environ["PIP_CONFIG_FILE"] = str(config_file)
# It's important that we test this particular config value ('no-index')
# because there is/was a bug which only shows up in cases in which
# 'config-item' and 'config_item' hash to the same value modulo the size
# of the config dictionary.
- config_file.write_text(textwrap.dedent("""\
+ config_file.write_text(
+ textwrap.dedent(
+ """\
[global]
no-index = 1
- """))
- result = script.pip('install', '-vvv', 'INITools', expect_error=True)
+ """
+ )
+ )
+ result = script.pip("install", "-vvv", "INITools", expect_error=True)
msg = "DistributionNotFound: No matching distribution found for INITools"
- # Case insensitive as the new resolver canonicalises the project name
+ # Case insensitive as the new resolver canonicalizes the project name
assert msg.lower() in result.stdout.lower(), str(result)
- script.environ['PIP_NO_INDEX'] = '0'
+ script.environ["PIP_NO_INDEX"] = "0"
virtualenv.clear()
- result = script.pip('install', '-vvv', 'INITools')
+ result = script.pip("install", "-vvv", "INITools")
assert "Successfully installed INITools" in result.stdout
@pytest.mark.network
-def test_command_line_append_flags(script, virtualenv, data):
+def test_command_line_append_flags(
+ script: PipTestEnvironment, virtualenv: VirtualEnvironment, data: TestData
+) -> None:
"""
Test command line flags that append to defaults set by environmental
variables.
"""
- script.environ['PIP_FIND_LINKS'] = 'https://test.pypi.org'
+ script.environ["PIP_FIND_LINKS"] = TEST_PYPI_INITOOLS
result = script.pip(
- 'install', '-vvv', 'INITools', '--trusted-host',
- 'test.pypi.org',
+ "install",
+ "-vvv",
+ "INITools",
+ "--trusted-host",
+ "test.pypi.org",
)
assert (
"Fetching project page and analyzing links: https://test.pypi.org"
@@ -92,31 +110,38 @@ def test_command_line_append_flags(script, virtualenv, data):
), str(result)
virtualenv.clear()
result = script.pip(
- 'install', '-vvv', '--find-links', data.find_links, 'INITools',
- '--trusted-host', 'test.pypi.org',
+ "install",
+ "-vvv",
+ "--find-links",
+ data.find_links,
+ "INITools",
+ "--trusted-host",
+ "test.pypi.org",
)
assert (
"Fetching project page and analyzing links: https://test.pypi.org"
in result.stdout
)
assert (
- f'Skipping link: not a file: {data.find_links}' in
- result.stdout
- ), f'stdout: {result.stdout}'
+ f"Skipping link: not a file: {data.find_links}" in result.stdout
+ ), f"stdout: {result.stdout}"
@pytest.mark.network
-def test_command_line_appends_correctly(script, data):
+def test_command_line_appends_correctly(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test multiple appending options set by environmental variables.
"""
- script.environ['PIP_FIND_LINKS'] = (
- f'https://test.pypi.org {data.find_links}'
- )
+ script.environ["PIP_FIND_LINKS"] = f"{TEST_PYPI_INITOOLS} {data.find_links}"
result = script.pip(
- 'install', '-vvv', 'INITools', '--trusted-host',
- 'test.pypi.org',
+ "install",
+ "-vvv",
+ "INITools",
+ "--trusted-host",
+ "test.pypi.org",
)
assert (
@@ -124,50 +149,68 @@ def test_command_line_appends_correctly(script, data):
in result.stdout
), result.stdout
assert (
- f'Skipping link: not a file: {data.find_links}' in
- result.stdout
- ), f'stdout: {result.stdout}'
+ f"Skipping link: not a file: {data.find_links}" in result.stdout
+ ), f"stdout: {result.stdout}"
def test_config_file_override_stack(
- script, virtualenv, mock_server, shared_data
-):
+ script: PipTestEnvironment,
+ virtualenv: VirtualEnvironment,
+ mock_server: MockServer,
+ shared_data: TestData,
+) -> None:
"""
Test config files (global, overriding a global config with a
local, overriding all with a command line flag).
"""
- mock_server.set_responses([
- package_page({}),
- package_page({}),
- package_page({"INITools-0.2.tar.gz": "/files/INITools-0.2.tar.gz"}),
- file_response(shared_data.packages.joinpath("INITools-0.2.tar.gz")),
- ])
+ mock_server.set_responses(
+ [
+ package_page({}),
+ package_page({}),
+ package_page({"INITools-0.2.tar.gz": "/files/INITools-0.2.tar.gz"}),
+ file_response(shared_data.packages.joinpath("INITools-0.2.tar.gz")),
+ ]
+ )
mock_server.start()
base_address = f"http://{mock_server.host}:{mock_server.port}"
config_file = script.scratch_path / "test-pip.cfg"
# set this to make pip load it
- script.environ['PIP_CONFIG_FILE'] = str(config_file)
+ script.environ["PIP_CONFIG_FILE"] = str(config_file)
- config_file.write_text(textwrap.dedent("""\
+ config_file.write_text(
+ textwrap.dedent(
+ """\
[global]
index-url = {}/simple1
- """.format(base_address)))
- script.pip('install', '-vvv', 'INITools', expect_error=True)
+ """.format(
+ base_address
+ )
+ )
+ )
+ script.pip("install", "-vvv", "INITools", expect_error=True)
virtualenv.clear()
- config_file.write_text(textwrap.dedent("""\
+ config_file.write_text(
+ textwrap.dedent(
+ """\
[global]
index-url = {address}/simple1
[install]
index-url = {address}/simple2
- """.format(address=base_address))
+ """.format(
+ address=base_address
+ )
+ )
)
- script.pip('install', '-vvv', 'INITools', expect_error=True)
+ script.pip("install", "-vvv", "INITools", expect_error=True)
script.pip(
- 'install', '-vvv', '--index-url', f"{base_address}/simple3",
- 'INITools',
+ "install",
+ "-vvv",
+ "--index-url",
+ f"{base_address}/simple3",
+ "INITools",
)
mock_server.stop()
@@ -179,36 +222,45 @@ def test_config_file_override_stack(
assert requests[3]["PATH_INFO"] == "/files/INITools-0.2.tar.gz"
-def test_options_from_venv_config(script, virtualenv):
+def test_options_from_venv_config(
+ script: PipTestEnvironment, virtualenv: VirtualEnvironment
+) -> None:
"""
Test if ConfigOptionParser reads a virtualenv-local config file
"""
from pip._internal.configuration import CONFIG_BASENAME
+
conf = "[global]\nno-index = true"
ini = virtualenv.location / CONFIG_BASENAME
- with open(ini, 'w') as f:
+ with open(ini, "w") as f:
f.write(conf)
- result = script.pip('install', '-vvv', 'INITools', expect_error=True)
+ result = script.pip("install", "-vvv", "INITools", expect_error=True)
assert "Ignoring indexes:" in result.stdout, str(result)
msg = "DistributionNotFound: No matching distribution found for INITools"
- # Case insensitive as the new resolver canonicalises the project name
+ # Case insensitive as the new resolver canonicalizes the project name
assert msg.lower() in result.stdout.lower(), str(result)
+@pytest.mark.usefixtures("with_wheel")
def test_install_no_binary_via_config_disables_cached_wheels(
- script, data, with_wheel):
- config_file = tempfile.NamedTemporaryFile(mode='wt', delete=False)
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ config_file = tempfile.NamedTemporaryFile(mode="wt", delete=False)
try:
- script.environ['PIP_CONFIG_FILE'] = config_file.name
- config_file.write(textwrap.dedent("""\
+ script.environ["PIP_CONFIG_FILE"] = config_file.name
+ config_file.write(
+ textwrap.dedent(
+ """\
[global]
no-binary = :all:
- """))
+ """
+ )
+ )
config_file.close()
res = script.pip(
- 'install', '--no-index', '-f', data.find_links,
- 'upper', expect_stderr=True)
+ "install", "--no-index", "-f", data.find_links, "upper", expect_stderr=True
+ )
finally:
os.unlink(config_file.name)
assert "Successfully installed upper-2.0" in str(res), str(res)
@@ -218,7 +270,9 @@ def test_install_no_binary_via_config_disables_cached_wheels(
assert "Running setup.py install for upper" in str(res), str(res)
-def test_prompt_for_authentication(script, data, cert_factory):
+def test_prompt_for_authentication(
+ script: PipTestEnvironment, data: TestData, cert_factory: CertFactory
+) -> None:
"""Test behaviour while installing from a index url
requiring authentication
"""
@@ -230,24 +284,35 @@ def test_prompt_for_authentication(script, data, cert_factory):
server = make_mock_server(ssl_context=ctx)
server.mock.side_effect = [
- package_page({
- "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz",
- }),
- authorization_response(str(data.packages / "simple-3.0.tar.gz")),
+ package_page(
+ {
+ "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz",
+ }
+ ),
+ authorization_response(data.packages / "simple-3.0.tar.gz"),
]
url = f"https://{server.host}:{server.port}/simple"
with server_running(server):
- result = script.pip('install', "--index-url", url,
- "--cert", cert_path, "--client-cert", cert_path,
- 'simple', expect_error=True)
-
- assert f'User for {server.host}:{server.port}' in \
- result.stdout, str(result)
-
-
-def test_do_not_prompt_for_authentication(script, data, cert_factory):
+ result = script.pip(
+ "install",
+ "--index-url",
+ url,
+ "--cert",
+ cert_path,
+ "--client-cert",
+ cert_path,
+ "simple",
+ expect_error=True,
+ )
+
+ assert f"User for {server.host}:{server.port}" in result.stdout, str(result)
+
+
+def test_do_not_prompt_for_authentication(
+ script: PipTestEnvironment, data: TestData, cert_factory: CertFactory
+) -> None:
"""Test behaviour if --no-input option is given while installing
from a index url requiring authentication
"""
@@ -260,24 +325,40 @@ def test_do_not_prompt_for_authentication(script, data, cert_factory):
server = make_mock_server(ssl_context=ctx)
server.mock.side_effect = [
- package_page({
- "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz",
- }),
- authorization_response(str(data.packages / "simple-3.0.tar.gz")),
+ package_page(
+ {
+ "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz",
+ }
+ ),
+ authorization_response(data.packages / "simple-3.0.tar.gz"),
]
url = f"https://{server.host}:{server.port}/simple"
with server_running(server):
- result = script.pip('install', "--index-url", url,
- "--cert", cert_path, "--client-cert", cert_path,
- '--no-input', 'simple', expect_error=True)
+ result = script.pip(
+ "install",
+ "--index-url",
+ url,
+ "--cert",
+ cert_path,
+ "--client-cert",
+ cert_path,
+ "--no-input",
+ "simple",
+ expect_error=True,
+ )
assert "ERROR: HTTP error 401" in result.stderr
@pytest.mark.parametrize("auth_needed", (True, False))
-def test_prompt_for_keyring_if_needed(script, data, cert_factory, auth_needed):
+def test_prompt_for_keyring_if_needed(
+ script: PipTestEnvironment,
+ data: TestData,
+ cert_factory: CertFactory,
+ auth_needed: bool,
+) -> None:
"""Test behaviour while installing from a index url
requiring authentication and keyring is possible.
"""
@@ -291,16 +372,19 @@ def test_prompt_for_keyring_if_needed(script, data, cert_factory, auth_needed):
server = make_mock_server(ssl_context=ctx)
server.mock.side_effect = [
- package_page({
- "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz",
- }),
- response(str(data.packages / "simple-3.0.tar.gz")),
- response(str(data.packages / "simple-3.0.tar.gz")),
+ package_page(
+ {
+ "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz",
+ }
+ ),
+ response(data.packages / "simple-3.0.tar.gz"),
+ response(data.packages / "simple-3.0.tar.gz"),
]
url = f"https://{server.host}:{server.port}/simple"
- keyring_content = textwrap.dedent("""\
+ keyring_content = textwrap.dedent(
+ """\
import os
import sys
from collections import namedtuple
@@ -310,14 +394,22 @@ def test_prompt_for_keyring_if_needed(script, data, cert_factory, auth_needed):
def get_credential(url, username):
sys.stderr.write("get_credential was called" + os.linesep)
return Cred("USERNAME", "PASSWORD")
- """)
- keyring_path = script.site_packages_path / 'keyring.py'
+ """
+ )
+ keyring_path = script.site_packages_path / "keyring.py"
keyring_path.write_text(keyring_content)
with server_running(server):
- result = script.pip('install', "--index-url", url,
- "--cert", cert_path, "--client-cert", cert_path,
- 'simple')
+ result = script.pip(
+ "install",
+ "--index-url",
+ url,
+ "--cert",
+ cert_path,
+ "--client-cert",
+ cert_path,
+ "simple",
+ )
if auth_needed:
assert "get_credential was called" in result.stderr
diff --git a/tests/functional/test_install_direct_url.py b/tests/functional/test_install_direct_url.py
index baa5a3f2c..cd2a4aea7 100644
--- a/tests/functional/test_install_direct_url.py
+++ b/tests/functional/test_install_direct_url.py
@@ -1,43 +1,50 @@
import pytest
-from tests.lib import _create_test_package, path_to_url
+from pip._internal.models.direct_url import VcsInfo
+from tests.lib import PipTestEnvironment, TestData, _create_test_package
from tests.lib.direct_url import get_created_direct_url
-def test_install_find_links_no_direct_url(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_find_links_no_direct_url(script: PipTestEnvironment) -> None:
result = script.pip_install_local("simple")
assert not get_created_direct_url(result, "simple")
-def test_install_vcs_editable_no_direct_url(script, with_wheel):
- pkg_path = _create_test_package(script, name="testpkg")
- args = ["install", "-e", "git+%s#egg=testpkg" % path_to_url(pkg_path)]
+@pytest.mark.usefixtures("with_wheel")
+def test_install_vcs_editable_no_direct_url(script: PipTestEnvironment) -> None:
+ pkg_path = _create_test_package(script.scratch_path, name="testpkg")
+ args = ["install", "-e", f"git+{pkg_path.as_uri()}#egg=testpkg"]
result = script.pip(*args)
# legacy editable installs do not generate .dist-info,
# hence no direct_url.json
assert not get_created_direct_url(result, "testpkg")
-def test_install_vcs_non_editable_direct_url(script, with_wheel):
- pkg_path = _create_test_package(script, name="testpkg")
- url = path_to_url(pkg_path)
+@pytest.mark.usefixtures("with_wheel")
+def test_install_vcs_non_editable_direct_url(script: PipTestEnvironment) -> None:
+ pkg_path = _create_test_package(script.scratch_path, name="testpkg")
+ url = pkg_path.as_uri()
args = ["install", f"git+{url}#egg=testpkg"]
result = script.pip(*args)
direct_url = get_created_direct_url(result, "testpkg")
assert direct_url
assert direct_url.url == url
+ assert isinstance(direct_url.info, VcsInfo)
assert direct_url.info.vcs == "git"
-def test_install_archive_direct_url(script, data, with_wheel):
- req = "simple @ " + path_to_url(data.packages / "simple-2.0.tar.gz")
+@pytest.mark.usefixtures("with_wheel")
+def test_install_archive_direct_url(script: PipTestEnvironment, data: TestData) -> None:
+ req = "simple @ " + data.packages.joinpath("simple-2.0.tar.gz").as_uri()
assert req.startswith("simple @ file://")
result = script.pip("install", req)
assert get_created_direct_url(result, "simple")
@pytest.mark.network
-def test_install_vcs_constraint_direct_url(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_vcs_constraint_direct_url(script: PipTestEnvironment) -> None:
constraints_file = script.scratch_path / "constraints.txt"
constraints_file.write_text(
"git+https://github.com/pypa/pip-test-package"
@@ -48,9 +55,10 @@ def test_install_vcs_constraint_direct_url(script, with_wheel):
assert get_created_direct_url(result, "pip_test_package")
-def test_install_vcs_constraint_direct_file_url(script, with_wheel):
- pkg_path = _create_test_package(script, name="testpkg")
- url = path_to_url(pkg_path)
+@pytest.mark.usefixtures("with_wheel")
+def test_install_vcs_constraint_direct_file_url(script: PipTestEnvironment) -> None:
+ pkg_path = _create_test_package(script.scratch_path, name="testpkg")
+ url = pkg_path.as_uri()
constraints_file = script.scratch_path / "constraints.txt"
constraints_file.write_text(f"git+{url}#egg=testpkg")
result = script.pip("install", "testpkg", "-c", constraints_file)
diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py
index 83bcc5482..c6cef00fa 100644
--- a/tests/functional/test_install_extras.py
+++ b/tests/functional/test_install_extras.py
@@ -4,55 +4,71 @@ from os.path import join
import pytest
+from tests.lib import PipTestEnvironment, ResolverVariant, TestData
+
@pytest.mark.network
-def test_simple_extras_install_from_pypi(script):
+def test_simple_extras_install_from_pypi(script: PipTestEnvironment) -> None:
"""
Test installing a package from PyPI using extras dependency Paste[openid].
"""
result = script.pip(
- 'install', 'Paste[openid]==1.7.5.1', expect_stderr=True,
+ "install",
+ "Paste[openid]==1.7.5.1",
+ expect_stderr=True,
)
- initools_folder = script.site_packages / 'openid'
+ initools_folder = script.site_packages / "openid"
result.did_create(initools_folder)
-def test_extras_after_wheel(script, data):
+def test_extras_after_wheel(script: PipTestEnvironment, data: TestData) -> None:
"""
Test installing a package with extras after installing from a wheel.
"""
- simple = script.site_packages / 'simple'
+ simple = script.site_packages / "simple"
no_extra = script.pip(
- 'install', '--no-index', '-f', data.find_links,
- 'requires_simple_extra', expect_stderr=True,
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "requires_simple_extra",
+ expect_stderr=True,
)
no_extra.did_not_create(simple)
extra = script.pip(
- 'install', '--no-index', '-f', data.find_links,
- 'requires_simple_extra[extra]', expect_stderr=True,
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "requires_simple_extra[extra]",
+ expect_stderr=True,
)
extra.did_create(simple)
@pytest.mark.network
-def test_no_extras_uninstall(script):
+def test_no_extras_uninstall(script: PipTestEnvironment) -> None:
"""
No extras dependency gets uninstalled when the root package is uninstalled
"""
result = script.pip(
- 'install', 'Paste[openid]==1.7.5.1', expect_stderr=True,
+ "install",
+ "Paste[openid]==1.7.5.1",
+ expect_stderr=True,
)
- result.did_create(join(script.site_packages, 'paste'))
- result.did_create(join(script.site_packages, 'openid'))
- result2 = script.pip('uninstall', 'Paste', '-y')
+ result.did_create(join(script.site_packages, "paste"))
+ result.did_create(join(script.site_packages, "openid"))
+ result2 = script.pip("uninstall", "Paste", "-y")
# openid should not be uninstalled
- initools_folder = script.site_packages / 'openid'
+ initools_folder = script.site_packages / "openid"
assert initools_folder not in result2.files_deleted, result.files_deleted
-def test_nonexistent_extra_warns_user_no_wheel(script, data):
+def test_nonexistent_extra_warns_user_no_wheel(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
A warning is logged telling the user that the extra option they requested
does not exist in the project they are wishing to install.
@@ -60,17 +76,21 @@ def test_nonexistent_extra_warns_user_no_wheel(script, data):
This exercises source installs.
"""
result = script.pip(
- 'install', '--no-binary=:all:', '--no-index',
- '--find-links=' + data.find_links,
- 'simple[nonexistent]', expect_stderr=True,
+ "install",
+ "--no-binary=:all:",
+ "--no-index",
+ "--find-links=" + data.find_links,
+ "simple[nonexistent]",
+ expect_stderr=True,
+ )
+ assert "simple 3.0 does not provide the extra 'nonexistent'" in result.stderr, str(
+ result
)
- assert (
- "simple 3.0 does not provide the extra 'nonexistent'"
- in result.stderr
- ), str(result)
-def test_nonexistent_extra_warns_user_with_wheel(script, data):
+def test_nonexistent_extra_warns_user_with_wheel(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
A warning is logged telling the user that the extra option they requested
does not exist in the project they are wishing to install.
@@ -78,33 +98,37 @@ def test_nonexistent_extra_warns_user_with_wheel(script, data):
This exercises wheel installs.
"""
result = script.pip(
- 'install', '--no-index',
- '--find-links=' + data.find_links,
- 'simplewheel[nonexistent]', expect_stderr=True,
- )
- assert (
- "simplewheel 2.0 does not provide the extra 'nonexistent'"
- in result.stderr
+ "install",
+ "--no-index",
+ "--find-links=" + data.find_links,
+ "simplewheel[nonexistent]",
+ expect_stderr=True,
)
+ assert "simplewheel 2.0 does not provide the extra 'nonexistent'" in result.stderr
-def test_nonexistent_options_listed_in_order(script, data):
+def test_nonexistent_options_listed_in_order(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Warn the user for each extra that doesn't exist.
"""
result = script.pip(
- 'install', '--no-index',
- '--find-links=' + data.find_links,
- 'simplewheel[nonexistent, nope]', expect_stderr=True,
+ "install",
+ "--no-index",
+ "--find-links=" + data.find_links,
+ "simplewheel[nonexistent, nope]",
+ expect_stderr=True,
)
matches = re.findall(
- "WARNING: simplewheel 2.0 does not provide the extra '([a-z]*)'",
- result.stderr
+ "WARNING: simplewheel 2.0 does not provide the extra '([a-z]*)'", result.stderr
)
- assert matches == ['nonexistent', 'nope']
+ assert matches == ["nonexistent", "nope"]
-def test_install_fails_if_extra_at_end(script, data):
+def test_install_fails_if_extra_at_end(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Fail if order of specifiers and extras is incorrect.
@@ -116,51 +140,70 @@ def test_install_fails_if_extra_at_end(script, data):
)
result = script.pip(
- 'install', '--no-index', '--find-links=' + data.find_links,
- '-r', script.scratch_path / 'requirements.txt', expect_error=True,
+ "install",
+ "--no-index",
+ "--find-links=" + data.find_links,
+ "-r",
+ script.scratch_path / "requirements.txt",
+ expect_error=True,
)
assert "Extras after version" in result.stderr
-def test_install_special_extra(script):
+def test_install_special_extra(script: PipTestEnvironment) -> None:
# Check that uppercase letters and '-' are dealt with
# make a dummy project
- pkga_path = script.scratch_path / 'pkga'
+ pkga_path = script.scratch_path / "pkga"
pkga_path.mkdir()
- pkga_path.joinpath("setup.py").write_text(textwrap.dedent("""
+ pkga_path.joinpath("setup.py").write_text(
+ textwrap.dedent(
+ """
from setuptools import setup
setup(name='pkga',
version='0.1',
extras_require={'Hop_hOp-hoP': ['missing_pkg']},
)
- """))
+ """
+ )
+ )
result = script.pip(
- 'install', '--no-index', f'{pkga_path}[Hop_hOp-hoP]',
- expect_error=True)
+ "install", "--no-index", f"{pkga_path}[Hop_hOp-hoP]", expect_error=True
+ )
assert (
"Could not find a version that satisfies the requirement missing_pkg"
) in result.stderr, str(result)
-def test_install_requirements_no_r_flag(script):
- '''Beginners sometimes forget the -r and this leads to confusion'''
- result = script.pip('install', 'requirements.txt', expect_error=True)
+def test_install_requirements_no_r_flag(script: PipTestEnvironment) -> None:
+ """Beginners sometimes forget the -r and this leads to confusion"""
+ result = script.pip("install", "requirements.txt", expect_error=True)
assert 'literally named "requirements.txt"' in result.stdout
@pytest.mark.parametrize(
- "extra_to_install, simple_version", [
- ['', '3.0'],
- pytest.param('[extra1]', '2.0', marks=pytest.mark.xfail),
- pytest.param('[extra2]', '1.0', marks=pytest.mark.xfail),
- pytest.param('[extra1,extra2]', '1.0', marks=pytest.mark.xfail),
- ])
-def test_install_extra_merging(script, data, extra_to_install, simple_version):
+ "extra_to_install, simple_version, fails_on_legacy",
+ [
+ ("", "3.0", False),
+ ("[extra1]", "2.0", True),
+ ("[extra2]", "1.0", True),
+ ("[extra1,extra2]", "1.0", True),
+ ],
+)
+@pytest.mark.usefixtures("data")
+def test_install_extra_merging(
+ script: PipTestEnvironment,
+ resolver_variant: ResolverVariant,
+ extra_to_install: str,
+ simple_version: str,
+ fails_on_legacy: bool,
+) -> None:
# Check that extra specifications in the extras section are honoured.
- pkga_path = script.scratch_path / 'pkga'
+ pkga_path = script.scratch_path / "pkga"
pkga_path.mkdir()
- pkga_path.joinpath("setup.py").write_text(textwrap.dedent("""
+ pkga_path.joinpath("setup.py").write_text(
+ textwrap.dedent(
+ """
from setuptools import setup
setup(name='pkga',
version='0.1',
@@ -168,10 +211,15 @@ def test_install_extra_merging(script, data, extra_to_install, simple_version):
extras_require={'extra1': ['simple<3'],
'extra2': ['simple==1.*']},
)
- """))
+ """
+ )
+ )
result = script.pip_install_local(
- f'{pkga_path}{extra_to_install}',
+ f"{pkga_path}{extra_to_install}",
+ expect_error=(fails_on_legacy and resolver_variant == "legacy"),
)
- assert f'Successfully installed pkga-0.1 simple-{simple_version}' in result.stdout
+ if not fails_on_legacy or resolver_variant == "2020-resolver":
+ expected = f"Successfully installed pkga-0.1 simple-{simple_version}"
+ assert expected in result.stdout
diff --git a/tests/functional/test_install_force_reinstall.py b/tests/functional/test_install_force_reinstall.py
index 265c52b20..9c49d58c4 100644
--- a/tests/functional/test_install_force_reinstall.py
+++ b/tests/functional/test_install_force_reinstall.py
@@ -1,55 +1,61 @@
import os
-from tests.lib import assert_all_changes
+from tests.lib import PipTestEnvironment, assert_all_changes
-def check_installed_version(script, package, expected):
- result = script.pip('show', package)
+def check_installed_version(
+ script: PipTestEnvironment, package: str, expected: str
+) -> None:
+ result = script.pip("show", package)
lines = result.stdout.splitlines()
version = None
for line in lines:
- if line.startswith('Version: '):
+ if line.startswith("Version: "):
version = line.split()[-1]
break
- assert version == expected, f'version {version} != {expected}'
+ assert version == expected, f"version {version} != {expected}"
-def check_force_reinstall(script, specifier, expected):
+def check_force_reinstall(
+ script: PipTestEnvironment, specifier: str, expected: str
+) -> None:
"""
Args:
specifier: the requirement specifier to force-reinstall.
expected: the expected version after force-reinstalling.
"""
- result = script.pip_install_local('simplewheel==1.0')
- check_installed_version(script, 'simplewheel', '1.0')
+ result = script.pip_install_local("simplewheel==1.0")
+ check_installed_version(script, "simplewheel", "1.0")
# Remove an installed file to test whether --force-reinstall fixes it.
to_fix = script.site_packages_path.joinpath("simplewheel", "__init__.py")
to_fix.unlink()
- result2 = script.pip_install_local('--force-reinstall', specifier)
- check_installed_version(script, 'simplewheel', expected)
+ result2 = script.pip_install_local("--force-reinstall", specifier)
+ check_installed_version(script, "simplewheel", expected)
# site_packages_path is absolute, but files_created mapping uses
# relative paths as key.
fixed_key = os.path.relpath(to_fix, script.base_path)
- result2.did_create(fixed_key, message='force-reinstall failed')
+ result2.did_create(fixed_key, message="force-reinstall failed")
- result3 = script.pip('uninstall', 'simplewheel', '-y')
- assert_all_changes(result, result3, [script.venv / 'build', 'cache'])
+ result3 = script.pip("uninstall", "simplewheel", "-y")
+ assert_all_changes(result, result3, [script.venv / "build", "cache"])
-def test_force_reinstall_with_no_version_specifier(script):
+def test_force_reinstall_with_no_version_specifier(script: PipTestEnvironment) -> None:
"""
Check --force-reinstall when there is no version specifier and the
installed version is not the newest version.
"""
- check_force_reinstall(script, 'simplewheel', '2.0')
+ check_force_reinstall(script, "simplewheel", "2.0")
-def test_force_reinstall_with_same_version_specifier(script):
+def test_force_reinstall_with_same_version_specifier(
+ script: PipTestEnvironment,
+) -> None:
"""
Check --force-reinstall when the version specifier equals the installed
version and the installed version is not the newest version.
"""
- check_force_reinstall(script, 'simplewheel==1.0', '1.0')
+ check_force_reinstall(script, "simplewheel==1.0", "1.0")
diff --git a/tests/functional/test_install_index.py b/tests/functional/test_install_index.py
index 42d2f7a67..c1f0ecbd7 100644
--- a/tests/functional/test_install_index.py
+++ b/tests/functional/test_install_index.py
@@ -1,68 +1,91 @@
-import os
+import shutil
import textwrap
-import urllib.parse
+import pytest
-def test_find_links_relative_path(script, data, with_wheel):
+from tests.lib import PipTestEnvironment, TestData
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_find_links_relative_path(script: PipTestEnvironment, data: TestData) -> None:
"""Test find-links as a relative path."""
result = script.pip(
- 'install',
- 'parent==0.1',
- '--no-index',
- '--find-links',
- 'packages/',
+ "install",
+ "parent==0.1",
+ "--no-index",
+ "--find-links",
+ "packages/",
cwd=data.root,
)
- dist_info_folder = (
- script.site_packages / 'parent-0.1.dist-info'
- )
- initools_folder = script.site_packages / 'parent'
+ dist_info_folder = script.site_packages / "parent-0.1.dist-info"
+ initools_folder = script.site_packages / "parent"
result.did_create(dist_info_folder)
result.did_create(initools_folder)
-def test_find_links_requirements_file_relative_path(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_find_links_no_doctype(script: PipTestEnvironment, data: TestData) -> None:
+ shutil.copy(data.packages / "simple-1.0.tar.gz", script.scratch_path)
+ html = script.scratch_path.joinpath("index.html")
+ html.write_text('<a href="simple-1.0.tar.gz"></a>')
+ result = script.pip(
+ "install",
+ "simple==1.0",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ expect_stderr=True,
+ )
+ assert not result.stderr
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_find_links_requirements_file_relative_path(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""Test find-links as a relative path to a reqs file."""
- script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""
+ script.scratch_path.joinpath("test-req.txt").write_text(
+ textwrap.dedent(
+ """
--no-index
--find-links={}
parent==0.1
- """ .format(data.packages.replace(os.path.sep, '/'))))
+ """.format(
+ data.packages.as_posix()
+ )
+ )
+ )
result = script.pip(
- 'install',
- '-r',
+ "install",
+ "-r",
script.scratch_path / "test-req.txt",
cwd=data.root,
)
- dist_info_folder = (
- script.site_packages / 'parent-0.1.dist-info'
- )
- initools_folder = script.site_packages / 'parent'
+ dist_info_folder = script.site_packages / "parent-0.1.dist-info"
+ initools_folder = script.site_packages / "parent"
result.did_create(dist_info_folder)
result.did_create(initools_folder)
-def test_install_from_file_index_hash_link(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_from_file_index_hash_link(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test that a pkg can be installed from a file:// index using a link with a
hash
"""
- result = script.pip('install', '-i', data.index_url(), 'simple==1.0')
- dist_info_folder = (
- script.site_packages / 'simple-1.0.dist-info'
- )
+ result = script.pip("install", "-i", data.index_url(), "simple==1.0")
+ dist_info_folder = script.site_packages / "simple-1.0.dist-info"
result.did_create(dist_info_folder)
-def test_file_index_url_quoting(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_file_index_url_quoting(script: PipTestEnvironment, data: TestData) -> None:
"""
Test url quoting of file index url with a space
"""
- index_url = data.index_url(urllib.parse.quote("in dex"))
- result = script.pip(
- 'install', '-vvv', '--index-url', index_url, 'simple'
- )
- result.did_create(script.site_packages / 'simple')
- result.did_create(
- script.site_packages / 'simple-1.0.dist-info'
- )
+ index_url = data.index_url("in dex")
+ result = script.pip("install", "-vvv", "--index-url", index_url, "simple")
+ result.did_create(script.site_packages / "simple")
+ result.did_create(script.site_packages / "simple-1.0.dist-info")
diff --git a/tests/functional/test_install_report.py b/tests/functional/test_install_report.py
new file mode 100644
index 000000000..b61fd89c6
--- /dev/null
+++ b/tests/functional/test_install_report.py
@@ -0,0 +1,214 @@
+import json
+from pathlib import Path
+from typing import Any, Dict
+
+import pytest
+from packaging.utils import canonicalize_name
+
+from ..lib import PipTestEnvironment, TestData
+
+
+def _install_dict(report: Dict[str, Any]) -> Dict[str, Any]:
+ return {canonicalize_name(i["metadata"]["name"]): i for i in report["install"]}
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_install_report_basic(
+ script: PipTestEnvironment, shared_data: TestData, tmp_path: Path
+) -> None:
+ report_path = tmp_path / "report.json"
+ script.pip(
+ "install",
+ "simplewheel",
+ "--dry-run",
+ "--no-index",
+ "--find-links",
+ str(shared_data.root / "packages/"),
+ "--report",
+ str(report_path),
+ allow_stderr_warning=True,
+ )
+ report = json.loads(report_path.read_text())
+ assert "install" in report
+ assert len(report["install"]) == 1
+ simplewheel_report = _install_dict(report)["simplewheel"]
+ assert simplewheel_report["metadata"]["name"] == "simplewheel"
+ assert simplewheel_report["requested"] is True
+ assert simplewheel_report["is_direct"] is False
+ url = simplewheel_report["download_info"]["url"]
+ assert url.startswith("file://")
+ assert url.endswith("/packages/simplewheel-2.0-1-py2.py3-none-any.whl")
+ assert (
+ simplewheel_report["download_info"]["archive_info"]["hash"]
+ == "sha256=191d6520d0570b13580bf7642c97ddfbb46dd04da5dd2cf7bef9f32391dfe716"
+ )
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_install_report_dep(
+ script: PipTestEnvironment, shared_data: TestData, tmp_path: Path
+) -> None:
+ """Test dependencies are present in the install report with requested=False."""
+ report_path = tmp_path / "report.json"
+ script.pip(
+ "install",
+ "require_simple",
+ "--dry-run",
+ "--no-index",
+ "--find-links",
+ str(shared_data.root / "packages/"),
+ "--report",
+ str(report_path),
+ allow_stderr_warning=True,
+ )
+ report = json.loads(report_path.read_text())
+ assert len(report["install"]) == 2
+ assert _install_dict(report)["require-simple"]["requested"] is True
+ assert _install_dict(report)["simple"]["requested"] is False
+
+
+@pytest.mark.network
+@pytest.mark.usefixtures("with_wheel")
+def test_install_report_index(script: PipTestEnvironment, tmp_path: Path) -> None:
+ """Test report for sdist obtained from index."""
+ report_path = tmp_path / "report.json"
+ script.pip(
+ "install",
+ "--dry-run",
+ "Paste[openid]==1.7.5.1",
+ "--report",
+ str(report_path),
+ allow_stderr_warning=True,
+ )
+ report = json.loads(report_path.read_text())
+ assert len(report["install"]) == 2
+ install_dict = _install_dict(report)
+ assert install_dict["paste"]["requested"] is True
+ assert install_dict["python-openid"]["requested"] is False
+ paste_report = install_dict["paste"]
+ assert paste_report["download_info"]["url"].startswith(
+ "https://files.pythonhosted.org/"
+ )
+ assert paste_report["download_info"]["url"].endswith("/Paste-1.7.5.1.tar.gz")
+ assert (
+ paste_report["download_info"]["archive_info"]["hash"]
+ == "sha256=11645842ba8ec986ae8cfbe4c6cacff5c35f0f4527abf4f5581ae8b4ad49c0b6"
+ )
+ assert paste_report["requested_extras"] == ["openid"]
+ assert "requires_dist" in paste_report["metadata"]
+
+
+@pytest.mark.network
+@pytest.mark.usefixtures("with_wheel")
+def test_install_report_vcs_and_wheel_cache(
+ script: PipTestEnvironment, tmp_path: Path
+) -> None:
+ """Test report for VCS reference, and interactions with the wheel cache."""
+ cache_dir = tmp_path / "cache"
+ report_path = tmp_path / "report.json"
+ script.pip(
+ "install",
+ "git+https://github.com/pypa/pip-test-package"
+ "@5547fa909e83df8bd743d3978d6667497983a4b7",
+ "--cache-dir",
+ str(cache_dir),
+ "--report",
+ str(report_path),
+ allow_stderr_warning=True,
+ )
+ report = json.loads(report_path.read_text())
+ assert len(report["install"]) == 1
+ pip_test_package_report = report["install"][0]
+ assert pip_test_package_report["is_direct"] is True
+ assert pip_test_package_report["requested"] is True
+ assert (
+ pip_test_package_report["download_info"]["url"]
+ == "https://github.com/pypa/pip-test-package"
+ )
+ assert pip_test_package_report["download_info"]["vcs_info"]["vcs"] == "git"
+ assert (
+ pip_test_package_report["download_info"]["vcs_info"]["commit_id"]
+ == "5547fa909e83df8bd743d3978d6667497983a4b7"
+ )
+ # Now do it again to make sure the cache is used and that the report still contains
+ # the original VCS url.
+ report_path.unlink()
+ result = script.pip(
+ "install",
+ "pip-test-package @ git+https://github.com/pypa/pip-test-package"
+ "@5547fa909e83df8bd743d3978d6667497983a4b7",
+ "--ignore-installed",
+ "--cache-dir",
+ str(cache_dir),
+ "--report",
+ str(report_path),
+ allow_stderr_warning=True,
+ )
+ assert "Using cached pip_test_package" in result.stdout
+ report = json.loads(report_path.read_text())
+ assert len(report["install"]) == 1
+ pip_test_package_report = report["install"][0]
+ assert pip_test_package_report["is_direct"] is True
+ assert pip_test_package_report["requested"] is True
+ assert (
+ pip_test_package_report["download_info"]["url"]
+ == "https://github.com/pypa/pip-test-package"
+ )
+ assert pip_test_package_report["download_info"]["vcs_info"]["vcs"] == "git"
+ assert (
+ pip_test_package_report["download_info"]["vcs_info"]["commit_id"]
+ == "5547fa909e83df8bd743d3978d6667497983a4b7"
+ )
+
+
+@pytest.mark.network
+@pytest.mark.usefixtures("with_wheel")
+def test_install_report_vcs_editable(
+ script: PipTestEnvironment, tmp_path: Path
+) -> None:
+ """Test report remote editable."""
+ report_path = tmp_path / "report.json"
+ script.pip(
+ "install",
+ "--editable",
+ "git+https://github.com/pypa/pip-test-package"
+ "@5547fa909e83df8bd743d3978d6667497983a4b7"
+ "#egg=pip-test-package",
+ "--report",
+ str(report_path),
+ allow_stderr_warning=True,
+ )
+ report = json.loads(report_path.read_text())
+ assert len(report["install"]) == 1
+ pip_test_package_report = report["install"][0]
+ assert pip_test_package_report["is_direct"] is True
+ assert pip_test_package_report["download_info"]["url"].startswith("file://")
+ assert pip_test_package_report["download_info"]["url"].endswith(
+ "/src/pip-test-package"
+ )
+ assert pip_test_package_report["download_info"]["dir_info"]["editable"] is True
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_install_report_to_stdout(
+ script: PipTestEnvironment, shared_data: TestData
+) -> None:
+ result = script.pip(
+ "install",
+ "simplewheel",
+ "--quiet",
+ "--dry-run",
+ "--no-index",
+ "--find-links",
+ str(shared_data.root / "packages/"),
+ "--report",
+ "-",
+ allow_stderr_warning=True,
+ )
+ assert result.stderr == (
+ "WARNING: --report is currently an experimental option. "
+ "The output format may change in a future release without prior warning.\n"
+ )
+ report = json.loads(result.stdout)
+ assert "install" in report
+ assert len(report["install"]) == 1
diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py
index 6df489275..c5fc11dd7 100644
--- a/tests/functional/test_install_reqs.py
+++ b/tests/functional/test_install_reqs.py
@@ -1,32 +1,37 @@
import json
import os
import textwrap
+from pathlib import Path
+from typing import Any, Callable
import pytest
from tests.lib import (
+ PipTestEnvironment,
+ ResolverVariant,
+ TestData,
_create_test_package_with_subdirectory,
create_basic_sdist_for_package,
create_basic_wheel_for_package,
need_svn,
- path_to_url,
requirements_file,
)
from tests.lib.local_repos import local_checkout
-from tests.lib.path import Path
class ArgRecordingSdist:
- def __init__(self, sdist_path, args_path):
+ def __init__(self, sdist_path: Path, args_path: Path) -> None:
self.sdist_path = sdist_path
self._args_path = args_path
- def args(self):
+ def args(self) -> Any:
return json.loads(self._args_path.read_text())
@pytest.fixture()
-def arg_recording_sdist_maker(script):
+def arg_recording_sdist_maker(
+ script: PipTestEnvironment,
+) -> Callable[[str], ArgRecordingSdist]:
arg_writing_setup_py = textwrap.dedent(
"""
import io
@@ -43,18 +48,13 @@ def arg_recording_sdist_maker(script):
setup(name={name!r}, version="0.1.0")
"""
)
- output_dir = script.scratch_path.joinpath(
- "args_recording_sdist_maker_output"
- )
+ output_dir = script.scratch_path.joinpath("args_recording_sdist_maker_output")
output_dir.mkdir(parents=True)
script.environ["OUTPUT_DIR"] = str(output_dir)
- def _arg_recording_sdist_maker(name):
- # type: (str) -> ArgRecordingSdist
+ def _arg_recording_sdist_maker(name: str) -> ArgRecordingSdist:
extra_files = {"setup.py": arg_writing_setup_py.format(name=name)}
- sdist_path = create_basic_sdist_for_package(
- script, name, "0.1.0", extra_files
- )
+ sdist_path = create_basic_sdist_for_package(script, name, "0.1.0", extra_files)
args_path = output_dir / f"{name}.json"
return ArgRecordingSdist(sdist_path, args_path)
@@ -62,31 +62,31 @@ def arg_recording_sdist_maker(script):
@pytest.mark.network
-def test_requirements_file(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_requirements_file(script: PipTestEnvironment) -> None:
"""
Test installing from a requirements file.
"""
- other_lib_name, other_lib_version = 'anyjson', '0.3'
- script.scratch_path.joinpath("initools-req.txt").write_text(textwrap.dedent(f"""\
+ other_lib_name, other_lib_version = "peppercorn", "0.6"
+ script.scratch_path.joinpath("initools-req.txt").write_text(
+ textwrap.dedent(
+ f"""\
INITools==0.2
# and something else to test out:
{other_lib_name}<={other_lib_version}
- """))
- result = script.pip(
- 'install', '-r', script.scratch_path / 'initools-req.txt'
- )
- result.did_create(
- script.site_packages / 'INITools-0.2.dist-info'
+ """
+ )
)
- result.did_create(script.site_packages / 'initools')
+ result = script.pip("install", "-r", script.scratch_path / "initools-req.txt")
+ result.did_create(script.site_packages / "INITools-0.2.dist-info")
+ result.did_create(script.site_packages / "initools")
assert result.files_created[script.site_packages / other_lib_name].dir
- fn = '{}-{}.dist-info'.format(
- other_lib_name, other_lib_version)
+ fn = "{}-{}.dist-info".format(other_lib_name, other_lib_version)
assert result.files_created[script.site_packages / fn].dir
-def test_schema_check_in_requirements_file(script):
+def test_schema_check_in_requirements_file(script: PipTestEnvironment) -> None:
"""
Test installing from a requirements file with an invalid vcs schema..
@@ -99,344 +99,424 @@ def test_schema_check_in_requirements_file(script):
)
with pytest.raises(AssertionError):
- script.pip(
- "install", "-vvv", "-r", script.scratch_path / "file-egg-req.txt"
- )
-
-
-@pytest.mark.parametrize("test_type,editable", [
- ("rel_path", False),
- ("rel_path", True),
- ("rel_url", False),
- ("rel_url", True),
- ("embedded_rel_path", False),
- ("embedded_rel_path", True),
-])
+ script.pip("install", "-vvv", "-r", script.scratch_path / "file-egg-req.txt")
+
+
+@pytest.mark.parametrize(
+ "test_type,editable",
+ [
+ ("rel_path", False),
+ ("rel_path", True),
+ ("rel_url", False),
+ ("rel_url", True),
+ ("embedded_rel_path", False),
+ ("embedded_rel_path", True),
+ ],
+)
+@pytest.mark.usefixtures("with_wheel")
def test_relative_requirements_file(
- script, data, test_type, editable, with_wheel
-):
+ script: PipTestEnvironment, data: TestData, test_type: str, editable: bool
+) -> None:
"""
Test installing from a requirements file with a relative path. For path
URLs, use an egg= definition.
"""
- dist_info_folder = (
- script.site_packages /
- 'FSPkg-0.1.dev0.dist-info'
- )
- egg_link_file = (
- script.site_packages / 'FSPkg.egg-link'
- )
- package_folder = script.site_packages / 'fspkg'
+ dist_info_folder = script.site_packages / "FSPkg-0.1.dev0.dist-info"
+ egg_link_file = script.site_packages / "FSPkg.egg-link"
+ package_folder = script.site_packages / "fspkg"
# Compute relative install path to FSPkg from scratch path.
- full_rel_path = Path(
- os.path.relpath(data.packages.joinpath('FSPkg'), script.scratch_path)
+ full_rel_path = os.path.relpath(
+ data.packages.joinpath("FSPkg"), script.scratch_path
)
- full_rel_url = 'file:' + full_rel_path + '#egg=FSPkg'
+ full_rel_url = "file:" + full_rel_path + "#egg=FSPkg"
embedded_rel_path = script.scratch_path.joinpath(full_rel_path)
req_path = {
"rel_path": full_rel_path,
"rel_url": full_rel_url,
- "embedded_rel_path": embedded_rel_path,
+ "embedded_rel_path": os.fspath(embedded_rel_path),
}[test_type]
- req_path = req_path.replace(os.path.sep, '/')
+ req_path = req_path.replace(os.path.sep, "/")
# Install as either editable or not.
if not editable:
- with requirements_file(req_path + '\n',
- script.scratch_path) as reqs_file:
- result = script.pip('install', '-vvv', '-r', reqs_file.name,
- cwd=script.scratch_path)
+ with requirements_file(req_path + "\n", script.scratch_path) as reqs_file:
+ result = script.pip(
+ "install", "-vvv", "-r", reqs_file.name, cwd=script.scratch_path
+ )
result.did_create(dist_info_folder)
result.did_create(package_folder)
else:
- with requirements_file('-e ' + req_path + '\n',
- script.scratch_path) as reqs_file:
- result = script.pip('install', '-vvv', '-r', reqs_file.name,
- cwd=script.scratch_path)
+ with requirements_file(
+ "-e " + req_path + "\n", script.scratch_path
+ ) as reqs_file:
+ result = script.pip(
+ "install", "-vvv", "-r", reqs_file.name, cwd=script.scratch_path
+ )
result.did_create(egg_link_file)
@pytest.mark.xfail
@pytest.mark.network
@need_svn
-def test_multiple_requirements_files(script, tmpdir, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_multiple_requirements_files(script: PipTestEnvironment, tmpdir: Path) -> None:
"""
Test installing from multiple nested requirements files.
"""
- other_lib_name, other_lib_version = 'anyjson', '0.3'
+ other_lib_name, other_lib_version = "six", "1.16.0"
script.scratch_path.joinpath("initools-req.txt").write_text(
- textwrap.dedent("""
+ textwrap.dedent(
+ """
-e {}@10#egg=INITools
-r {}-req.txt
- """).format
- (
- local_checkout('svn+http://svn.colorstudy.com/INITools', tmpdir),
- other_lib_name
+ """
+ ).format(
+ local_checkout("svn+http://svn.colorstudy.com/INITools", tmpdir),
+ other_lib_name,
),
)
script.scratch_path.joinpath(f"{other_lib_name}-req.txt").write_text(
- f"{other_lib_name}<={other_lib_version}"
- )
- result = script.pip(
- 'install', '-r', script.scratch_path / 'initools-req.txt'
+ f"{other_lib_name}<={other_lib_version}"
)
+ result = script.pip("install", "-r", script.scratch_path / "initools-req.txt")
assert result.files_created[script.site_packages / other_lib_name].dir
- fn = f'{other_lib_name}-{other_lib_version}.dist-info'
+ fn = f"{other_lib_name}-{other_lib_version}.dist-info"
assert result.files_created[script.site_packages / fn].dir
- result.did_create(script.venv / 'src' / 'initools')
+ result.did_create(script.venv / "src" / "initools")
-def test_package_in_constraints_and_dependencies(script, data):
+def test_package_in_constraints_and_dependencies(
+ script: PipTestEnvironment, data: TestData
+) -> None:
script.scratch_path.joinpath("constraints.txt").write_text(
"TopoRequires2==0.0.1\nTopoRequires==0.0.1"
)
- result = script.pip('install', '--no-index', '-f',
- data.find_links, '-c', script.scratch_path /
- 'constraints.txt', 'TopoRequires2')
- assert 'installed TopoRequires-0.0.1' in result.stdout
+ result = script.pip(
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "-c",
+ script.scratch_path / "constraints.txt",
+ "TopoRequires2",
+ )
+ assert "installed TopoRequires-0.0.1" in result.stdout
-def test_multiple_constraints_files(script, data):
+def test_multiple_constraints_files(script: PipTestEnvironment, data: TestData) -> None:
script.scratch_path.joinpath("outer.txt").write_text("-c inner.txt")
- script.scratch_path.joinpath("inner.txt").write_text(
- "Upper==1.0")
+ script.scratch_path.joinpath("inner.txt").write_text("Upper==1.0")
result = script.pip(
- 'install', '--no-index', '-f', data.find_links, '-c',
- script.scratch_path / 'outer.txt', 'Upper')
- assert 'installed Upper-1.0' in result.stdout
-
-
-@pytest.mark.xfail(reason="Unclear what this guarantee is for.")
-def test_respect_order_in_requirements_file(script, data):
- script.scratch_path.joinpath("frameworks-req.txt").write_text(textwrap.dedent("""\
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "-c",
+ script.scratch_path / "outer.txt",
+ "Upper",
+ )
+ assert "installed Upper-1.0" in result.stdout
+
+
+# FIXME: Unclear what this guarantee is for.
+def test_respect_order_in_requirements_file(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ script.scratch_path.joinpath("frameworks-req.txt").write_text(
+ textwrap.dedent(
+ """\
parent
child
simple
- """))
-
- result = script.pip(
- 'install', '--no-index', '-f', data.find_links, '-r',
- script.scratch_path / 'frameworks-req.txt'
- )
-
- downloaded = [line for line in result.stdout.split('\n')
- if 'Processing' in line]
-
- assert 'parent' in downloaded[0], (
- 'First download should be "parent" but was "{}"'.format(downloaded[0])
- )
- assert 'child' in downloaded[1], (
- 'Second download should be "child" but was "{}"'.format(downloaded[1])
- )
- assert 'simple' in downloaded[2], (
- 'Third download should be "simple" but was "{}"'.format(downloaded[2])
+ """
+ )
)
-
-def test_install_local_editable_with_extras(script, data):
+ result = script.pip(
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "-r",
+ script.scratch_path / "frameworks-req.txt",
+ )
+
+ downloaded = [line for line in result.stdout.split("\n") if "Processing" in line]
+
+ assert (
+ "parent" in downloaded[0]
+ ), 'First download should be "parent" but was "{}"'.format(downloaded[0])
+ assert (
+ "child" in downloaded[1]
+ ), 'Second download should be "child" but was "{}"'.format(downloaded[1])
+ assert (
+ "simple" in downloaded[2]
+ ), 'Third download should be "simple" but was "{}"'.format(downloaded[2])
+
+
+def test_install_local_editable_with_extras(
+ script: PipTestEnvironment, data: TestData
+) -> None:
to_install = data.packages.joinpath("LocalExtras")
res = script.pip_install_local(
- '-e', to_install + '[bar]', allow_stderr_warning=True
+ "-e", f"{to_install}[bar]", allow_stderr_warning=True
)
- res.did_update(script.site_packages / 'easy-install.pth')
- res.did_create(script.site_packages / 'LocalExtras.egg-link')
- res.did_create(script.site_packages / 'simple')
+ res.did_update(script.site_packages / "easy-install.pth")
+ res.did_create(script.site_packages / "LocalExtras.egg-link")
+ res.did_create(script.site_packages / "simple")
-def test_install_collected_dependencies_first(script):
+def test_install_collected_dependencies_first(script: PipTestEnvironment) -> None:
result = script.pip_install_local(
- 'toporequires2',
+ "toporequires2",
)
- text = [line for line in result.stdout.split('\n')
- if 'Installing' in line][0]
- assert text.endswith('toporequires2')
+ text = [line for line in result.stdout.split("\n") if "Installing" in line][0]
+ assert text.endswith("toporequires2")
@pytest.mark.network
-def test_install_local_editable_with_subdirectory(script):
- version_pkg_path = _create_test_package_with_subdirectory(script,
- 'version_subdir')
+def test_install_local_editable_with_subdirectory(script: PipTestEnvironment) -> None:
+ version_pkg_path = _create_test_package_with_subdirectory(script, "version_subdir")
result = script.pip(
- 'install', '-e',
- '{uri}#egg=version_subpkg&subdirectory=version_subdir'.format(
- uri='git+' + path_to_url(version_pkg_path),
+ "install",
+ "-e",
+ "{uri}#egg=version_subpkg&subdirectory=version_subdir".format(
+ uri=f"git+{version_pkg_path.as_uri()}",
),
)
- result.assert_installed('version-subpkg', sub_dir='version_subdir')
+ result.assert_installed("version-subpkg", sub_dir="version_subdir")
@pytest.mark.network
-def test_install_local_with_subdirectory(script):
- version_pkg_path = _create_test_package_with_subdirectory(script,
- 'version_subdir')
+def test_install_local_with_subdirectory(script: PipTestEnvironment) -> None:
+ version_pkg_path = _create_test_package_with_subdirectory(script, "version_subdir")
result = script.pip(
- 'install',
- '{uri}#egg=version_subpkg&subdirectory=version_subdir'.format(
- uri='git+' + path_to_url(version_pkg_path),
+ "install",
+ "{uri}#egg=version_subpkg&subdirectory=version_subdir".format(
+ uri=f"git+{version_pkg_path.as_uri()}",
),
)
- result.assert_installed('version_subpkg.py', editable=False)
+ result.assert_installed("version_subpkg.py", editable=False)
@pytest.mark.incompatible_with_test_venv
+@pytest.mark.usefixtures("with_wheel")
def test_wheel_user_with_prefix_in_pydistutils_cfg(
- script, data, with_wheel):
- if os.name == 'posix':
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ if os.name == "posix":
user_filename = ".pydistutils.cfg"
else:
user_filename = "pydistutils.cfg"
- user_cfg = os.path.join(os.path.expanduser('~'), user_filename)
+ user_cfg = os.path.join(os.path.expanduser("~"), user_filename)
script.scratch_path.joinpath("bin").mkdir()
with open(user_cfg, "w") as cfg:
- cfg.write(textwrap.dedent(f"""
+ cfg.write(
+ textwrap.dedent(
+ f"""
[install]
- prefix={script.scratch_path}"""))
+ prefix={script.scratch_path}"""
+ )
+ )
result = script.pip(
- 'install', '--user', '--no-index',
- '-f', data.find_links,
- 'requiresupper')
+ "install", "--user", "--no-index", "-f", data.find_links, "requiresupper"
+ )
# Check that we are really installing a wheel
- assert 'Running setup.py install for requiresupper' not in result.stdout
- assert 'installed requiresupper' in result.stdout
+ assert "Running setup.py install for requiresupper" not in result.stdout
+ assert "installed requiresupper" in result.stdout
def test_install_option_in_requirements_file_overrides_cli(
- script, arg_recording_sdist_maker
-):
+ script: PipTestEnvironment,
+ arg_recording_sdist_maker: Callable[[str], ArgRecordingSdist],
+) -> None:
simple_sdist = arg_recording_sdist_maker("simple")
reqs_file = script.scratch_path.joinpath("reqs.txt")
reqs_file.write_text("simple --install-option='-O0'")
script.pip(
- 'install', '--no-index', '-f', str(simple_sdist.sdist_path.parent),
- '-r', str(reqs_file), '--install-option=-O1',
+ "install",
+ "--no-index",
+ "-f",
+ str(simple_sdist.sdist_path.parent),
+ "-r",
+ str(reqs_file),
+ "--install-option=-O1",
+ allow_stderr_warning=True,
)
simple_args = simple_sdist.args()
- assert 'install' in simple_args
- assert simple_args.index('-O1') < simple_args.index('-O0')
+ assert "install" in simple_args
+ assert simple_args.index("-O1") < simple_args.index("-O0")
-def test_constraints_not_installed_by_default(script, data):
+def test_constraints_not_installed_by_default(
+ script: PipTestEnvironment, data: TestData
+) -> None:
script.scratch_path.joinpath("c.txt").write_text("requiresupper")
result = script.pip(
- 'install', '--no-index', '-f', data.find_links, '-c',
- script.scratch_path / 'c.txt', 'Upper')
- assert 'requiresupper' not in result.stdout
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "-c",
+ script.scratch_path / "c.txt",
+ "Upper",
+ )
+ assert "requiresupper" not in result.stdout
-def test_constraints_only_causes_error(script, data):
+def test_constraints_only_causes_error(
+ script: PipTestEnvironment, data: TestData
+) -> None:
script.scratch_path.joinpath("c.txt").write_text("requiresupper")
result = script.pip(
- 'install', '--no-index', '-f', data.find_links, '-c',
- script.scratch_path / 'c.txt', expect_error=True)
- assert 'installed requiresupper' not in result.stdout
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "-c",
+ script.scratch_path / "c.txt",
+ expect_error=True,
+ )
+ assert "installed requiresupper" not in result.stdout
def test_constraints_local_editable_install_causes_error(
- script,
- data,
- resolver_variant,
-):
- script.scratch_path.joinpath("constraints.txt").write_text(
- "singlemodule==0.0.0"
- )
+ script: PipTestEnvironment,
+ data: TestData,
+ resolver_variant: ResolverVariant,
+) -> None:
+ script.scratch_path.joinpath("constraints.txt").write_text("singlemodule==0.0.0")
to_install = data.src.joinpath("singlemodule")
result = script.pip(
- 'install', '--no-index', '-f', data.find_links, '-c',
- script.scratch_path / 'constraints.txt', '-e',
- to_install, expect_error=True)
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "-c",
+ script.scratch_path / "constraints.txt",
+ "-e",
+ to_install,
+ expect_error=True,
+ )
if resolver_variant == "legacy-resolver":
- assert 'Could not satisfy constraints' in result.stderr, str(result)
+ assert "Could not satisfy constraints" in result.stderr, str(result)
else:
# Because singlemodule only has 0.0.1 available.
- assert 'Cannot install singlemodule 0.0.1' in result.stderr, str(result)
+ assert "Cannot install singlemodule 0.0.1" in result.stderr, str(result)
@pytest.mark.network
-def test_constraints_local_editable_install_pep518(script, data):
+def test_constraints_local_editable_install_pep518(
+ script: PipTestEnvironment, data: TestData
+) -> None:
to_install = data.src.joinpath("pep518-3.0")
- script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
- script.pip(
- 'install', '--no-index', '-f', data.find_links, '-e', to_install)
+ script.pip("download", "setuptools", "wheel", "-d", data.packages)
+ script.pip("install", "--no-index", "-f", data.find_links, "-e", to_install)
def test_constraints_local_install_causes_error(
- script,
- data,
- resolver_variant,
-):
- script.scratch_path.joinpath("constraints.txt").write_text(
- "singlemodule==0.0.0"
- )
+ script: PipTestEnvironment,
+ data: TestData,
+ resolver_variant: ResolverVariant,
+) -> None:
+ script.scratch_path.joinpath("constraints.txt").write_text("singlemodule==0.0.0")
to_install = data.src.joinpath("singlemodule")
result = script.pip(
- 'install', '--no-index', '-f', data.find_links, '-c',
- script.scratch_path / 'constraints.txt',
- to_install, expect_error=True)
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "-c",
+ script.scratch_path / "constraints.txt",
+ to_install,
+ expect_error=True,
+ )
if resolver_variant == "legacy-resolver":
- assert 'Could not satisfy constraints' in result.stderr, str(result)
+ assert "Could not satisfy constraints" in result.stderr, str(result)
else:
# Because singlemodule only has 0.0.1 available.
- assert 'Cannot install singlemodule 0.0.1' in result.stderr, str(result)
+ assert "Cannot install singlemodule 0.0.1" in result.stderr, str(result)
def test_constraints_constrain_to_local_editable(
- script,
- data,
- resolver_variant,
-):
+ script: PipTestEnvironment,
+ data: TestData,
+ resolver_variant: ResolverVariant,
+) -> None:
to_install = data.src.joinpath("singlemodule")
script.scratch_path.joinpath("constraints.txt").write_text(
- "-e {url}#egg=singlemodule".format(url=path_to_url(to_install))
+ f"-e {to_install.as_uri()}#egg=singlemodule"
)
result = script.pip(
- 'install', '--no-index', '-f', data.find_links, '-c',
- script.scratch_path / 'constraints.txt', 'singlemodule',
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "-c",
+ script.scratch_path / "constraints.txt",
+ "singlemodule",
allow_stderr_warning=True,
expect_error=(resolver_variant == "2020-resolver"),
)
if resolver_variant == "2020-resolver":
- assert 'Editable requirements are not allowed as constraints' in result.stderr
+ assert "Editable requirements are not allowed as constraints" in result.stderr
else:
- assert 'Running setup.py develop for singlemodule' in result.stdout
+ assert "Running setup.py develop for singlemodule" in result.stdout
-def test_constraints_constrain_to_local(script, data, resolver_variant):
+def test_constraints_constrain_to_local(
+ script: PipTestEnvironment, data: TestData, resolver_variant: ResolverVariant
+) -> None:
to_install = data.src.joinpath("singlemodule")
script.scratch_path.joinpath("constraints.txt").write_text(
- "{url}#egg=singlemodule".format(url=path_to_url(to_install))
+ f"{to_install.as_uri()}#egg=singlemodule"
)
result = script.pip(
- 'install', '--no-index', '-f', data.find_links, '-c',
- script.scratch_path / 'constraints.txt', 'singlemodule',
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "-c",
+ script.scratch_path / "constraints.txt",
+ "singlemodule",
allow_stderr_warning=True,
)
- assert 'Running setup.py install for singlemodule' in result.stdout
+ assert "Running setup.py install for singlemodule" in result.stdout
-def test_constrained_to_url_install_same_url(script, data):
+def test_constrained_to_url_install_same_url(
+ script: PipTestEnvironment, data: TestData
+) -> None:
to_install = data.src.joinpath("singlemodule")
- constraints = path_to_url(to_install) + "#egg=singlemodule"
+ constraints = f"{to_install.as_uri()}#egg=singlemodule"
script.scratch_path.joinpath("constraints.txt").write_text(constraints)
result = script.pip(
- 'install', '--no-index', '-f', data.find_links, '-c',
- script.scratch_path / 'constraints.txt', to_install,
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "-c",
+ script.scratch_path / "constraints.txt",
+ to_install,
allow_stderr_warning=True,
)
- assert 'Running setup.py install for singlemodule' in result.stdout, str(result)
+ assert "Running setup.py install for singlemodule" in result.stdout, str(result)
+@pytest.mark.usefixtures("with_wheel")
def test_double_install_spurious_hash_mismatch(
- script, tmpdir, data, with_wheel):
+ script: PipTestEnvironment, tmpdir: Path, data: TestData
+) -> None:
"""Make sure installing the same hashed sdist twice doesn't throw hash
mismatch errors.
@@ -447,207 +527,242 @@ def test_double_install_spurious_hash_mismatch(
"""
# Install wheel package, otherwise, it won't try to build wheels.
- with requirements_file('simple==1.0 --hash=sha256:393043e672415891885c9a2a'
- '0929b1af95fb866d6ca016b42d2e6ce53619b653',
- tmpdir) as reqs_file:
+ with requirements_file(
+ "simple==1.0 --hash=sha256:393043e672415891885c9a2a"
+ "0929b1af95fb866d6ca016b42d2e6ce53619b653",
+ tmpdir,
+ ) as reqs_file:
# Install a package (and build its wheel):
result = script.pip_install_local(
- '--find-links', data.find_links,
- '-r', reqs_file.resolve(),
+ "--find-links",
+ data.find_links,
+ "-r",
+ reqs_file.resolve(),
)
- assert 'Successfully installed simple-1.0' in str(result)
+ assert "Successfully installed simple-1.0" in str(result)
# Uninstall it:
- script.pip('uninstall', '-y', 'simple')
+ script.pip("uninstall", "-y", "simple")
# Then install it again. We should not hit a hash mismatch, and the
# package should install happily.
result = script.pip_install_local(
- '--find-links', data.find_links,
- '-r', reqs_file.resolve(),
+ "--find-links",
+ data.find_links,
+ "-r",
+ reqs_file.resolve(),
)
- assert 'Successfully installed simple-1.0' in str(result)
+ assert "Successfully installed simple-1.0" in str(result)
-def test_install_with_extras_from_constraints(script, data, resolver_variant):
+def test_install_with_extras_from_constraints(
+ script: PipTestEnvironment, data: TestData, resolver_variant: ResolverVariant
+) -> None:
to_install = data.packages.joinpath("LocalExtras")
script.scratch_path.joinpath("constraints.txt").write_text(
- "{url}#egg=LocalExtras[bar]".format(url=path_to_url(to_install))
+ f"{to_install.as_uri()}#egg=LocalExtras[bar]"
)
result = script.pip_install_local(
- '-c', script.scratch_path / 'constraints.txt', 'LocalExtras',
+ "-c",
+ script.scratch_path / "constraints.txt",
+ "LocalExtras",
allow_stderr_warning=True,
expect_error=(resolver_variant == "2020-resolver"),
)
if resolver_variant == "2020-resolver":
- assert 'Constraints cannot have extras' in result.stderr
+ assert "Constraints cannot have extras" in result.stderr
else:
- result.did_create(script.site_packages / 'simple')
+ result.did_create(script.site_packages / "simple")
-def test_install_with_extras_from_install(script):
+def test_install_with_extras_from_install(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
name="LocalExtras",
version="0.0.1",
- extras={"bar": "simple", "baz": ["singlemodule"]},
+ extras={"bar": ["simple"], "baz": ["singlemodule"]},
)
script.scratch_path.joinpath("constraints.txt").write_text("LocalExtras")
result = script.pip_install_local(
- '--find-links', script.scratch_path,
- '-c', script.scratch_path / 'constraints.txt',
- 'LocalExtras[baz]',
+ "--find-links",
+ script.scratch_path,
+ "-c",
+ script.scratch_path / "constraints.txt",
+ "LocalExtras[baz]",
)
- result.did_create(script.site_packages / 'singlemodule.py')
+ result.did_create(script.site_packages / "singlemodule.py")
-def test_install_with_extras_joined(script, data, resolver_variant):
+def test_install_with_extras_joined(
+ script: PipTestEnvironment, data: TestData, resolver_variant: ResolverVariant
+) -> None:
to_install = data.packages.joinpath("LocalExtras")
script.scratch_path.joinpath("constraints.txt").write_text(
- "{url}#egg=LocalExtras[bar]".format(url=path_to_url(to_install))
+ f"{to_install.as_uri()}#egg=LocalExtras[bar]"
)
result = script.pip_install_local(
- '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]',
+ "-c",
+ script.scratch_path / "constraints.txt",
+ "LocalExtras[baz]",
allow_stderr_warning=True,
expect_error=(resolver_variant == "2020-resolver"),
)
if resolver_variant == "2020-resolver":
- assert 'Constraints cannot have extras' in result.stderr
+ assert "Constraints cannot have extras" in result.stderr
else:
- result.did_create(script.site_packages / 'simple')
- result.did_create(script.site_packages / 'singlemodule.py')
+ result.did_create(script.site_packages / "simple")
+ result.did_create(script.site_packages / "singlemodule.py")
-def test_install_with_extras_editable_joined(script, data, resolver_variant):
+def test_install_with_extras_editable_joined(
+ script: PipTestEnvironment, data: TestData, resolver_variant: ResolverVariant
+) -> None:
to_install = data.packages.joinpath("LocalExtras")
script.scratch_path.joinpath("constraints.txt").write_text(
- "-e {url}#egg=LocalExtras[bar]".format(url=path_to_url(to_install))
+ f"-e {to_install.as_uri()}#egg=LocalExtras[bar]"
)
result = script.pip_install_local(
- '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]',
+ "-c",
+ script.scratch_path / "constraints.txt",
+ "LocalExtras[baz]",
allow_stderr_warning=True,
expect_error=(resolver_variant == "2020-resolver"),
)
if resolver_variant == "2020-resolver":
- assert 'Editable requirements are not allowed as constraints' in result.stderr
+ assert "Editable requirements are not allowed as constraints" in result.stderr
else:
- result.did_create(script.site_packages / 'simple')
- result.did_create(script.site_packages / 'singlemodule.py')
+ result.did_create(script.site_packages / "simple")
+ result.did_create(script.site_packages / "singlemodule.py")
-def test_install_distribution_full_union(script, data):
+def test_install_distribution_full_union(
+ script: PipTestEnvironment, data: TestData
+) -> None:
to_install = data.packages.joinpath("LocalExtras")
result = script.pip_install_local(
- to_install, to_install + "[bar]", to_install + "[baz]")
- assert 'Running setup.py install for LocalExtras' in result.stdout
- result.did_create(script.site_packages / 'simple')
- result.did_create(script.site_packages / 'singlemodule.py')
+ to_install, f"{to_install}[bar]", f"{to_install}[baz]"
+ )
+ assert "Running setup.py install for LocalExtras" in result.stdout
+ result.did_create(script.site_packages / "simple")
+ result.did_create(script.site_packages / "singlemodule.py")
-def test_install_distribution_duplicate_extras(script, data):
+def test_install_distribution_duplicate_extras(
+ script: PipTestEnvironment, data: TestData
+) -> None:
to_install = data.packages.joinpath("LocalExtras")
- package_name = to_install + "[bar]"
+ package_name = f"{to_install}[bar]"
with pytest.raises(AssertionError):
result = script.pip_install_local(package_name, package_name)
- expected = (f'Double requirement given: {package_name}')
+ expected = f"Double requirement given: {package_name}"
assert expected in result.stderr
def test_install_distribution_union_with_constraints(
- script,
- data,
- resolver_variant,
-):
+ script: PipTestEnvironment,
+ data: TestData,
+ resolver_variant: ResolverVariant,
+) -> None:
to_install = data.packages.joinpath("LocalExtras")
- script.scratch_path.joinpath("constraints.txt").write_text(
- f"{to_install}[bar]")
+ script.scratch_path.joinpath("constraints.txt").write_text(f"{to_install}[bar]")
result = script.pip_install_local(
- '-c', script.scratch_path / 'constraints.txt', to_install + '[baz]',
+ "-c",
+ script.scratch_path / "constraints.txt",
+ f"{to_install}[baz]",
allow_stderr_warning=True,
expect_error=(resolver_variant == "2020-resolver"),
)
if resolver_variant == "2020-resolver":
- msg = 'Unnamed requirements are not allowed as constraints'
+ msg = "Unnamed requirements are not allowed as constraints"
assert msg in result.stderr
else:
- assert 'Running setup.py install for LocalExtras' in result.stdout
- result.did_create(script.site_packages / 'singlemodule.py')
+ assert "Running setup.py install for LocalExtras" in result.stdout
+ result.did_create(script.site_packages / "singlemodule.py")
def test_install_distribution_union_with_versions(
- script,
- data,
- resolver_variant,
-):
+ script: PipTestEnvironment,
+ data: TestData,
+ resolver_variant: ResolverVariant,
+) -> None:
to_install_001 = data.packages.joinpath("LocalExtras")
to_install_002 = data.packages.joinpath("LocalExtras-0.0.2")
result = script.pip_install_local(
- to_install_001 + "[bar]",
- to_install_002 + "[baz]",
+ f"{to_install_001}[bar]",
+ f"{to_install_002}[baz]",
expect_error=(resolver_variant == "2020-resolver"),
)
if resolver_variant == "2020-resolver":
assert "Cannot install localextras[bar]" in result.stderr
- assert (
- "localextras[bar] 0.0.1 depends on localextras 0.0.1"
- ) in result.stdout
- assert (
- "localextras[baz] 0.0.2 depends on localextras 0.0.2"
- ) in result.stdout
+ assert ("localextras[bar] 0.0.1 depends on localextras 0.0.1") in result.stdout
+ assert ("localextras[baz] 0.0.2 depends on localextras 0.0.2") in result.stdout
else:
assert (
- "Successfully installed LocalExtras-0.0.1 simple-3.0 "
- "singlemodule-0.0.1"
+ "Successfully installed LocalExtras-0.0.1 simple-3.0 singlemodule-0.0.1"
) in result.stdout
@pytest.mark.xfail
-def test_install_distribution_union_conflicting_extras(script, data):
+def test_install_distribution_union_conflicting_extras(
+ script: PipTestEnvironment, data: TestData
+) -> None:
# LocalExtras requires simple==1.0, LocalExtras[bar] requires simple==2.0;
# without a resolver, pip does not detect the conflict between simple==1.0
# and simple==2.0. Once a resolver is added, this conflict should be
# detected.
to_install = data.packages.joinpath("LocalExtras-0.0.2")
- result = script.pip_install_local(to_install, to_install + "[bar]",
- expect_error=True)
- assert 'installed' not in result.stdout
+ result = script.pip_install_local(
+ to_install, f"{to_install}[bar]", expect_error=True
+ )
+ assert "installed" not in result.stdout
assert "Conflict" in result.stderr
-def test_install_unsupported_wheel_link_with_marker(script):
+def test_install_unsupported_wheel_link_with_marker(script: PipTestEnvironment) -> None:
script.scratch_path.joinpath("with-marker.txt").write_text(
- textwrap.dedent("""\
+ textwrap.dedent(
+ """\
{url}; {req}
- """).format(
- url='https://github.com/a/b/c/asdf-1.5.2-cp27-none-xyz.whl',
+ """
+ ).format(
+ url="https://github.com/a/b/c/asdf-1.5.2-cp27-none-xyz.whl",
req='sys_platform == "xyz"',
)
)
- result = script.pip(
- 'install', '-r', script.scratch_path / 'with-marker.txt'
- )
+ result = script.pip("install", "-r", script.scratch_path / "with-marker.txt")
- assert ("Ignoring asdf: markers 'sys_platform == \"xyz\"' don't match "
- "your environment") in result.stdout
+ assert (
+ "Ignoring asdf: markers 'sys_platform == \"xyz\"' don't match "
+ "your environment"
+ ) in result.stdout
assert len(result.files_created) == 0
-def test_install_unsupported_wheel_file(script, data):
+def test_install_unsupported_wheel_file(
+ script: PipTestEnvironment, data: TestData
+) -> None:
# Trying to install a local wheel with an incompatible version/type
# should fail.
path = data.packages.joinpath("simple.dist-0.1-py1-none-invalid.whl")
- script.scratch_path.joinpath("wheel-file.txt").write_text(path + '\n')
+ script.scratch_path.joinpath("wheel-file.txt").write_text(f"{path}\n")
result = script.pip(
- 'install', '-r', script.scratch_path / 'wheel-file.txt',
+ "install",
+ "-r",
+ script.scratch_path / "wheel-file.txt",
expect_error=True,
expect_stderr=True,
)
- assert ("simple.dist-0.1-py1-none-invalid.whl is not a supported " +
- "wheel on this platform" in result.stderr)
+ assert (
+ "simple.dist-0.1-py1-none-invalid.whl is not a supported wheel on this platform"
+ in result.stderr
+ )
assert len(result.files_created) == 0
-def test_install_options_local_to_package(script, arg_recording_sdist_maker):
+def test_install_options_local_to_package(
+ script: PipTestEnvironment,
+ arg_recording_sdist_maker: Callable[[str], ArgRecordingSdist],
+) -> None:
"""Make sure --install-options does not leak across packages.
A requirements.txt file can have per-package --install-options; these
@@ -669,27 +784,34 @@ def test_install_options_local_to_package(script, arg_recording_sdist_maker):
)
)
script.pip(
- 'install',
- '--no-index', '-f', str(simple1_sdist.sdist_path.parent),
- '-r', reqs_file,
+ "install",
+ "--no-index",
+ "-f",
+ str(simple1_sdist.sdist_path.parent),
+ "-r",
+ reqs_file,
+ allow_stderr_warning=True,
)
simple1_args = simple1_sdist.args()
- assert 'install' in simple1_args
- assert '-O0' in simple1_args
+ assert "install" in simple1_args
+ assert "-O0" in simple1_args
simple2_args = simple2_sdist.args()
- assert 'install' in simple2_args
- assert '-O0' not in simple2_args
+ assert "install" in simple2_args
+ assert "-O0" not in simple2_args
-def test_location_related_install_option_fails(script):
+def test_location_related_install_option_fails(script: PipTestEnvironment) -> None:
simple_sdist = create_basic_sdist_for_package(script, "simple", "0.1.0")
reqs_file = script.scratch_path.joinpath("reqs.txt")
reqs_file.write_text("simple --install-option='--home=/tmp'")
result = script.pip(
- 'install',
- '--no-index', '-f', str(simple_sdist.parent),
- '-r', reqs_file,
- expect_error=True
+ "install",
+ "--no-index",
+ "-f",
+ str(simple_sdist.parent),
+ "-r",
+ reqs_file,
+ expect_error=True,
)
assert "['--home'] from simple" in result.stderr
diff --git a/tests/functional/test_install_requested.py b/tests/functional/test_install_requested.py
index 6caec988e..edc289f43 100644
--- a/tests/functional/test_install_requested.py
+++ b/tests/functional/test_install_requested.py
@@ -1,18 +1,28 @@
-def _assert_requested_present(script, result, name, version):
- dist_info = script.site_packages / name + "-" + version + ".dist-info"
+import pytest
+
+from tests.lib import PipTestEnvironment, TestData, TestPipResult
+
+
+def _assert_requested_present(
+ script: PipTestEnvironment, result: TestPipResult, name: str, version: str
+) -> None:
+ dist_info = script.site_packages / f"{name}-{version}.dist-info"
requested = dist_info / "REQUESTED"
assert dist_info in result.files_created
assert requested in result.files_created
-def _assert_requested_absent(script, result, name, version):
- dist_info = script.site_packages / name + "-" + version + ".dist-info"
+def _assert_requested_absent(
+ script: PipTestEnvironment, result: TestPipResult, name: str, version: str
+) -> None:
+ dist_info = script.site_packages / f"{name}-{version}.dist-info"
requested = dist_info / "REQUESTED"
assert dist_info in result.files_created
assert requested not in result.files_created
-def test_install_requested_basic(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_requested_basic(script: PipTestEnvironment, data: TestData) -> None:
result = script.pip(
"install", "--no-index", "-f", data.find_links, "require_simple"
)
@@ -21,10 +31,11 @@ def test_install_requested_basic(script, data, with_wheel):
_assert_requested_absent(script, result, "simple", "3.0")
-def test_install_requested_requirements(script, data, with_wheel):
- script.scratch_path.joinpath("requirements.txt").write_text(
- "require_simple\n"
- )
+@pytest.mark.usefixtures("with_wheel")
+def test_install_requested_requirements(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ script.scratch_path.joinpath("requirements.txt").write_text("require_simple\n")
result = script.pip(
"install",
"--no-index",
@@ -37,7 +48,10 @@ def test_install_requested_requirements(script, data, with_wheel):
_assert_requested_absent(script, result, "simple", "3.0")
-def test_install_requested_dep_in_requirements(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_requested_dep_in_requirements(
+ script: PipTestEnvironment, data: TestData
+) -> None:
script.scratch_path.joinpath("requirements.txt").write_text(
"require_simple\nsimple<3\n"
)
@@ -54,10 +68,11 @@ def test_install_requested_dep_in_requirements(script, data, with_wheel):
_assert_requested_present(script, result, "simple", "2.0")
-def test_install_requested_reqs_and_constraints(script, data, with_wheel):
- script.scratch_path.joinpath("requirements.txt").write_text(
- "require_simple\n"
- )
+@pytest.mark.usefixtures("with_wheel")
+def test_install_requested_reqs_and_constraints(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ script.scratch_path.joinpath("requirements.txt").write_text("require_simple\n")
script.scratch_path.joinpath("constraints.txt").write_text("simple<3\n")
result = script.pip(
"install",
@@ -74,7 +89,10 @@ def test_install_requested_reqs_and_constraints(script, data, with_wheel):
_assert_requested_absent(script, result, "simple", "2.0")
-def test_install_requested_in_reqs_and_constraints(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_requested_in_reqs_and_constraints(
+ script: PipTestEnvironment, data: TestData
+) -> None:
script.scratch_path.joinpath("requirements.txt").write_text(
"require_simple\nsimple\n"
)
@@ -92,3 +110,40 @@ def test_install_requested_in_reqs_and_constraints(script, data, with_wheel):
_assert_requested_present(script, result, "require_simple", "1.0")
# simple must have REQUESTED because it is in requirements.txt
_assert_requested_present(script, result, "simple", "2.0")
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_install_requested_from_cli_with_constraint(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ script.scratch_path.joinpath("constraints.txt").write_text("simple<3\n")
+ result = script.pip(
+ "install",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "-c",
+ script.scratch_path / "constraints.txt",
+ "simple",
+ )
+ # simple must have REQUESTED because it was provided on the command line
+ _assert_requested_present(script, result, "simple", "2.0")
+
+
+@pytest.mark.usefixtures("with_wheel")
+@pytest.mark.network
+def test_install_requested_from_cli_with_url_constraint(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ script.scratch_path.joinpath("constraints.txt").write_text(
+ "pip-test-package @ git+https://github.com/pypa/pip-test-package@0.1.1\n"
+ )
+ result = script.pip(
+ "install",
+ "--no-index",
+ "-c",
+ script.scratch_path / "constraints.txt",
+ "pip-test-package",
+ )
+ # pip-test-package must have REQUESTED because it was provided on the command line
+ _assert_requested_present(script, result, "pip_test_package", "0.1.1")
diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py
index d7586cd58..0da195c05 100644
--- a/tests/functional/test_install_upgrade.py
+++ b/tests/functional/test_install_upgrade.py
@@ -2,64 +2,61 @@ import itertools
import os
import sys
import textwrap
+from pathlib import Path
import pytest
from tests.lib import pyversion # noqa: F401
-from tests.lib import assert_all_changes
+from tests.lib import PipTestEnvironment, ResolverVariant, TestData, assert_all_changes
from tests.lib.local_repos import local_checkout
from tests.lib.wheel import make_wheel
@pytest.mark.network
-def test_no_upgrade_unless_requested(script):
+def test_no_upgrade_unless_requested(script: PipTestEnvironment) -> None:
"""
No upgrade if not specifically requested.
"""
- script.pip('install', 'INITools==0.1')
- result = script.pip('install', 'INITools')
- assert not result.files_created, (
- 'pip install INITools upgraded when it should not have'
- )
+ script.pip("install", "INITools==0.1")
+ result = script.pip("install", "INITools")
+ assert (
+ not result.files_created
+ ), "pip install INITools upgraded when it should not have"
-def test_invalid_upgrade_strategy_causes_error(script):
+def test_invalid_upgrade_strategy_causes_error(script: PipTestEnvironment) -> None:
"""
It errors out when the upgrade-strategy is an invalid/unrecognised one
"""
result = script.pip_install_local(
- '--upgrade', '--upgrade-strategy=bazinga', 'simple',
- expect_error=True
+ "--upgrade", "--upgrade-strategy=bazinga", "simple", expect_error=True
)
assert result.returncode
assert "invalid choice" in result.stderr
+@pytest.mark.usefixtures("with_wheel")
def test_only_if_needed_does_not_upgrade_deps_when_satisfied(
- script,
- resolver_variant,
- with_wheel
-):
+ script: PipTestEnvironment, resolver_variant: ResolverVariant
+) -> None:
"""
It doesn't upgrade a dependency if it already satisfies the requirements.
"""
- script.pip_install_local('simple==2.0')
+ script.pip_install_local("simple==2.0")
result = script.pip_install_local(
- '--upgrade', '--upgrade-strategy=only-if-needed', 'require_simple'
+ "--upgrade", "--upgrade-strategy=only-if-needed", "require_simple"
)
assert (
- (script.site_packages / 'require_simple-1.0.dist-info')
- not in result.files_deleted
- ), "should have installed require_simple==1.0"
+ script.site_packages / "require_simple-1.0.dist-info"
+ ) not in result.files_deleted, "should have installed require_simple==1.0"
assert (
- (script.site_packages / 'simple-2.0.dist-info')
- not in result.files_deleted
- ), "should not have uninstalled simple==2.0"
+ script.site_packages / "simple-2.0.dist-info"
+ ) not in result.files_deleted, "should not have uninstalled simple==2.0"
msg = "Requirement already satisfied"
if resolver_variant == "legacy":
@@ -69,131 +66,113 @@ def test_only_if_needed_does_not_upgrade_deps_when_satisfied(
), "did not print correct message for not-upgraded requirement"
+@pytest.mark.usefixtures("with_wheel")
def test_only_if_needed_does_upgrade_deps_when_no_longer_satisfied(
- script, with_wheel
-):
+ script: PipTestEnvironment,
+) -> None:
"""
It does upgrade a dependency if it no longer satisfies the requirements.
"""
- script.pip_install_local('simple==1.0')
+ script.pip_install_local("simple==1.0")
result = script.pip_install_local(
- '--upgrade', '--upgrade-strategy=only-if-needed', 'require_simple'
+ "--upgrade", "--upgrade-strategy=only-if-needed", "require_simple"
)
assert (
- (script.site_packages / 'require_simple-1.0.dist-info')
- not in result.files_deleted
- ), "should have installed require_simple==1.0"
- expected = (
- script.site_packages /
- 'simple-3.0.dist-info'
- )
+ script.site_packages / "require_simple-1.0.dist-info"
+ ) not in result.files_deleted, "should have installed require_simple==1.0"
+ expected = script.site_packages / "simple-3.0.dist-info"
result.did_create(expected, message="should have installed simple==3.0")
- expected = (
- script.site_packages /
- 'simple-1.0.dist-info'
- )
- assert (
- expected in result.files_deleted
- ), "should have uninstalled simple==1.0"
+ expected = script.site_packages / "simple-1.0.dist-info"
+ assert expected in result.files_deleted, "should have uninstalled simple==1.0"
-def test_eager_does_upgrade_dependecies_when_currently_satisfied(
- script, with_wheel
-):
+@pytest.mark.usefixtures("with_wheel")
+def test_eager_does_upgrade_dependencies_when_currently_satisfied(
+ script: PipTestEnvironment,
+) -> None:
"""
It does upgrade a dependency even if it already satisfies the requirements.
"""
- script.pip_install_local('simple==2.0')
+ script.pip_install_local("simple==2.0")
result = script.pip_install_local(
- '--upgrade', '--upgrade-strategy=eager', 'require_simple'
+ "--upgrade", "--upgrade-strategy=eager", "require_simple"
)
assert (
- (script.site_packages /
- 'require_simple-1.0.dist-info')
- not in result.files_deleted
- ), "should have installed require_simple==1.0"
+ script.site_packages / "require_simple-1.0.dist-info"
+ ) not in result.files_deleted, "should have installed require_simple==1.0"
assert (
- (script.site_packages /
- 'simple-2.0.dist-info')
- in result.files_deleted
- ), "should have uninstalled simple==2.0"
+ script.site_packages / "simple-2.0.dist-info"
+ ) in result.files_deleted, "should have uninstalled simple==2.0"
-def test_eager_does_upgrade_dependecies_when_no_longer_satisfied(
- script, with_wheel
-):
+@pytest.mark.usefixtures("with_wheel")
+def test_eager_does_upgrade_dependencies_when_no_longer_satisfied(
+ script: PipTestEnvironment,
+) -> None:
"""
It does upgrade a dependency if it no longer satisfies the requirements.
"""
- script.pip_install_local('simple==1.0')
+ script.pip_install_local("simple==1.0")
result = script.pip_install_local(
- '--upgrade', '--upgrade-strategy=eager', 'require_simple'
+ "--upgrade", "--upgrade-strategy=eager", "require_simple"
)
assert (
- (script.site_packages / 'require_simple-1.0.dist-info')
- not in result.files_deleted
- ), "should have installed require_simple==1.0"
+ script.site_packages / "require_simple-1.0.dist-info"
+ ) not in result.files_deleted, "should have installed require_simple==1.0"
result.did_create(
- script.site_packages / 'simple-3.0.dist-info',
- message="should have installed simple==3.0"
+ script.site_packages / "simple-3.0.dist-info",
+ message="should have installed simple==3.0",
)
assert (
- script.site_packages / 'simple-1.0.dist-info'
- in result.files_deleted
+ script.site_packages / "simple-1.0.dist-info" in result.files_deleted
), "should have uninstalled simple==1.0"
@pytest.mark.network
-def test_upgrade_to_specific_version(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_upgrade_to_specific_version(script: PipTestEnvironment) -> None:
"""
It does upgrade to specific version requested.
"""
- script.pip('install', 'INITools==0.1')
- result = script.pip('install', 'INITools==0.2')
- assert result.files_created, (
- 'pip install with specific version did not upgrade'
- )
- assert (
- script.site_packages / 'INITools-0.1.dist-info'
- in result.files_deleted
- )
- result.did_create(
- script.site_packages / 'INITools-0.2.dist-info'
- )
+ script.pip("install", "INITools==0.1")
+ result = script.pip("install", "INITools==0.2")
+ assert result.files_created, "pip install with specific version did not upgrade"
+ assert script.site_packages / "INITools-0.1.dist-info" in result.files_deleted
+ result.did_create(script.site_packages / "INITools-0.2.dist-info")
@pytest.mark.network
-def test_upgrade_if_requested(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_upgrade_if_requested(script: PipTestEnvironment) -> None:
"""
And it does upgrade if requested.
"""
- script.pip('install', 'INITools==0.1')
- result = script.pip('install', '--upgrade', 'INITools')
- assert result.files_created, 'pip install --upgrade did not upgrade'
- result.did_not_create(
- script.site_packages /
- 'INITools-0.1.dist-info'
- )
+ script.pip("install", "INITools==0.1")
+ result = script.pip("install", "--upgrade", "INITools")
+ assert result.files_created, "pip install --upgrade did not upgrade"
+ result.did_not_create(script.site_packages / "INITools-0.1.dist-info")
-def test_upgrade_with_newest_already_installed(script, data, resolver_variant):
+def test_upgrade_with_newest_already_installed(
+ script: PipTestEnvironment, data: TestData, resolver_variant: ResolverVariant
+) -> None:
"""
If the newest version of a package is already installed, the package should
not be reinstalled and the user should be informed.
"""
- script.pip('install', '-f', data.find_links, '--no-index', 'simple')
+ script.pip("install", "-f", data.find_links, "--no-index", "simple")
result = script.pip(
- 'install', '--upgrade', '-f', data.find_links, '--no-index', 'simple'
+ "install", "--upgrade", "-f", data.find_links, "--no-index", "simple"
)
- assert not result.files_created, 'simple upgraded when it should not have'
+ assert not result.files_created, "simple upgraded when it should not have"
if resolver_variant == "2020-resolver":
msg = "Requirement already satisfied"
else:
@@ -202,175 +181,173 @@ def test_upgrade_with_newest_already_installed(script, data, resolver_variant):
@pytest.mark.network
-def test_upgrade_force_reinstall_newest(script):
+def test_upgrade_force_reinstall_newest(script: PipTestEnvironment) -> None:
"""
Force reinstallation of a package even if it is already at its newest
version if --force-reinstall is supplied.
"""
- result = script.pip('install', 'INITools')
- result.did_create(script.site_packages / 'initools')
- result2 = script.pip(
- 'install', '--upgrade', '--force-reinstall', 'INITools'
- )
- assert result2.files_updated, 'upgrade to INITools 0.3 failed'
- result3 = script.pip('uninstall', 'initools', '-y')
- assert_all_changes(result, result3, [script.venv / 'build', 'cache'])
+ result = script.pip("install", "INITools")
+ result.did_create(script.site_packages / "initools")
+ result2 = script.pip("install", "--upgrade", "--force-reinstall", "INITools")
+ assert result2.files_updated, "upgrade to INITools 0.3 failed"
+ result3 = script.pip("uninstall", "initools", "-y")
+ assert_all_changes(result, result3, [script.venv / "build", "cache"])
@pytest.mark.network
-def test_uninstall_before_upgrade(script):
+def test_uninstall_before_upgrade(script: PipTestEnvironment) -> None:
"""
Automatic uninstall-before-upgrade.
"""
- result = script.pip('install', 'INITools==0.2')
- result.did_create(script.site_packages / 'initools')
- result2 = script.pip('install', 'INITools==0.3')
- assert result2.files_created, 'upgrade to INITools 0.3 failed'
- result3 = script.pip('uninstall', 'initools', '-y')
- assert_all_changes(result, result3, [script.venv / 'build', 'cache'])
+ result = script.pip("install", "INITools==0.2")
+ result.did_create(script.site_packages / "initools")
+ result2 = script.pip("install", "INITools==0.3")
+ assert result2.files_created, "upgrade to INITools 0.3 failed"
+ result3 = script.pip("uninstall", "initools", "-y")
+ assert_all_changes(result, result3, [script.venv / "build", "cache"])
@pytest.mark.network
-def test_uninstall_before_upgrade_from_url(script):
+def test_uninstall_before_upgrade_from_url(script: PipTestEnvironment) -> None:
"""
Automatic uninstall-before-upgrade from URL.
"""
- result = script.pip('install', 'INITools==0.2')
- result.did_create(script.site_packages / 'initools')
+ result = script.pip("install", "INITools==0.2")
+ result.did_create(script.site_packages / "initools")
result2 = script.pip(
- 'install',
- 'https://files.pythonhosted.org/packages/source/I/INITools/INITools-'
- '0.3.tar.gz',
+ "install",
+ "https://files.pythonhosted.org/packages/source/I/INITools/INITools-"
+ "0.3.tar.gz",
)
- assert result2.files_created, 'upgrade to INITools 0.3 failed'
- result3 = script.pip('uninstall', 'initools', '-y')
- assert_all_changes(result, result3, [script.venv / 'build', 'cache'])
+ assert result2.files_created, "upgrade to INITools 0.3 failed"
+ result3 = script.pip("uninstall", "initools", "-y")
+ assert_all_changes(result, result3, [script.venv / "build", "cache"])
@pytest.mark.network
-def test_upgrade_to_same_version_from_url(script):
+def test_upgrade_to_same_version_from_url(script: PipTestEnvironment) -> None:
"""
When installing from a URL the same version that is already installed, no
need to uninstall and reinstall if --upgrade is not specified.
"""
- result = script.pip('install', 'INITools==0.3')
- result.did_create(script.site_packages / 'initools')
+ result = script.pip("install", "INITools==0.3")
+ result.did_create(script.site_packages / "initools")
result2 = script.pip(
- 'install',
- 'https://files.pythonhosted.org/packages/source/I/INITools/INITools-'
- '0.3.tar.gz',
- )
- assert script.site_packages / 'initools' not in result2.files_updated, (
- 'INITools 0.3 reinstalled same version'
+ "install",
+ "https://files.pythonhosted.org/packages/source/I/INITools/INITools-"
+ "0.3.tar.gz",
)
- result3 = script.pip('uninstall', 'initools', '-y')
- assert_all_changes(result, result3, [script.venv / 'build', 'cache'])
+ assert (
+ script.site_packages / "initools" not in result2.files_updated
+ ), "INITools 0.3 reinstalled same version"
+ result3 = script.pip("uninstall", "initools", "-y")
+ assert_all_changes(result, result3, [script.venv / "build", "cache"])
@pytest.mark.network
-def test_upgrade_from_reqs_file(script):
+def test_upgrade_from_reqs_file(script: PipTestEnvironment) -> None:
"""
Upgrade from a requirements file.
"""
- script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""\
+ script.scratch_path.joinpath("test-req.txt").write_text(
+ textwrap.dedent(
+ """\
PyLogo<0.4
# and something else to test out:
INITools==0.3
- """))
- install_result = script.pip(
- 'install', '-r', script.scratch_path / 'test-req.txt'
+ """
+ )
)
- script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent("""\
+ install_result = script.pip("install", "-r", script.scratch_path / "test-req.txt")
+ script.scratch_path.joinpath("test-req.txt").write_text(
+ textwrap.dedent(
+ """\
PyLogo
# and something else to test out:
INITools
- """))
- script.pip(
- 'install', '--upgrade', '-r', script.scratch_path / 'test-req.txt'
+ """
+ )
)
+ script.pip("install", "--upgrade", "-r", script.scratch_path / "test-req.txt")
uninstall_result = script.pip(
- 'uninstall', '-r', script.scratch_path / 'test-req.txt', '-y'
+ "uninstall", "-r", script.scratch_path / "test-req.txt", "-y"
)
assert_all_changes(
install_result,
uninstall_result,
- [script.venv / 'build', 'cache', script.scratch / 'test-req.txt'],
+ [script.venv / "build", "cache", script.scratch / "test-req.txt"],
)
-def test_uninstall_rollback(script, data):
+def test_uninstall_rollback(script: PipTestEnvironment, data: TestData) -> None:
"""
Test uninstall-rollback (using test package with a setup.py
crafted to fail on install).
"""
- result = script.pip(
- 'install', '-f', data.find_links, '--no-index', 'broken==0.1'
- )
- result.did_create(script.site_packages / 'broken.py')
+ result = script.pip("install", "-f", data.find_links, "--no-index", "broken==0.1")
+ result.did_create(script.site_packages / "broken.py")
result2 = script.pip(
- 'install', '-f', data.find_links, '--no-index', 'broken===0.2broken',
+ "install",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "broken===0.2broken",
expect_error=True,
)
assert result2.returncode == 1, str(result2)
- assert script.run(
- 'python', '-c', "import broken; print(broken.VERSION)"
- ).stdout == '0.1\n'
+ assert (
+ script.run("python", "-c", "import broken; print(broken.VERSION)").stdout
+ == "0.1\n"
+ )
assert_all_changes(
result.files_after,
result2,
- [script.venv / 'build'],
+ [script.venv / "build"],
)
@pytest.mark.network
-def test_should_not_install_always_from_cache(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_should_not_install_always_from_cache(script: PipTestEnvironment) -> None:
"""
If there is an old cached package, pip should download the newer version
Related to issue #175
"""
- script.pip('install', 'INITools==0.2')
- script.pip('uninstall', '-y', 'INITools')
- result = script.pip('install', 'INITools==0.1')
- result.did_not_create(
- script.site_packages /
- 'INITools-0.2.dist-info'
- )
- result.did_create(
- script.site_packages /
- 'INITools-0.1.dist-info'
- )
+ script.pip("install", "INITools==0.2")
+ script.pip("uninstall", "-y", "INITools")
+ result = script.pip("install", "INITools==0.1")
+ result.did_not_create(script.site_packages / "INITools-0.2.dist-info")
+ result.did_create(script.site_packages / "INITools-0.1.dist-info")
@pytest.mark.network
-def test_install_with_ignoreinstalled_requested(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_with_ignoreinstalled_requested(script: PipTestEnvironment) -> None:
"""
Test old conflicting package is completely ignored
"""
- script.pip('install', 'INITools==0.1')
- result = script.pip('install', '-I', 'INITools==0.3')
- assert result.files_created, 'pip install -I did not install'
+ script.pip("install", "INITools==0.1")
+ result = script.pip("install", "-I", "INITools==0.3")
+ assert result.files_created, "pip install -I did not install"
# both the old and new metadata should be present.
- assert os.path.exists(
- script.site_packages_path /
- 'INITools-0.1.dist-info'
- )
- assert os.path.exists(
- script.site_packages_path /
- 'INITools-0.3.dist-info'
- )
+ assert os.path.exists(script.site_packages_path / "INITools-0.1.dist-info")
+ assert os.path.exists(script.site_packages_path / "INITools-0.3.dist-info")
@pytest.mark.network
-def test_upgrade_vcs_req_with_no_dists_found(script, tmpdir):
+def test_upgrade_vcs_req_with_no_dists_found(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""It can upgrade a VCS requirement that has no distributions otherwise."""
req = "{checkout}#egg=pip-test-package".format(
checkout=local_checkout(
- "git+https://github.com/pypa/pip-test-package.git", tmpdir,
+ "git+https://github.com/pypa/pip-test-package.git",
+ tmpdir,
)
)
script.pip("install", req)
@@ -379,73 +356,33 @@ def test_upgrade_vcs_req_with_no_dists_found(script, tmpdir):
@pytest.mark.network
-def test_upgrade_vcs_req_with_dist_found(script):
+def test_upgrade_vcs_req_with_dist_found(script: PipTestEnvironment) -> None:
"""It can upgrade a VCS requirement that has distributions on the index."""
# TODO(pnasrat) Using local_checkout fails on windows - oddness with the
# test path urls/git.
- req = (
- "{url}#egg=pretend".format(
- url=(
- "git+git://github.com/alex/pretend@e7f26ad7dbcb4a02a4995aade4"
- "743aad47656b27"
- ),
- )
+ req = "{url}#egg=pretend".format(
+ url=(
+ "git+https://github.com/alex/pretend@e7f26ad7dbcb4a02a4995aade4"
+ "743aad47656b27"
+ ),
)
script.pip("install", req, expect_stderr=True)
result = script.pip("install", "-U", req, expect_stderr=True)
assert "pypi.org" not in result.stdout, result.stdout
-class TestUpgradeDistributeToSetuptools:
- """
- From pip1.4 to pip6, pip supported a set of "hacks" (see Issue #1122) to
- allow distribute to conflict with setuptools, so that the following would
- work to upgrade distribute:
-
- ``pip install -U setuptools``
-
- In pip7, the hacks were removed. This test remains to at least confirm pip
- can upgrade distribute to setuptools using:
-
- ``pip install -U distribute``
-
- The reason this works is that a final version of distribute (v0.7.3) was
- released that is simple wrapper with:
-
- install_requires=['setuptools>=0.7']
-
- The test use a fixed set of packages from our test packages dir. Note that
- virtualenv-1.9.1 contains distribute-0.6.34 and virtualenv-1.10 contains
- setuptools-0.9.7
- """
-
- def prep_ve(self, script, version, pip_src, distribute=False):
- self.script = script
- self.script.pip_install_local(f'virtualenv=={version}')
- args = ['virtualenv', self.script.scratch_path / 'VE']
- if distribute:
- args.insert(1, '--distribute')
- if version == "1.9.1" and not distribute:
- # setuptools 0.6 didn't support PYTHONDONTWRITEBYTECODE
- del self.script.environ["PYTHONDONTWRITEBYTECODE"]
- self.script.run(*args)
- if sys.platform == 'win32':
- bindir = "Scripts"
- else:
- bindir = "bin"
- self.ve_bin = self.script.scratch_path / 'VE' / bindir
- self.script.run(self.ve_bin / 'pip', 'uninstall', '-y', 'pip')
- self.script.run(
- self.ve_bin / 'python', 'setup.py', 'install',
- cwd=pip_src,
- expect_stderr=True,
+@pytest.mark.parametrize(
+ "req1, req2",
+ list(
+ itertools.product(
+ ["foo.bar", "foo_bar", "foo-bar"],
+ ["foo.bar", "foo_bar", "foo-bar"],
)
-
-
-@pytest.mark.parametrize("req1, req2", list(itertools.product(
- ["foo.bar", "foo_bar", "foo-bar"], ["foo.bar", "foo_bar", "foo-bar"],
-)))
-def test_install_find_existing_package_canonicalize(script, req1, req2):
+ ),
+)
+def test_install_find_existing_package_canonicalize(
+ script: PipTestEnvironment, req1: str, req2: str
+) -> None:
"""Ensure an already-installed dist is found no matter how the dist name
was normalized on installation. (pypa/pip#8645)
"""
@@ -467,7 +404,22 @@ def test_install_find_existing_package_canonicalize(script, req1, req2):
# Ensure the previously installed package can be correctly used to match
# the dependency.
result = script.pip(
- "install", "--no-index", "--find-links", pkg_container, "pkg",
+ "install",
+ "--no-index",
+ "--find-links",
+ pkg_container,
+ "pkg",
)
satisfied_message = f"Requirement already satisfied: {req2}"
assert satisfied_message in result.stdout, str(result)
+
+
+@pytest.mark.network
+@pytest.mark.skipif(sys.platform != "win32", reason="Windows-only test")
+def test_modifying_pip_presents_error(script: PipTestEnvironment) -> None:
+ result = script.pip(
+ "install", "pip", "--force-reinstall", use_module=False, expect_error=True
+ )
+
+ assert "python.exe" in result.stderr or "python.EXE" in result.stderr, str(result)
+ assert " -m " in result.stderr, str(result)
diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py
index 7ef5d540f..d0bdbc3a5 100644
--- a/tests/functional/test_install_user.py
+++ b/tests/functional/test_install_user.py
@@ -3,89 +3,100 @@ tests specific to "pip install --user"
"""
import textwrap
from os.path import curdir, isdir, isfile
+from pathlib import Path
import pytest
from tests.lib import pyversion # noqa: F401
-from tests.lib import need_svn
+from tests.lib import PipTestEnvironment, TestData, need_svn
from tests.lib.local_repos import local_checkout
+from tests.lib.venv import VirtualEnvironment
-def _patch_dist_in_site_packages(virtualenv):
+def _patch_dist_in_site_packages(virtualenv: VirtualEnvironment) -> None:
# Since the tests are run from a virtualenv, and to avoid the "Will not
# install to the usersite because it will lack sys.path precedence..."
- # error: Monkey patch `pip._internal.req.req_install.dist_in_site_packages`
- # and `pip._internal.utils.misc.dist_in_site_packages`
- # so it's possible to install a conflicting distribution in the user site.
- virtualenv.sitecustomize = textwrap.dedent("""
+ # error: Monkey patch the Distribution class so it's possible to install a
+ # conflicting distribution in the user site.
+ virtualenv.sitecustomize = textwrap.dedent(
+ """
def dist_in_site_packages(dist):
return False
- from pip._internal.req import req_install
- from pip._internal.utils import misc
- req_install.dist_in_site_packages = dist_in_site_packages
- misc.dist_in_site_packages = dist_in_site_packages
- """)
+ from pip._internal.metadata.base import BaseDistribution
+ BaseDistribution.in_site_packages = property(dist_in_site_packages)
+ """
+ )
class Tests_UserSite:
-
@pytest.mark.network
@pytest.mark.incompatible_with_test_venv
- def test_reset_env_system_site_packages_usersite(self, script):
+ def test_reset_env_system_site_packages_usersite(
+ self, script: PipTestEnvironment
+ ) -> None:
"""
Check user site works as expected.
"""
- script.pip('install', '--user', 'INITools==0.2')
+ script.pip("install", "--user", "INITools==0.2")
result = script.run(
- 'python', '-c',
+ "python",
+ "-c",
"import pkg_resources; print(pkg_resources.get_distribution"
"('initools').project_name)",
)
project_name = result.stdout.strip()
- assert 'INITools' == project_name, project_name
+ assert "INITools" == project_name, project_name
@pytest.mark.xfail
@pytest.mark.network
@need_svn
@pytest.mark.incompatible_with_test_venv
def test_install_subversion_usersite_editable_with_distribute(
- self, script, tmpdir):
+ self, script: PipTestEnvironment, tmpdir: Path
+ ) -> None:
"""
Test installing current directory ('.') into usersite after installing
distribute
"""
result = script.pip(
- 'install', '--user', '-e',
- '{checkout}#egg=initools'.format(
+ "install",
+ "--user",
+ "-e",
+ "{checkout}#egg=initools".format(
checkout=local_checkout(
- 'svn+http://svn.colorstudy.com/INITools', tmpdir)
- )
+ "svn+http://svn.colorstudy.com/INITools", tmpdir
+ )
+ ),
)
- result.assert_installed('INITools', use_user_site=True)
+ result.assert_installed("INITools", use_user_site=True)
@pytest.mark.incompatible_with_test_venv
+ @pytest.mark.usefixtures("with_wheel")
def test_install_from_current_directory_into_usersite(
- self, script, data, with_wheel):
+ self, script: PipTestEnvironment, data: TestData
+ ) -> None:
"""
Test installing current directory ('.') into usersite
"""
run_from = data.packages.joinpath("FSPkg")
result = script.pip(
- 'install', '-vvv', '--user', curdir,
+ "install",
+ "-vvv",
+ "--user",
+ curdir,
cwd=run_from,
)
- fspkg_folder = script.user_site / 'fspkg'
+ fspkg_folder = script.user_site / "fspkg"
result.did_create(fspkg_folder)
- dist_info_folder = (
- script.user_site / 'FSPkg-0.1.dev0.dist-info'
- )
+ dist_info_folder = script.user_site / "FSPkg-0.1.dev0.dist-info"
result.did_create(dist_info_folder)
- def test_install_user_venv_nositepkgs_fails(self, virtualenv,
- script, data):
+ def test_install_user_venv_nositepkgs_fails(
+ self, virtualenv: VirtualEnvironment, script: PipTestEnvironment, data: TestData
+ ) -> None:
"""
user install in virtualenv (with no system packages) fails with message
"""
@@ -94,7 +105,9 @@ class Tests_UserSite:
virtualenv.user_site_packages = False
run_from = data.packages.joinpath("FSPkg")
result = script.pip(
- 'install', '--user', curdir,
+ "install",
+ "--user",
+ curdir,
cwd=run_from,
expect_error=True,
)
@@ -105,147 +118,155 @@ class Tests_UserSite:
@pytest.mark.network
@pytest.mark.incompatible_with_test_venv
- def test_install_user_conflict_in_usersite(self, script):
+ def test_install_user_conflict_in_usersite(
+ self, script: PipTestEnvironment
+ ) -> None:
"""
Test user install with conflict in usersite updates usersite.
"""
- script.pip('install', '--user', 'INITools==0.3', '--no-binary=:all:')
+ script.pip("install", "--user", "INITools==0.3", "--no-binary=:all:")
- result2 = script.pip(
- 'install', '--user', 'INITools==0.1', '--no-binary=:all:')
+ result2 = script.pip("install", "--user", "INITools==0.1", "--no-binary=:all:")
# usersite has 0.1
# we still test for egg-info because no-binary implies setup.py install
- egg_info_folder = (
- script.user_site / f'INITools-0.1-py{pyversion}.egg-info'
- )
+ egg_info_folder = script.user_site / f"INITools-0.1-py{pyversion}.egg-info"
initools_v3_file = (
# file only in 0.3
- script.base_path / script.user_site / 'initools' /
- 'configparser.py'
+ script.base_path
+ / script.user_site
+ / "initools"
+ / "configparser.py"
)
result2.did_create(egg_info_folder)
assert not isfile(initools_v3_file), initools_v3_file
@pytest.mark.network
@pytest.mark.incompatible_with_test_venv
- def test_install_user_conflict_in_globalsite(self, virtualenv, script):
+ def test_install_user_conflict_in_globalsite(
+ self, virtualenv: VirtualEnvironment, script: PipTestEnvironment
+ ) -> None:
"""
Test user install with conflict in global site ignores site and
installs to usersite
"""
_patch_dist_in_site_packages(virtualenv)
- script.pip('install', 'INITools==0.2', '--no-binary=:all:')
+ script.pip("install", "INITools==0.2", "--no-binary=:all:")
- result2 = script.pip(
- 'install', '--user', 'INITools==0.1', '--no-binary=:all:')
+ result2 = script.pip("install", "--user", "INITools==0.1", "--no-binary=:all:")
# usersite has 0.1
# we still test for egg-info because no-binary implies setup.py install
- egg_info_folder = (
- script.user_site / f'INITools-0.1-py{pyversion}.egg-info'
- )
- initools_folder = script.user_site / 'initools'
+ egg_info_folder = script.user_site / f"INITools-0.1-py{pyversion}.egg-info"
+ initools_folder = script.user_site / "initools"
result2.did_create(egg_info_folder)
result2.did_create(initools_folder)
# site still has 0.2 (can't look in result1; have to check)
egg_info_folder = (
- script.base_path / script.site_packages /
- f'INITools-0.2-py{pyversion}.egg-info'
+ script.base_path
+ / script.site_packages
+ / f"INITools-0.2-py{pyversion}.egg-info"
)
- initools_folder = script.base_path / script.site_packages / 'initools'
+ initools_folder = script.base_path / script.site_packages / "initools"
assert isdir(egg_info_folder)
assert isdir(initools_folder)
@pytest.mark.network
@pytest.mark.incompatible_with_test_venv
- def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script):
+ def test_upgrade_user_conflict_in_globalsite(
+ self, virtualenv: VirtualEnvironment, script: PipTestEnvironment
+ ) -> None:
"""
Test user install/upgrade with conflict in global site ignores site and
installs to usersite
"""
_patch_dist_in_site_packages(virtualenv)
- script.pip('install', 'INITools==0.2', '--no-binary=:all:')
+ script.pip("install", "INITools==0.2", "--no-binary=:all:")
result2 = script.pip(
- 'install', '--user', '--upgrade', 'INITools', '--no-binary=:all:')
+ "install", "--user", "--upgrade", "INITools", "--no-binary=:all:"
+ )
# usersite has 0.3.1
# we still test for egg-info because no-binary implies setup.py install
- egg_info_folder = (
- script.user_site / f'INITools-0.3.1-py{pyversion}.egg-info'
- )
- initools_folder = script.user_site / 'initools'
+ egg_info_folder = script.user_site / f"INITools-0.3.1-py{pyversion}.egg-info"
+ initools_folder = script.user_site / "initools"
result2.did_create(egg_info_folder)
result2.did_create(initools_folder)
# site still has 0.2 (can't look in result1; have to check)
egg_info_folder = (
- script.base_path / script.site_packages /
- f'INITools-0.2-py{pyversion}.egg-info'
+ script.base_path
+ / script.site_packages
+ / f"INITools-0.2-py{pyversion}.egg-info"
)
- initools_folder = script.base_path / script.site_packages / 'initools'
+ initools_folder = script.base_path / script.site_packages / "initools"
assert isdir(egg_info_folder), result2.stdout
assert isdir(initools_folder)
@pytest.mark.network
@pytest.mark.incompatible_with_test_venv
def test_install_user_conflict_in_globalsite_and_usersite(
- self, virtualenv, script):
+ self, virtualenv: VirtualEnvironment, script: PipTestEnvironment
+ ) -> None:
"""
Test user install with conflict in globalsite and usersite ignores
global site and updates usersite.
"""
_patch_dist_in_site_packages(virtualenv)
- script.pip('install', 'INITools==0.2', '--no-binary=:all:')
- script.pip('install', '--user', 'INITools==0.3', '--no-binary=:all:')
+ script.pip("install", "INITools==0.2", "--no-binary=:all:")
+ script.pip("install", "--user", "INITools==0.3", "--no-binary=:all:")
- result3 = script.pip(
- 'install', '--user', 'INITools==0.1', '--no-binary=:all:')
+ result3 = script.pip("install", "--user", "INITools==0.1", "--no-binary=:all:")
# usersite has 0.1
# we still test for egg-info because no-binary implies setup.py install
- egg_info_folder = (
- script.user_site / f'INITools-0.1-py{pyversion}.egg-info'
- )
+ egg_info_folder = script.user_site / f"INITools-0.1-py{pyversion}.egg-info"
initools_v3_file = (
# file only in 0.3
- script.base_path / script.user_site / 'initools' /
- 'configparser.py'
+ script.base_path
+ / script.user_site
+ / "initools"
+ / "configparser.py"
)
result3.did_create(egg_info_folder)
assert not isfile(initools_v3_file), initools_v3_file
# site still has 0.2 (can't just look in result1; have to check)
egg_info_folder = (
- script.base_path / script.site_packages /
- f'INITools-0.2-py{pyversion}.egg-info'
+ script.base_path
+ / script.site_packages
+ / f"INITools-0.2-py{pyversion}.egg-info"
)
- initools_folder = script.base_path / script.site_packages / 'initools'
+ initools_folder = script.base_path / script.site_packages / "initools"
assert isdir(egg_info_folder)
assert isdir(initools_folder)
@pytest.mark.network
@pytest.mark.incompatible_with_test_venv
def test_install_user_in_global_virtualenv_with_conflict_fails(
- self, script):
+ self, script: PipTestEnvironment
+ ) -> None:
"""
Test user install in --system-site-packages virtualenv with conflict in
site fails.
"""
- script.pip('install', 'INITools==0.2')
+ script.pip("install", "INITools==0.2")
result2 = script.pip(
- 'install', '--user', 'INITools==0.1',
+ "install",
+ "--user",
+ "INITools==0.1",
expect_error=True,
)
resultp = script.run(
- 'python', '-c',
+ "python",
+ "-c",
"import pkg_resources; print(pkg_resources.get_distribution"
"('initools').location)",
)
@@ -253,7 +274,7 @@ class Tests_UserSite:
assert (
"Will not install to the user site because it will lack sys.path "
"precedence to {name} in {location}".format(
- name='INITools',
+ name="INITools",
location=dist_location,
)
in result2.stderr
diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py
index 977011ae5..2171d3162 100644
--- a/tests/functional/test_install_vcs_git.py
+++ b/tests/functional/test_install_vcs_git.py
@@ -1,11 +1,13 @@
+from pathlib import Path
+from typing import Optional
+
import pytest
-from pip._internal.utils.urls import path_to_url
from tests.lib import pyversion # noqa: F401
from tests.lib import (
+ PipTestEnvironment,
_change_test_package_version,
_create_test_package,
- _test_path_to_file_url,
)
from tests.lib.git_submodule_helpers import (
_change_test_package_submodule,
@@ -15,36 +17,38 @@ from tests.lib.git_submodule_helpers import (
from tests.lib.local_repos import local_checkout
-def _get_editable_repo_dir(script, package_name):
+def _get_editable_repo_dir(script: PipTestEnvironment, package_name: str) -> Path:
"""
Return the repository directory for an editable install.
"""
- return script.venv_path / 'src' / package_name
+ return script.venv_path / "src" / package_name
-def _get_editable_branch(script, package_name):
+def _get_editable_branch(script: PipTestEnvironment, package_name: str) -> str:
"""
Return the current branch of an editable install.
"""
repo_dir = _get_editable_repo_dir(script, package_name)
- result = script.run(
- 'git', 'rev-parse', '--abbrev-ref', 'HEAD', cwd=repo_dir
- )
+ result = script.run("git", "rev-parse", "--abbrev-ref", "HEAD", cwd=repo_dir)
return result.stdout.strip()
-def _get_branch_remote(script, package_name, branch):
- """
-
- """
+def _get_branch_remote(
+ script: PipTestEnvironment, package_name: str, branch: str
+) -> str:
+ """ """
repo_dir = _get_editable_repo_dir(script, package_name)
- result = script.run(
- 'git', 'config', f'branch.{branch}.remote', cwd=repo_dir
- )
+ result = script.run("git", "config", f"branch.{branch}.remote", cwd=repo_dir)
return result.stdout.strip()
-def _github_checkout(url_path, temp_dir, rev=None, egg=None, scheme=None):
+def _github_checkout(
+ url_path: str,
+ tmpdir: Path,
+ rev: Optional[str] = None,
+ egg: Optional[str] = None,
+ scheme: Optional[str] = None,
+) -> str:
"""
Call local_checkout() with a GitHub URL, and return the resulting URL.
@@ -57,18 +61,20 @@ def _github_checkout(url_path, temp_dir, rev=None, egg=None, scheme=None):
scheme: the scheme without the "git+" prefix. Defaults to "https".
"""
if scheme is None:
- scheme = 'https'
- url = f'git+{scheme}://github.com/{url_path}'
- local_url = local_checkout(url, temp_dir)
+ scheme = "https"
+ url = f"git+{scheme}://github.com/{url_path}"
+ local_url = local_checkout(url, tmpdir)
if rev is not None:
- local_url += f'@{rev}'
+ local_url += f"@{rev}"
if egg is not None:
- local_url += f'#egg={egg}'
+ local_url += f"#egg={egg}"
return local_url
-def _make_version_pkg_url(path, rev=None, name="version_pkg"):
+def _make_version_pkg_url(
+ path: Path, rev: Optional[str] = None, name: str = "version_pkg"
+) -> str:
"""
Return a "git+file://" URL to the version_pkg test package.
@@ -77,14 +83,19 @@ def _make_version_pkg_url(path, rev=None, name="version_pkg"):
containing the version_pkg package.
rev: an optional revision to install like a branch name, tag, or SHA.
"""
- file_url = _test_path_to_file_url(path)
- url_rev = '' if rev is None else f'@{rev}'
- url = f'git+{file_url}{url_rev}#egg={name}'
+ file_url = path.as_uri()
+ url_rev = "" if rev is None else f"@{rev}"
+ url = f"git+{file_url}{url_rev}#egg={name}"
return url
-def _install_version_pkg_only(script, path, rev=None, expect_stderr=False):
+def _install_version_pkg_only(
+ script: PipTestEnvironment,
+ path: Path,
+ rev: Optional[str] = None,
+ allow_stderr_warning: bool = False,
+) -> None:
"""
Install the version_pkg package in editable mode (without returning
the version).
@@ -95,10 +106,17 @@ def _install_version_pkg_only(script, path, rev=None, expect_stderr=False):
rev: an optional revision to install like a branch name or tag.
"""
version_pkg_url = _make_version_pkg_url(path, rev=rev)
- script.pip('install', '-e', version_pkg_url, expect_stderr=expect_stderr)
+ script.pip(
+ "install", "-e", version_pkg_url, allow_stderr_warning=allow_stderr_warning
+ )
-def _install_version_pkg(script, path, rev=None, expect_stderr=False):
+def _install_version_pkg(
+ script: PipTestEnvironment,
+ path: Path,
+ rev: Optional[str] = None,
+ allow_stderr_warning: bool = False,
+) -> str:
"""
Install the version_pkg package in editable mode, and return the version
installed.
@@ -109,15 +127,18 @@ def _install_version_pkg(script, path, rev=None, expect_stderr=False):
rev: an optional revision to install like a branch name or tag.
"""
_install_version_pkg_only(
- script, path, rev=rev, expect_stderr=expect_stderr,
+ script,
+ path,
+ rev=rev,
+ allow_stderr_warning=allow_stderr_warning,
)
- result = script.run('version_pkg')
+ result = script.run("version_pkg")
version = result.stdout.strip()
return version
-def test_git_install_again_after_changes(script):
+def test_git_install_again_after_changes(script: PipTestEnvironment) -> None:
"""
Test installing a repository a second time without specifying a revision,
and after updates to the remote repository.
@@ -126,162 +147,187 @@ def test_git_install_again_after_changes(script):
logged on the update: "Did not find branch or tag ..., assuming ref or
revision."
"""
- version_pkg_path = _create_test_package(script)
+ version_pkg_path = _create_test_package(script.scratch_path)
version = _install_version_pkg(script, version_pkg_path)
- assert version == '0.1'
+ assert version == "0.1"
_change_test_package_version(script, version_pkg_path)
version = _install_version_pkg(script, version_pkg_path)
- assert version == 'some different version'
+ assert version == "some different version"
-def test_git_install_branch_again_after_branch_changes(script):
+def test_git_install_branch_again_after_branch_changes(
+ script: PipTestEnvironment,
+) -> None:
"""
Test installing a branch again after the branch is updated in the remote
repository.
"""
- version_pkg_path = _create_test_package(script)
- version = _install_version_pkg(script, version_pkg_path, rev='master')
- assert version == '0.1'
+ version_pkg_path = _create_test_package(script.scratch_path)
+ version = _install_version_pkg(script, version_pkg_path, rev="master")
+ assert version == "0.1"
_change_test_package_version(script, version_pkg_path)
- version = _install_version_pkg(script, version_pkg_path, rev='master')
- assert version == 'some different version'
+ version = _install_version_pkg(script, version_pkg_path, rev="master")
+ assert version == "some different version"
@pytest.mark.network
-def test_install_editable_from_git_with_https(script, tmpdir):
+def test_install_editable_from_git_with_https(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""
Test cloning from Git with https.
"""
- url_path = 'pypa/pip-test-package.git'
- local_url = _github_checkout(url_path, tmpdir, egg='pip-test-package')
- result = script.pip('install', '-e', local_url)
- result.assert_installed('pip-test-package', with_files=['.git'])
+ url_path = "pypa/pip-test-package.git"
+ local_url = _github_checkout(url_path, tmpdir, egg="pip-test-package")
+ result = script.pip("install", "-e", local_url)
+ result.assert_installed("pip-test-package", with_files=[".git"])
@pytest.mark.network
-def test_install_noneditable_git(script, tmpdir, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_noneditable_git(script: PipTestEnvironment) -> None:
"""
Test installing from a non-editable git URL with a given tag.
"""
result = script.pip(
- 'install',
- 'git+https://github.com/pypa/pip-test-package.git'
- '@0.1.1#egg=pip-test-package'
- )
- dist_info_folder = (
- script.site_packages /
- 'pip_test_package-0.1.1.dist-info'
+ "install",
+ "git+https://github.com/pypa/pip-test-package.git"
+ "@0.1.1#egg=pip-test-package",
)
- result.assert_installed('piptestpackage',
- without_egg_link=True,
- editable=False)
+ dist_info_folder = script.site_packages / "pip_test_package-0.1.1.dist-info"
+ result.assert_installed("piptestpackage", without_egg_link=True, editable=False)
result.did_create(dist_info_folder)
-def test_git_with_sha1_revisions(script):
+def test_git_with_sha1_revisions(script: PipTestEnvironment) -> None:
"""
Git backend should be able to install from SHA1 revisions
"""
- version_pkg_path = _create_test_package(script)
+ version_pkg_path = _create_test_package(script.scratch_path)
_change_test_package_version(script, version_pkg_path)
sha1 = script.run(
- 'git', 'rev-parse', 'HEAD~1',
+ "git",
+ "rev-parse",
+ "HEAD~1",
cwd=version_pkg_path,
).stdout.strip()
version = _install_version_pkg(script, version_pkg_path, rev=sha1)
- assert '0.1' == version
+ assert "0.1" == version
-def test_git_with_short_sha1_revisions(script):
+def test_git_with_short_sha1_revisions(script: PipTestEnvironment) -> None:
"""
Git backend should be able to install from SHA1 revisions
"""
- version_pkg_path = _create_test_package(script)
+ version_pkg_path = _create_test_package(script.scratch_path)
_change_test_package_version(script, version_pkg_path)
sha1 = script.run(
- 'git', 'rev-parse', 'HEAD~1',
+ "git",
+ "rev-parse",
+ "HEAD~1",
cwd=version_pkg_path,
).stdout.strip()[:7]
- version = _install_version_pkg(script, version_pkg_path, rev=sha1)
- assert '0.1' == version
+ version = _install_version_pkg(
+ script,
+ version_pkg_path,
+ rev=sha1,
+ # WARNING: Did not find branch or tag ..., assuming revision or ref.
+ allow_stderr_warning=True,
+ )
+ assert "0.1" == version
-def test_git_with_branch_name_as_revision(script):
+def test_git_with_branch_name_as_revision(script: PipTestEnvironment) -> None:
"""
Git backend should be able to install from branch names
"""
- version_pkg_path = _create_test_package(script)
- branch = 'test_branch'
- script.run('git', 'checkout', '-b', branch, cwd=version_pkg_path)
+ version_pkg_path = _create_test_package(script.scratch_path)
+ branch = "test_branch"
+ script.run("git", "checkout", "-b", branch, cwd=version_pkg_path)
_change_test_package_version(script, version_pkg_path)
version = _install_version_pkg(script, version_pkg_path, rev=branch)
- assert 'some different version' == version
+ assert "some different version" == version
-def test_git_with_tag_name_as_revision(script):
+def test_git_with_tag_name_as_revision(script: PipTestEnvironment) -> None:
"""
Git backend should be able to install from tag names
"""
- version_pkg_path = _create_test_package(script)
- script.run('git', 'tag', 'test_tag', cwd=version_pkg_path)
+ version_pkg_path = _create_test_package(script.scratch_path)
+ script.run("git", "tag", "test_tag", cwd=version_pkg_path)
_change_test_package_version(script, version_pkg_path)
- version = _install_version_pkg(script, version_pkg_path, rev='test_tag')
- assert '0.1' == version
+ version = _install_version_pkg(script, version_pkg_path, rev="test_tag")
+ assert "0.1" == version
-def _add_ref(script, path, ref):
+def _add_ref(script: PipTestEnvironment, path: Path, ref: str) -> None:
"""
Add a new ref to a repository at the given path.
"""
- script.run('git', 'update-ref', ref, 'HEAD', cwd=path)
+ script.run("git", "update-ref", ref, "HEAD", cwd=path)
-def test_git_install_ref(script):
+def test_git_install_ref(script: PipTestEnvironment) -> None:
"""
The Git backend should be able to install a ref with the first install.
"""
- version_pkg_path = _create_test_package(script)
- _add_ref(script, version_pkg_path, 'refs/foo/bar')
+ version_pkg_path = _create_test_package(script.scratch_path)
+ _add_ref(script, version_pkg_path, "refs/foo/bar")
_change_test_package_version(script, version_pkg_path)
version = _install_version_pkg(
- script, version_pkg_path, rev='refs/foo/bar',
+ script,
+ version_pkg_path,
+ rev="refs/foo/bar",
+ # WARNING: Did not find branch or tag ..., assuming revision or ref.
+ allow_stderr_warning=True,
)
- assert '0.1' == version
+ assert "0.1" == version
-def test_git_install_then_install_ref(script):
+def test_git_install_then_install_ref(script: PipTestEnvironment) -> None:
"""
The Git backend should be able to install a ref after a package has
already been installed.
"""
- version_pkg_path = _create_test_package(script)
- _add_ref(script, version_pkg_path, 'refs/foo/bar')
+ version_pkg_path = _create_test_package(script.scratch_path)
+ _add_ref(script, version_pkg_path, "refs/foo/bar")
_change_test_package_version(script, version_pkg_path)
version = _install_version_pkg(script, version_pkg_path)
- assert 'some different version' == version
+ assert "some different version" == version
# Now install the ref.
version = _install_version_pkg(
- script, version_pkg_path, rev='refs/foo/bar',
+ script,
+ version_pkg_path,
+ rev="refs/foo/bar",
+ # WARNING: Did not find branch or tag ..., assuming revision or ref.
+ allow_stderr_warning=True,
)
- assert '0.1' == version
+ assert "0.1" == version
@pytest.mark.network
-@pytest.mark.parametrize('rev, expected_sha', [
- # Clone the default branch
- ("", "5547fa909e83df8bd743d3978d6667497983a4b7"),
- # Clone a specific tag
- ("@0.1.1", "7d654e66c8fa7149c165ddeffa5b56bc06619458"),
- # Clone a specific commit
- ("@65cf0a5bdd906ecf48a0ac241c17d656d2071d56",
- "65cf0a5bdd906ecf48a0ac241c17d656d2071d56")
-])
-def test_install_git_logs_commit_sha(script, rev, expected_sha, tmpdir):
+@pytest.mark.parametrize(
+ "rev, expected_sha",
+ [
+ # Clone the default branch
+ ("", "5547fa909e83df8bd743d3978d6667497983a4b7"),
+ # Clone a specific tag
+ ("@0.1.1", "7d654e66c8fa7149c165ddeffa5b56bc06619458"),
+ # Clone a specific commit
+ (
+ "@65cf0a5bdd906ecf48a0ac241c17d656d2071d56",
+ "65cf0a5bdd906ecf48a0ac241c17d656d2071d56",
+ ),
+ ],
+)
+def test_install_git_logs_commit_sha(
+ script: PipTestEnvironment, rev: str, expected_sha: str, tmpdir: Path
+) -> None:
"""
Test installing from a git repository logs a commit SHA.
"""
@@ -294,243 +340,269 @@ def test_install_git_logs_commit_sha(script, rev, expected_sha, tmpdir):
@pytest.mark.network
-def test_git_with_tag_name_and_update(script, tmpdir):
+def test_git_with_tag_name_and_update(script: PipTestEnvironment, tmpdir: Path) -> None:
"""
Test cloning a git repository and updating to a different version.
"""
- url_path = 'pypa/pip-test-package.git'
+ url_path = "pypa/pip-test-package.git"
base_local_url = _github_checkout(url_path, tmpdir)
- local_url = f'{base_local_url}#egg=pip-test-package'
- result = script.pip('install', '-e', local_url)
- result.assert_installed('pip-test-package', with_files=['.git'])
+ local_url = f"{base_local_url}#egg=pip-test-package"
+ result = script.pip("install", "-e", local_url)
+ result.assert_installed("pip-test-package", with_files=[".git"])
- new_local_url = f'{base_local_url}@0.1.2#egg=pip-test-package'
+ new_local_url = f"{base_local_url}@0.1.2#egg=pip-test-package"
result = script.pip(
- 'install', '--global-option=--version', '-e', new_local_url,
+ "install",
+ "--global-option=--version",
+ "-e",
+ new_local_url,
+ allow_stderr_warning=True,
)
- assert '0.1.2' in result.stdout
+ assert "0.1.2" in result.stdout
@pytest.mark.network
-def test_git_branch_should_not_be_changed(script, tmpdir):
+def test_git_branch_should_not_be_changed(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""
Editable installations should not change branch
related to issue #32 and #161
"""
- url_path = 'pypa/pip-test-package.git'
- local_url = _github_checkout(url_path, tmpdir, egg='pip-test-package')
- script.pip('install', '-e', local_url)
- branch = _get_editable_branch(script, 'pip-test-package')
- assert 'master' == branch
+ url_path = "pypa/pip-test-package.git"
+ local_url = _github_checkout(url_path, tmpdir, egg="pip-test-package")
+ script.pip("install", "-e", local_url)
+ branch = _get_editable_branch(script, "pip-test-package")
+ assert "master" == branch
@pytest.mark.network
-def test_git_with_non_editable_unpacking(script, tmpdir):
+def test_git_with_non_editable_unpacking(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""
Test cloning a git repository from a non-editable URL with a given tag.
"""
- url_path = 'pypa/pip-test-package.git'
+ url_path = "pypa/pip-test-package.git"
local_url = _github_checkout(
- url_path, tmpdir, rev='0.1.2', egg='pip-test-package',
+ url_path,
+ tmpdir,
+ rev="0.1.2",
+ egg="pip-test-package",
+ )
+ result = script.pip(
+ "install",
+ "--global-option=--version",
+ local_url,
+ allow_stderr_warning=True,
)
- result = script.pip('install', '--global-option=--version', local_url)
- assert '0.1.2' in result.stdout
+ assert "0.1.2" in result.stdout
@pytest.mark.network
-def test_git_with_editable_where_egg_contains_dev_string(script, tmpdir):
+def test_git_with_editable_where_egg_contains_dev_string(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""
Test cloning a git repository from an editable url which contains "dev"
string
"""
- url_path = 'dcramer/django-devserver.git'
+ url_path = "dcramer/django-devserver.git"
local_url = _github_checkout(
- url_path, tmpdir, egg='django-devserver', scheme='git',
+ url_path,
+ tmpdir,
+ egg="django-devserver",
+ scheme="https",
)
- result = script.pip('install', '-e', local_url)
- result.assert_installed('django-devserver', with_files=['.git'])
+ result = script.pip("install", "-e", local_url)
+ result.assert_installed("django-devserver", with_files=[".git"])
@pytest.mark.network
-def test_git_with_non_editable_where_egg_contains_dev_string(script, tmpdir):
+def test_git_with_non_editable_where_egg_contains_dev_string(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""
Test cloning a git repository from a non-editable url which contains "dev"
string
"""
- url_path = 'dcramer/django-devserver.git'
+ url_path = "dcramer/django-devserver.git"
local_url = _github_checkout(
- url_path, tmpdir, egg='django-devserver', scheme='git',
+ url_path,
+ tmpdir,
+ egg="django-devserver",
+ scheme="https",
)
- result = script.pip('install', local_url)
- devserver_folder = script.site_packages / 'devserver'
+ result = script.pip("install", local_url)
+ devserver_folder = script.site_packages / "devserver"
result.did_create(devserver_folder)
-def test_git_with_ambiguous_revs(script):
+def test_git_with_ambiguous_revs(script: PipTestEnvironment) -> None:
"""
Test git with two "names" (tag/branch) pointing to the same commit
"""
- version_pkg_path = _create_test_package(script)
- version_pkg_url = _make_version_pkg_url(version_pkg_path, rev='0.1')
- script.run('git', 'tag', '0.1', cwd=version_pkg_path)
- result = script.pip('install', '-e', version_pkg_url)
- assert 'Could not find a tag or branch' not in result.stdout
+ version_pkg_path = _create_test_package(script.scratch_path)
+ version_pkg_url = _make_version_pkg_url(version_pkg_path, rev="0.1")
+ script.run("git", "tag", "0.1", cwd=version_pkg_path)
+ result = script.pip("install", "-e", version_pkg_url)
+ assert "Could not find a tag or branch" not in result.stdout
# it is 'version-pkg' instead of 'version_pkg' because
# egg-link name is version-pkg.egg-link because it is a single .py module
- result.assert_installed('version-pkg', with_files=['.git'])
+ result.assert_installed("version-pkg", with_files=[".git"])
-def test_editable__no_revision(script):
+def test_editable__no_revision(script: PipTestEnvironment) -> None:
"""
Test a basic install in editable mode specifying no revision.
"""
- version_pkg_path = _create_test_package(script)
+ version_pkg_path = _create_test_package(script.scratch_path)
_install_version_pkg_only(script, version_pkg_path)
- branch = _get_editable_branch(script, 'version-pkg')
- assert branch == 'master'
+ branch = _get_editable_branch(script, "version-pkg")
+ assert branch == "master"
- remote = _get_branch_remote(script, 'version-pkg', 'master')
- assert remote == 'origin'
+ remote = _get_branch_remote(script, "version-pkg", "master")
+ assert remote == "origin"
-def test_editable__branch_with_sha_same_as_default(script):
+def test_editable__branch_with_sha_same_as_default(script: PipTestEnvironment) -> None:
"""
Test installing in editable mode a branch whose sha matches the sha
of the default branch, but is different from the default branch.
"""
- version_pkg_path = _create_test_package(script)
+ version_pkg_path = _create_test_package(script.scratch_path)
# Create a second branch with the same SHA.
- script.run('git', 'branch', 'develop', cwd=version_pkg_path)
- _install_version_pkg_only(script, version_pkg_path, rev='develop')
+ script.run("git", "branch", "develop", cwd=version_pkg_path)
+ _install_version_pkg_only(script, version_pkg_path, rev="develop")
- branch = _get_editable_branch(script, 'version-pkg')
- assert branch == 'develop'
+ branch = _get_editable_branch(script, "version-pkg")
+ assert branch == "develop"
- remote = _get_branch_remote(script, 'version-pkg', 'develop')
- assert remote == 'origin'
+ remote = _get_branch_remote(script, "version-pkg", "develop")
+ assert remote == "origin"
-def test_editable__branch_with_sha_different_from_default(script):
+def test_editable__branch_with_sha_different_from_default(
+ script: PipTestEnvironment,
+) -> None:
"""
Test installing in editable mode a branch whose sha is different from
the sha of the default branch.
"""
- version_pkg_path = _create_test_package(script)
+ version_pkg_path = _create_test_package(script.scratch_path)
# Create a second branch.
- script.run('git', 'branch', 'develop', cwd=version_pkg_path)
+ script.run("git", "branch", "develop", cwd=version_pkg_path)
# Add another commit to the master branch to give it a different sha.
_change_test_package_version(script, version_pkg_path)
- version = _install_version_pkg(script, version_pkg_path, rev='develop')
- assert version == '0.1'
+ version = _install_version_pkg(script, version_pkg_path, rev="develop")
+ assert version == "0.1"
- branch = _get_editable_branch(script, 'version-pkg')
- assert branch == 'develop'
+ branch = _get_editable_branch(script, "version-pkg")
+ assert branch == "develop"
- remote = _get_branch_remote(script, 'version-pkg', 'develop')
- assert remote == 'origin'
+ remote = _get_branch_remote(script, "version-pkg", "develop")
+ assert remote == "origin"
-def test_editable__non_master_default_branch(script):
+def test_editable__non_master_default_branch(script: PipTestEnvironment) -> None:
"""
Test the branch you get after an editable install from a remote repo
with a non-master default branch.
"""
- version_pkg_path = _create_test_package(script)
+ version_pkg_path = _create_test_package(script.scratch_path)
# Change the default branch of the remote repo to a name that is
# alphabetically after "master".
- script.run('git', 'checkout', '-b', 'release', cwd=version_pkg_path)
+ script.run("git", "checkout", "-b", "release", cwd=version_pkg_path)
_install_version_pkg_only(script, version_pkg_path)
- branch = _get_editable_branch(script, 'version-pkg')
- assert branch == 'release'
+ branch = _get_editable_branch(script, "version-pkg")
+ assert branch == "release"
-def test_reinstalling_works_with_editable_non_master_branch(script):
+def test_reinstalling_works_with_editable_non_master_branch(
+ script: PipTestEnvironment,
+) -> None:
"""
Reinstalling an editable installation should not assume that the "master"
branch exists. See https://github.com/pypa/pip/issues/4448.
"""
- version_pkg_path = _create_test_package(script)
+ version_pkg_path = _create_test_package(script.scratch_path)
# Switch the default branch to something other than 'master'
- script.run('git', 'branch', '-m', 'foobar', cwd=version_pkg_path)
+ script.run("git", "branch", "-m", "foobar", cwd=version_pkg_path)
version = _install_version_pkg(script, version_pkg_path)
- assert '0.1' == version
+ assert "0.1" == version
_change_test_package_version(script, version_pkg_path)
version = _install_version_pkg(script, version_pkg_path)
- assert 'some different version' == version
+ assert "some different version" == version
# TODO(pnasrat) fix all helpers to do right things with paths on windows.
@pytest.mark.skipif("sys.platform == 'win32'")
-def test_check_submodule_addition(script):
+def test_check_submodule_addition(script: PipTestEnvironment) -> None:
"""
Submodules are pulled in on install and updated on upgrade.
"""
- module_path, submodule_path = (
- _create_test_package_with_submodule(script, rel_path='testpkg/static')
+ module_path, submodule_path = _create_test_package_with_submodule(
+ script, rel_path="testpkg/static"
)
install_result = script.pip(
- 'install', '-e', 'git+' + path_to_url(module_path) + '#egg=version_pkg'
- )
- install_result.did_create(
- script.venv / 'src/version-pkg/testpkg/static/testfile'
+ "install", "-e", f"git+{module_path.as_uri()}#egg=version_pkg"
)
+ install_result.did_create(script.venv / "src/version-pkg/testpkg/static/testfile")
_change_test_package_submodule(script, submodule_path)
_pull_in_submodule_changes_to_module(
- script, module_path, rel_path='testpkg/static',
+ script,
+ module_path,
+ rel_path="testpkg/static",
)
# expect error because git may write to stderr
update_result = script.pip(
- 'install', '-e', 'git+' + path_to_url(module_path) + '#egg=version_pkg',
- '--upgrade',
+ "install",
+ "-e",
+ f"git+{module_path.as_uri()}#egg=version_pkg",
+ "--upgrade",
)
- update_result.did_create(
- script.venv / 'src/version-pkg/testpkg/static/testfile2'
- )
+ update_result.did_create(script.venv / "src/version-pkg/testpkg/static/testfile2")
-def test_install_git_branch_not_cached(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_git_branch_not_cached(script: PipTestEnvironment) -> None:
"""
Installing git urls with a branch revision does not cause wheel caching.
"""
PKG = "gitbranchnotcached"
- repo_dir = _create_test_package(script, name=PKG)
+ repo_dir = _create_test_package(script.scratch_path, name=PKG)
url = _make_version_pkg_url(repo_dir, rev="master", name=PKG)
result = script.pip("install", url, "--only-binary=:all:")
assert f"Successfully built {PKG}" in result.stdout, result.stdout
script.pip("uninstall", "-y", PKG)
# build occurs on the second install too because it is not cached
result = script.pip("install", url)
- assert (
- f"Successfully built {PKG}" in result.stdout
- ), result.stdout
+ assert f"Successfully built {PKG}" in result.stdout, result.stdout
-def test_install_git_sha_cached(script, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_git_sha_cached(script: PipTestEnvironment) -> None:
"""
Installing git urls with a sha revision does cause wheel caching.
"""
PKG = "gitshacached"
- repo_dir = _create_test_package(script, name=PKG)
- commit = script.run(
- 'git', 'rev-parse', 'HEAD', cwd=repo_dir
- ).stdout.strip()
+ repo_dir = _create_test_package(script.scratch_path, name=PKG)
+ commit = script.run("git", "rev-parse", "HEAD", cwd=repo_dir).stdout.strip()
url = _make_version_pkg_url(repo_dir, rev=commit, name=PKG)
result = script.pip("install", url)
assert f"Successfully built {PKG}" in result.stdout, result.stdout
script.pip("uninstall", "-y", PKG)
# build does not occur on the second install because it is cached
result = script.pip("install", url)
- assert (
- f"Successfully built {PKG}" not in result.stdout
- ), result.stdout
+ assert f"Successfully built {PKG}" not in result.stdout, result.stdout
diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py
index 8df208bb7..189853429 100644
--- a/tests/functional/test_install_wheel.py
+++ b/tests/functional/test_install_wheel.py
@@ -1,197 +1,225 @@
+import base64
import csv
import distutils
-import glob
+import hashlib
import os
import shutil
+from pathlib import Path
+from typing import Any
import pytest
-from tests.lib import create_basic_wheel_for_package
-from tests.lib.path import Path
-from tests.lib.wheel import make_wheel
+from tests.lib import PipTestEnvironment, TestData, create_basic_wheel_for_package
+from tests.lib.wheel import WheelBuilder, make_wheel
# assert_installed expects a package subdirectory, so give it to them
-def make_wheel_with_file(name, version, **kwargs):
+def make_wheel_with_file(name: str, version: str, **kwargs: Any) -> WheelBuilder:
extra_files = kwargs.setdefault("extra_files", {})
extra_files[f"{name}/__init__.py"] = "# example"
return make_wheel(name=name, version=version, **kwargs)
-def test_install_from_future_wheel_version(script, tmpdir):
+def test_install_from_future_wheel_version(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""
Test installing a wheel with a WHEEL metadata version that is:
- a major version ahead of what we expect (not ok), and
- a minor version ahead of what we expect (ok)
"""
from tests.lib import TestFailure
+
package = make_wheel_with_file(
name="futurewheel",
version="3.0",
wheel_metadata_updates={"Wheel-Version": "3.0"},
).save_to_dir(tmpdir)
- result = script.pip('install', package, '--no-index', expect_error=True)
+ result = script.pip("install", package, "--no-index", expect_error=True)
with pytest.raises(TestFailure):
- result.assert_installed('futurewheel', without_egg_link=True,
- editable=False)
+ result.assert_installed("futurewheel", without_egg_link=True, editable=False)
package = make_wheel_with_file(
name="futurewheel",
version="1.9",
wheel_metadata_updates={"Wheel-Version": "1.9"},
).save_to_dir(tmpdir)
- result = script.pip(
- 'install', package, '--no-index', expect_stderr=True
- )
- result.assert_installed('futurewheel', without_egg_link=True,
- editable=False)
+ result = script.pip("install", package, "--no-index", expect_stderr=True)
+ result.assert_installed("futurewheel", without_egg_link=True, editable=False)
-def test_install_from_broken_wheel(script, data):
+@pytest.mark.parametrize(
+ "wheel_name",
+ [
+ "brokenwheel-1.0-py2.py3-none-any.whl",
+ "corruptwheel-1.0-py2.py3-none-any.whl",
+ ],
+)
+def test_install_from_broken_wheel(
+ script: PipTestEnvironment, data: TestData, wheel_name: str
+) -> None:
"""
Test that installing a broken wheel fails properly
"""
from tests.lib import TestFailure
- package = data.packages.joinpath("brokenwheel-1.0-py2.py3-none-any.whl")
- result = script.pip('install', package, '--no-index', expect_error=True)
+
+ package = data.packages.joinpath(wheel_name)
+ result = script.pip("install", package, "--no-index", expect_error=True)
with pytest.raises(TestFailure):
- result.assert_installed('futurewheel', without_egg_link=True,
- editable=False)
+ result.assert_installed("futurewheel", without_egg_link=True, editable=False)
-def test_basic_install_from_wheel(script, shared_data, tmpdir):
+def test_basic_install_from_wheel(
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test installing from a wheel (that has a script)
"""
- shutil.copy(
- shared_data.packages / "has.script-1.0-py2.py3-none-any.whl", tmpdir
- )
+ shutil.copy(shared_data.packages / "has.script-1.0-py2.py3-none-any.whl", tmpdir)
result = script.pip(
- 'install', 'has.script==1.0', '--no-index',
- '--find-links', tmpdir,
+ "install",
+ "has.script==1.0",
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
- dist_info_folder = script.site_packages / 'has.script-1.0.dist-info'
+ dist_info_folder = script.site_packages / "has.script-1.0.dist-info"
result.did_create(dist_info_folder)
- script_file = script.bin / 'script.py'
+ script_file = script.bin / "script.py"
result.did_create(script_file)
-def test_basic_install_from_wheel_with_extras(script, shared_data, tmpdir):
+def test_basic_install_from_wheel_with_extras(
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test installing from a wheel with extras.
"""
- shutil.copy(
- shared_data.packages / "complex_dist-0.1-py2.py3-none-any.whl", tmpdir
- )
- shutil.copy(
- shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir
- )
+ shutil.copy(shared_data.packages / "complex_dist-0.1-py2.py3-none-any.whl", tmpdir)
+ shutil.copy(shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir)
result = script.pip(
- 'install', 'complex-dist[simple]', '--no-index',
- '--find-links', tmpdir,
+ "install",
+ "complex-dist[simple]",
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
- dist_info_folder = script.site_packages / 'complex_dist-0.1.dist-info'
+ dist_info_folder = script.site_packages / "complex_dist-0.1.dist-info"
result.did_create(dist_info_folder)
- dist_info_folder = script.site_packages / 'simple.dist-0.1.dist-info'
+ dist_info_folder = script.site_packages / "simple.dist-0.1.dist-info"
result.did_create(dist_info_folder)
-def test_basic_install_from_wheel_file(script, data):
+def test_basic_install_from_wheel_file(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test installing directly from a wheel file.
"""
package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl")
- result = script.pip('install', package, '--no-index')
- dist_info_folder = script.site_packages / 'simple.dist-0.1.dist-info'
+ result = script.pip("install", package, "--no-index")
+ dist_info_folder = script.site_packages / "simple.dist-0.1.dist-info"
result.did_create(dist_info_folder)
- installer = dist_info_folder / 'INSTALLER'
+ installer = dist_info_folder / "INSTALLER"
result.did_create(installer)
- with open(script.base_path / installer, 'rb') as installer_file:
+ with open(script.base_path / installer, "rb") as installer_file:
installer_details = installer_file.read()
- assert installer_details == b'pip\n'
- installer_temp = dist_info_folder / 'INSTALLER.pip'
+ assert installer_details == b"pip\n"
+ installer_temp = dist_info_folder / "INSTALLER.pip"
result.did_not_create(installer_temp)
# Installation seems to work, but scripttest fails to check.
# I really don't care now since we're desupporting it soon anyway.
-def test_basic_install_from_unicode_wheel(script, data):
+def test_basic_install_from_unicode_wheel(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test installing from a wheel (that has a script)
"""
make_wheel(
- 'unicode_package',
- '1.0',
+ "unicode_package",
+ "1.0",
extra_files={
- 'வணகà¯à®•à®®à¯/__init__.py': b'',
- 'வணகà¯à®•à®®à¯/નમસà«àª¤à«‡.py': b'',
+ "வணகà¯à®•à®®à¯/__init__.py": b"",
+ "வணகà¯à®•à®®à¯/નમસà«àª¤à«‡.py": b"",
},
).save_to_dir(script.scratch_path)
result = script.pip(
- 'install', 'unicode_package==1.0', '--no-index',
- '--find-links', script.scratch_path,
+ "install",
+ "unicode_package==1.0",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
)
- dist_info_folder = script.site_packages / 'unicode_package-1.0.dist-info'
+ dist_info_folder = script.site_packages / "unicode_package-1.0.dist-info"
result.did_create(dist_info_folder)
- file1 = script.site_packages.joinpath('வணகà¯à®•à®®à¯', '__init__.py')
+ file1 = script.site_packages.joinpath("வணகà¯à®•à®®à¯", "__init__.py")
result.did_create(file1)
- file2 = script.site_packages.joinpath('வணகà¯à®•à®®à¯', 'નમસà«àª¤à«‡.py')
+ file2 = script.site_packages.joinpath("வணகà¯à®•à®®à¯", "નમસà«àª¤à«‡.py")
result.did_create(file2)
-def get_header_scheme_path_for_script(script, dist_name):
+def get_header_scheme_path_for_script(
+ script: PipTestEnvironment, dist_name: str
+) -> Path:
command = (
"from pip._internal.locations import get_scheme;"
"scheme = get_scheme({!r});"
"print(scheme.headers);"
).format(dist_name)
- result = script.run('python', '-c', command).stdout
+ result = script.run("python", "-c", command).stdout
return Path(result.strip())
-def test_install_from_wheel_with_headers(script):
+def test_install_from_wheel_with_headers(script: PipTestEnvironment) -> None:
"""
Test installing from a wheel file with headers
"""
- header_text = '/* hello world */\n'
+ header_text = "/* hello world */\n"
package = make_wheel(
- 'headers.dist',
- '0.1',
- extra_data_files={
- 'headers/header.h': header_text
- },
+ "headers.dist",
+ "0.1",
+ extra_data_files={"headers/header.h": header_text},
).save_to_dir(script.scratch_path)
- result = script.pip('install', package, '--no-index')
- dist_info_folder = script.site_packages / 'headers.dist-0.1.dist-info'
+ result = script.pip("install", package, "--no-index")
+ dist_info_folder = script.site_packages / "headers.dist-0.1.dist-info"
result.did_create(dist_info_folder)
- header_scheme_path = get_header_scheme_path_for_script(
- script, 'headers.dist'
- )
- header_path = header_scheme_path / 'header.h'
+ header_scheme_path = get_header_scheme_path_for_script(script, "headers.dist")
+ header_path = header_scheme_path / "header.h"
assert header_path.read_text() == header_text
-def test_install_wheel_with_target(script, shared_data, with_wheel, tmpdir):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_wheel_with_target(
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test installing a wheel using pip install --target
"""
- shutil.copy(
- shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir
- )
- target_dir = script.scratch_path / 'target'
+ shutil.copy(shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir)
+ target_dir = script.scratch_path / "target"
result = script.pip(
- 'install', 'simple.dist==0.1', '-t', target_dir,
- '--no-index', '--find-links', tmpdir,
+ "install",
+ "simple.dist==0.1",
+ "-t",
+ target_dir,
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
- result.did_create(Path('scratch') / 'target' / 'simpledist')
+ result.did_create(Path("scratch") / "target" / "simpledist")
-def test_install_wheel_with_target_and_data_files(script, data, with_wheel):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_wheel_with_target_and_data_files(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test for issue #4092. It will be checked that a data_files specification in
setup.py is handled correctly when a wheel is installed with the --target
@@ -210,100 +238,116 @@ def test_install_wheel_with_target_and_data_files(script, data, with_wheel):
]
)
"""
- target_dir = script.scratch_path / 'prjwithdatafile'
- package = data.packages.joinpath(
- "prjwithdatafile-1.0-py2.py3-none-any.whl"
- )
- result = script.pip('install', package,
- '-t', target_dir,
- '--no-index')
- project_path = Path('scratch') / 'prjwithdatafile'
- result.did_create(project_path / 'packages1' / 'README.txt')
- result.did_create(project_path / 'packages2' / 'README.txt')
- result.did_not_create(project_path / 'lib' / 'python')
+ target_dir = script.scratch_path / "prjwithdatafile"
+ package = data.packages.joinpath("prjwithdatafile-1.0-py2.py3-none-any.whl")
+ result = script.pip("install", package, "-t", target_dir, "--no-index")
+ project_path = Path("scratch") / "prjwithdatafile"
+ result.did_create(project_path / "packages1" / "README.txt")
+ result.did_create(project_path / "packages2" / "README.txt")
+ result.did_not_create(project_path / "lib" / "python")
-def test_install_wheel_with_root(script, shared_data, tmpdir):
+def test_install_wheel_with_root(
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test installing a wheel using pip install --root
"""
- root_dir = script.scratch_path / 'root'
- shutil.copy(
- shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir
- )
+ root_dir = script.scratch_path / "root"
+ shutil.copy(shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir)
result = script.pip(
- 'install', 'simple.dist==0.1', '--root', root_dir,
- '--no-index', '--find-links', tmpdir,
+ "install",
+ "simple.dist==0.1",
+ "--root",
+ root_dir,
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
- result.did_create(Path('scratch') / 'root')
+ result.did_create(Path("scratch") / "root")
-def test_install_wheel_with_prefix(script, shared_data, tmpdir):
+def test_install_wheel_with_prefix(
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test installing a wheel using pip install --prefix
"""
- prefix_dir = script.scratch_path / 'prefix'
- shutil.copy(
- shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir
- )
+ prefix_dir = script.scratch_path / "prefix"
+ shutil.copy(shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir)
result = script.pip(
- 'install', 'simple.dist==0.1', '--prefix', prefix_dir,
- '--no-index', '--find-links', tmpdir,
+ "install",
+ "simple.dist==0.1",
+ "--prefix",
+ prefix_dir,
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
- lib = distutils.sysconfig.get_python_lib(prefix=Path('scratch') / 'prefix')
+ lib = distutils.sysconfig.get_python_lib(prefix=os.path.join("scratch", "prefix"))
result.did_create(lib)
-def test_install_from_wheel_installs_deps(script, data, tmpdir):
+def test_install_from_wheel_installs_deps(
+ script: PipTestEnvironment, data: TestData, tmpdir: Path
+) -> None:
"""
Test can install dependencies of wheels
"""
# 'requires_source' depends on the 'source' project
- package = data.packages.joinpath(
- "requires_source-1.0-py2.py3-none-any.whl"
- )
+ package = data.packages.joinpath("requires_source-1.0-py2.py3-none-any.whl")
shutil.copy(data.packages / "source-1.0.tar.gz", tmpdir)
result = script.pip(
- 'install', '--no-index', '--find-links', tmpdir, package,
+ "install",
+ "--no-index",
+ "--find-links",
+ tmpdir,
+ package,
)
- result.assert_installed('source', editable=False)
+ result.assert_installed("source", editable=False)
-def test_install_from_wheel_no_deps(script, data, tmpdir):
+def test_install_from_wheel_no_deps(
+ script: PipTestEnvironment, data: TestData, tmpdir: Path
+) -> None:
"""
Test --no-deps works with wheel installs
"""
# 'requires_source' depends on the 'source' project
- package = data.packages.joinpath(
- "requires_source-1.0-py2.py3-none-any.whl"
- )
+ package = data.packages.joinpath("requires_source-1.0-py2.py3-none-any.whl")
shutil.copy(data.packages / "source-1.0.tar.gz", tmpdir)
result = script.pip(
- 'install', '--no-index', '--find-links', tmpdir, '--no-deps',
+ "install",
+ "--no-index",
+ "--find-links",
+ tmpdir,
+ "--no-deps",
package,
)
- pkg_folder = script.site_packages / 'source'
+ pkg_folder = script.site_packages / "source"
result.did_not_create(pkg_folder)
-def test_wheel_record_lines_in_deterministic_order(script, data):
+def test_wheel_record_lines_in_deterministic_order(
+ script: PipTestEnvironment, data: TestData
+) -> None:
to_install = data.packages.joinpath("simplewheel-1.0-py2.py3-none-any.whl")
- result = script.pip('install', to_install)
+ result = script.pip("install", to_install)
- dist_info_folder = script.site_packages / 'simplewheel-1.0.dist-info'
- record_path = dist_info_folder / 'RECORD'
+ dist_info_folder = script.site_packages / "simplewheel-1.0.dist-info"
+ record_path = dist_info_folder / "RECORD"
result.did_create(dist_info_folder)
result.did_create(record_path)
record_path = result.files_created[record_path].full
- record_lines = [
- p for p in Path(record_path).read_text().split('\n') if p
- ]
+ record_lines = [p for p in Path(record_path).read_text().split("\n") if p]
assert record_lines == sorted(record_lines)
-def test_wheel_record_lines_have_hash_for_data_files(script):
+def test_wheel_record_lines_have_hash_for_data_files(
+ script: PipTestEnvironment,
+) -> None:
package = make_wheel(
"simple",
"0.1.0",
@@ -312,38 +356,80 @@ def test_wheel_record_lines_have_hash_for_data_files(script):
},
).save_to_dir(script.scratch_path)
script.pip("install", package)
- record_file = (
- script.site_packages_path / "simple-0.1.0.dist-info" / "RECORD"
- )
+ record_file = script.site_packages_path / "simple-0.1.0.dist-info" / "RECORD"
record_text = record_file.read_text()
record_rows = list(csv.reader(record_text.splitlines()))
- records = {
- r[0]: r[1:] for r in record_rows
- }
+ records = {r[0]: r[1:] for r in record_rows}
assert records["info.txt"] == [
- "sha256=Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y", "1"
+ "sha256=Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y",
+ "1",
+ ]
+
+
+def test_wheel_record_lines_have_updated_hash_for_scripts(
+ script: PipTestEnvironment,
+) -> None:
+ """
+ pip rewrites "#!python" shebang lines in scripts when it installs them;
+ make sure it updates the RECORD file correspondingly.
+ """
+ package = make_wheel(
+ "simple",
+ "0.1.0",
+ extra_data_files={
+ "scripts/dostuff": "#!python\n",
+ },
+ ).save_to_dir(script.scratch_path)
+ script.pip("install", package)
+ record_file = script.site_packages_path / "simple-0.1.0.dist-info" / "RECORD"
+ record_text = record_file.read_text()
+ record_rows = list(csv.reader(record_text.splitlines()))
+ records = {r[0]: r[1:] for r in record_rows}
+
+ script_path = script.bin_path / "dostuff"
+ script_contents = script_path.read_bytes()
+ assert not script_contents.startswith(b"#!python\n")
+
+ script_digest = hashlib.sha256(script_contents).digest()
+ script_digest_b64 = (
+ base64.urlsafe_b64encode(script_digest).decode("US-ASCII").rstrip("=")
+ )
+
+ script_record_path = os.path.relpath(
+ script_path, script.site_packages_path
+ ).replace(os.path.sep, "/")
+ assert records[script_record_path] == [
+ f"sha256={script_digest_b64}",
+ str(len(script_contents)),
]
@pytest.mark.incompatible_with_test_venv
-def test_install_user_wheel(script, shared_data, with_wheel, tmpdir):
+@pytest.mark.usefixtures("with_wheel")
+def test_install_user_wheel(
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test user install from wheel (that has a script)
"""
- shutil.copy(
- shared_data.packages / "has.script-1.0-py2.py3-none-any.whl", tmpdir
- )
+ shutil.copy(shared_data.packages / "has.script-1.0-py2.py3-none-any.whl", tmpdir)
result = script.pip(
- 'install', 'has.script==1.0', '--user', '--no-index',
- '--find-links', tmpdir,
+ "install",
+ "has.script==1.0",
+ "--user",
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
- dist_info_folder = script.user_site / 'has.script-1.0.dist-info'
+ dist_info_folder = script.user_site / "has.script-1.0.dist-info"
result.did_create(dist_info_folder)
- script_file = script.user_bin / 'script.py'
+ script_file = script.user_bin / "script.py"
result.did_create(script_file)
-def test_install_from_wheel_gen_entrypoint(script, shared_data, tmpdir):
+def test_install_from_wheel_gen_entrypoint(
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test installing scripts (entry points are generated)
"""
@@ -352,13 +438,16 @@ def test_install_from_wheel_gen_entrypoint(script, shared_data, tmpdir):
tmpdir,
)
result = script.pip(
- 'install', 'script.wheel1a==0.1', '--no-index',
- '--find-links', tmpdir,
+ "install",
+ "script.wheel1a==0.1",
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
- if os.name == 'nt':
- wrapper_file = script.bin / 't1.exe'
+ if os.name == "nt":
+ wrapper_file = script.bin / "t1.exe"
else:
- wrapper_file = script.bin / 't1'
+ wrapper_file = script.bin / "t1"
result.did_create(wrapper_file)
if os.name != "nt":
@@ -366,32 +455,34 @@ def test_install_from_wheel_gen_entrypoint(script, shared_data, tmpdir):
def test_install_from_wheel_gen_uppercase_entrypoint(
- script, shared_data, tmpdir
-):
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test installing scripts with uppercase letters in entry point names
"""
shutil.copy(
- shared_data.packages /
- "console_scripts_uppercase-1.0-py2.py3-none-any.whl",
+ shared_data.packages / "console_scripts_uppercase-1.0-py2.py3-none-any.whl",
tmpdir,
)
result = script.pip(
- 'install', 'console-scripts-uppercase==1.0', '--no-index',
- '--find-links', tmpdir,
+ "install",
+ "console-scripts-uppercase==1.0",
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
- if os.name == 'nt':
+ if os.name == "nt":
# Case probably doesn't make any difference on NT
- wrapper_file = script.bin / 'cmdName.exe'
+ wrapper_file = script.bin / "cmdName.exe"
else:
- wrapper_file = script.bin / 'cmdName'
+ wrapper_file = script.bin / "cmdName"
result.did_create(wrapper_file)
if os.name != "nt":
assert bool(os.access(script.base_path / wrapper_file, os.X_OK))
-def test_install_from_wheel_gen_unicode_entrypoint(script):
+def test_install_from_wheel_gen_unicode_entrypoint(script: PipTestEnvironment) -> None:
make_wheel(
"script_wheel_unicode",
"1.0",
@@ -411,7 +502,9 @@ def test_install_from_wheel_gen_unicode_entrypoint(script):
result.did_create(script.bin.joinpath("進入點"))
-def test_install_from_wheel_with_legacy(script, shared_data, tmpdir):
+def test_install_from_wheel_with_legacy(
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test installing scripts (legacy scripts are preserved)
"""
@@ -420,36 +513,40 @@ def test_install_from_wheel_with_legacy(script, shared_data, tmpdir):
tmpdir,
)
result = script.pip(
- 'install', 'script.wheel2a==0.1', '--no-index',
- '--find-links', tmpdir,
+ "install",
+ "script.wheel2a==0.1",
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
- legacy_file1 = script.bin / 'testscript1.bat'
- legacy_file2 = script.bin / 'testscript2'
+ legacy_file1 = script.bin / "testscript1.bat"
+ legacy_file2 = script.bin / "testscript2"
result.did_create(legacy_file1)
result.did_create(legacy_file2)
def test_install_from_wheel_no_setuptools_entrypoint(
- script, shared_data, tmpdir
-):
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test that when we generate scripts, any existing setuptools wrappers in
the wheel are skipped.
"""
- shutil.copy(
- shared_data.packages / "script.wheel1-0.1-py2.py3-none-any.whl", tmpdir
- )
+ shutil.copy(shared_data.packages / "script.wheel1-0.1-py2.py3-none-any.whl", tmpdir)
result = script.pip(
- 'install', 'script.wheel1==0.1', '--no-index',
- '--find-links', tmpdir,
+ "install",
+ "script.wheel1==0.1",
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
- if os.name == 'nt':
- wrapper_file = script.bin / 't1.exe'
+ if os.name == "nt":
+ wrapper_file = script.bin / "t1.exe"
else:
- wrapper_file = script.bin / 't1'
- wrapper_helper = script.bin / 't1-script.py'
+ wrapper_file = script.bin / "t1"
+ wrapper_helper = script.bin / "t1-script.py"
# The wheel has t1.exe and t1-script.py. We will be generating t1 or
# t1.exe depending on the platform. So we check that the correct wrapper
@@ -460,131 +557,139 @@ def test_install_from_wheel_no_setuptools_entrypoint(
result.did_not_create(wrapper_helper)
-def test_skipping_setuptools_doesnt_skip_legacy(script, shared_data, tmpdir):
+def test_skipping_setuptools_doesnt_skip_legacy(
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test installing scripts (legacy scripts are preserved even when we skip
setuptools wrappers)
"""
- shutil.copy(
- shared_data.packages / "script.wheel2-0.1-py2.py3-none-any.whl", tmpdir
- )
+ shutil.copy(shared_data.packages / "script.wheel2-0.1-py2.py3-none-any.whl", tmpdir)
result = script.pip(
- 'install', 'script.wheel2==0.1', '--no-index',
- '--find-links', tmpdir,
+ "install",
+ "script.wheel2==0.1",
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
- legacy_file1 = script.bin / 'testscript1.bat'
- legacy_file2 = script.bin / 'testscript2'
- wrapper_helper = script.bin / 't1-script.py'
+ legacy_file1 = script.bin / "testscript1.bat"
+ legacy_file2 = script.bin / "testscript2"
+ wrapper_helper = script.bin / "t1-script.py"
result.did_create(legacy_file1)
result.did_create(legacy_file2)
result.did_not_create(wrapper_helper)
-def test_install_from_wheel_gui_entrypoint(script, shared_data, tmpdir):
+def test_install_from_wheel_gui_entrypoint(
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test installing scripts (gui entry points are generated)
"""
- shutil.copy(
- shared_data.packages / "script.wheel3-0.1-py2.py3-none-any.whl", tmpdir
- )
+ shutil.copy(shared_data.packages / "script.wheel3-0.1-py2.py3-none-any.whl", tmpdir)
result = script.pip(
- 'install', 'script.wheel3==0.1', '--no-index',
- '--find-links', tmpdir,
+ "install",
+ "script.wheel3==0.1",
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
- if os.name == 'nt':
- wrapper_file = script.bin / 't1.exe'
+ if os.name == "nt":
+ wrapper_file = script.bin / "t1.exe"
else:
- wrapper_file = script.bin / 't1'
+ wrapper_file = script.bin / "t1"
result.did_create(wrapper_file)
-def test_wheel_compiles_pyc(script, shared_data, tmpdir):
+def test_wheel_compiles_pyc(
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test installing from wheel with --compile on
"""
- shutil.copy(
- shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir
- )
+ shutil.copy(shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir)
script.pip(
- "install", "--compile", "simple.dist==0.1", "--no-index",
- "--find-links", tmpdir,
+ "install",
+ "--compile",
+ "simple.dist==0.1",
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
# There are many locations for the __init__.pyc file so attempt to find
# any of them
exists = [
os.path.exists(script.site_packages_path / "simpledist/__init__.pyc"),
+ *script.site_packages_path.glob("simpledist/__pycache__/__init__*.pyc"),
]
-
- exists += glob.glob(
- script.site_packages_path / "simpledist/__pycache__/__init__*.pyc"
- )
-
assert any(exists)
-def test_wheel_no_compiles_pyc(script, shared_data, tmpdir):
+def test_wheel_no_compiles_pyc(
+ script: PipTestEnvironment, shared_data: TestData, tmpdir: Path
+) -> None:
"""
Test installing from wheel with --compile on
"""
- shutil.copy(
- shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir
- )
+ shutil.copy(shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir)
script.pip(
- "install", "--no-compile", "simple.dist==0.1", "--no-index",
- "--find-links", tmpdir,
+ "install",
+ "--no-compile",
+ "simple.dist==0.1",
+ "--no-index",
+ "--find-links",
+ tmpdir,
)
# There are many locations for the __init__.pyc file so attempt to find
# any of them
exists = [
os.path.exists(script.site_packages_path / "simpledist/__init__.pyc"),
+ *script.site_packages_path.glob("simpledist/__pycache__/__init__*.pyc"),
]
- exists += glob.glob(
- script.site_packages_path / "simpledist/__pycache__/__init__*.pyc"
- )
-
assert not any(exists)
-def test_install_from_wheel_uninstalls_old_version(script, data):
+def test_install_from_wheel_uninstalls_old_version(
+ script: PipTestEnvironment, data: TestData
+) -> None:
# regression test for https://github.com/pypa/pip/issues/1825
package = data.packages.joinpath("simplewheel-1.0-py2.py3-none-any.whl")
- result = script.pip('install', package, '--no-index')
+ result = script.pip("install", package, "--no-index")
package = data.packages.joinpath("simplewheel-2.0-py2.py3-none-any.whl")
- result = script.pip('install', package, '--no-index')
- dist_info_folder = script.site_packages / 'simplewheel-2.0.dist-info'
+ result = script.pip("install", package, "--no-index")
+ dist_info_folder = script.site_packages / "simplewheel-2.0.dist-info"
result.did_create(dist_info_folder)
- dist_info_folder = script.site_packages / 'simplewheel-1.0.dist-info'
+ dist_info_folder = script.site_packages / "simplewheel-1.0.dist-info"
result.did_not_create(dist_info_folder)
-def test_wheel_compile_syntax_error(script, data):
+def test_wheel_compile_syntax_error(script: PipTestEnvironment, data: TestData) -> None:
package = data.packages.joinpath("compilewheel-1.0-py2.py3-none-any.whl")
- result = script.pip('install', '--compile', package, '--no-index')
- assert 'yield from' not in result.stdout
- assert 'SyntaxError: ' not in result.stdout
+ result = script.pip("install", "--compile", package, "--no-index")
+ assert "yield from" not in result.stdout
+ assert "SyntaxError: " not in result.stdout
-def test_wheel_install_with_no_cache_dir(script, tmpdir, data):
- """Check wheel installations work, even with no cache.
- """
+def test_wheel_install_with_no_cache_dir(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ """Check wheel installations work, even with no cache."""
package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl")
- result = script.pip('install', '--no-cache-dir', '--no-index', package)
- result.assert_installed('simpledist', editable=False)
+ result = script.pip("install", "--no-cache-dir", "--no-index", package)
+ result.assert_installed("simpledist", editable=False)
-def test_wheel_install_fails_with_extra_dist_info(script):
+def test_wheel_install_fails_with_extra_dist_info(script: PipTestEnvironment) -> None:
package = create_basic_wheel_for_package(
script,
"simple",
"0.1.0",
extra_files={
"unrelated-2.0.0.dist-info/WHEEL": "Wheel-Version: 1.0",
- "unrelated-2.0.0.dist-info/METADATA": (
- "Name: unrelated\nVersion: 2.0.0\n"
- ),
+ "unrelated-2.0.0.dist-info/METADATA": ("Name: unrelated\nVersion: 2.0.0\n"),
},
)
result = script.pip(
@@ -593,11 +698,13 @@ def test_wheel_install_fails_with_extra_dist_info(script):
assert "multiple .dist-info directories" in result.stderr
-def test_wheel_install_fails_with_unrelated_dist_info(script):
+def test_wheel_install_fails_with_unrelated_dist_info(
+ script: PipTestEnvironment,
+) -> None:
package = create_basic_wheel_for_package(script, "simple", "0.1.0")
new_name = "unrelated-2.0.0-py2.py3-none-any.whl"
new_package = os.path.join(os.path.dirname(package), new_name)
- shutil.move(package, new_package)
+ shutil.move(os.fspath(package), new_package)
result = script.pip(
"install",
@@ -607,13 +714,10 @@ def test_wheel_install_fails_with_unrelated_dist_info(script):
expect_error=True,
)
- assert (
- "'simple-0.1.0.dist-info' does not start with 'unrelated'"
- in result.stderr
- )
+ assert "'simple-0.1.0.dist-info' does not start with 'unrelated'" in result.stderr
-def test_wheel_installs_ok_with_nested_dist_info(script):
+def test_wheel_installs_ok_with_nested_dist_info(script: PipTestEnvironment) -> None:
package = create_basic_wheel_for_package(
script,
"simple",
@@ -625,35 +729,29 @@ def test_wheel_installs_ok_with_nested_dist_info(script):
),
},
)
- script.pip(
- "install", "--no-cache-dir", "--no-index", package
- )
+ script.pip("install", "--no-cache-dir", "--no-index", package)
def test_wheel_installs_ok_with_badly_encoded_irrelevant_dist_info_file(
- script
-):
+ script: PipTestEnvironment,
+) -> None:
package = create_basic_wheel_for_package(
script,
"simple",
"0.1.0",
- extra_files={
- "simple-0.1.0.dist-info/AUTHORS.txt": b"\xff"
- },
- )
- script.pip(
- "install", "--no-cache-dir", "--no-index", package
+ extra_files={"simple-0.1.0.dist-info/AUTHORS.txt": b"\xff"},
)
+ script.pip("install", "--no-cache-dir", "--no-index", package)
-def test_wheel_install_fails_with_badly_encoded_metadata(script):
+def test_wheel_install_fails_with_badly_encoded_metadata(
+ script: PipTestEnvironment,
+) -> None:
package = create_basic_wheel_for_package(
script,
"simple",
"0.1.0",
- extra_files={
- "simple-0.1.0.dist-info/METADATA": b"\xff"
- },
+ extra_files={"simple-0.1.0.dist-info/METADATA": b"\xff"},
)
result = script.pip(
"install", "--no-cache-dir", "--no-index", package, expect_error=True
@@ -664,22 +762,24 @@ def test_wheel_install_fails_with_badly_encoded_metadata(script):
@pytest.mark.parametrize(
- 'package_name',
- ['simple-package', 'simple_package'],
+ "package_name",
+ ["simple-package", "simple_package"],
)
-def test_correct_package_name_while_creating_wheel_bug(script, package_name):
+def test_correct_package_name_while_creating_wheel_bug(
+ script: PipTestEnvironment, package_name: str
+) -> None:
"""Check that the package name is correctly named while creating
a .whl file with a given format
"""
- package = create_basic_wheel_for_package(script, package_name, '1.0')
+ package = create_basic_wheel_for_package(script, package_name, "1.0")
wheel_name = os.path.basename(package)
- assert wheel_name == 'simple_package-1.0-py2.py3-none-any.whl'
+ assert wheel_name == "simple_package-1.0-py2.py3-none-any.whl"
@pytest.mark.parametrize("name", ["purelib", "abc"])
def test_wheel_with_file_in_data_dir_has_reasonable_error(
- script, tmpdir, name
-):
+ script: PipTestEnvironment, tmpdir: Path, name: str
+) -> None:
"""Normally we expect entities in the .data directory to be in a
subdirectory, but if they are not then we should show a reasonable error
message that includes the path.
@@ -688,22 +788,16 @@ def test_wheel_with_file_in_data_dir_has_reasonable_error(
"simple", "0.1.0", extra_data_files={name: "hello world"}
).save_to_dir(tmpdir)
- result = script.pip(
- "install", "--no-index", str(wheel_path), expect_error=True
- )
+ result = script.pip("install", "--no-index", str(wheel_path), expect_error=True)
assert f"simple-0.1.0.data/{name}" in result.stderr
def test_wheel_with_unknown_subdir_in_data_dir_has_reasonable_error(
- script, tmpdir
-):
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
wheel_path = make_wheel(
- "simple",
- "0.1.0",
- extra_data_files={"unknown/hello.txt": "hello world"}
+ "simple", "0.1.0", extra_data_files={"unknown/hello.txt": "hello world"}
).save_to_dir(tmpdir)
- result = script.pip(
- "install", "--no-index", str(wheel_path), expect_error=True
- )
+ result = script.pip("install", "--no-index", str(wheel_path), expect_error=True)
assert "simple-0.1.0.data/unknown/hello.txt" in result.stderr
diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py
index 1732f93e6..c799aabd2 100644
--- a/tests/functional/test_list.py
+++ b/tests/functional/test_list.py
@@ -1,102 +1,123 @@
import json
import os
+from pathlib import Path
import pytest
-from tests.lib import create_test_package_with_setup, wheel
-from tests.lib.path import Path
+from pip._internal.models.direct_url import DirectUrl, DirInfo
+from tests.conftest import ScriptFactory
+from tests.lib import (
+ PipTestEnvironment,
+ TestData,
+ _create_test_package,
+ create_test_package_with_setup,
+ wheel,
+)
+from tests.lib.direct_url import get_created_direct_url_path
@pytest.fixture(scope="session")
-def simple_script(tmpdir_factory, script_factory, shared_data):
- tmpdir = Path(str(tmpdir_factory.mktemp("pip_test_package")))
+def simple_script(
+ tmpdir_factory: pytest.TempPathFactory,
+ script_factory: ScriptFactory,
+ shared_data: TestData,
+) -> PipTestEnvironment:
+ tmpdir = tmpdir_factory.mktemp("pip_test_package")
script = script_factory(tmpdir.joinpath("workspace"))
script.pip(
- 'install', '-f', shared_data.find_links, '--no-index', 'simple==1.0',
- 'simple2==3.0',
+ "install",
+ "-f",
+ shared_data.find_links,
+ "--no-index",
+ "simple==1.0",
+ "simple2==3.0",
)
return script
-def test_basic_list(simple_script):
+def test_basic_list(simple_script: PipTestEnvironment) -> None:
"""
Test default behavior of list command without format specifier.
"""
- result = simple_script.pip('list')
- assert 'simple 1.0' in result.stdout, str(result)
- assert 'simple2 3.0' in result.stdout, str(result)
+ result = simple_script.pip("list")
+ assert "simple 1.0" in result.stdout, str(result)
+ assert "simple2 3.0" in result.stdout, str(result)
-def test_verbose_flag(simple_script):
+def test_verbose_flag(simple_script: PipTestEnvironment) -> None:
"""
Test the list command with the '-v' option
"""
- result = simple_script.pip('list', '-v', '--format=columns')
- assert 'Package' in result.stdout, str(result)
- assert 'Version' in result.stdout, str(result)
- assert 'Location' in result.stdout, str(result)
- assert 'Installer' in result.stdout, str(result)
- assert 'simple 1.0' in result.stdout, str(result)
- assert 'simple2 3.0' in result.stdout, str(result)
+ result = simple_script.pip("list", "-v", "--format=columns")
+ assert "Package" in result.stdout, str(result)
+ assert "Version" in result.stdout, str(result)
+ assert "Location" in result.stdout, str(result)
+ assert "Installer" in result.stdout, str(result)
+ assert "simple 1.0" in result.stdout, str(result)
+ assert "simple2 3.0" in result.stdout, str(result)
-def test_columns_flag(simple_script):
+def test_columns_flag(simple_script: PipTestEnvironment) -> None:
"""
Test the list command with the '--format=columns' option
"""
- result = simple_script.pip('list', '--format=columns')
- assert 'Package' in result.stdout, str(result)
- assert 'Version' in result.stdout, str(result)
- assert 'simple (1.0)' not in result.stdout, str(result)
- assert 'simple 1.0' in result.stdout, str(result)
- assert 'simple2 3.0' in result.stdout, str(result)
+ result = simple_script.pip("list", "--format=columns")
+ assert "Package" in result.stdout, str(result)
+ assert "Version" in result.stdout, str(result)
+ assert "simple (1.0)" not in result.stdout, str(result)
+ assert "simple 1.0" in result.stdout, str(result)
+ assert "simple2 3.0" in result.stdout, str(result)
-def test_format_priority(simple_script):
+def test_format_priority(simple_script: PipTestEnvironment) -> None:
"""
Test that latest format has priority over previous ones.
"""
- result = simple_script.pip('list', '--format=columns', '--format=freeze',
- expect_stderr=True)
- assert 'simple==1.0' in result.stdout, str(result)
- assert 'simple2==3.0' in result.stdout, str(result)
- assert 'simple 1.0' not in result.stdout, str(result)
- assert 'simple2 3.0' not in result.stdout, str(result)
+ result = simple_script.pip(
+ "list", "--format=columns", "--format=freeze", expect_stderr=True
+ )
+ assert "simple==1.0" in result.stdout, str(result)
+ assert "simple2==3.0" in result.stdout, str(result)
+ assert "simple 1.0" not in result.stdout, str(result)
+ assert "simple2 3.0" not in result.stdout, str(result)
- result = simple_script.pip('list', '--format=freeze', '--format=columns')
- assert 'Package' in result.stdout, str(result)
- assert 'Version' in result.stdout, str(result)
- assert 'simple==1.0' not in result.stdout, str(result)
- assert 'simple2==3.0' not in result.stdout, str(result)
- assert 'simple 1.0' in result.stdout, str(result)
- assert 'simple2 3.0' in result.stdout, str(result)
+ result = simple_script.pip("list", "--format=freeze", "--format=columns")
+ assert "Package" in result.stdout, str(result)
+ assert "Version" in result.stdout, str(result)
+ assert "simple==1.0" not in result.stdout, str(result)
+ assert "simple2==3.0" not in result.stdout, str(result)
+ assert "simple 1.0" in result.stdout, str(result)
+ assert "simple2 3.0" in result.stdout, str(result)
-def test_local_flag(simple_script):
+def test_local_flag(simple_script: PipTestEnvironment) -> None:
"""
Test the behavior of --local flag in the list command
"""
- result = simple_script.pip('list', '--local', '--format=json')
+ result = simple_script.pip("list", "--local", "--format=json")
assert {"name": "simple", "version": "1.0"} in json.loads(result.stdout)
-def test_local_columns_flag(simple_script):
+def test_local_columns_flag(simple_script: PipTestEnvironment) -> None:
"""
Test the behavior of --local --format=columns flags in the list command
"""
- result = simple_script.pip('list', '--local', '--format=columns')
- assert 'Package' in result.stdout
- assert 'Version' in result.stdout
- assert 'simple (1.0)' not in result.stdout
- assert 'simple 1.0' in result.stdout, str(result)
+ result = simple_script.pip("list", "--local", "--format=columns")
+ assert "Package" in result.stdout
+ assert "Version" in result.stdout
+ assert "simple (1.0)" not in result.stdout
+ assert "simple 1.0" in result.stdout, str(result)
-def test_multiple_exclude_and_normalization(script, tmpdir):
- req_path = wheel.make_wheel(
- name="Normalizable_Name", version="1.0").save_to_dir(tmpdir)
+def test_multiple_exclude_and_normalization(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
+ req_path = wheel.make_wheel(name="Normalizable_Name", version="1.0").save_to_dir(
+ tmpdir
+ )
script.pip("install", "--no-index", req_path)
result = script.pip("list")
print(result.stdout)
@@ -109,434 +130,551 @@ def test_multiple_exclude_and_normalization(script, tmpdir):
@pytest.mark.network
@pytest.mark.incompatible_with_test_venv
-def test_user_flag(script, data):
+def test_user_flag(script: PipTestEnvironment, data: TestData) -> None:
"""
Test the behavior of --user flag in the list command
"""
- script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
- script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
- script.pip('install', '-f', data.find_links, '--no-index',
- '--user', 'simple2==2.0')
- result = script.pip('list', '--user', '--format=json')
- assert {"name": "simple", "version": "1.0"} \
- not in json.loads(result.stdout)
+ script.pip("download", "setuptools", "wheel", "-d", data.packages)
+ script.pip("install", "-f", data.find_links, "--no-index", "simple==1.0")
+ script.pip("install", "-f", data.find_links, "--no-index", "--user", "simple2==2.0")
+ result = script.pip("list", "--user", "--format=json")
+ assert {"name": "simple", "version": "1.0"} not in json.loads(result.stdout)
assert {"name": "simple2", "version": "2.0"} in json.loads(result.stdout)
@pytest.mark.network
@pytest.mark.incompatible_with_test_venv
-def test_user_columns_flag(script, data):
+def test_user_columns_flag(script: PipTestEnvironment, data: TestData) -> None:
"""
Test the behavior of --user --format=columns flags in the list command
"""
- script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
- script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
- script.pip('install', '-f', data.find_links, '--no-index',
- '--user', 'simple2==2.0')
- result = script.pip('list', '--user', '--format=columns')
- assert 'Package' in result.stdout
- assert 'Version' in result.stdout
- assert 'simple2 (2.0)' not in result.stdout
- assert 'simple2 2.0' in result.stdout, str(result)
+ script.pip("download", "setuptools", "wheel", "-d", data.packages)
+ script.pip("install", "-f", data.find_links, "--no-index", "simple==1.0")
+ script.pip("install", "-f", data.find_links, "--no-index", "--user", "simple2==2.0")
+ result = script.pip("list", "--user", "--format=columns")
+ assert "Package" in result.stdout
+ assert "Version" in result.stdout
+ assert "simple2 (2.0)" not in result.stdout
+ assert "simple2 2.0" in result.stdout, str(result)
@pytest.mark.network
-def test_uptodate_flag(script, data):
+def test_uptodate_flag(script: PipTestEnvironment, data: TestData) -> None:
"""
Test the behavior of --uptodate flag in the list command
"""
script.pip(
- 'install', '-f', data.find_links, '--no-index', 'simple==1.0',
- 'simple2==3.0',
+ "install",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "simple==1.0",
+ "simple2==3.0",
)
script.pip(
- 'install', '-e',
- 'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package'
+ "install",
+ "-e",
+ "git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package",
)
result = script.pip(
- 'list', '-f', data.find_links, '--no-index', '--uptodate',
- '--format=json',
- )
- assert {"name": "simple", "version": "1.0"} \
- not in json.loads(result.stdout) # 3.0 is latest
- assert {"name": "pip-test-package", "version": "0.1.1"} \
- in json.loads(result.stdout) # editables included
- assert {"name": "simple2", "version": "3.0"} in json.loads(result.stdout)
+ "list",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "--uptodate",
+ "--format=json",
+ )
+ json_output = json.loads(result.stdout)
+ for item in json_output:
+ if "editable_project_location" in item:
+ item["editable_project_location"] = "<location>"
+ assert {"name": "simple", "version": "1.0"} not in json_output # 3.0 is latest
+ assert {
+ "name": "pip-test-package",
+ "version": "0.1.1",
+ "editable_project_location": "<location>",
+ } in json_output # editables included
+ assert {"name": "simple2", "version": "3.0"} in json_output
@pytest.mark.network
-def test_uptodate_columns_flag(script, data):
+def test_uptodate_columns_flag(script: PipTestEnvironment, data: TestData) -> None:
"""
Test the behavior of --uptodate --format=columns flag in the list command
"""
script.pip(
- 'install', '-f', data.find_links, '--no-index', 'simple==1.0',
- 'simple2==3.0',
+ "install",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "simple==1.0",
+ "simple2==3.0",
)
script.pip(
- 'install', '-e',
- 'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package'
+ "install",
+ "-e",
+ "git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package",
)
result = script.pip(
- 'list', '-f', data.find_links, '--no-index', '--uptodate',
- '--format=columns',
+ "list",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "--uptodate",
+ "--format=columns",
)
- assert 'Package' in result.stdout
- assert 'Version' in result.stdout
- assert 'Location' in result.stdout # editables included
- assert 'pip-test-package (0.1.1,' not in result.stdout
- assert 'pip-test-package 0.1.1' in result.stdout, str(result)
- assert 'simple2 3.0' in result.stdout, str(result)
+ assert "Package" in result.stdout
+ assert "Version" in result.stdout
+ assert "Editable project location" in result.stdout # editables included
+ assert "pip-test-package (0.1.1," not in result.stdout
+ assert "pip-test-package 0.1.1" in result.stdout, str(result)
+ assert "simple2 3.0" in result.stdout, str(result)
@pytest.mark.network
-def test_outdated_flag(script, data):
+def test_outdated_flag(script: PipTestEnvironment, data: TestData) -> None:
"""
Test the behavior of --outdated flag in the list command
"""
script.pip(
- 'install', '-f', data.find_links, '--no-index', 'simple==1.0',
- 'simple2==3.0', 'simplewheel==1.0',
+ "install",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "simple==1.0",
+ "simple2==3.0",
+ "simplewheel==1.0",
)
script.pip(
- 'install', '-e',
- 'git+https://github.com/pypa/pip-test-package.git'
- '@0.1#egg=pip-test-package'
+ "install",
+ "-e",
+ "git+https://github.com/pypa/pip-test-package.git@0.1#egg=pip-test-package",
)
result = script.pip(
- 'list', '-f', data.find_links, '--no-index', '--outdated',
- '--format=json',
- )
- assert {"name": "simple", "version": "1.0",
- "latest_version": "3.0", "latest_filetype": "sdist"} \
- in json.loads(result.stdout)
- assert dict(name="simplewheel", version="1.0",
- latest_version="2.0", latest_filetype="wheel") \
- in json.loads(result.stdout)
- assert dict(name="pip-test-package", version="0.1",
- latest_version="0.1.1", latest_filetype="sdist") \
- in json.loads(result.stdout)
- assert "simple2" not in {p["name"] for p in json.loads(result.stdout)}
+ "list",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "--outdated",
+ "--format=json",
+ )
+ json_output = json.loads(result.stdout)
+ for item in json_output:
+ if "editable_project_location" in item:
+ item["editable_project_location"] = "<location>"
+ assert {
+ "name": "simple",
+ "version": "1.0",
+ "latest_version": "3.0",
+ "latest_filetype": "sdist",
+ } in json_output
+ assert (
+ dict(
+ name="simplewheel",
+ version="1.0",
+ latest_version="2.0",
+ latest_filetype="wheel",
+ )
+ in json_output
+ )
+ assert (
+ dict(
+ name="pip-test-package",
+ version="0.1",
+ latest_version="0.1.1",
+ latest_filetype="sdist",
+ editable_project_location="<location>",
+ )
+ in json_output
+ )
+ assert "simple2" not in {p["name"] for p in json_output}
@pytest.mark.network
-def test_outdated_columns_flag(script, data):
+def test_outdated_columns_flag(script: PipTestEnvironment, data: TestData) -> None:
"""
Test the behavior of --outdated --format=columns flag in the list command
"""
script.pip(
- 'install', '-f', data.find_links, '--no-index', 'simple==1.0',
- 'simple2==3.0', 'simplewheel==1.0',
+ "install",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "simple==1.0",
+ "simple2==3.0",
+ "simplewheel==1.0",
)
script.pip(
- 'install', '-e',
- 'git+https://github.com/pypa/pip-test-package.git'
- '@0.1#egg=pip-test-package'
+ "install",
+ "-e",
+ "git+https://github.com/pypa/pip-test-package.git@0.1#egg=pip-test-package",
)
result = script.pip(
- 'list', '-f', data.find_links, '--no-index', '--outdated',
- '--format=columns',
- )
- assert 'Package' in result.stdout
- assert 'Version' in result.stdout
- assert 'Latest' in result.stdout
- assert 'Type' in result.stdout
- assert 'simple (1.0) - Latest: 3.0 [sdist]' not in result.stdout
- assert 'simplewheel (1.0) - Latest: 2.0 [wheel]' not in result.stdout
- assert 'simple 1.0 3.0 sdist' in result.stdout, (
- str(result)
- )
- assert 'simplewheel 1.0 2.0 wheel' in result.stdout, (
- str(result)
- )
- assert 'simple2' not in result.stdout, str(result) # 3.0 is latest
+ "list",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "--outdated",
+ "--format=columns",
+ )
+ assert "Package" in result.stdout
+ assert "Version" in result.stdout
+ assert "Latest" in result.stdout
+ assert "Type" in result.stdout
+ assert "simple (1.0) - Latest: 3.0 [sdist]" not in result.stdout
+ assert "simplewheel (1.0) - Latest: 2.0 [wheel]" not in result.stdout
+ assert "simple 1.0 3.0 sdist" in result.stdout, str(result)
+ assert "simplewheel 1.0 2.0 wheel" in result.stdout, str(result)
+ assert "simple2" not in result.stdout, str(result) # 3.0 is latest
@pytest.fixture(scope="session")
-def pip_test_package_script(tmpdir_factory, script_factory, shared_data):
- tmpdir = Path(str(tmpdir_factory.mktemp("pip_test_package")))
+def pip_test_package_script(
+ tmpdir_factory: pytest.TempPathFactory,
+ script_factory: ScriptFactory,
+ shared_data: TestData,
+) -> PipTestEnvironment:
+ tmpdir = tmpdir_factory.mktemp("pip_test_package")
script = script_factory(tmpdir.joinpath("workspace"))
+ script.pip("install", "-f", shared_data.find_links, "--no-index", "simple==1.0")
script.pip(
- 'install', '-f', shared_data.find_links, '--no-index', 'simple==1.0'
- )
- script.pip(
- 'install', '-e',
- 'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package'
+ "install",
+ "-e",
+ "git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package",
)
return script
@pytest.mark.network
-def test_editables_flag(pip_test_package_script):
+def test_editables_flag(pip_test_package_script: PipTestEnvironment) -> None:
"""
Test the behavior of --editables flag in the list command
"""
- result = pip_test_package_script.pip('list', '--editable', '--format=json')
- result2 = pip_test_package_script.pip('list', '--editable')
- assert {"name": "simple", "version": "1.0"} \
- not in json.loads(result.stdout)
- assert os.path.join('src', 'pip-test-package') in result2.stdout
+ result = pip_test_package_script.pip("list", "--editable", "--format=json")
+ result2 = pip_test_package_script.pip("list", "--editable")
+ assert {"name": "simple", "version": "1.0"} not in json.loads(result.stdout)
+ assert os.path.join("src", "pip-test-package") in result2.stdout
@pytest.mark.network
-def test_exclude_editable_flag(pip_test_package_script):
+def test_exclude_editable_flag(pip_test_package_script: PipTestEnvironment) -> None:
"""
Test the behavior of --editables flag in the list command
"""
- result = pip_test_package_script.pip(
- 'list', '--exclude-editable', '--format=json'
- )
+ result = pip_test_package_script.pip("list", "--exclude-editable", "--format=json")
assert {"name": "simple", "version": "1.0"} in json.loads(result.stdout)
- assert "pip-test-package" \
- not in {p["name"] for p in json.loads(result.stdout)}
+ assert "pip-test-package" not in {p["name"] for p in json.loads(result.stdout)}
@pytest.mark.network
-def test_editables_columns_flag(pip_test_package_script):
+def test_editables_columns_flag(pip_test_package_script: PipTestEnvironment) -> None:
"""
Test the behavior of --editables flag in the list command
"""
- result = pip_test_package_script.pip(
- 'list', '--editable', '--format=columns'
- )
- assert 'Package' in result.stdout
- assert 'Version' in result.stdout
- assert 'Location' in result.stdout
- assert os.path.join('src', 'pip-test-package') in result.stdout, (
- str(result)
- )
+ result = pip_test_package_script.pip("list", "--editable", "--format=columns")
+ assert "Package" in result.stdout
+ assert "Version" in result.stdout
+ assert "Editable project location" in result.stdout
+ assert os.path.join("src", "pip-test-package") in result.stdout, str(result)
@pytest.mark.network
-def test_uptodate_editables_flag(pip_test_package_script, data):
+def test_uptodate_editables_flag(
+ pip_test_package_script: PipTestEnvironment, data: TestData
+) -> None:
"""
test the behavior of --editable --uptodate flag in the list command
"""
result = pip_test_package_script.pip(
- 'list', '-f', data.find_links, '--no-index',
- '--editable', '--uptodate',
- )
- assert 'simple' not in result.stdout
- assert os.path.join('src', 'pip-test-package') in result.stdout, (
- str(result)
+ "list",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "--editable",
+ "--uptodate",
)
+ assert "simple" not in result.stdout
+ assert os.path.join("src", "pip-test-package") in result.stdout, str(result)
@pytest.mark.network
-def test_uptodate_editables_columns_flag(pip_test_package_script, data):
+def test_uptodate_editables_columns_flag(
+ pip_test_package_script: PipTestEnvironment, data: TestData
+) -> None:
"""
test the behavior of --editable --uptodate --format=columns flag in the
list command
"""
result = pip_test_package_script.pip(
- 'list', '-f', data.find_links, '--no-index',
- '--editable', '--uptodate', '--format=columns',
- )
- assert 'Package' in result.stdout
- assert 'Version' in result.stdout
- assert 'Location' in result.stdout
- assert os.path.join('src', 'pip-test-package') in result.stdout, (
- str(result)
+ "list",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "--editable",
+ "--uptodate",
+ "--format=columns",
)
+ assert "Package" in result.stdout
+ assert "Version" in result.stdout
+ assert "Editable project location" in result.stdout
+ assert os.path.join("src", "pip-test-package") in result.stdout, str(result)
@pytest.mark.network
-def test_outdated_editables_flag(script, data):
+def test_outdated_editables_flag(script: PipTestEnvironment, data: TestData) -> None:
"""
test the behavior of --editable --outdated flag in the list command
"""
- script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+ script.pip("install", "-f", data.find_links, "--no-index", "simple==1.0")
result = script.pip(
- 'install', '-e',
- 'git+https://github.com/pypa/pip-test-package.git'
- '@0.1#egg=pip-test-package'
+ "install",
+ "-e",
+ "git+https://github.com/pypa/pip-test-package.git@0.1#egg=pip-test-package",
)
result = script.pip(
- 'list', '-f', data.find_links, '--no-index',
- '--editable', '--outdated',
+ "list",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "--editable",
+ "--outdated",
)
- assert 'simple' not in result.stdout
- assert os.path.join('src', 'pip-test-package') in result.stdout
+ assert "simple" not in result.stdout
+ assert os.path.join("src", "pip-test-package") in result.stdout
@pytest.mark.network
-def test_outdated_editables_columns_flag(script, data):
+def test_outdated_editables_columns_flag(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
test the behavior of --editable --outdated flag in the list command
"""
- script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+ script.pip("install", "-f", data.find_links, "--no-index", "simple==1.0")
result = script.pip(
- 'install', '-e',
- 'git+https://github.com/pypa/pip-test-package.git'
- '@0.1#egg=pip-test-package'
+ "install",
+ "-e",
+ "git+https://github.com/pypa/pip-test-package.git@0.1#egg=pip-test-package",
)
result = script.pip(
- 'list', '-f', data.find_links, '--no-index',
- '--editable', '--outdated', '--format=columns',
- )
- assert 'Package' in result.stdout
- assert 'Version' in result.stdout
- assert 'Location' in result.stdout
- assert os.path.join('src', 'pip-test-package') in result.stdout, (
- str(result)
+ "list",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "--editable",
+ "--outdated",
+ "--format=columns",
)
+ assert "Package" in result.stdout
+ assert "Version" in result.stdout
+ assert "Editable project location" in result.stdout
+ assert os.path.join("src", "pip-test-package") in result.stdout, str(result)
-def test_outdated_not_required_flag(script, data):
+def test_outdated_not_required_flag(script: PipTestEnvironment, data: TestData) -> None:
"""
test the behavior of --outdated --not-required flag in the list command
"""
script.pip(
- 'install', '-f', data.find_links, '--no-index',
- 'simple==2.0', 'require_simple==1.0'
+ "install",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "simple==2.0",
+ "require_simple==1.0",
)
result = script.pip(
- 'list', '-f', data.find_links, '--no-index', '--outdated',
- '--not-required', '--format=json',
+ "list",
+ "-f",
+ data.find_links,
+ "--no-index",
+ "--outdated",
+ "--not-required",
+ "--format=json",
)
assert [] == json.loads(result.stdout)
-def test_outdated_pre(script, data):
- script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+def test_outdated_pre(script: PipTestEnvironment, data: TestData) -> None:
+ script.pip("install", "-f", data.find_links, "--no-index", "simple==1.0")
# Let's build a fake wheelhouse
script.scratch_path.joinpath("wheelhouse").mkdir()
- wheelhouse_path = script.scratch_path / 'wheelhouse'
- wheelhouse_path.joinpath('simple-1.1-py2.py3-none-any.whl').write_text('')
- wheelhouse_path.joinpath(
- 'simple-2.0.dev0-py2.py3-none-any.whl'
- ).write_text('')
+ wheelhouse_path = script.scratch_path / "wheelhouse"
+ wheelhouse_path.joinpath("simple-1.1-py2.py3-none-any.whl").write_text("")
+ wheelhouse_path.joinpath("simple-2.0.dev0-py2.py3-none-any.whl").write_text("")
result = script.pip(
- 'list', '--no-index', '--find-links', wheelhouse_path,
- '--format=json',
+ "list",
+ "--no-index",
+ "--find-links",
+ wheelhouse_path,
+ "--format=json",
)
assert {"name": "simple", "version": "1.0"} in json.loads(result.stdout)
result = script.pip(
- 'list', '--no-index', '--find-links', wheelhouse_path, '--outdated',
- '--format=json',
- )
- assert {"name": "simple", "version": "1.0",
- "latest_version": "1.1", "latest_filetype": "wheel"} \
- in json.loads(result.stdout)
- result_pre = script.pip('list', '--no-index',
- '--find-links', wheelhouse_path,
- '--outdated', '--pre', '--format=json')
- assert {"name": "simple", "version": "1.0",
- "latest_version": "2.0.dev0", "latest_filetype": "wheel"} \
- in json.loads(result_pre.stdout)
-
-
-def test_outdated_formats(script, data):
- """ Test of different outdated formats """
- script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+ "list",
+ "--no-index",
+ "--find-links",
+ wheelhouse_path,
+ "--outdated",
+ "--format=json",
+ )
+ assert {
+ "name": "simple",
+ "version": "1.0",
+ "latest_version": "1.1",
+ "latest_filetype": "wheel",
+ } in json.loads(result.stdout)
+ result_pre = script.pip(
+ "list",
+ "--no-index",
+ "--find-links",
+ wheelhouse_path,
+ "--outdated",
+ "--pre",
+ "--format=json",
+ )
+ assert {
+ "name": "simple",
+ "version": "1.0",
+ "latest_version": "2.0.dev0",
+ "latest_filetype": "wheel",
+ } in json.loads(result_pre.stdout)
+
+
+def test_outdated_formats(script: PipTestEnvironment, data: TestData) -> None:
+ """Test of different outdated formats"""
+ script.pip("install", "-f", data.find_links, "--no-index", "simple==1.0")
# Let's build a fake wheelhouse
script.scratch_path.joinpath("wheelhouse").mkdir()
- wheelhouse_path = script.scratch_path / 'wheelhouse'
- wheelhouse_path.joinpath('simple-1.1-py2.py3-none-any.whl').write_text('')
+ wheelhouse_path = script.scratch_path / "wheelhouse"
+ wheelhouse_path.joinpath("simple-1.1-py2.py3-none-any.whl").write_text("")
result = script.pip(
- 'list', '--no-index', '--find-links', wheelhouse_path,
- '--format=freeze',
+ "list",
+ "--no-index",
+ "--find-links",
+ wheelhouse_path,
+ "--format=freeze",
)
- assert 'simple==1.0' in result.stdout
+ assert "simple==1.0" in result.stdout
# Check columns
result = script.pip(
- 'list', '--no-index', '--find-links', wheelhouse_path,
- '--outdated', '--format=columns',
+ "list",
+ "--no-index",
+ "--find-links",
+ wheelhouse_path,
+ "--outdated",
+ "--format=columns",
)
- assert 'Package Version Latest Type' in result.stdout
- assert 'simple 1.0 1.1 wheel' in result.stdout
+ assert "Package Version Latest Type" in result.stdout
+ assert "simple 1.0 1.1 wheel" in result.stdout
# Check freeze
result = script.pip(
- 'list', '--no-index', '--find-links', wheelhouse_path,
- '--outdated', '--format=freeze',
+ "list",
+ "--no-index",
+ "--find-links",
+ wheelhouse_path,
+ "--outdated",
+ "--format=freeze",
)
- assert 'simple==1.0' in result.stdout
+ assert "simple==1.0" in result.stdout
# Check json
result = script.pip(
- 'list', '--no-index', '--find-links', wheelhouse_path,
- '--outdated', '--format=json',
+ "list",
+ "--no-index",
+ "--find-links",
+ wheelhouse_path,
+ "--outdated",
+ "--format=json",
)
data = json.loads(result.stdout)
- assert data == [{'name': 'simple', 'version': '1.0',
- 'latest_version': '1.1', 'latest_filetype': 'wheel'}]
+ assert data == [
+ {
+ "name": "simple",
+ "version": "1.0",
+ "latest_version": "1.1",
+ "latest_filetype": "wheel",
+ }
+ ]
-def test_not_required_flag(script, data):
- script.pip(
- 'install', '-f', data.find_links, '--no-index', 'TopoRequires4'
- )
- result = script.pip('list', '--not-required', expect_stderr=True)
- assert 'TopoRequires4 ' in result.stdout, str(result)
- assert 'TopoRequires ' not in result.stdout
- assert 'TopoRequires2 ' not in result.stdout
- assert 'TopoRequires3 ' not in result.stdout
+def test_not_required_flag(script: PipTestEnvironment, data: TestData) -> None:
+ script.pip("install", "-f", data.find_links, "--no-index", "TopoRequires4")
+ result = script.pip("list", "--not-required", expect_stderr=True)
+ assert "TopoRequires4 " in result.stdout, str(result)
+ assert "TopoRequires " not in result.stdout
+ assert "TopoRequires2 " not in result.stdout
+ assert "TopoRequires3 " not in result.stdout
-def test_list_freeze(simple_script):
+def test_list_freeze(simple_script: PipTestEnvironment) -> None:
"""
Test freeze formatting of list command
"""
- result = simple_script.pip('list', '--format=freeze')
- assert 'simple==1.0' in result.stdout, str(result)
- assert 'simple2==3.0' in result.stdout, str(result)
+ result = simple_script.pip("list", "--format=freeze")
+ assert "simple==1.0" in result.stdout, str(result)
+ assert "simple2==3.0" in result.stdout, str(result)
-def test_list_json(simple_script):
+def test_list_json(simple_script: PipTestEnvironment) -> None:
"""
Test json formatting of list command
"""
- result = simple_script.pip('list', '--format=json')
+ result = simple_script.pip("list", "--format=json")
data = json.loads(result.stdout)
- assert {'name': 'simple', 'version': '1.0'} in data
- assert {'name': 'simple2', 'version': '3.0'} in data
+ assert {"name": "simple", "version": "1.0"} in data
+ assert {"name": "simple2", "version": "3.0"} in data
-def test_list_path(tmpdir, script, data):
+def test_list_path(tmpdir: Path, script: PipTestEnvironment, data: TestData) -> None:
"""
Test list with --path.
"""
- result = script.pip('list', '--path', tmpdir, '--format=json')
+ result = script.pip("list", "--path", tmpdir, "--format=json")
json_result = json.loads(result.stdout)
- assert {'name': 'simple', 'version': '2.0'} not in json_result
+ assert {"name": "simple", "version": "2.0"} not in json_result
- script.pip_install_local('--target', tmpdir, 'simple==2.0')
- result = script.pip('list', '--path', tmpdir, '--format=json')
+ script.pip_install_local("--target", tmpdir, "simple==2.0")
+ result = script.pip("list", "--path", tmpdir, "--format=json")
json_result = json.loads(result.stdout)
- assert {'name': 'simple', 'version': '2.0'} in json_result
+ assert {"name": "simple", "version": "2.0"} in json_result
@pytest.mark.incompatible_with_test_venv
-def test_list_path_exclude_user(tmpdir, script, data):
+def test_list_path_exclude_user(
+ tmpdir: Path, script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test list with --path and make sure packages from --user are not picked
up.
"""
- script.pip_install_local('--user', 'simple2')
- script.pip_install_local('--target', tmpdir, 'simple==1.0')
+ script.pip_install_local("--user", "simple2")
+ script.pip_install_local("--target", tmpdir, "simple==1.0")
- result = script.pip('list', '--user', '--format=json')
+ result = script.pip("list", "--user", "--format=json")
json_result = json.loads(result.stdout)
- assert {'name': 'simple2', 'version': '3.0'} in json_result
+ assert {"name": "simple2", "version": "3.0"} in json_result
- result = script.pip('list', '--path', tmpdir, '--format=json')
+ result = script.pip("list", "--path", tmpdir, "--format=json")
json_result = json.loads(result.stdout)
- assert {'name': 'simple', 'version': '1.0'} in json_result
+ assert {"name": "simple", "version": "1.0"} in json_result
-def test_list_path_multiple(tmpdir, script, data):
+def test_list_path_multiple(
+ tmpdir: Path, script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test list with multiple --path arguments.
"""
@@ -545,53 +683,74 @@ def test_list_path_multiple(tmpdir, script, data):
path2 = tmpdir / "path2"
os.mkdir(path2)
- script.pip_install_local('--target', path1, 'simple==2.0')
- script.pip_install_local('--target', path2, 'simple2==3.0')
+ script.pip_install_local("--target", path1, "simple==2.0")
+ script.pip_install_local("--target", path2, "simple2==3.0")
- result = script.pip('list', '--path', path1, '--format=json')
+ result = script.pip("list", "--path", path1, "--format=json")
json_result = json.loads(result.stdout)
- assert {'name': 'simple', 'version': '2.0'} in json_result
+ assert {"name": "simple", "version": "2.0"} in json_result
- result = script.pip('list', '--path', path1, '--path', path2,
- '--format=json')
+ result = script.pip("list", "--path", path1, "--path", path2, "--format=json")
json_result = json.loads(result.stdout)
- assert {'name': 'simple', 'version': '2.0'} in json_result
- assert {'name': 'simple2', 'version': '3.0'} in json_result
+ assert {"name": "simple", "version": "2.0"} in json_result
+ assert {"name": "simple2", "version": "3.0"} in json_result
-def test_list_skip_work_dir_pkg(script):
+def test_list_skip_work_dir_pkg(script: PipTestEnvironment) -> None:
"""
Test that list should not include package in working directory
"""
# Create a test package and create .egg-info dir
- pkg_path = create_test_package_with_setup(
- script, name='simple', version='1.0')
- script.run('python', 'setup.py', 'egg_info',
- expect_stderr=True, cwd=pkg_path)
+ pkg_path = create_test_package_with_setup(script, name="simple", version="1.0")
+ script.run("python", "setup.py", "egg_info", expect_stderr=True, cwd=pkg_path)
# List should not include package simple when run from package directory
- result = script.pip('list', '--format=json', cwd=pkg_path)
+ result = script.pip("list", "--format=json", cwd=pkg_path)
json_result = json.loads(result.stdout)
- assert {'name': 'simple', 'version': '1.0'} not in json_result
+ assert {"name": "simple", "version": "1.0"} not in json_result
-def test_list_include_work_dir_pkg(script):
+def test_list_include_work_dir_pkg(script: PipTestEnvironment) -> None:
"""
Test that list should include package in working directory
if working directory is added in PYTHONPATH
"""
# Create a test package and create .egg-info dir
- pkg_path = create_test_package_with_setup(
- script, name='simple', version='1.0')
- script.run('python', 'setup.py', 'egg_info',
- expect_stderr=True, cwd=pkg_path)
+ pkg_path = create_test_package_with_setup(script, name="simple", version="1.0")
+ script.run("python", "setup.py", "egg_info", expect_stderr=True, cwd=pkg_path)
- script.environ.update({'PYTHONPATH': pkg_path})
+ script.environ.update({"PYTHONPATH": pkg_path})
# List should include package simple when run from package directory
# when the package directory is in PYTHONPATH
- result = script.pip('list', '--format=json', cwd=pkg_path)
+ result = script.pip("list", "--format=json", cwd=pkg_path)
json_result = json.loads(result.stdout)
- assert {'name': 'simple', 'version': '1.0'} in json_result
+ assert {"name": "simple", "version": "1.0"} in json_result
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_list_pep610_editable(script: PipTestEnvironment) -> None:
+ """
+ Test that a package installed with a direct_url.json with editable=true
+ is correctly listed as editable.
+ """
+ pkg_path = _create_test_package(script.scratch_path, name="testpkg")
+ result = script.pip("install", pkg_path)
+ direct_url_path = get_created_direct_url_path(result, "testpkg")
+ assert direct_url_path
+ # patch direct_url.json to simulate an editable install
+ with open(direct_url_path) as f:
+ direct_url = DirectUrl.from_json(f.read())
+ assert isinstance(direct_url.info, DirInfo)
+ direct_url.info.editable = True
+ with open(direct_url_path, "w") as f:
+ f.write(direct_url.to_json())
+ result = script.pip("list", "--format=json")
+ for item in json.loads(result.stdout):
+ if item["name"] == "testpkg":
+ assert item["editable_project_location"]
+ break
+ else:
+ assert False, "package 'testpkg' not found in pip list result"
diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py
index 1dca963e1..fc52ab9c8 100644
--- a/tests/functional/test_new_resolver.py
+++ b/tests/functional/test_new_resolver.py
@@ -2,43 +2,38 @@ import os
import pathlib
import sys
import textwrap
+from typing import TYPE_CHECKING, Callable, Dict, List, Tuple
import pytest
from tests.lib import (
+ PipTestEnvironment,
create_basic_sdist_for_package,
create_basic_wheel_for_package,
create_test_package_with_setup,
- path_to_url,
)
from tests.lib.direct_url import get_created_direct_url
-from tests.lib.path import Path
from tests.lib.wheel import make_wheel
+if TYPE_CHECKING:
+ from typing import Protocol
-# TODO: Remove me.
-def assert_installed(script, **kwargs):
- script.assert_installed(**kwargs)
+MakeFakeWheel = Callable[[str, str, str], pathlib.Path]
-# TODO: Remove me.
-def assert_not_installed(script, *args):
- script.assert_not_installed(*args)
-
-
-def assert_editable(script, *args):
+def assert_editable(script: PipTestEnvironment, *args: str) -> None:
# This simply checks whether all of the listed packages have a
# corresponding .egg-link file installed.
# TODO: Implement a more rigorous way to test for editable installations.
egg_links = {f"{arg}.egg-link" for arg in args}
- assert egg_links <= set(os.listdir(script.site_packages_path)), \
- f"{args!r} not all found in {script.site_packages_path!r}"
+ assert egg_links <= set(
+ os.listdir(script.site_packages_path)
+ ), f"{args!r} not all found in {script.site_packages_path!r}"
@pytest.fixture()
-def make_fake_wheel(script):
-
- def _make_fake_wheel(name, version, wheel_tag):
+def make_fake_wheel(script: PipTestEnvironment) -> MakeFakeWheel:
+ def _make_fake_wheel(name: str, version: str, wheel_tag: str) -> pathlib.Path:
wheel_house = script.scratch_path.joinpath("wheelhouse")
wheel_house.mkdir()
wheel_builder = make_wheel(
@@ -53,7 +48,7 @@ def make_fake_wheel(script):
return _make_fake_wheel
-def test_new_resolver_can_install(script):
+def test_new_resolver_can_install(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
"simple",
@@ -61,14 +56,16 @@ def test_new_resolver_can_install(script):
)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "simple"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "simple",
)
- assert_installed(script, simple="0.1.0")
+ script.assert_installed(simple="0.1.0")
-def test_new_resolver_can_install_with_version(script):
+def test_new_resolver_can_install_with_version(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
"simple",
@@ -76,14 +73,16 @@ def test_new_resolver_can_install_with_version(script):
)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "simple==0.1.0"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "simple==0.1.0",
)
- assert_installed(script, simple="0.1.0")
+ script.assert_installed(simple="0.1.0")
-def test_new_resolver_picks_latest_version(script):
+def test_new_resolver_picks_latest_version(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
"simple",
@@ -96,14 +95,16 @@ def test_new_resolver_picks_latest_version(script):
)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "simple"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "simple",
)
- assert_installed(script, simple="0.2.0")
+ script.assert_installed(simple="0.2.0")
-def test_new_resolver_picks_installed_version(script):
+def test_new_resolver_picks_installed_version(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
"simple",
@@ -116,23 +117,29 @@ def test_new_resolver_picks_installed_version(script):
)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "simple==0.1.0"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "simple==0.1.0",
)
- assert_installed(script, simple="0.1.0")
+ script.assert_installed(simple="0.1.0")
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "simple"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "simple",
)
assert "Collecting" not in result.stdout, "Should not fetch new version"
- assert_installed(script, simple="0.1.0")
+ script.assert_installed(simple="0.1.0")
-def test_new_resolver_picks_installed_version_if_no_match_found(script):
+def test_new_resolver_picks_installed_version_if_no_match_found(
+ script: PipTestEnvironment,
+) -> None:
create_basic_wheel_for_package(
script,
"simple",
@@ -145,22 +152,20 @@ def test_new_resolver_picks_installed_version_if_no_match_found(script):
)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "simple==0.1.0"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "simple==0.1.0",
)
- assert_installed(script, simple="0.1.0")
+ script.assert_installed(simple="0.1.0")
- result = script.pip(
- "install",
- "--no-cache-dir", "--no-index",
- "simple"
- )
+ result = script.pip("install", "--no-cache-dir", "--no-index", "simple")
assert "Collecting" not in result.stdout, "Should not fetch new version"
- assert_installed(script, simple="0.1.0")
+ script.assert_installed(simple="0.1.0")
-def test_new_resolver_installs_dependencies(script):
+def test_new_resolver_installs_dependencies(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
"base",
@@ -174,14 +179,16 @@ def test_new_resolver_installs_dependencies(script):
)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "base"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "base",
)
- assert_installed(script, base="0.1.0", dep="0.1.0")
+ script.assert_installed(base="0.1.0", dep="0.1.0")
-def test_new_resolver_ignore_dependencies(script):
+def test_new_resolver_ignore_dependencies(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
"base",
@@ -195,12 +202,15 @@ def test_new_resolver_ignore_dependencies(script):
)
script.pip(
"install",
- "--no-cache-dir", "--no-index", "--no-deps",
- "--find-links", script.scratch_path,
- "base"
+ "--no-cache-dir",
+ "--no-index",
+ "--no-deps",
+ "--find-links",
+ script.scratch_path,
+ "base",
)
- assert_installed(script, base="0.1.0")
- assert_not_installed(script, "dep")
+ script.assert_installed(base="0.1.0")
+ script.assert_not_installed("dep")
@pytest.mark.parametrize(
@@ -210,7 +220,9 @@ def test_new_resolver_ignore_dependencies(script):
"base[add] >= 0.1.0",
],
)
-def test_new_resolver_installs_extras(tmpdir, script, root_dep):
+def test_new_resolver_installs_extras(
+ tmpdir: pathlib.Path, script: PipTestEnvironment, root_dep: str
+) -> None:
req_file = tmpdir.joinpath("requirements.txt")
req_file.write_text(root_dep)
@@ -227,14 +239,17 @@ def test_new_resolver_installs_extras(tmpdir, script, root_dep):
)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "-r", req_file,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "-r",
+ req_file,
)
- assert_installed(script, base="0.1.0", dep="0.1.0")
+ script.assert_installed(base="0.1.0", dep="0.1.0")
-def test_new_resolver_installs_extras_warn_missing(script):
+def test_new_resolver_installs_extras_warn_missing(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
"base",
@@ -248,34 +263,40 @@ def test_new_resolver_installs_extras_warn_missing(script):
)
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base[add,missing]",
expect_stderr=True,
)
assert "does not provide the extra" in result.stderr, str(result)
assert "missing" in result.stderr, str(result)
- assert_installed(script, base="0.1.0", dep="0.1.0")
+ script.assert_installed(base="0.1.0", dep="0.1.0")
-def test_new_resolver_installed_message(script):
+def test_new_resolver_installed_message(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(script, "A", "1.0")
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"A",
expect_stderr=False,
)
assert "Successfully installed A-1.0" in result.stdout, str(result)
-def test_new_resolver_no_dist_message(script):
+def test_new_resolver_no_dist_message(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(script, "A", "1.0")
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"B",
expect_error=True,
expect_stderr=True,
@@ -286,12 +307,13 @@ def test_new_resolver_no_dist_message(script):
# requirement xxx (from versions: none)
# ERROR: No matching distribution found for xxx
- assert "Could not find a version that satisfies the requirement B" \
- in result.stderr, str(result)
+ assert (
+ "Could not find a version that satisfies the requirement B" in result.stderr
+ ), str(result)
assert "No matching distribution found for B" in result.stderr, str(result)
-def test_new_resolver_installs_editable(script):
+def test_new_resolver_installs_editable(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
"base",
@@ -305,12 +327,15 @@ def test_new_resolver_installs_editable(script):
)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base",
- "--editable", source_dir,
+ "--editable",
+ source_dir,
)
- assert_installed(script, base="0.1.0", dep="0.1.0")
+ script.assert_installed(base="0.1.0", dep="0.1.0")
assert_editable(script, "dep")
@@ -320,18 +345,17 @@ def test_new_resolver_installs_editable(script):
# Something impossible to satisfy.
("<2", False, "0.1.0"),
("<2", True, "0.2.0"),
-
# Something guaranteed to satisfy.
(">=2", False, "0.2.0"),
(">=2", True, "0.2.0"),
],
)
def test_new_resolver_requires_python(
- script,
- requires_python,
- ignore_requires_python,
- dep_version,
-):
+ script: PipTestEnvironment,
+ requires_python: str,
+ ignore_requires_python: bool,
+ dep_version: str,
+) -> None:
create_basic_wheel_for_package(
script,
"base",
@@ -354,7 +378,8 @@ def test_new_resolver_requires_python(
"install",
"--no-cache-dir",
"--no-index",
- "--find-links", script.scratch_path,
+ "--find-links",
+ os.fspath(script.scratch_path),
]
if ignore_requires_python:
args.append("--ignore-requires-python")
@@ -362,10 +387,10 @@ def test_new_resolver_requires_python(
script.pip(*args)
- assert_installed(script, base="0.1.0", dep=dep_version)
+ script.assert_installed(base="0.1.0", dep=dep_version)
-def test_new_resolver_requires_python_error(script):
+def test_new_resolver_requires_python_error(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
"base",
@@ -374,8 +399,10 @@ def test_new_resolver_requires_python_error(script):
)
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base",
expect_error=True,
)
@@ -387,7 +414,7 @@ def test_new_resolver_requires_python_error(script):
assert message in result.stderr, str(result)
-def test_new_resolver_installed(script):
+def test_new_resolver_installed(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
"base",
@@ -402,27 +429,29 @@ def test_new_resolver_installed(script):
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base",
)
assert "Requirement already satisfied" not in result.stdout, str(result)
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base~=0.1.0",
)
- assert "Requirement already satisfied: base~=0.1.0" in result.stdout, \
- str(result)
+ assert "Requirement already satisfied: base~=0.1.0" in result.stdout, str(result)
result.did_not_update(
- script.site_packages / "base",
- message="base 0.1.0 reinstalled"
+ script.site_packages / "base", message="base 0.1.0 reinstalled"
)
-def test_new_resolver_ignore_installed(script):
+def test_new_resolver_ignore_installed(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
"base",
@@ -432,26 +461,32 @@ def test_new_resolver_ignore_installed(script):
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base",
)
assert satisfied_output not in result.stdout, str(result)
result = script.pip(
"install",
- "--no-cache-dir", "--no-index", "--ignore-installed",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--ignore-installed",
+ "--find-links",
+ script.scratch_path,
"base",
)
assert satisfied_output not in result.stdout, str(result)
result.did_update(
- script.site_packages / "base",
- message="base 0.1.0 not reinstalled"
+ script.site_packages / "base", message="base 0.1.0 not reinstalled"
)
-def test_new_resolver_only_builds_sdists_when_needed(script):
+def test_new_resolver_only_builds_sdists_when_needed(
+ script: PipTestEnvironment,
+) -> None:
create_basic_wheel_for_package(
script,
"base",
@@ -473,57 +508,65 @@ def test_new_resolver_only_builds_sdists_when_needed(script):
# We only ever need to check dep 0.2.0 as it's the latest version
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "base"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "base",
)
- assert_installed(script, base="0.1.0", dep="0.2.0")
+ script.assert_installed(base="0.1.0", dep="0.2.0")
# We merge criteria here, as we have two "dep" requirements
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "base", "dep"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "base",
+ "dep",
)
- assert_installed(script, base="0.1.0", dep="0.2.0")
+ script.assert_installed(base="0.1.0", dep="0.2.0")
-def test_new_resolver_install_different_version(script):
+def test_new_resolver_install_different_version(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(script, "base", "0.1.0")
create_basic_wheel_for_package(script, "base", "0.2.0")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base==0.1.0",
)
# This should trigger an uninstallation of base.
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base==0.2.0",
)
assert "Uninstalling base-0.1.0" in result.stdout, str(result)
assert "Successfully uninstalled base-0.1.0" in result.stdout, str(result)
- result.did_update(
- script.site_packages / "base",
- message="base not upgraded"
- )
- assert_installed(script, base="0.2.0")
+ result.did_update(script.site_packages / "base", message="base not upgraded")
+ script.assert_installed(base="0.2.0")
-def test_new_resolver_force_reinstall(script):
+def test_new_resolver_force_reinstall(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(script, "base", "0.1.0")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base==0.1.0",
)
@@ -531,19 +574,18 @@ def test_new_resolver_force_reinstall(script):
# even though the installed version matches.
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--force-reinstall",
"base==0.1.0",
)
assert "Uninstalling base-0.1.0" in result.stdout, str(result)
assert "Successfully uninstalled base-0.1.0" in result.stdout, str(result)
- result.did_update(
- script.site_packages / "base",
- message="base not reinstalled"
- )
- assert_installed(script, base="0.1.0")
+ result.did_update(script.site_packages / "base", message="base not reinstalled")
+ script.assert_installed(base="0.1.0")
@pytest.mark.parametrize(
@@ -561,20 +603,22 @@ def test_new_resolver_force_reinstall(script):
ids=["default", "exact-pre", "explicit-pre", "no-stable"],
)
def test_new_resolver_handles_prerelease(
- script,
- available_versions,
- pip_args,
- expected_version,
-):
+ script: PipTestEnvironment,
+ available_versions: List[str],
+ pip_args: List[str],
+ expected_version: str,
+) -> None:
for version in available_versions:
create_basic_wheel_for_package(script, "pkg", version)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- *pip_args
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ *pip_args,
)
- assert_installed(script, pkg=expected_version)
+ script.assert_installed(pkg=expected_version)
@pytest.mark.parametrize(
@@ -584,20 +628,24 @@ def test_new_resolver_handles_prerelease(
(["dep; os_name == 'nonexist_os'"], ["pkg"]),
# This tests the marker is picked up from a root dependency.
([], ["pkg", "dep; os_name == 'nonexist_os'"]),
- ]
+ ],
)
-def test_new_reolver_skips_marker(script, pkg_deps, root_deps):
+def test_new_resolver_skips_marker(
+ script: PipTestEnvironment, pkg_deps: List[str], root_deps: List[str]
+) -> None:
create_basic_wheel_for_package(script, "pkg", "1.0", depends=pkg_deps)
create_basic_wheel_for_package(script, "dep", "1.0")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- *root_deps
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ *root_deps,
)
- assert_installed(script, pkg="1.0")
- assert_not_installed(script, "dep")
+ script.assert_installed(pkg="1.0")
+ script.assert_not_installed("dep")
@pytest.mark.parametrize(
@@ -607,9 +655,11 @@ def test_new_reolver_skips_marker(script, pkg_deps, root_deps):
# This also tests the pkg constraint don't get merged with the
# requirement prematurely. (pypa/pip#8134)
["pkg<2.0"],
- ]
+ ],
)
-def test_new_resolver_constraints(script, constraints):
+def test_new_resolver_constraints(
+ script: PipTestEnvironment, constraints: List[str]
+) -> None:
create_basic_wheel_for_package(script, "pkg", "1.0")
create_basic_wheel_for_package(script, "pkg", "2.0")
create_basic_wheel_for_package(script, "pkg", "3.0")
@@ -617,28 +667,34 @@ def test_new_resolver_constraints(script, constraints):
constraints_file.write_text("\n".join(constraints))
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "-c", constraints_file,
- "pkg"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "-c",
+ constraints_file,
+ "pkg",
)
- assert_installed(script, pkg="1.0")
- assert_not_installed(script, "constraint_only")
+ script.assert_installed(pkg="1.0")
+ script.assert_not_installed("constraint_only")
-def test_new_resolver_constraint_no_specifier(script):
+def test_new_resolver_constraint_no_specifier(script: PipTestEnvironment) -> None:
"It's allowed (but useless...) for a constraint to have no specifier"
create_basic_wheel_for_package(script, "pkg", "1.0")
constraints_file = script.scratch_path / "constraints.txt"
constraints_file.write_text("pkg")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "-c", constraints_file,
- "pkg"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "-c",
+ constraints_file,
+ "pkg",
)
- assert_installed(script, pkg="1.0")
+ script.assert_installed(pkg="1.0")
@pytest.mark.parametrize(
@@ -658,15 +714,20 @@ def test_new_resolver_constraint_no_specifier(script):
),
],
)
-def test_new_resolver_constraint_reject_invalid(script, constraint, error):
+def test_new_resolver_constraint_reject_invalid(
+ script: PipTestEnvironment, constraint: str, error: str
+) -> None:
create_basic_wheel_for_package(script, "pkg", "1.0")
constraints_file = script.scratch_path / "constraints.txt"
constraints_file.write_text(constraint)
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "-c", constraints_file,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "-c",
+ constraints_file,
"pkg",
expect_error=True,
expect_stderr=True,
@@ -674,7 +735,7 @@ def test_new_resolver_constraint_reject_invalid(script, constraint, error):
assert error in result.stderr, str(result)
-def test_new_resolver_constraint_on_dependency(script):
+def test_new_resolver_constraint_on_dependency(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(script, "base", "1.0", depends=["dep"])
create_basic_wheel_for_package(script, "dep", "1.0")
create_basic_wheel_for_package(script, "dep", "2.0")
@@ -683,13 +744,16 @@ def test_new_resolver_constraint_on_dependency(script):
constraints_file.write_text("dep==2.0")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "-c", constraints_file,
- "base"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "-c",
+ constraints_file,
+ "base",
)
- assert_installed(script, base="1.0")
- assert_installed(script, dep="2.0")
+ script.assert_installed(base="1.0")
+ script.assert_installed(dep="2.0")
@pytest.mark.parametrize(
@@ -700,13 +764,12 @@ def test_new_resolver_constraint_on_dependency(script):
],
)
def test_new_resolver_constraint_on_path_empty(
- script,
- constraint_version,
- expect_error,
- message,
-):
- """A path requirement can be filtered by a constraint.
- """
+ script: PipTestEnvironment,
+ constraint_version: str,
+ expect_error: bool,
+ message: str,
+) -> None:
+ """A path requirement can be filtered by a constraint."""
setup_py = script.scratch_path / "setup.py"
text = "from setuptools import setup\nsetup(name='foo', version='2.0')"
setup_py.write_text(text)
@@ -716,8 +779,10 @@ def test_new_resolver_constraint_on_path_empty(
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "-c", constraints_txt,
+ "--no-cache-dir",
+ "--no-index",
+ "-c",
+ constraints_txt,
str(script.scratch_path),
expect_error=expect_error,
)
@@ -728,37 +793,42 @@ def test_new_resolver_constraint_on_path_empty(
assert message in result.stdout, str(result)
-def test_new_resolver_constraint_only_marker_match(script):
+def test_new_resolver_constraint_only_marker_match(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(script, "pkg", "1.0")
create_basic_wheel_for_package(script, "pkg", "2.0")
create_basic_wheel_for_package(script, "pkg", "3.0")
- constrants_content = textwrap.dedent(
+ constraints_content = textwrap.dedent(
"""
pkg==1.0; python_version == "{ver[0]}.{ver[1]}" # Always satisfies.
pkg==2.0; python_version < "0" # Never satisfies.
"""
).format(ver=sys.version_info)
constraints_txt = script.scratch_path / "constraints.txt"
- constraints_txt.write_text(constrants_content)
+ constraints_txt.write_text(constraints_content)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "-c", constraints_txt,
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "-c",
+ constraints_txt,
+ "--find-links",
+ script.scratch_path,
"pkg",
)
- assert_installed(script, pkg="1.0")
+ script.assert_installed(pkg="1.0")
-def test_new_resolver_upgrade_needs_option(script):
+def test_new_resolver_upgrade_needs_option(script: PipTestEnvironment) -> None:
# Install pkg 1.0.0
create_basic_wheel_for_package(script, "pkg", "1.0.0")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"pkg",
)
@@ -768,44 +838,47 @@ def test_new_resolver_upgrade_needs_option(script):
# This should not upgrade because we don't specify --upgrade
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"pkg",
)
assert "Requirement already satisfied" in result.stdout, str(result)
- assert_installed(script, pkg="1.0.0")
+ script.assert_installed(pkg="1.0.0")
# This should upgrade
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--upgrade",
"PKG", # Deliberately uppercase to check canonicalization
)
assert "Uninstalling pkg-1.0.0" in result.stdout, str(result)
assert "Successfully uninstalled pkg-1.0.0" in result.stdout, str(result)
- result.did_update(
- script.site_packages / "pkg",
- message="pkg not upgraded"
- )
- assert_installed(script, pkg="2.0.0")
+ result.did_update(script.site_packages / "pkg", message="pkg not upgraded")
+ script.assert_installed(pkg="2.0.0")
-def test_new_resolver_upgrade_strategy(script):
+def test_new_resolver_upgrade_strategy(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(script, "base", "1.0.0", depends=["dep"])
create_basic_wheel_for_package(script, "dep", "1.0.0")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base",
)
- assert_installed(script, base="1.0.0")
- assert_installed(script, dep="1.0.0")
+ script.assert_installed(base="1.0.0")
+ script.assert_installed(dep="1.0.0")
# Now release new versions
create_basic_wheel_for_package(script, "base", "2.0.0", depends=["dep"])
@@ -813,29 +886,102 @@ def test_new_resolver_upgrade_strategy(script):
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--upgrade",
"base",
)
# With upgrade strategy "only-if-needed" (the default), dep should not
# be upgraded.
- assert_installed(script, base="2.0.0")
- assert_installed(script, dep="1.0.0")
+ script.assert_installed(base="2.0.0")
+ script.assert_installed(dep="1.0.0")
create_basic_wheel_for_package(script, "base", "3.0.0", depends=["dep"])
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "--upgrade", "--upgrade-strategy=eager",
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "--upgrade",
+ "--upgrade-strategy=eager",
"base",
)
# With upgrade strategy "eager", dep should be upgraded.
- assert_installed(script, base="3.0.0")
- assert_installed(script, dep="2.0.0")
+ script.assert_installed(base="3.0.0")
+ script.assert_installed(dep="2.0.0")
+
+
+if TYPE_CHECKING:
+
+ class PackageBuilder(Protocol):
+ def __call__(
+ self,
+ script: PipTestEnvironment,
+ name: str,
+ version: str,
+ requires: List[str],
+ extras: Dict[str, List[str]],
+ ) -> str:
+ ...
+
+
+def _local_with_setup(
+ script: PipTestEnvironment,
+ name: str,
+ version: str,
+ requires: List[str],
+ extras: Dict[str, List[str]],
+) -> str:
+ """Create the package as a local source directory to install from path."""
+ path = create_test_package_with_setup(
+ script,
+ name=name,
+ version=version,
+ install_requires=requires,
+ extras_require=extras,
+ )
+ return str(path)
+
+
+def _direct_wheel(
+ script: PipTestEnvironment,
+ name: str,
+ version: str,
+ requires: List[str],
+ extras: Dict[str, List[str]],
+) -> str:
+ """Create the package as a wheel to install from path directly."""
+ path = create_basic_wheel_for_package(
+ script,
+ name=name,
+ version=version,
+ depends=requires,
+ extras=extras,
+ )
+ return str(path)
+
+
+def _wheel_from_index(
+ script: PipTestEnvironment,
+ name: str,
+ version: str,
+ requires: List[str],
+ extras: Dict[str, List[str]],
+) -> str:
+ """Create the package as a wheel to install from index."""
+ create_basic_wheel_for_package(
+ script,
+ name=name,
+ version=version,
+ depends=requires,
+ extras=extras,
+ )
+ return name
class TestExtraMerge:
@@ -844,40 +990,6 @@ class TestExtraMerge:
extras, one listed as required and the other as in extra.
"""
- def _local_with_setup(script, name, version, requires, extras):
- """Create the package as a local source directory to install from path.
- """
- return create_test_package_with_setup(
- script,
- name=name,
- version=version,
- install_requires=requires,
- extras_require=extras,
- )
-
- def _direct_wheel(script, name, version, requires, extras):
- """Create the package as a wheel to install from path directly.
- """
- return create_basic_wheel_for_package(
- script,
- name=name,
- version=version,
- depends=requires,
- extras=extras,
- )
-
- def _wheel_from_index(script, name, version, requires, extras):
- """Create the package as a wheel to install from index.
- """
- create_basic_wheel_for_package(
- script,
- name=name,
- version=version,
- depends=requires,
- extras=extras,
- )
- return name
-
@pytest.mark.parametrize(
"pkg_builder",
[
@@ -887,8 +999,8 @@ class TestExtraMerge:
],
)
def test_new_resolver_extra_merge_in_package(
- self, monkeypatch, script, pkg_builder,
- ):
+ self, script: PipTestEnvironment, pkg_builder: "PackageBuilder"
+ ) -> None:
create_basic_wheel_for_package(script, "depdev", "1.0.0")
create_basic_wheel_for_package(
script,
@@ -906,14 +1018,16 @@ class TestExtraMerge:
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
requirement + "[dev]",
)
- assert_installed(script, pkg="1.0.0", dep="1.0.0", depdev="1.0.0")
+ script.assert_installed(pkg="1.0.0", dep="1.0.0", depdev="1.0.0")
-def test_new_resolver_build_directory_error_zazo_19(script):
+def test_new_resolver_build_directory_error_zazo_19(script: PipTestEnvironment) -> None:
"""https://github.com/pradyunsg/zazo/issues/19#issuecomment-631615674
This will first resolve like this:
@@ -935,7 +1049,10 @@ def test_new_resolver_build_directory_error_zazo_19(script):
can delete this. Please delete it and try again.
"""
create_basic_wheel_for_package(
- script, "pkg_a", "3.0.0", depends=["pkg-b<2"],
+ script,
+ "pkg_a",
+ "3.0.0",
+ depends=["pkg-b<2"],
)
create_basic_wheel_for_package(script, "pkg_a", "2.0.0")
create_basic_wheel_for_package(script, "pkg_a", "1.0.0")
@@ -945,36 +1062,43 @@ def test_new_resolver_build_directory_error_zazo_19(script):
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "pkg-a", "pkg-b",
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "pkg-a",
+ "pkg-b",
)
- assert_installed(script, pkg_a="3.0.0", pkg_b="1.0.0")
+ script.assert_installed(pkg_a="3.0.0", pkg_b="1.0.0")
-def test_new_resolver_upgrade_same_version(script):
+def test_new_resolver_upgrade_same_version(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(script, "pkg", "2")
create_basic_wheel_for_package(script, "pkg", "1")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"pkg",
)
- assert_installed(script, pkg="2")
+ script.assert_installed(pkg="2")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--upgrade",
"pkg",
)
- assert_installed(script, pkg="2")
+ script.assert_installed(pkg="2")
-def test_new_resolver_local_and_req(script):
+def test_new_resolver_local_and_req(script: PipTestEnvironment) -> None:
source_dir = create_test_package_with_setup(
script,
name="pkg",
@@ -982,13 +1106,17 @@ def test_new_resolver_local_and_req(script):
)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- source_dir, "pkg!=0.1.0",
+ "--no-cache-dir",
+ "--no-index",
+ source_dir,
+ "pkg!=0.1.0",
expect_error=True,
)
-def test_new_resolver_no_deps_checks_requires_python(script):
+def test_new_resolver_no_deps_checks_requires_python(
+ script: PipTestEnvironment,
+) -> None:
create_basic_wheel_for_package(
script,
"base",
@@ -1007,7 +1135,8 @@ def test_new_resolver_no_deps_checks_requires_python(script):
"--no-cache-dir",
"--no-index",
"--no-deps",
- "--find-links", script.scratch_path,
+ "--find-links",
+ script.scratch_path,
"base",
expect_error=True,
)
@@ -1019,7 +1148,9 @@ def test_new_resolver_no_deps_checks_requires_python(script):
assert message in result.stderr
-def test_new_resolver_prefers_installed_in_upgrade_if_latest(script):
+def test_new_resolver_prefers_installed_in_upgrade_if_latest(
+ script: PipTestEnvironment,
+) -> None:
create_basic_wheel_for_package(script, "pkg", "1")
local_pkg = create_test_package_with_setup(script, name="pkg", version="2")
@@ -1036,17 +1167,20 @@ def test_new_resolver_prefers_installed_in_upgrade_if_latest(script):
"install",
"--no-cache-dir",
"--no-index",
- "--find-links", script.scratch_path,
+ "--find-links",
+ script.scratch_path,
"--upgrade",
"pkg",
)
- assert_installed(script, pkg="2")
+ script.assert_installed(pkg="2")
@pytest.mark.parametrize("N", [2, 10, 20])
-def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N):
+def test_new_resolver_presents_messages_when_backtracking_a_lot(
+ script: PipTestEnvironment, N: int
+) -> None:
# Generate a set of wheels that will definitely cause backtracking.
- for index in range(1, N+1):
+ for index in range(1, N + 1):
A_version = f"{index}.0.0"
B_version = f"{index}.0.0"
C_version = "{index_minus_one}.0.0".format(index_minus_one=index - 1)
@@ -1058,7 +1192,7 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N):
print("A", A_version, "B", B_version, "C", C_version)
create_basic_wheel_for_package(script, "A", A_version, depends=depends)
- for index in range(1, N+1):
+ for index in range(1, N + 1):
B_version = f"{index}.0.0"
C_version = f"{index}.0.0"
depends = ["C == " + C_version]
@@ -1066,7 +1200,7 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N):
print("B", B_version, "C", C_version)
create_basic_wheel_for_package(script, "B", B_version, depends=depends)
- for index in range(1, N+1):
+ for index in range(1, N + 1):
C_version = f"{index}.0.0"
print("C", C_version)
create_basic_wheel_for_package(script, "C", C_version)
@@ -1076,11 +1210,12 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N):
"install",
"--no-cache-dir",
"--no-index",
- "--find-links", script.scratch_path,
- "A"
+ "--find-links",
+ script.scratch_path,
+ "A",
)
- assert_installed(script, A="1.0.0", B="1.0.0", C="1.0.0")
+ script.assert_installed(A="1.0.0", B="1.0.0", C="1.0.0")
# These numbers are hard-coded in the code.
if N >= 1:
assert "This could take a while." in result.stdout
@@ -1095,13 +1230,12 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N):
[
"0.1.0+local.1", # Normalized form.
"0.1.0+local_1", # Non-normalized form containing an underscore.
-
# Non-normalized form containing a dash. This is allowed, installation
# works correctly, but assert_installed() fails because pkg_resources
# cannot handle it correctly. Nobody is complaining about it right now,
# we're probably dropping it for importlib.metadata soon(tm), so let's
# ignore it for the time being.
- pytest.param("0.1.0+local-1", marks=pytest.mark.xfail),
+ pytest.param("0.1.0+local-1", marks=pytest.mark.xfail(strict=False)),
],
ids=["meta_dot", "meta_underscore", "meta_dash"],
)
@@ -1114,10 +1248,10 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N):
ids=["file_dot", "file_underscore"],
)
def test_new_resolver_check_wheel_version_normalized(
- script,
- metadata_version,
- filename_version,
-):
+ script: PipTestEnvironment,
+ metadata_version: str,
+ filename_version: str,
+) -> None:
filename = f"simple-{filename_version}-py2.py3-none-any.whl"
wheel_builder = make_wheel(name="simple", version=metadata_version)
@@ -1125,56 +1259,63 @@ def test_new_resolver_check_wheel_version_normalized(
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "simple"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "simple",
)
- assert_installed(script, simple="0.1.0+local.1")
+ script.assert_installed(simple="0.1.0+local.1")
-def test_new_resolver_does_reinstall_local_sdists(script):
+def test_new_resolver_does_reinstall_local_sdists(script: PipTestEnvironment) -> None:
archive_path = create_basic_sdist_for_package(
script,
"pkg",
"1.0",
)
script.pip(
- "install", "--no-cache-dir", "--no-index",
+ "install",
+ "--no-cache-dir",
+ "--no-index",
archive_path,
)
- assert_installed(script, pkg="1.0")
+ script.assert_installed(pkg="1.0")
result = script.pip(
- "install", "--no-cache-dir", "--no-index",
+ "install",
+ "--no-cache-dir",
+ "--no-index",
archive_path,
expect_stderr=True,
)
assert "Installing collected packages: pkg" in result.stdout, str(result)
- assert "DEPRECATION" in result.stderr, str(result)
- assert_installed(script, pkg="1.0")
+ script.assert_installed(pkg="1.0")
-def test_new_resolver_does_reinstall_local_paths(script):
- pkg = create_test_package_with_setup(
- script,
- name="pkg",
- version="1.0"
- )
+def test_new_resolver_does_reinstall_local_paths(script: PipTestEnvironment) -> None:
+ pkg = create_test_package_with_setup(script, name="pkg", version="1.0")
script.pip(
- "install", "--no-cache-dir", "--no-index",
+ "install",
+ "--no-cache-dir",
+ "--no-index",
pkg,
)
- assert_installed(script, pkg="1.0")
+ script.assert_installed(pkg="1.0")
result = script.pip(
- "install", "--no-cache-dir", "--no-index",
+ "install",
+ "--no-cache-dir",
+ "--no-index",
pkg,
)
assert "Installing collected packages: pkg" in result.stdout, str(result)
- assert_installed(script, pkg="1.0")
+ script.assert_installed(pkg="1.0")
-def test_new_resolver_does_not_reinstall_when_from_a_local_index(script):
+def test_new_resolver_does_not_reinstall_when_from_a_local_index(
+ script: PipTestEnvironment,
+) -> None:
create_basic_sdist_for_package(
script,
"simple",
@@ -1182,25 +1323,29 @@ def test_new_resolver_does_not_reinstall_when_from_a_local_index(script):
)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "simple"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "simple",
)
- assert_installed(script, simple="0.1.0")
+ script.assert_installed(simple="0.1.0")
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "simple"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "simple",
)
# Should not reinstall!
assert "Installing collected packages: simple" not in result.stdout, str(result)
assert "Requirement already satisfied: simple" in result.stdout, str(result)
- assert_installed(script, simple="0.1.0")
+ script.assert_installed(simple="0.1.0")
-def test_new_resolver_skip_inconsistent_metadata(script):
+def test_new_resolver_skip_inconsistent_metadata(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(script, "A", "1")
a_2 = create_basic_wheel_for_package(script, "A", "2")
@@ -1208,17 +1353,19 @@ def test_new_resolver_skip_inconsistent_metadata(script):
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--verbose",
"A",
allow_stderr_warning=True,
)
assert (
- " inconsistent version: filename has '3', but metadata has '2'"
- ) in result.stderr, str(result)
- assert_installed(script, a="1")
+ " inconsistent version: expected '3', but metadata has '2'"
+ ) in result.stdout, str(result)
+ script.assert_installed(a="1")
@pytest.mark.parametrize(
@@ -1226,7 +1373,9 @@ def test_new_resolver_skip_inconsistent_metadata(script):
[True, False],
ids=["upgrade", "no-upgrade"],
)
-def test_new_resolver_lazy_fetch_candidates(script, upgrade):
+def test_new_resolver_lazy_fetch_candidates(
+ script: PipTestEnvironment, upgrade: bool
+) -> None:
create_basic_wheel_for_package(script, "myuberpkg", "1")
create_basic_wheel_for_package(script, "myuberpkg", "2")
create_basic_wheel_for_package(script, "myuberpkg", "3")
@@ -1234,8 +1383,10 @@ def test_new_resolver_lazy_fetch_candidates(script, upgrade):
# Install an old version first.
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"myuberpkg==1",
)
@@ -1246,32 +1397,36 @@ def test_new_resolver_lazy_fetch_candidates(script, upgrade):
pip_upgrade_args = []
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"myuberpkg",
- *pip_upgrade_args # Trailing comma fails on Python 2.
+ *pip_upgrade_args, # Trailing comma fails on Python 2.
)
# pip should install the version preferred by the strategy...
if upgrade:
- assert_installed(script, myuberpkg="3")
+ script.assert_installed(myuberpkg="3")
else:
- assert_installed(script, myuberpkg="1")
+ script.assert_installed(myuberpkg="1")
# But should reach there in the best route possible, without trying
# candidates it does not need to.
assert "myuberpkg-2" not in result.stdout, str(result)
-def test_new_resolver_no_fetch_no_satisfying(script):
+def test_new_resolver_no_fetch_no_satisfying(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(script, "myuberpkg", "1")
# Install the package. This should emit a "Processing" message for
# fetching the distribution from the --find-links page.
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"myuberpkg",
)
assert "Processing " in result.stdout, str(result)
@@ -1280,15 +1435,19 @@ def test_new_resolver_no_fetch_no_satisfying(script):
# message because the currently installed version is latest.
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--upgrade",
"myuberpkg",
)
assert "Processing " not in result.stdout, str(result)
-def test_new_resolver_does_not_install_unneeded_packages_with_url_constraint(script):
+def test_new_resolver_does_not_install_unneeded_packages_with_url_constraint(
+ script: PipTestEnvironment,
+) -> None:
archive_path = create_basic_wheel_for_package(
script,
"installed",
@@ -1301,24 +1460,29 @@ def test_new_resolver_does_not_install_unneeded_packages_with_url_constraint(scr
)
constraints_file = script.scratch_path / "constraints.txt"
- constraints_file.write_text("not_installed @ " + path_to_url(not_installed_path))
+ constraints_file.write_text(f"not_installed @ {not_installed_path.as_uri()}")
(script.scratch_path / "index").mkdir()
archive_path.rename(script.scratch_path / "index" / archive_path.name)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path / "index",
- "-c", constraints_file,
- "installed"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path / "index",
+ "-c",
+ constraints_file,
+ "installed",
)
- assert_installed(script, installed="0.1.0")
- assert_not_installed(script, "not_installed")
+ script.assert_installed(installed="0.1.0")
+ script.assert_not_installed("not_installed")
-def test_new_resolver_installs_packages_with_url_constraint(script):
+def test_new_resolver_installs_packages_with_url_constraint(
+ script: PipTestEnvironment,
+) -> None:
installed_path = create_basic_wheel_for_package(
script,
"installed",
@@ -1326,19 +1490,18 @@ def test_new_resolver_installs_packages_with_url_constraint(script):
)
constraints_file = script.scratch_path / "constraints.txt"
- constraints_file.write_text("installed @ " + path_to_url(installed_path))
+ constraints_file.write_text(f"installed @ {installed_path.as_uri()}")
script.pip(
- "install",
- "--no-cache-dir", "--no-index",
- "-c", constraints_file,
- "installed"
+ "install", "--no-cache-dir", "--no-index", "-c", constraints_file, "installed"
)
- assert_installed(script, installed="0.1.0")
+ script.assert_installed(installed="0.1.0")
-def test_new_resolver_reinstall_link_requirement_with_constraint(script):
+def test_new_resolver_reinstall_link_requirement_with_constraint(
+ script: PipTestEnvironment,
+) -> None:
installed_path = create_basic_wheel_for_package(
script,
"installed",
@@ -1346,27 +1509,32 @@ def test_new_resolver_reinstall_link_requirement_with_constraint(script):
)
cr_file = script.scratch_path / "constraints.txt"
- cr_file.write_text("installed @ " + path_to_url(installed_path))
+ cr_file.write_text(f"installed @ {installed_path.as_uri()}")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "-r", cr_file,
+ "--no-cache-dir",
+ "--no-index",
+ "-r",
+ cr_file,
)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "-c", cr_file,
- "-r", cr_file,
+ "--no-cache-dir",
+ "--no-index",
+ "-c",
+ cr_file,
+ "-r",
+ cr_file,
)
# TODO: strengthen assertion to "second invocation does no work"
# I don't think this is true yet, but it should be in the future.
- assert_installed(script, installed="0.1.0")
+ script.assert_installed(installed="0.1.0")
-def test_new_resolver_prefers_url_constraint(script):
+def test_new_resolver_prefers_url_constraint(script: PipTestEnvironment) -> None:
installed_path = create_basic_wheel_for_package(
script,
"test_pkg",
@@ -1379,23 +1547,28 @@ def test_new_resolver_prefers_url_constraint(script):
)
constraints_file = script.scratch_path / "constraints.txt"
- constraints_file.write_text("test_pkg @ " + path_to_url(installed_path))
+ constraints_file.write_text(f"test_pkg @ {installed_path.as_uri()}")
(script.scratch_path / "index").mkdir()
not_installed_path.rename(script.scratch_path / "index" / not_installed_path.name)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path / "index",
- "-c", constraints_file,
- "test_pkg"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path / "index",
+ "-c",
+ constraints_file,
+ "test_pkg",
)
- assert_installed(script, test_pkg="0.1.0")
+ script.assert_installed(test_pkg="0.1.0")
-def test_new_resolver_prefers_url_constraint_on_update(script):
+def test_new_resolver_prefers_url_constraint_on_update(
+ script: PipTestEnvironment,
+) -> None:
installed_path = create_basic_wheel_for_package(
script,
"test_pkg",
@@ -1408,35 +1581,41 @@ def test_new_resolver_prefers_url_constraint_on_update(script):
)
constraints_file = script.scratch_path / "constraints.txt"
- constraints_file.write_text("test_pkg @ " + path_to_url(installed_path))
+ constraints_file.write_text(f"test_pkg @ {installed_path.as_uri()}")
(script.scratch_path / "index").mkdir()
not_installed_path.rename(script.scratch_path / "index" / not_installed_path.name)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path / "index",
- "test_pkg"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path / "index",
+ "test_pkg",
)
- assert_installed(script, test_pkg="0.2.0")
+ script.assert_installed(test_pkg="0.2.0")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path / "index",
- "-c", constraints_file,
- "test_pkg"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path / "index",
+ "-c",
+ constraints_file,
+ "test_pkg",
)
- assert_installed(script, test_pkg="0.1.0")
+ script.assert_installed(test_pkg="0.1.0")
@pytest.mark.parametrize("version_option", ["--constraint", "--requirement"])
def test_new_resolver_fails_with_url_constraint_and_incompatible_version(
- script, version_option,
-):
+ script: PipTestEnvironment,
+ version_option: str,
+) -> None:
not_installed_path = create_basic_wheel_for_package(
script,
"test_pkg",
@@ -1449,17 +1628,21 @@ def test_new_resolver_fails_with_url_constraint_and_incompatible_version(
)
url_constraint = script.scratch_path / "constraints.txt"
- url_constraint.write_text("test_pkg @ " + path_to_url(not_installed_path))
+ url_constraint.write_text(f"test_pkg @ {not_installed_path.as_uri()}")
version_req = script.scratch_path / "requirements.txt"
version_req.write_text("test_pkg<0.2.0")
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "--constraint", url_constraint,
- version_option, version_req,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "--constraint",
+ url_constraint,
+ version_option,
+ version_req,
"test_pkg",
expect_error=True,
)
@@ -1469,19 +1652,24 @@ def test_new_resolver_fails_with_url_constraint_and_incompatible_version(
"because these package versions have conflicting dependencies."
) in result.stderr, str(result)
- assert_not_installed(script, "test_pkg")
+ script.assert_not_installed("test_pkg")
# Assert that pip works properly in the absence of the constraints file.
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- version_option, version_req,
- "test_pkg"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ version_option,
+ version_req,
+ "test_pkg",
)
-def test_new_resolver_ignores_unneeded_conflicting_constraints(script):
+def test_new_resolver_ignores_unneeded_conflicting_constraints(
+ script: PipTestEnvironment,
+) -> None:
version_1 = create_basic_wheel_for_package(
script,
"test_pkg",
@@ -1499,8 +1687,8 @@ def test_new_resolver_ignores_unneeded_conflicting_constraints(script):
)
constraints = [
- "test_pkg @ " + path_to_url(version_1),
- "test_pkg @ " + path_to_url(version_2),
+ f"test_pkg @ {version_1.as_uri()}",
+ f"test_pkg @ {version_2.as_uri()}",
]
constraints_file = script.scratch_path / "constraints.txt"
@@ -1508,17 +1696,22 @@ def test_new_resolver_ignores_unneeded_conflicting_constraints(script):
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "-c", constraints_file,
- "installed"
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "-c",
+ constraints_file,
+ "installed",
)
- assert_not_installed(script, "test_pkg")
- assert_installed(script, installed="0.1.0")
+ script.assert_not_installed("test_pkg")
+ script.assert_installed(installed="0.1.0")
-def test_new_resolver_fails_on_needed_conflicting_constraints(script):
+def test_new_resolver_fails_on_needed_conflicting_constraints(
+ script: PipTestEnvironment,
+) -> None:
version_1 = create_basic_wheel_for_package(
script,
"test_pkg",
@@ -1531,8 +1724,8 @@ def test_new_resolver_fails_on_needed_conflicting_constraints(script):
)
constraints = [
- "test_pkg @ " + path_to_url(version_1),
- "test_pkg @ " + path_to_url(version_2),
+ f"test_pkg @ {version_1.as_uri()}",
+ f"test_pkg @ {version_2.as_uri()}",
]
constraints_file = script.scratch_path / "constraints.txt"
@@ -1540,9 +1733,12 @@ def test_new_resolver_fails_on_needed_conflicting_constraints(script):
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "-c", constraints_file,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "-c",
+ constraints_file,
"test_pkg",
expect_error=True,
)
@@ -1552,18 +1748,22 @@ def test_new_resolver_fails_on_needed_conflicting_constraints(script):
"dependencies."
) in result.stderr, str(result)
- assert_not_installed(script, "test_pkg")
+ script.assert_not_installed("test_pkg")
# Assert that pip works properly in the absence of the constraints file.
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"test_pkg",
)
-def test_new_resolver_fails_on_conflicting_constraint_and_requirement(script):
+def test_new_resolver_fails_on_conflicting_constraint_and_requirement(
+ script: PipTestEnvironment,
+) -> None:
version_1 = create_basic_wheel_for_package(
script,
"test_pkg",
@@ -1576,14 +1776,17 @@ def test_new_resolver_fails_on_conflicting_constraint_and_requirement(script):
)
constraints_file = script.scratch_path / "constraints.txt"
- constraints_file.write_text("test_pkg @ " + path_to_url(version_1))
+ constraints_file.write_text(f"test_pkg @ {version_1.as_uri()}")
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "-c", constraints_file,
- "test_pkg @ " + path_to_url(version_2),
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "-c",
+ constraints_file,
+ f"test_pkg @ {version_2.as_uri()}",
expect_error=True,
)
@@ -1592,24 +1795,26 @@ def test_new_resolver_fails_on_conflicting_constraint_and_requirement(script):
"because these package versions have conflicting dependencies."
) in result.stderr, str(result)
- assert_not_installed(script, "test_pkg")
+ script.assert_not_installed("test_pkg")
# Assert that pip works properly in the absence of the constraints file.
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "test_pkg @ " + path_to_url(version_2),
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ f"test_pkg @ {version_2.as_uri()}",
)
@pytest.mark.parametrize("editable", [False, True])
-def test_new_resolver_succeeds_on_matching_constraint_and_requirement(script, editable):
+def test_new_resolver_succeeds_on_matching_constraint_and_requirement(
+ script: PipTestEnvironment, editable: bool
+) -> None:
if editable:
source_dir = create_test_package_with_setup(
- script,
- name="test_pkg",
- version="0.1.0"
+ script, name="test_pkg", version="0.1.0"
)
else:
source_dir = create_basic_wheel_for_package(
@@ -1618,29 +1823,32 @@ def test_new_resolver_succeeds_on_matching_constraint_and_requirement(script, ed
"0.1.0",
)
- req_line = "test_pkg @ " + path_to_url(source_dir)
+ req_line = f"test_pkg @ {source_dir.as_uri()}"
constraints_file = script.scratch_path / "constraints.txt"
constraints_file.write_text(req_line)
+ last_args: Tuple[str, ...]
if editable:
- last_args = ("-e", source_dir)
+ last_args = ("-e", os.fspath(source_dir))
else:
last_args = (req_line,)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "-c", constraints_file,
+ "--no-cache-dir",
+ "--no-index",
+ "-c",
+ constraints_file,
*last_args,
)
- assert_installed(script, test_pkg="0.1.0")
+ script.assert_installed(test_pkg="0.1.0")
if editable:
assert_editable(script, "test-pkg")
-def test_new_resolver_applies_url_constraint_to_dep(script):
+def test_new_resolver_applies_url_constraint_to_dep(script: PipTestEnvironment) -> None:
version_1 = create_basic_wheel_for_package(
script,
"dep",
@@ -1659,22 +1867,25 @@ def test_new_resolver_applies_url_constraint_to_dep(script):
version_2.rename(script.scratch_path / "index" / version_2.name)
constraints_file = script.scratch_path / "constraints.txt"
- constraints_file.write_text("dep @ " + path_to_url(version_1))
+ constraints_file.write_text(f"dep @ {version_1.as_uri()}")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "-c", constraints_file,
- "--find-links", script.scratch_path / "index",
+ "--no-cache-dir",
+ "--no-index",
+ "-c",
+ constraints_file,
+ "--find-links",
+ script.scratch_path / "index",
"base",
)
- assert_installed(script, dep="0.1.0")
+ script.assert_installed(dep="0.1.0")
def test_new_resolver_handles_compatible_wheel_tags_in_constraint_url(
- script, make_fake_wheel
-):
+ script: PipTestEnvironment, make_fake_wheel: MakeFakeWheel
+) -> None:
initial_path = make_fake_wheel("base", "0.1.0", "fakepy1-fakeabi-fakeplat")
constrained = script.scratch_path / "constrained"
@@ -1685,28 +1896,35 @@ def test_new_resolver_handles_compatible_wheel_tags_in_constraint_url(
initial_path.rename(final_path)
constraints_file = script.scratch_path / "constraints.txt"
- constraints_file.write_text("base @ " + path_to_url(final_path))
+ constraints_file.write_text(f"base @ {final_path.as_uri()}")
result = script.pip(
"install",
- "--implementation", "fakepy",
- '--only-binary=:all:',
- "--python-version", "1",
- "--abi", "fakeabi",
- "--platform", "fakeplat",
- "--target", script.scratch_path / "target",
- "--no-cache-dir", "--no-index",
- "-c", constraints_file,
+ "--implementation",
+ "fakepy",
+ "--only-binary=:all:",
+ "--python-version",
+ "1",
+ "--abi",
+ "fakeabi",
+ "--platform",
+ "fakeplat",
+ "--target",
+ script.scratch_path / "target",
+ "--no-cache-dir",
+ "--no-index",
+ "-c",
+ constraints_file,
"base",
)
- dist_info = Path("scratch", "target", "base-0.1.0.dist-info")
+ dist_info = pathlib.Path("scratch", "target", "base-0.1.0.dist-info")
result.did_create(dist_info)
def test_new_resolver_handles_incompatible_wheel_tags_in_constraint_url(
- script, make_fake_wheel
-):
+ script: PipTestEnvironment, make_fake_wheel: MakeFakeWheel
+) -> None:
initial_path = make_fake_wheel("base", "0.1.0", "fakepy1-fakeabi-fakeplat")
constrained = script.scratch_path / "constrained"
@@ -1717,12 +1935,14 @@ def test_new_resolver_handles_incompatible_wheel_tags_in_constraint_url(
initial_path.rename(final_path)
constraints_file = script.scratch_path / "constraints.txt"
- constraints_file.write_text("base @ " + path_to_url(final_path))
+ constraints_file.write_text(f"base @ {final_path.as_uri()}")
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "-c", constraints_file,
+ "--no-cache-dir",
+ "--no-index",
+ "-c",
+ constraints_file,
"base",
expect_error=True,
)
@@ -1732,12 +1952,12 @@ def test_new_resolver_handles_incompatible_wheel_tags_in_constraint_url(
"dependencies."
) in result.stderr, str(result)
- assert_not_installed(script, "base")
+ script.assert_not_installed("base")
def test_new_resolver_avoids_incompatible_wheel_tags_in_constraint_url(
- script, make_fake_wheel
-):
+ script: PipTestEnvironment, make_fake_wheel: MakeFakeWheel
+) -> None:
initial_path = make_fake_wheel("dep", "0.1.0", "fakepy1-fakeabi-fakeplat")
constrained = script.scratch_path / "constrained"
@@ -1748,19 +1968,15 @@ def test_new_resolver_avoids_incompatible_wheel_tags_in_constraint_url(
initial_path.rename(final_path)
constraints_file = script.scratch_path / "constraints.txt"
- constraints_file.write_text("dep @ " + path_to_url(final_path))
+ constraints_file.write_text(f"dep @ {final_path.as_uri()}")
index = script.scratch_path / "index"
index.mkdir()
index_dep = create_basic_wheel_for_package(script, "dep", "0.2.0")
- base = create_basic_wheel_for_package(
- script, "base", "0.1.0"
- )
- base_2 = create_basic_wheel_for_package(
- script, "base", "0.2.0", depends=["dep"]
- )
+ base = create_basic_wheel_for_package(script, "base", "0.1.0")
+ base_2 = create_basic_wheel_for_package(script, "base", "0.2.0", depends=["dep"])
index_dep.rename(index / index_dep.name)
base.rename(index / base.name)
@@ -1768,14 +1984,17 @@ def test_new_resolver_avoids_incompatible_wheel_tags_in_constraint_url(
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "-c", constraints_file,
- "--find-links", script.scratch_path / "index",
+ "--no-cache-dir",
+ "--no-index",
+ "-c",
+ constraints_file,
+ "--find-links",
+ script.scratch_path / "index",
"base",
)
- assert_installed(script, base="0.1.0")
- assert_not_installed(script, "dep")
+ script.assert_installed(base="0.1.0")
+ script.assert_not_installed("dep")
@pytest.mark.parametrize(
@@ -1832,18 +2051,18 @@ def test_new_resolver_avoids_incompatible_wheel_tags_in_constraint_url(
],
)
def test_new_resolver_direct_url_equivalent(
- tmp_path,
- script,
- suffixes_equivalent,
- depend_suffix,
- request_suffix,
-):
+ tmp_path: pathlib.Path,
+ script: PipTestEnvironment,
+ suffixes_equivalent: bool,
+ depend_suffix: str,
+ request_suffix: str,
+) -> None:
pkga = create_basic_wheel_for_package(script, name="pkga", version="1")
pkgb = create_basic_wheel_for_package(
script,
name="pkgb",
version="1",
- depends=[f"pkga@{path_to_url(pkga)}{depend_suffix}"],
+ depends=[f"pkga@{pkga.as_uri()}{depend_suffix}"],
)
# Make pkgb visible via --find-links, but not pkga.
@@ -1856,19 +2075,24 @@ def test_new_resolver_direct_url_equivalent(
# URL suffix as specified in pkgb. This should work!
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", str(find_links),
- f"{path_to_url(pkga)}{request_suffix}", "pkgb",
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ str(find_links),
+ f"{pkga.as_uri()}{request_suffix}",
+ "pkgb",
expect_error=(not suffixes_equivalent),
)
if suffixes_equivalent:
- assert_installed(script, pkga="1", pkgb="1")
+ script.assert_installed(pkga="1", pkgb="1")
else:
- assert_not_installed(script, "pkga", "pkgb")
+ script.assert_not_installed("pkga", "pkgb")
-def test_new_resolver_direct_url_with_extras(tmp_path, script):
+def test_new_resolver_direct_url_with_extras(
+ tmp_path: pathlib.Path, script: PipTestEnvironment
+) -> None:
pkg1 = create_basic_wheel_for_package(script, name="pkg1", version="1")
pkg2 = create_basic_wheel_for_package(
script,
@@ -1895,18 +2119,23 @@ def test_new_resolver_direct_url_with_extras(tmp_path, script):
# URL pkg2 should be able to provide pkg2[ext] required by pkg3.
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", str(find_links),
- pkg2, "pkg3",
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ str(find_links),
+ pkg2,
+ "pkg3",
)
- assert_installed(script, pkg1="1", pkg2="1", pkg3="1")
+ script.assert_installed(pkg1="1", pkg2="1", pkg3="1")
assert not get_created_direct_url(result, "pkg1")
assert get_created_direct_url(result, "pkg2")
assert not get_created_direct_url(result, "pkg3")
-def test_new_resolver_modifies_installed_incompatible(script):
+def test_new_resolver_modifies_installed_incompatible(
+ script: PipTestEnvironment,
+) -> None:
create_basic_wheel_for_package(script, name="a", version="1")
create_basic_wheel_for_package(script, name="a", version="2")
create_basic_wheel_for_package(script, name="a", version="3")
@@ -1918,8 +2147,10 @@ def test_new_resolver_modifies_installed_incompatible(script):
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"b==1",
)
@@ -1929,20 +2160,24 @@ def test_new_resolver_modifies_installed_incompatible(script):
# discard b-1 and backtrack, so b-2 is selected instead.
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"d==1",
)
- assert_installed(script, d="1", c="2", b="2", a="2")
+ script.assert_installed(d="1", c="2", b="2", a="2")
-def test_new_resolver_transitively_depends_on_unnamed_local(script):
+def test_new_resolver_transitively_depends_on_unnamed_local(
+ script: PipTestEnvironment,
+) -> None:
create_basic_wheel_for_package(script, name="certbot-docs", version="1")
certbot = create_test_package_with_setup(
script,
name="certbot",
version="99.99.0.dev0",
- extras_require={"docs": ["certbot-docs"]}
+ extras_require={"docs": ["certbot-docs"]},
)
certbot_apache = create_test_package_with_setup(
script,
@@ -1953,44 +2188,45 @@ def test_new_resolver_transitively_depends_on_unnamed_local(script):
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- f"{certbot}[docs]", certbot_apache,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ f"{certbot}[docs]",
+ certbot_apache,
)
- assert_installed(
- script,
+ script.assert_installed(
certbot="99.99.0.dev0",
certbot_apache="99.99.0.dev0",
certbot_docs="1",
)
-def _to_uri(path):
- # Something like file:///path/to/package
- return pathlib.Path(path).as_uri()
-
-
-def _to_localhost_uri(path):
+def _to_localhost_uri(path: pathlib.Path) -> str:
# Something like file://localhost/path/to/package
- return pathlib.Path(path).as_uri().replace("///", "//localhost/")
+ return path.as_uri().replace("///", "//localhost/")
@pytest.mark.parametrize(
"format_dep",
[
- pytest.param(_to_uri, id="emptyhost"),
+ pytest.param(pathlib.Path.as_uri, id="emptyhost"),
pytest.param(_to_localhost_uri, id="localhost"),
],
)
@pytest.mark.parametrize(
"format_input",
[
- pytest.param(lambda path: path, id="path"),
- pytest.param(_to_uri, id="emptyhost"),
+ pytest.param(pathlib.Path, id="path"),
+ pytest.param(pathlib.Path.as_uri, id="emptyhost"),
pytest.param(_to_localhost_uri, id="localhost"),
],
)
-def test_new_resolver_file_url_normalize(script, format_dep, format_input):
+def test_new_resolver_file_url_normalize(
+ script: PipTestEnvironment,
+ format_dep: Callable[[pathlib.Path], str],
+ format_input: Callable[[pathlib.Path], str],
+) -> None:
lib_a = create_test_package_with_setup(
script,
name="lib_a",
@@ -2005,13 +2241,17 @@ def test_new_resolver_file_url_normalize(script, format_dep, format_input):
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- format_input(lib_a), lib_b,
+ "--no-cache-dir",
+ "--no-index",
+ format_input(lib_a),
+ lib_b,
)
script.assert_installed(lib_a="1", lib_b="1")
-def test_new_resolver_dont_backtrack_on_extra_if_base_constrained(script):
+def test_new_resolver_dont_backtrack_on_extra_if_base_constrained(
+ script: PipTestEnvironment,
+) -> None:
create_basic_wheel_for_package(script, "dep", "1.0")
create_basic_wheel_for_package(script, "pkg", "1.0", extras={"ext": ["dep"]})
create_basic_wheel_for_package(script, "pkg", "2.0", extras={"ext": ["dep"]})
@@ -2020,10 +2260,90 @@ def test_new_resolver_dont_backtrack_on_extra_if_base_constrained(script):
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "--constraint", constraints_file,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "--constraint",
+ constraints_file,
"pkg[ext]",
)
assert "pkg-2.0" not in result.stdout, "Should not try 2.0 due to constraint"
script.assert_installed(pkg="1.0", dep="1.0")
+
+
+def test_new_resolver_respect_user_requested_if_extra_is_installed(
+ script: PipTestEnvironment,
+) -> None:
+ create_basic_wheel_for_package(script, "pkg1", "1.0")
+ create_basic_wheel_for_package(script, "pkg2", "1.0", extras={"ext": ["pkg1"]})
+ create_basic_wheel_for_package(script, "pkg2", "2.0", extras={"ext": ["pkg1"]})
+ create_basic_wheel_for_package(script, "pkg3", "1.0", depends=["pkg2[ext]"])
+
+ # Install pkg3 with an older pkg2.
+ script.pip(
+ "install",
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "pkg3",
+ "pkg2==1.0",
+ )
+ script.assert_installed(pkg3="1.0", pkg2="1.0", pkg1="1.0")
+
+ # Now upgrade both pkg3 and pkg2. pkg2 should be upgraded although pkg2[ext]
+ # is not requested by the user.
+ script.pip(
+ "install",
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "--upgrade",
+ "pkg3",
+ "pkg2",
+ )
+ script.assert_installed(pkg3="1.0", pkg2="2.0", pkg1="1.0")
+
+
+def test_new_resolver_do_not_backtrack_on_build_failure(
+ script: PipTestEnvironment,
+) -> None:
+ create_basic_sdist_for_package(script, "pkg1", "2.0", fails_egg_info=True)
+ create_basic_wheel_for_package(script, "pkg1", "1.0")
+
+ result = script.pip(
+ "install",
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "pkg1",
+ expect_error=True,
+ )
+
+ assert "egg_info" in result.stderr
+
+
+def test_new_resolver_works_when_failing_package_builds_are_disallowed(
+ script: PipTestEnvironment,
+) -> None:
+ create_basic_wheel_for_package(script, "pkg2", "1.0", depends=["pkg1"])
+ create_basic_sdist_for_package(script, "pkg1", "2.0", fails_egg_info=True)
+ create_basic_wheel_for_package(script, "pkg1", "1.0")
+ constraints_file = script.scratch_path / "constraints.txt"
+ constraints_file.write_text("pkg1 != 2.0")
+
+ script.pip(
+ "install",
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "-c",
+ constraints_file,
+ "pkg2",
+ )
+
+ script.assert_installed(pkg2="1.0", pkg1="1.0")
diff --git a/tests/functional/test_new_resolver_errors.py b/tests/functional/test_new_resolver_errors.py
index b2e7af7c6..623041312 100644
--- a/tests/functional/test_new_resolver_errors.py
+++ b/tests/functional/test_new_resolver_errors.py
@@ -1,17 +1,29 @@
import pathlib
import sys
-from tests.lib import create_basic_wheel_for_package, create_test_package_with_setup
+from tests.lib import (
+ PipTestEnvironment,
+ create_basic_wheel_for_package,
+ create_test_package_with_setup,
+)
-def test_new_resolver_conflict_requirements_file(tmpdir, script):
+def test_new_resolver_conflict_requirements_file(
+ tmpdir: pathlib.Path, script: PipTestEnvironment
+) -> None:
create_basic_wheel_for_package(script, "base", "1.0")
create_basic_wheel_for_package(script, "base", "2.0")
create_basic_wheel_for_package(
- script, "pkga", "1.0", depends=["base==1.0"],
+ script,
+ "pkga",
+ "1.0",
+ depends=["base==1.0"],
)
create_basic_wheel_for_package(
- script, "pkgb", "1.0", depends=["base==2.0"],
+ script,
+ "pkgb",
+ "1.0",
+ depends=["base==2.0"],
)
req_file = tmpdir.joinpath("requirements.txt")
@@ -19,9 +31,12 @@ def test_new_resolver_conflict_requirements_file(tmpdir, script):
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "-r", req_file,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "-r",
+ req_file,
expect_error=True,
)
@@ -29,17 +44,22 @@ def test_new_resolver_conflict_requirements_file(tmpdir, script):
assert message in result.stderr, str(result)
-def test_new_resolver_conflict_constraints_file(tmpdir, script):
+def test_new_resolver_conflict_constraints_file(
+ tmpdir: pathlib.Path, script: PipTestEnvironment
+) -> None:
create_basic_wheel_for_package(script, "pkg", "1.0")
- constrats_file = tmpdir.joinpath("constraints.txt")
- constrats_file.write_text("pkg!=1.0")
+ constraints_file = tmpdir.joinpath("constraints.txt")
+ constraints_file.write_text("pkg!=1.0")
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
- "-c", constrats_file,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "-c",
+ constraints_file,
"pkg==1.0",
expect_error=True,
)
@@ -50,7 +70,7 @@ def test_new_resolver_conflict_constraints_file(tmpdir, script):
assert message in result.stdout, str(result)
-def test_new_resolver_requires_python_error(script):
+def test_new_resolver_requires_python_error(script: PipTestEnvironment) -> None:
compatible_python = ">={0.major}.{0.minor}".format(sys.version_info)
incompatible_python = "<{0.major}.{0.minor}".format(sys.version_info)
@@ -76,7 +96,9 @@ def test_new_resolver_requires_python_error(script):
assert compatible_python not in result.stderr, str(result)
-def test_new_resolver_checks_requires_python_before_dependencies(script):
+def test_new_resolver_checks_requires_python_before_dependencies(
+ script: PipTestEnvironment,
+) -> None:
incompatible_python = "<{0.major}.{0.minor}".format(sys.version_info)
pkg_dep = create_basic_wheel_for_package(
@@ -95,8 +117,11 @@ def test_new_resolver_checks_requires_python_before_dependencies(script):
)
result = script.pip(
- "install", "--no-cache-dir",
- "--no-index", "--find-links", script.scratch_path,
+ "install",
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"pkg-root",
expect_error=True,
)
diff --git a/tests/functional/test_new_resolver_hashes.py b/tests/functional/test_new_resolver_hashes.py
index 5540e9b54..6db2efd0e 100644
--- a/tests/functional/test_new_resolver_hashes.py
+++ b/tests/functional/test_new_resolver_hashes.py
@@ -3,15 +3,19 @@ import hashlib
import pytest
-from pip._internal.utils.urls import path_to_url
-from tests.lib import create_basic_sdist_for_package, create_basic_wheel_for_package
+from tests.lib import (
+ PipTestEnvironment,
+ create_basic_sdist_for_package,
+ create_basic_wheel_for_package,
+)
_FindLinks = collections.namedtuple(
- "_FindLinks", "index_html sdist_hash wheel_hash",
+ "_FindLinks",
+ "index_html sdist_hash wheel_hash",
)
-def _create_find_links(script):
+def _create_find_links(script: PipTestEnvironment) -> _FindLinks:
sdist_path = create_basic_sdist_for_package(script, "base", "0.1.0")
wheel_path = create_basic_wheel_for_package(script, "base", "0.1.0")
@@ -21,16 +25,17 @@ def _create_find_links(script):
index_html = script.scratch_path / "index.html"
index_html.write_text(
"""
+ <!DOCTYPE html>
<a href="{sdist_url}#sha256={sdist_hash}">{sdist_path.stem}</a>
<a href="{wheel_url}#sha256={wheel_hash}">{wheel_path.stem}</a>
""".format(
- sdist_url=path_to_url(sdist_path),
+ sdist_url=sdist_path.as_uri(),
sdist_hash=sdist_hash,
sdist_path=sdist_path,
- wheel_url=path_to_url(wheel_path),
+ wheel_url=wheel_path.as_uri(),
wheel_hash=wheel_hash,
wheel_path=wheel_path,
- )
+ ).strip()
)
return _FindLinks(index_html, sdist_hash, wheel_hash)
@@ -59,7 +64,9 @@ def _create_find_links(script):
],
ids=["identical", "intersect"],
)
-def test_new_resolver_hash_intersect(script, requirements_template, message):
+def test_new_resolver_hash_intersect(
+ script: PipTestEnvironment, requirements_template: str, message: str
+) -> None:
find_links = _create_find_links(script)
requirements_txt = script.scratch_path / "requirements.txt"
@@ -75,15 +82,19 @@ def test_new_resolver_hash_intersect(script, requirements_template, message):
"--no-cache-dir",
"--no-deps",
"--no-index",
- "--find-links", find_links.index_html,
+ "--find-links",
+ find_links.index_html,
"-vv",
- "--requirement", requirements_txt,
+ "--requirement",
+ requirements_txt,
)
assert message.format(name="base") in result.stdout, str(result)
-def test_new_resolver_hash_intersect_from_constraint(script):
+def test_new_resolver_hash_intersect_from_constraint(
+ script: PipTestEnvironment,
+) -> None:
find_links = _create_find_links(script)
constraints_txt = script.scratch_path / "constraints.txt"
@@ -107,10 +118,13 @@ def test_new_resolver_hash_intersect_from_constraint(script):
"--no-cache-dir",
"--no-deps",
"--no-index",
- "--find-links", find_links.index_html,
+ "--find-links",
+ find_links.index_html,
"-vv",
- "--constraint", constraints_txt,
- "--requirement", requirements_txt,
+ "--constraint",
+ constraints_txt,
+ "--requirement",
+ requirements_txt,
)
message = (
@@ -138,8 +152,10 @@ def test_new_resolver_hash_intersect_from_constraint(script):
ids=["both-requirements", "one-each"],
)
def test_new_resolver_hash_intersect_empty(
- script, requirements_template, constraints_template,
-):
+ script: PipTestEnvironment,
+ requirements_template: str,
+ constraints_template: str,
+) -> None:
find_links = _create_find_links(script)
constraints_txt = script.scratch_path / "constraints.txt"
@@ -163,9 +179,12 @@ def test_new_resolver_hash_intersect_empty(
"--no-cache-dir",
"--no-deps",
"--no-index",
- "--find-links", find_links.index_html,
- "--constraint", constraints_txt,
- "--requirement", requirements_txt,
+ "--find-links",
+ find_links.index_html,
+ "--constraint",
+ constraints_txt,
+ "--requirement",
+ requirements_txt,
expect_error=True,
)
@@ -174,7 +193,9 @@ def test_new_resolver_hash_intersect_empty(
) in result.stderr, str(result)
-def test_new_resolver_hash_intersect_empty_from_constraint(script):
+def test_new_resolver_hash_intersect_empty_from_constraint(
+ script: PipTestEnvironment,
+) -> None:
find_links = _create_find_links(script)
constraints_txt = script.scratch_path / "constraints.txt"
@@ -193,8 +214,10 @@ def test_new_resolver_hash_intersect_empty_from_constraint(script):
"--no-cache-dir",
"--no-deps",
"--no-index",
- "--find-links", find_links.index_html,
- "--constraint", constraints_txt,
+ "--find-links",
+ find_links.index_html,
+ "--constraint",
+ constraints_txt,
"base==0.1.0",
expect_error=True,
)
@@ -208,8 +231,9 @@ def test_new_resolver_hash_intersect_empty_from_constraint(script):
@pytest.mark.parametrize("constrain_by_hash", [False, True])
def test_new_resolver_hash_requirement_and_url_constraint_can_succeed(
- script, constrain_by_hash,
-):
+ script: PipTestEnvironment,
+ constrain_by_hash: bool,
+) -> None:
wheel_path = create_basic_wheel_for_package(script, "base", "0.1.0")
wheel_hash = hashlib.sha256(wheel_path.read_bytes()).hexdigest()
@@ -224,7 +248,7 @@ def test_new_resolver_hash_requirement_and_url_constraint_can_succeed(
)
constraints_txt = script.scratch_path / "constraints.txt"
- constraint_text = "base @ {wheel_url}\n".format(wheel_url=path_to_url(wheel_path))
+ constraint_text = "base @ {wheel_url}\n".format(wheel_url=wheel_path.as_uri())
if constrain_by_hash:
constraint_text += "base==0.1.0 --hash=sha256:{wheel_hash}\n".format(
wheel_hash=wheel_hash,
@@ -235,8 +259,10 @@ def test_new_resolver_hash_requirement_and_url_constraint_can_succeed(
"install",
"--no-cache-dir",
"--no-index",
- "--constraint", constraints_txt,
- "--requirement", requirements_txt,
+ "--constraint",
+ constraints_txt,
+ "--requirement",
+ requirements_txt,
)
script.assert_installed(base="0.1.0")
@@ -244,8 +270,9 @@ def test_new_resolver_hash_requirement_and_url_constraint_can_succeed(
@pytest.mark.parametrize("constrain_by_hash", [False, True])
def test_new_resolver_hash_requirement_and_url_constraint_can_fail(
- script, constrain_by_hash,
-):
+ script: PipTestEnvironment,
+ constrain_by_hash: bool,
+) -> None:
wheel_path = create_basic_wheel_for_package(script, "base", "0.1.0")
other_path = create_basic_wheel_for_package(script, "other", "0.1.0")
@@ -261,7 +288,7 @@ def test_new_resolver_hash_requirement_and_url_constraint_can_fail(
)
constraints_txt = script.scratch_path / "constraints.txt"
- constraint_text = "base @ {wheel_url}\n".format(wheel_url=path_to_url(wheel_path))
+ constraint_text = "base @ {wheel_url}\n".format(wheel_url=wheel_path.as_uri())
if constrain_by_hash:
constraint_text += "base==0.1.0 --hash=sha256:{other_hash}\n".format(
other_hash=other_hash,
@@ -272,8 +299,10 @@ def test_new_resolver_hash_requirement_and_url_constraint_can_fail(
"install",
"--no-cache-dir",
"--no-index",
- "--constraint", constraints_txt,
- "--requirement", requirements_txt,
+ "--constraint",
+ constraints_txt,
+ "--requirement",
+ requirements_txt,
expect_error=True,
)
@@ -282,3 +311,64 @@ def test_new_resolver_hash_requirement_and_url_constraint_can_fail(
) in result.stderr, str(result)
script.assert_not_installed("base", "other")
+
+
+def test_new_resolver_hash_with_extras(script: PipTestEnvironment) -> None:
+ parent_with_extra_path = create_basic_wheel_for_package(
+ script, "parent_with_extra", "0.1.0", depends=["child[extra]"]
+ )
+ parent_with_extra_hash = hashlib.sha256(
+ parent_with_extra_path.read_bytes()
+ ).hexdigest()
+
+ parent_without_extra_path = create_basic_wheel_for_package(
+ script, "parent_without_extra", "0.1.0", depends=["child"]
+ )
+ parent_without_extra_hash = hashlib.sha256(
+ parent_without_extra_path.read_bytes()
+ ).hexdigest()
+
+ child_path = create_basic_wheel_for_package(
+ script, "child", "0.1.0", extras={"extra": ["extra"]}
+ )
+ child_hash = hashlib.sha256(child_path.read_bytes()).hexdigest()
+
+ # Newer release
+ create_basic_wheel_for_package(
+ script, "child", "0.2.0", extras={"extra": ["extra"]}
+ )
+
+ extra_path = create_basic_wheel_for_package(script, "extra", "0.1.0")
+ extra_hash = hashlib.sha256(extra_path.read_bytes()).hexdigest()
+
+ requirements_txt = script.scratch_path / "requirements.txt"
+ requirements_txt.write_text(
+ """
+ child[extra]==0.1.0 --hash=sha256:{child_hash}
+ parent_with_extra==0.1.0 --hash=sha256:{parent_with_extra_hash}
+ parent_without_extra==0.1.0 --hash=sha256:{parent_without_extra_hash}
+ extra==0.1.0 --hash=sha256:{extra_hash}
+ """.format(
+ child_hash=child_hash,
+ parent_with_extra_hash=parent_with_extra_hash,
+ parent_without_extra_hash=parent_without_extra_hash,
+ extra_hash=extra_hash,
+ ),
+ )
+
+ script.pip(
+ "install",
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
+ "--requirement",
+ requirements_txt,
+ )
+
+ script.assert_installed(
+ parent_with_extra="0.1.0",
+ parent_without_extra="0.1.0",
+ child="0.1.0",
+ extra="0.1.0",
+ )
diff --git a/tests/functional/test_new_resolver_target.py b/tests/functional/test_new_resolver_target.py
index f5ec6ac7a..811ae935a 100644
--- a/tests/functional/test_new_resolver_target.py
+++ b/tests/functional/test_new_resolver_target.py
@@ -1,14 +1,18 @@
+from pathlib import Path
+from typing import Callable, Optional
+
import pytest
from pip._internal.cli.status_codes import ERROR, SUCCESS
-from tests.lib.path import Path
+from tests.lib import PipTestEnvironment
from tests.lib.wheel import make_wheel
+MakeFakeWheel = Callable[[str], str]
-@pytest.fixture()
-def make_fake_wheel(script):
- def _make_fake_wheel(wheel_tag):
+@pytest.fixture()
+def make_fake_wheel(script: PipTestEnvironment) -> MakeFakeWheel:
+ def _make_fake_wheel(wheel_tag: str) -> str:
wheel_house = script.scratch_path.joinpath("wheelhouse")
wheel_house.mkdir()
wheel_builder = make_wheel(
@@ -18,7 +22,7 @@ def make_fake_wheel(script):
)
wheel_path = wheel_house.joinpath(f"fake-1.0-{wheel_tag}.whl")
wheel_builder.save_to(wheel_path)
- return wheel_path
+ return str(wheel_path)
return _make_fake_wheel
@@ -28,19 +32,21 @@ def make_fake_wheel(script):
@pytest.mark.parametrize("abi", [None, "fakeabi"])
@pytest.mark.parametrize("platform", [None, "fakeplat"])
def test_new_resolver_target_checks_compatibility_failure(
- script,
- make_fake_wheel,
- implementation,
- python_version,
- abi,
- platform,
-):
+ script: PipTestEnvironment,
+ make_fake_wheel: MakeFakeWheel,
+ implementation: Optional[str],
+ python_version: Optional[str],
+ abi: Optional[str],
+ platform: Optional[str],
+) -> None:
fake_wheel_tag = "fakepy1-fakeabi-fakeplat"
args = [
"install",
"--only-binary=:all:",
- "--no-cache-dir", "--no-index",
- "--target", str(script.scratch_path.joinpath("target")),
+ "--no-cache-dir",
+ "--no-index",
+ "--target",
+ str(script.scratch_path.joinpath("target")),
make_fake_wheel(fake_wheel_tag),
]
if implementation:
@@ -58,7 +64,7 @@ def test_new_resolver_target_checks_compatibility_failure(
abi,
platform,
)
- wheel_tag_matches = (args_tag == fake_wheel_tag)
+ wheel_tag_matches = args_tag == fake_wheel_tag
result = script.pip(*args, expect_error=(not wheel_tag_matches))
diff --git a/tests/functional/test_new_resolver_user.py b/tests/functional/test_new_resolver_user.py
index c2c1ff18f..2f9fb65ba 100644
--- a/tests/functional/test_new_resolver_user.py
+++ b/tests/functional/test_new_resolver_user.py
@@ -3,16 +3,19 @@ import textwrap
import pytest
-from tests.lib import create_basic_wheel_for_package
+from tests.lib import PipTestEnvironment, create_basic_wheel_for_package
+from tests.lib.venv import VirtualEnvironment
@pytest.mark.incompatible_with_test_venv
-def test_new_resolver_install_user(script):
+def test_new_resolver_install_user(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(script, "base", "0.1.0")
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--user",
"base",
)
@@ -20,7 +23,9 @@ def test_new_resolver_install_user(script):
@pytest.mark.incompatible_with_test_venv
-def test_new_resolver_install_user_satisfied_by_global_site(script):
+def test_new_resolver_install_user_satisfied_by_global_site(
+ script: PipTestEnvironment,
+) -> None:
"""
An install a matching version to user site should re-use a global site
installation if it satisfies.
@@ -29,14 +34,18 @@ def test_new_resolver_install_user_satisfied_by_global_site(script):
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base==1.0.0",
)
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--user",
"base==1.0.0",
)
@@ -45,7 +54,9 @@ def test_new_resolver_install_user_satisfied_by_global_site(script):
@pytest.mark.incompatible_with_test_venv
-def test_new_resolver_install_user_conflict_in_user_site(script):
+def test_new_resolver_install_user_conflict_in_user_site(
+ script: PipTestEnvironment,
+) -> None:
"""
Installing a different version in user site should uninstall an existing
different version in user site.
@@ -55,16 +66,20 @@ def test_new_resolver_install_user_conflict_in_user_site(script):
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--user",
"base==2.0.0",
)
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--user",
"base==1.0.0",
)
@@ -77,20 +92,26 @@ def test_new_resolver_install_user_conflict_in_user_site(script):
@pytest.mark.incompatible_with_test_venv
-def test_new_resolver_install_user_in_virtualenv_with_conflict_fails(script):
+def test_new_resolver_install_user_in_virtualenv_with_conflict_fails(
+ script: PipTestEnvironment,
+) -> None:
create_basic_wheel_for_package(script, "base", "1.0.0")
create_basic_wheel_for_package(script, "base", "2.0.0")
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base==2.0.0",
)
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--user",
"base==1.0.0",
expect_error=True,
@@ -104,23 +125,27 @@ def test_new_resolver_install_user_in_virtualenv_with_conflict_fails(script):
@pytest.fixture()
-def patch_dist_in_site_packages(virtualenv):
+def patch_dist_in_site_packages(virtualenv: VirtualEnvironment) -> None:
# Since the tests are run from a virtualenv, and to avoid the "Will not
# install to the usersite because it will lack sys.path precedence..."
# error: Monkey patch `pip._internal.utils.misc.dist_in_site_packages`
# so it's possible to install a conflicting distribution in the user site.
- virtualenv.sitecustomize = textwrap.dedent("""
+ virtualenv.sitecustomize = textwrap.dedent(
+ """
def dist_in_site_packages(dist):
return False
- from pip._internal.utils import misc
- misc.dist_in_site_packages = dist_in_site_packages
- """)
+ from pip._internal.metadata.base import BaseDistribution
+ BaseDistribution.in_site_packages = property(dist_in_site_packages)
+ """
+ )
@pytest.mark.incompatible_with_test_venv
@pytest.mark.usefixtures("patch_dist_in_site_packages")
-def test_new_resolver_install_user_reinstall_global_site(script):
+def test_new_resolver_install_user_reinstall_global_site(
+ script: PipTestEnvironment,
+) -> None:
"""
Specifying --force-reinstall makes a different version in user site,
ignoring the matching installation in global site.
@@ -129,14 +154,18 @@ def test_new_resolver_install_user_reinstall_global_site(script):
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base==1.0.0",
)
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--user",
"--force-reinstall",
"base==1.0.0",
@@ -150,7 +179,9 @@ def test_new_resolver_install_user_reinstall_global_site(script):
@pytest.mark.incompatible_with_test_venv
@pytest.mark.usefixtures("patch_dist_in_site_packages")
-def test_new_resolver_install_user_conflict_in_global_site(script):
+def test_new_resolver_install_user_conflict_in_global_site(
+ script: PipTestEnvironment,
+) -> None:
"""
Installing a different version in user site should ignore an existing
different version in global site, and simply add to the user site.
@@ -160,15 +191,19 @@ def test_new_resolver_install_user_conflict_in_global_site(script):
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base==1.0.0",
)
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--user",
"base==2.0.0",
)
@@ -182,7 +217,9 @@ def test_new_resolver_install_user_conflict_in_global_site(script):
@pytest.mark.incompatible_with_test_venv
@pytest.mark.usefixtures("patch_dist_in_site_packages")
-def test_new_resolver_install_user_conflict_in_global_and_user_sites(script):
+def test_new_resolver_install_user_conflict_in_global_and_user_sites(
+ script: PipTestEnvironment,
+) -> None:
"""
Installing a different version in user site should ignore an existing
different version in global site, but still upgrade the user site.
@@ -192,14 +229,18 @@ def test_new_resolver_install_user_conflict_in_global_and_user_sites(script):
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"base==2.0.0",
)
script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--user",
"--force-reinstall",
"base==2.0.0",
@@ -207,8 +248,10 @@ def test_new_resolver_install_user_conflict_in_global_and_user_sites(script):
result = script.pip(
"install",
- "--no-cache-dir", "--no-index",
- "--find-links", script.scratch_path,
+ "--no-cache-dir",
+ "--no-index",
+ "--find-links",
+ script.scratch_path,
"--user",
"base==1.0.0",
)
@@ -217,7 +260,7 @@ def test_new_resolver_install_user_conflict_in_global_and_user_sites(script):
base_2_dist_info = script.user_site / "base-2.0.0.dist-info"
result.did_create(base_1_dist_info)
- assert base_2_dist_info in result.files_deleted, str(result)
+ assert base_2_dist_info in result.files_deleted
site_packages_content = set(os.listdir(script.site_packages_path))
assert "base-2.0.0.dist-info" in site_packages_content
diff --git a/tests/functional/test_no_color.py b/tests/functional/test_no_color.py
index 3fd943f93..4094bdd16 100644
--- a/tests/functional/test_no_color.py
+++ b/tests/functional/test_no_color.py
@@ -2,14 +2,18 @@
Test specific for the --no-color option
"""
import os
+import shutil
import subprocess
+import sys
import pytest
+from tests.lib import PipTestEnvironment
-def test_no_color(script):
- """Ensure colour output disabled when --no-color is passed.
- """
+
+@pytest.mark.skipif(shutil.which("script") is None, reason="no 'script' executable")
+def test_no_color(script: PipTestEnvironment) -> None:
+ """Ensure colour output disabled when --no-color is passed."""
# Using 'script' in this test allows for transparently testing pip's output
# since pip is smart enough to disable colour output when piped, which is
# not the behaviour we want to be testing here.
@@ -18,19 +22,21 @@ def test_no_color(script):
# 'script' and well as the mere use of the same.
#
# This test will stay until someone has the time to rewrite it.
- command = (
- 'script --flush --quiet --return /tmp/pip-test-no-color.txt '
- '--command "pip uninstall {} noSuchPackage"'
- )
+ pip_command = "pip uninstall {} noSuchPackage"
+ if sys.platform == "darwin":
+ command = f"script -q /tmp/pip-test-no-color.txt {pip_command}"
+ else:
+ command = f'script -q /tmp/pip-test-no-color.txt --command "{pip_command}"'
- def get_run_output(option):
+ def get_run_output(option: str = "") -> str:
cmd = command.format(option)
proc = subprocess.Popen(
- cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ cmd,
+ shell=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
)
proc.communicate()
- if proc.returncode:
- pytest.skip("Unable to capture output using script: " + cmd)
try:
with open("/tmp/pip-test-no-color.txt") as output_file:
@@ -39,6 +45,5 @@ def test_no_color(script):
finally:
os.unlink("/tmp/pip-test-no-color.txt")
- assert "\x1b" in get_run_output(option=""), "Expected color in output"
- assert "\x1b" not in get_run_output(option="--no-color"), \
- "Expected no color in output"
+ assert "\x1b[3" in get_run_output(""), "Expected color in output"
+ assert "\x1b[3" not in get_run_output("--no-color"), "Expected no color in output"
diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py
index b45565844..a642a3f8b 100644
--- a/tests/functional/test_pep517.py
+++ b/tests/functional/test_pep517.py
@@ -1,41 +1,53 @@
-import pytest
+import os
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple
-# The vendored `tomli` package is not used here because it doesn't
-# have write capability
-import toml
+import pytest
+import tomli_w
from pip._internal.build_env import BuildEnvironment
from pip._internal.req import InstallRequirement
-from tests.lib import make_test_finder, path_to_url
-
-
-def make_project(tmpdir, requires=None, backend=None, backend_path=None):
+from tests.lib import (
+ PipTestEnvironment,
+ TestData,
+ create_basic_wheel_for_package,
+ make_test_finder,
+)
+
+
+def make_project(
+ tmpdir: Path,
+ requires: Optional[List[str]] = None,
+ backend: Optional[str] = None,
+ backend_path: Optional[List[str]] = None,
+) -> Path:
requires = requires or []
- project_dir = tmpdir / 'project'
+ project_dir = tmpdir / "project"
project_dir.mkdir()
- buildsys = {'requires': requires}
+ buildsys: Dict[str, Any] = {"requires": requires}
if backend:
- buildsys['build-backend'] = backend
+ buildsys["build-backend"] = backend
if backend_path:
- buildsys['backend-path'] = backend_path
- data = toml.dumps({'build-system': buildsys})
- project_dir.joinpath('pyproject.toml').write_text(data)
+ buildsys["backend-path"] = backend_path
+ data = tomli_w.dumps({"build-system": buildsys})
+ project_dir.joinpath("pyproject.toml").write_text(data)
return project_dir
-def test_backend(tmpdir, data):
+def test_backend(tmpdir: Path, data: TestData) -> None:
"""Check we can call a requirement's backend successfully"""
project_dir = make_project(tmpdir, backend="dummy_backend")
req = InstallRequirement(None, None)
- req.source_dir = project_dir # make req believe it has been unpacked
+ req.source_dir = os.fspath(project_dir) # make req believe it has been unpacked
req.load_pyproject_toml()
env = BuildEnvironment()
finder = make_test_finder(find_links=[data.backends])
- env.install_requirements(finder, ["dummy_backend"], 'normal', "Installing")
+ env.install_requirements(finder, ["dummy_backend"], "normal", kind="Installing")
conflicting, missing = env.check_requirements(["dummy_backend"])
assert not conflicting and not missing
- assert hasattr(req.pep517_backend, 'build_wheel')
+ assert hasattr(req.pep517_backend, "build_wheel")
with env:
+ assert req.pep517_backend is not None
assert req.pep517_backend.build_wheel("dir") == "Backend called"
@@ -49,237 +61,364 @@ def build_wheel(
"""
-def test_backend_path(tmpdir, data):
+def test_backend_path(tmpdir: Path, data: TestData) -> None:
"""Check we can call a backend inside the project"""
- project_dir = make_project(
- tmpdir, backend="dummy_backend", backend_path=['.']
- )
- (project_dir / 'dummy_backend.py').write_text(dummy_backend_code)
+ project_dir = make_project(tmpdir, backend="dummy_backend", backend_path=["."])
+ (project_dir / "dummy_backend.py").write_text(dummy_backend_code)
req = InstallRequirement(None, None)
- req.source_dir = project_dir # make req believe it has been unpacked
+ req.source_dir = os.fspath(project_dir) # make req believe it has been unpacked
req.load_pyproject_toml()
env = BuildEnvironment()
- assert hasattr(req.pep517_backend, 'build_wheel')
+ assert hasattr(req.pep517_backend, "build_wheel")
with env:
+ assert req.pep517_backend is not None
assert req.pep517_backend.build_wheel("dir") == "Backend called"
-def test_backend_path_and_dep(tmpdir, data):
+def test_backend_path_and_dep(tmpdir: Path, data: TestData) -> None:
"""Check we can call a requirement's backend successfully"""
project_dir = make_project(
- tmpdir, backend="dummy_internal_backend", backend_path=['.']
+ tmpdir, backend="dummy_internal_backend", backend_path=["."]
)
- (project_dir / 'dummy_internal_backend.py').write_text(
+ (project_dir / "dummy_internal_backend.py").write_text(
"from dummy_backend import build_wheel"
)
req = InstallRequirement(None, None)
- req.source_dir = project_dir # make req believe it has been unpacked
+ req.source_dir = os.fspath(project_dir) # make req believe it has been unpacked
req.load_pyproject_toml()
env = BuildEnvironment()
finder = make_test_finder(find_links=[data.backends])
- env.install_requirements(finder, ["dummy_backend"], 'normal', "Installing")
+ env.install_requirements(finder, ["dummy_backend"], "normal", kind="Installing")
- assert hasattr(req.pep517_backend, 'build_wheel')
+ assert hasattr(req.pep517_backend, "build_wheel")
with env:
+ assert req.pep517_backend is not None
assert req.pep517_backend.build_wheel("dir") == "Backend called"
-def test_pep517_install(script, tmpdir, data):
+def test_pep517_install(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData
+) -> None:
"""Check we can build with a custom backend"""
project_dir = make_project(
- tmpdir, requires=['test_backend'],
- backend="test_backend"
+ tmpdir, requires=["test_backend"], backend="test_backend"
)
- result = script.pip(
- 'install', '--no-index', '-f', data.backends, project_dir
- )
- result.assert_installed('project', editable=False)
+ result = script.pip("install", "--no-index", "-f", data.backends, project_dir)
+ result.assert_installed("project", editable=False)
-def test_pep517_install_with_reqs(script, tmpdir, data):
+def test_pep517_install_with_reqs(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData
+) -> None:
"""Backend generated requirements are installed in the build env"""
project_dir = make_project(
- tmpdir, requires=['test_backend'],
- backend="test_backend"
+ tmpdir, requires=["test_backend"], backend="test_backend"
)
project_dir.joinpath("backend_reqs.txt").write_text("simplewheel")
result = script.pip(
- 'install', '--no-index',
- '-f', data.backends,
- '-f', data.packages,
- project_dir
+ "install", "--no-index", "-f", data.backends, "-f", data.packages, project_dir
)
- result.assert_installed('project', editable=False)
+ result.assert_installed("project", editable=False)
-def test_no_use_pep517_without_setup_py(script, tmpdir, data):
+def test_no_use_pep517_without_setup_py(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData
+) -> None:
"""Using --no-use-pep517 requires setup.py"""
project_dir = make_project(
- tmpdir, requires=['test_backend'],
- backend="test_backend"
+ tmpdir, requires=["test_backend"], backend="test_backend"
)
result = script.pip(
- 'install', '--no-index', '--no-use-pep517',
- '-f', data.backends,
+ "install",
+ "--no-index",
+ "--no-use-pep517",
+ "-f",
+ data.backends,
project_dir,
- expect_error=True
+ expect_error=True,
)
- assert 'project does not have a setup.py' in result.stderr
+ assert "project does not have a setup.py" in result.stderr
-def test_conflicting_pep517_backend_requirements(script, tmpdir, data):
+def test_conflicting_pep517_backend_requirements(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData
+) -> None:
project_dir = make_project(
- tmpdir, requires=['test_backend', 'simplewheel==1.0'],
- backend="test_backend"
+ tmpdir, requires=["test_backend", "simplewheel==1.0"], backend="test_backend"
)
project_dir.joinpath("backend_reqs.txt").write_text("simplewheel==2.0")
result = script.pip(
- 'install', '--no-index',
- '-f', data.backends,
- '-f', data.packages,
+ "install",
+ "--no-index",
+ "-f",
+ data.backends,
+ "-f",
+ data.packages,
+ project_dir,
+ expect_error=True,
+ )
+ msg = (
+ "Some build dependencies for {url} conflict with the backend "
+ "dependencies: simplewheel==1.0 is incompatible with "
+ "simplewheel==2.0.".format(url=project_dir.as_uri())
+ )
+ assert result.returncode != 0 and msg in result.stderr, str(result)
+
+
+def test_no_check_build_deps(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData
+) -> None:
+ project_dir = make_project(
+ tmpdir, requires=["simplewheel==2.0"], backend="test_backend"
+ )
+ script.pip(
+ "install",
+ "simplewheel==1.0",
+ "test_backend",
+ "--no-index",
+ "-f",
+ data.packages,
+ "-f",
+ data.backends,
+ )
+ result = script.pip("install", "--no-build-isolation", project_dir)
+ result.assert_installed("project", editable=False)
+
+
+def test_validate_missing_pep517_backend_requirements(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData
+) -> None:
+ project_dir = make_project(
+ tmpdir, requires=["test_backend", "simplewheel==1.0"], backend="test_backend"
+ )
+ result = script.pip(
+ "install",
+ "--no-index",
+ "-f",
+ data.backends,
+ "-f",
+ data.packages,
+ "--no-build-isolation",
+ "--check-build-dependencies",
project_dir,
- expect_error=True
+ expect_error=True,
)
msg = (
- 'Some build dependencies for {url} conflict with the backend '
- 'dependencies: simplewheel==1.0 is incompatible with '
- 'simplewheel==2.0.'.format(url=path_to_url(project_dir)))
- assert (
- result.returncode != 0 and
- msg in result.stderr
- ), str(result)
+ "Some build dependencies for {url} are missing: "
+ "'simplewheel==1.0', 'test_backend'.".format(url=project_dir.as_uri())
+ )
+ assert result.returncode != 0 and msg in result.stderr, str(result)
-def test_pep517_backend_requirements_already_satisfied(script, tmpdir, data):
+def test_validate_conflicting_pep517_backend_requirements(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData
+) -> None:
project_dir = make_project(
- tmpdir, requires=['test_backend', 'simplewheel==1.0'],
- backend="test_backend"
+ tmpdir, requires=["simplewheel==1.0"], backend="test_backend"
+ )
+ script.pip("install", "simplewheel==2.0", "--no-index", "-f", data.packages)
+ result = script.pip(
+ "install",
+ "--no-index",
+ "-f",
+ data.backends,
+ "-f",
+ data.packages,
+ "--no-build-isolation",
+ "--check-build-dependencies",
+ project_dir,
+ expect_error=True,
+ )
+ msg = (
+ "Some build dependencies for {url} conflict with the backend "
+ "dependencies: simplewheel==2.0 is incompatible with "
+ "simplewheel==1.0.".format(url=project_dir.as_uri())
+ )
+ assert result.returncode != 0 and msg in result.stderr, str(result)
+
+
+def test_pep517_backend_requirements_satisfied_by_prerelease(
+ script: PipTestEnvironment,
+ data: TestData,
+) -> None:
+ create_basic_wheel_for_package(script, "myreq", "1.0a1")
+ script.pip("install", "myreq==1.0a1", "--no-index", "-f", script.scratch_path)
+ script.pip("install", "test_backend", "--no-index", "-f", data.backends)
+
+ project_dir = make_project(
+ script.temp_path,
+ requires=["test_backend", "myreq"],
+ backend="test_backend",
+ )
+ project_dir.joinpath("backend_reqs.txt").write_text("myreq")
+
+ result = script.pip("install", "--no-index", "--no-build-isolation", project_dir)
+ assert "Installing backend dependencies:" not in result.stdout
+
+
+def test_pep517_backend_requirements_already_satisfied(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData
+) -> None:
+ project_dir = make_project(
+ tmpdir, requires=["test_backend", "simplewheel==1.0"], backend="test_backend"
)
project_dir.joinpath("backend_reqs.txt").write_text("simplewheel")
result = script.pip(
- 'install', '--no-index',
- '-f', data.backends,
- '-f', data.packages,
+ "install",
+ "--no-index",
+ "-f",
+ data.backends,
+ "-f",
+ data.packages,
project_dir,
)
- assert 'Installing backend dependencies:' not in result.stdout
+ assert "Installing backend dependencies:" not in result.stdout
-def test_pep517_install_with_no_cache_dir(script, tmpdir, data):
- """Check builds with a custom backends work, even with no cache.
- """
+def test_pep517_install_with_no_cache_dir(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData
+) -> None:
+ """Check builds with a custom backends work, even with no cache."""
project_dir = make_project(
- tmpdir, requires=['test_backend'],
- backend="test_backend"
+ tmpdir, requires=["test_backend"], backend="test_backend"
)
result = script.pip(
- 'install', '--no-cache-dir', '--no-index', '-f', data.backends,
+ "install",
+ "--no-cache-dir",
+ "--no-index",
+ "-f",
+ data.backends,
project_dir,
)
- result.assert_installed('project', editable=False)
+ result.assert_installed("project", editable=False)
-def make_pyproject_with_setup(tmpdir, build_system=True, set_backend=True):
- project_dir = tmpdir / 'project'
+def make_pyproject_with_setup(
+ tmpdir: Path, build_system: bool = True, set_backend: bool = True
+) -> Tuple[Path, str]:
+ project_dir = tmpdir / "project"
project_dir.mkdir()
- setup_script = (
- 'from setuptools import setup\n'
- )
+ setup_script = "from setuptools import setup\n"
expect_script_dir_on_path = True
if build_system:
- buildsys = {
- 'requires': ['setuptools', 'wheel'],
+ buildsys: Dict[str, Any] = {
+ "requires": ["setuptools", "wheel"],
}
if set_backend:
- buildsys['build-backend'] = 'setuptools.build_meta'
+ buildsys["build-backend"] = "setuptools.build_meta"
expect_script_dir_on_path = False
- project_data = toml.dumps({'build-system': buildsys})
+ project_data = tomli_w.dumps({"build-system": buildsys})
else:
- project_data = ''
+ project_data = ""
if expect_script_dir_on_path:
- setup_script += (
- 'from pep517_test import __version__\n'
- )
+ setup_script += "from pep517_test import __version__\n"
else:
setup_script += (
- 'try:\n'
- ' import pep517_test\n'
- 'except ImportError:\n'
- ' pass\n'
- 'else:\n'
+ "try:\n"
+ " import pep517_test\n"
+ "except ImportError:\n"
+ " pass\n"
+ "else:\n"
' raise RuntimeError("Source dir incorrectly on sys.path")\n'
)
- setup_script += (
- 'setup(name="pep517_test", version="0.1", packages=["pep517_test"])'
- )
+ setup_script += 'setup(name="pep517_test", version="0.1", packages=["pep517_test"])'
- project_dir.joinpath('pyproject.toml').write_text(project_data)
- project_dir.joinpath('setup.py').write_text(setup_script)
+ project_dir.joinpath("pyproject.toml").write_text(project_data)
+ project_dir.joinpath("setup.py").write_text(setup_script)
package_dir = project_dir / "pep517_test"
package_dir.mkdir()
- package_dir.joinpath('__init__.py').write_text('__version__ = "0.1"')
+ package_dir.joinpath("__init__.py").write_text('__version__ = "0.1"')
return project_dir, "pep517_test"
-def test_no_build_system_section(script, tmpdir, data, common_wheels):
- """Check builds with setup.py, pyproject.toml, but no build-system section.
- """
+def test_no_build_system_section(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData, common_wheels: Path
+) -> None:
+ """Check builds with setup.py, pyproject.toml, but no build-system section."""
project_dir, name = make_pyproject_with_setup(tmpdir, build_system=False)
result = script.pip(
- 'install', '--no-cache-dir', '--no-index', '-f', common_wheels,
+ "install",
+ "--no-cache-dir",
+ "--no-index",
+ "-f",
+ common_wheels,
project_dir,
)
result.assert_installed(name, editable=False)
-def test_no_build_backend_entry(script, tmpdir, data, common_wheels):
- """Check builds with setup.py, pyproject.toml, but no build-backend entry.
- """
+def test_no_build_backend_entry(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData, common_wheels: Path
+) -> None:
+ """Check builds with setup.py, pyproject.toml, but no build-backend entry."""
project_dir, name = make_pyproject_with_setup(tmpdir, set_backend=False)
result = script.pip(
- 'install', '--no-cache-dir', '--no-index', '-f', common_wheels,
+ "install",
+ "--no-cache-dir",
+ "--no-index",
+ "-f",
+ common_wheels,
project_dir,
)
result.assert_installed(name, editable=False)
-def test_explicit_setuptools_backend(script, tmpdir, data, common_wheels):
- """Check builds with setup.py, pyproject.toml, and a build-backend entry.
- """
+def test_explicit_setuptools_backend(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData, common_wheels: Path
+) -> None:
+ """Check builds with setup.py, pyproject.toml, and a build-backend entry."""
project_dir, name = make_pyproject_with_setup(tmpdir)
result = script.pip(
- 'install', '--no-cache-dir', '--no-index', '-f', common_wheels,
+ "install",
+ "--no-cache-dir",
+ "--no-index",
+ "-f",
+ common_wheels,
project_dir,
)
result.assert_installed(name, editable=False)
@pytest.mark.network
-def test_pep517_and_build_options(script, tmpdir, data, common_wheels):
+def test_pep517_and_build_options(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData, common_wheels: Path
+) -> None:
"""Backend generated requirements are installed in the build env"""
project_dir, name = make_pyproject_with_setup(tmpdir)
result = script.pip(
- 'wheel', '--wheel-dir', tmpdir,
- '--build-option', 'foo',
- '-f', common_wheels,
+ "wheel",
+ "--wheel-dir",
+ tmpdir,
+ "--build-option",
+ "foo",
+ "-f",
+ common_wheels,
project_dir,
+ allow_stderr_warning=True,
)
- assert 'Ignoring --build-option when building' in result.stderr
- assert 'using PEP 517' in result.stderr
+ assert "Ignoring --build-option when building" in result.stderr
+ assert "using PEP 517" in result.stderr
@pytest.mark.network
-def test_pep517_and_global_options(script, tmpdir, data, common_wheels):
+def test_pep517_and_global_options(
+ script: PipTestEnvironment, tmpdir: Path, data: TestData, common_wheels: Path
+) -> None:
"""Backend generated requirements are installed in the build env"""
project_dir, name = make_pyproject_with_setup(tmpdir)
result = script.pip(
- 'wheel', '--wheel-dir', tmpdir,
- '--global-option', 'foo',
- '-f', common_wheels,
+ "wheel",
+ "--wheel-dir",
+ tmpdir,
+ "--global-option",
+ "foo",
+ "-f",
+ common_wheels,
project_dir,
+ allow_stderr_warning=True,
)
- assert 'Ignoring --global-option when building' in result.stderr
- assert 'using PEP 517' in result.stderr
+ assert "Ignoring --global-option when building" in result.stderr
+ assert "using PEP 517" in result.stderr
diff --git a/tests/functional/test_pep660.py b/tests/functional/test_pep660.py
new file mode 100644
index 000000000..874f72036
--- /dev/null
+++ b/tests/functional/test_pep660.py
@@ -0,0 +1,232 @@
+import os
+from pathlib import Path
+from typing import Any, Dict
+
+import pytest
+import tomli_w
+
+from tests.lib import PipTestEnvironment
+
+SETUP_PY = """
+from setuptools import setup
+
+setup()
+"""
+
+SETUP_CFG = """
+[metadata]
+name = project
+version = 1.0.0
+"""
+
+BACKEND_WITHOUT_PEP660 = """
+from setuptools.build_meta import (
+ build_wheel as _build_wheel,
+ prepare_metadata_for_build_wheel as _prepare_metadata_for_build_wheel,
+ get_requires_for_build_wheel as _get_requires_for_build_wheel,
+)
+
+def get_requires_for_build_wheel(config_settings=None):
+ with open("log.txt", "a") as f:
+ print(":get_requires_for_build_wheel called", file=f)
+ return _get_requires_for_build_wheel(config_settings)
+
+def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
+ with open("log.txt", "a") as f:
+ print(":prepare_metadata_for_build_wheel called", file=f)
+ return _prepare_metadata_for_build_wheel(metadata_directory, config_settings)
+
+def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
+ with open("log.txt", "a") as f:
+ print(":build_wheel called", file=f)
+ return _build_wheel(wheel_directory, config_settings, metadata_directory)
+"""
+
+# fmt: off
+BACKEND_WITH_PEP660 = BACKEND_WITHOUT_PEP660 + """
+def get_requires_for_build_editable(config_settings=None):
+ with open("log.txt", "a") as f:
+ print(":get_requires_for_build_editable called", file=f)
+ return _get_requires_for_build_wheel(config_settings)
+
+def prepare_metadata_for_build_editable(metadata_directory, config_settings=None):
+ with open("log.txt", "a") as f:
+ print(":prepare_metadata_for_build_editable called", file=f)
+ return _prepare_metadata_for_build_wheel(metadata_directory, config_settings)
+
+def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
+ with open("log.txt", "a") as f:
+ print(":build_editable called", file=f)
+ return _build_wheel(wheel_directory, config_settings, metadata_directory)
+"""
+# fmt: on
+
+
+def _make_project(
+ tmpdir: Path, backend_code: str, with_setup_py: bool, with_pyproject: bool = True
+) -> Path:
+ project_dir = tmpdir / "project"
+ project_dir.mkdir()
+ project_dir.joinpath("setup.cfg").write_text(SETUP_CFG)
+ if with_setup_py:
+ project_dir.joinpath("setup.py").write_text(SETUP_PY)
+ if backend_code:
+ assert with_pyproject
+ buildsys: Dict[str, Any] = {"requires": ["setuptools", "wheel"]}
+ buildsys["build-backend"] = "test_backend"
+ buildsys["backend-path"] = ["."]
+ data = tomli_w.dumps({"build-system": buildsys})
+ project_dir.joinpath("pyproject.toml").write_text(data)
+ project_dir.joinpath("test_backend.py").write_text(backend_code)
+ elif with_pyproject:
+ project_dir.joinpath("pyproject.toml").touch()
+ project_dir.joinpath("log.txt").touch()
+ return project_dir
+
+
+def _assert_hook_called(project_dir: Path, hook: str) -> None:
+ log = project_dir.joinpath("log.txt").read_text()
+ assert f":{hook} called" in log, f"{hook} has not been called"
+
+
+def _assert_hook_not_called(project_dir: Path, hook: str) -> None:
+ log = project_dir.joinpath("log.txt").read_text()
+ assert f":{hook} called" not in log, f"{hook} should not have been called"
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_install_pep517_basic(tmpdir: Path, script: PipTestEnvironment) -> None:
+ """
+ Check that the test harness we have in this file is sane.
+ """
+ project_dir = _make_project(tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=False)
+ script.pip(
+ "install",
+ "--no-index",
+ "--no-build-isolation",
+ project_dir,
+ )
+ _assert_hook_called(project_dir, "prepare_metadata_for_build_wheel")
+ _assert_hook_called(project_dir, "build_wheel")
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_install_pep660_basic(tmpdir: Path, script: PipTestEnvironment) -> None:
+ """
+ Test with backend that supports build_editable.
+ """
+ project_dir = _make_project(tmpdir, BACKEND_WITH_PEP660, with_setup_py=False)
+ result = script.pip(
+ "install",
+ "--no-index",
+ "--no-build-isolation",
+ "--editable",
+ project_dir,
+ )
+ _assert_hook_called(project_dir, "prepare_metadata_for_build_editable")
+ _assert_hook_called(project_dir, "build_editable")
+ assert (
+ result.test_env.site_packages.joinpath("project.egg-link")
+ not in result.files_created
+ ), "a .egg-link file should not have been created"
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_install_no_pep660_setup_py_fallback(
+ tmpdir: Path, script: PipTestEnvironment
+) -> None:
+ """
+ Test that we fall back to setuptools develop when using a backend that
+ does not support build_editable. Since there is a pyproject.toml,
+ the prepare_metadata_for_build_wheel hook is called.
+ """
+ project_dir = _make_project(tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=True)
+ result = script.pip(
+ "install",
+ "--no-index",
+ "--no-build-isolation",
+ "--editable",
+ project_dir,
+ allow_stderr_warning=False,
+ )
+ _assert_hook_called(project_dir, "prepare_metadata_for_build_wheel")
+ assert (
+ result.test_env.site_packages.joinpath("project.egg-link")
+ in result.files_created
+ ), "a .egg-link file should have been created"
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_install_no_pep660_setup_cfg_fallback(
+ tmpdir: Path, script: PipTestEnvironment
+) -> None:
+ """
+ Test that we fall back to setuptools develop when using a backend that
+ does not support build_editable. Since there is a pyproject.toml,
+ the prepare_metadata_for_build_wheel hook is called.
+ """
+ project_dir = _make_project(tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=False)
+ result = script.pip(
+ "install",
+ "--no-index",
+ "--no-build-isolation",
+ "--editable",
+ project_dir,
+ allow_stderr_warning=False,
+ )
+ print(result.stdout, result.stderr)
+ _assert_hook_called(project_dir, "prepare_metadata_for_build_wheel")
+ assert (
+ result.test_env.site_packages.joinpath("project.egg-link")
+ in result.files_created
+ ), ".egg-link file should have been created"
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_wheel_editable_pep660_basic(tmpdir: Path, script: PipTestEnvironment) -> None:
+ """
+ Test 'pip wheel' of an editable pep 660 project.
+ It must *not* call prepare_metadata_for_build_editable.
+ """
+ project_dir = _make_project(tmpdir, BACKEND_WITH_PEP660, with_setup_py=False)
+ wheel_dir = tmpdir / "dist"
+ script.pip(
+ "wheel",
+ "--no-index",
+ "--no-build-isolation",
+ "--editable",
+ project_dir,
+ "-w",
+ wheel_dir,
+ )
+ _assert_hook_not_called(project_dir, "prepare_metadata_for_build_editable")
+ _assert_hook_not_called(project_dir, "build_editable")
+ _assert_hook_called(project_dir, "prepare_metadata_for_build_wheel")
+ _assert_hook_called(project_dir, "build_wheel")
+ assert len(os.listdir(str(wheel_dir))) == 1, "a wheel should have been created"
+
+
+@pytest.mark.usefixtures("with_wheel")
+def test_download_editable_pep660_basic(
+ tmpdir: Path, script: PipTestEnvironment
+) -> None:
+ """
+ Test 'pip download' of an editable pep 660 project.
+ It must *not* call prepare_metadata_for_build_editable.
+ """
+ project_dir = _make_project(tmpdir, BACKEND_WITH_PEP660, with_setup_py=False)
+ reqs_file = tmpdir / "requirements.txt"
+ reqs_file.write_text(f"-e {project_dir.as_uri()}\n")
+ download_dir = tmpdir / "download"
+ script.pip(
+ "download",
+ "--no-index",
+ "--no-build-isolation",
+ "-r",
+ reqs_file,
+ "-d",
+ download_dir,
+ )
+ _assert_hook_not_called(project_dir, "prepare_metadata_for_build_editable")
+ _assert_hook_called(project_dir, "prepare_metadata_for_build_wheel")
+ assert len(os.listdir(str(download_dir))) == 1, "a zip should have been created"
diff --git a/tests/functional/test_pip_runner_script.py b/tests/functional/test_pip_runner_script.py
new file mode 100644
index 000000000..f2f879b82
--- /dev/null
+++ b/tests/functional/test_pip_runner_script.py
@@ -0,0 +1,22 @@
+import os
+from pathlib import Path
+
+from pip import __version__
+from tests.lib import PipTestEnvironment
+
+
+def test_runner_work_in_environments_with_no_pip(
+ script: PipTestEnvironment, pip_src: Path
+) -> None:
+ runner = pip_src / "src" / "pip" / "__pip-runner__.py"
+
+ # Ensure there's no pip installed in the environment
+ script.pip("uninstall", "pip", "--yes", use_module=True)
+ # We don't use script.pip to check here, as when testing a
+ # zipapp, script.pip will run pip from the zipapp.
+ script.run("python", "-c", "import pip", expect_error=True)
+
+ # The runner script should still invoke a usable pip
+ result = script.run("python", os.fspath(runner), "--version")
+
+ assert __version__ in result.stdout
diff --git a/tests/functional/test_python_option.py b/tests/functional/test_python_option.py
new file mode 100644
index 000000000..8bf16d7a5
--- /dev/null
+++ b/tests/functional/test_python_option.py
@@ -0,0 +1,41 @@
+import json
+import os
+from pathlib import Path
+from venv import EnvBuilder
+
+from tests.lib import PipTestEnvironment, TestData
+
+
+def test_python_interpreter(
+ script: PipTestEnvironment,
+ tmpdir: Path,
+ shared_data: TestData,
+) -> None:
+ env_path = os.fspath(tmpdir / "venv")
+ env = EnvBuilder(with_pip=False)
+ env.create(env_path)
+
+ result = script.pip("--python", env_path, "list", "--format=json")
+ before = json.loads(result.stdout)
+
+ # Ideally we would assert that before==[], but there's a problem in CI
+ # that means this isn't true. See https://github.com/pypa/pip/pull/11326
+ # for details.
+
+ script.pip(
+ "--python",
+ env_path,
+ "install",
+ "-f",
+ shared_data.find_links,
+ "--no-index",
+ "simplewheel==1.0",
+ )
+
+ result = script.pip("--python", env_path, "list", "--format=json")
+ installed = json.loads(result.stdout)
+ assert {"name": "simplewheel", "version": "1.0"} in installed
+
+ script.pip("--python", env_path, "uninstall", "simplewheel", "--yes")
+ result = script.pip("--python", env_path, "list", "--format=json")
+ assert json.loads(result.stdout) == before
diff --git a/tests/functional/test_requests.py b/tests/functional/test_requests.py
index f8eef787c..2ef121fed 100644
--- a/tests/functional/test_requests.py
+++ b/tests/functional/test_requests.py
@@ -1,17 +1,22 @@
import pytest
+from tests.lib import PipTestEnvironment
-@pytest.mark.skipif
-def test_timeout(script):
+
+@pytest.mark.network
+def test_timeout(script: PipTestEnvironment) -> None:
result = script.pip(
- "--timeout", "0.01", "install", "-vvv", "INITools",
+ "--retries",
+ "0",
+ "--timeout",
+ "0.00001",
+ "install",
+ "-vvv",
+ "INITools",
expect_error=True,
)
assert (
- "Could not fetch URL https://pypi.org/simple/INITools/: "
- "timed out" in result.stdout
- )
- assert (
- "Could not fetch URL https://pypi.org/simple/: "
- "timed out" in result.stdout
- )
+ "Could not fetch URL https://pypi.org/simple/initools/: "
+ "connection error: HTTPSConnectionPool(host='pypi.org', port=443): "
+ "Max retries exceeded with url: /simple/initools/ "
+ ) in result.stdout
diff --git a/tests/functional/test_search.py b/tests/functional/test_search.py
index b8bc6d51e..3f784e5dd 100644
--- a/tests/functional/test_search.py
+++ b/tests/functional/test_search.py
@@ -1,55 +1,60 @@
import logging
+from typing import TYPE_CHECKING, Dict, List
+from unittest import mock
-import pretend
import pytest
from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS
from pip._internal.commands import create_command
from pip._internal.commands.search import highest_version, print_results, transform_hits
+from tests.lib import PipTestEnvironment
+if TYPE_CHECKING:
+ from pip._internal.commands.search import TransformedHit
-def test_version_compare():
+
+def test_version_compare() -> None:
"""
Test version comparison.
"""
- assert highest_version(['1.0', '2.0', '0.1']) == '2.0'
- assert highest_version(['1.0a1', '1.0']) == '1.0'
+ assert highest_version(["1.0", "2.0", "0.1"]) == "2.0"
+ assert highest_version(["1.0a1", "1.0"]) == "1.0"
-def test_pypi_xml_transformation():
+def test_pypi_xml_transformation() -> None:
"""
Test transformation of data structures (PyPI xmlrpc to custom list).
"""
- pypi_hits = [
+ pypi_hits: List[Dict[str, str]] = [
{
- 'name': 'foo',
- 'summary': 'foo summary',
- 'version': '1.0',
+ "name": "foo",
+ "summary": "foo summary",
+ "version": "1.0",
},
{
- 'name': 'foo',
- 'summary': 'foo summary v2',
- 'version': '2.0',
+ "name": "foo",
+ "summary": "foo summary v2",
+ "version": "2.0",
},
{
- '_pypi_ordering': 50,
- 'name': 'bar',
- 'summary': 'bar summary',
- 'version': '1.0',
+ "_pypi_ordering": 50, # type: ignore[dict-item]
+ "name": "bar",
+ "summary": "bar summary",
+ "version": "1.0",
},
]
- expected = [
+ expected: List["TransformedHit"] = [
{
- 'versions': ['1.0', '2.0'],
- 'name': 'foo',
- 'summary': 'foo summary v2',
+ "versions": ["1.0", "2.0"],
+ "name": "foo",
+ "summary": "foo summary v2",
},
{
- 'versions': ['1.0'],
- 'name': 'bar',
- 'summary': 'bar summary',
+ "versions": ["1.0"],
+ "name": "bar",
+ "summary": "bar summary",
},
]
assert transform_hits(pypi_hits) == expected
@@ -57,55 +62,51 @@ def test_pypi_xml_transformation():
@pytest.mark.network
@pytest.mark.search
-def test_basic_search(script):
+def test_basic_search(script: PipTestEnvironment) -> None:
"""
End to end test of search command.
"""
- output = script.pip('search', 'pip')
- assert (
- 'The PyPA recommended tool for installing '
- 'Python packages.' in output.stdout
- )
+ output = script.pip("search", "pip")
+ assert "The PyPA recommended tool for installing Python packages." in output.stdout
@pytest.mark.network
@pytest.mark.skip(
- reason=("Warehouse search behavior is different and no longer returns "
- "multiple results. See "
- "https://github.com/pypa/warehouse/issues/3717 for more "
- "information."),
+ reason=(
+ "Warehouse search behavior is different and no longer returns "
+ "multiple results. See "
+ "https://github.com/pypa/warehouse/issues/3717 for more "
+ "information."
+ ),
)
@pytest.mark.search
-def test_multiple_search(script):
+def test_multiple_search(script: PipTestEnvironment) -> None:
"""
Test searching for multiple packages at once.
"""
- output = script.pip('search', 'pip', 'INITools')
- assert (
- 'The PyPA recommended tool for installing '
- 'Python packages.' in output.stdout
- )
- assert 'Tools for parsing and using INI-style files' in output.stdout
+ output = script.pip("search", "pip", "INITools")
+ assert "The PyPA recommended tool for installing Python packages." in output.stdout
+ assert "Tools for parsing and using INI-style files" in output.stdout
@pytest.mark.search
-def test_search_missing_argument(script):
+def test_search_missing_argument(script: PipTestEnvironment) -> None:
"""
Test missing required argument for search
"""
- result = script.pip('search', expect_error=True)
- assert 'ERROR: Missing required argument (search query).' in result.stderr
+ result = script.pip("search", expect_error=True)
+ assert "ERROR: Missing required argument (search query)." in result.stderr
@pytest.mark.network
@pytest.mark.search
-def test_run_method_should_return_success_when_find_packages():
+def test_run_method_should_return_success_when_find_packages() -> None:
"""
Test SearchCommand.run for found package
"""
- command = create_command('search')
+ command = create_command("search")
cmdline = "--index=https://pypi.org/pypi pip"
with command.main_context():
options, args = command.parse_args(cmdline.split())
@@ -115,11 +116,11 @@ def test_run_method_should_return_success_when_find_packages():
@pytest.mark.network
@pytest.mark.search
-def test_run_method_should_return_no_matches_found_when_does_not_find_pkgs():
+def test_run_method_should_return_no_matches_found_when_does_not_find_pkgs() -> None:
"""
Test SearchCommand.run for no matches
"""
- command = create_command('search')
+ command = create_command("search")
cmdline = "--index=https://pypi.org/pypi nonexistentpackage"
with command.main_context():
options, args = command.parse_args(cmdline.split())
@@ -129,74 +130,80 @@ def test_run_method_should_return_no_matches_found_when_does_not_find_pkgs():
@pytest.mark.network
@pytest.mark.search
-def test_search_should_exit_status_code_zero_when_find_packages(script):
+def test_search_should_exit_status_code_zero_when_find_packages(
+ script: PipTestEnvironment,
+) -> None:
"""
Test search exit status code for package found
"""
- result = script.pip('search', 'pip')
+ result = script.pip("search", "pip")
assert result.returncode == SUCCESS
@pytest.mark.network
@pytest.mark.search
-def test_search_exit_status_code_when_finds_no_package(script):
+def test_search_exit_status_code_when_finds_no_package(
+ script: PipTestEnvironment,
+) -> None:
"""
Test search exit status code for no matches
"""
- result = script.pip('search', 'nonexistentpackage', expect_error=True)
+ result = script.pip("search", "nonexistentpackage", expect_error=True)
assert result.returncode == NO_MATCHES_FOUND, result.returncode
@pytest.mark.search
-def test_latest_prerelease_install_message(caplog, monkeypatch):
+def test_latest_prerelease_install_message(
+ caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
+) -> None:
"""
Test documentation for installing pre-release packages is displayed
"""
- hits = [
+ hits: List["TransformedHit"] = [
{
- 'name': 'ni',
- 'summary': 'For knights who say Ni!',
- 'versions': ['1.0.0', '1.0.1a']
+ "name": "ni",
+ "summary": "For knights who say Ni!",
+ "versions": ["1.0.0", "1.0.1a"],
}
]
- installed_package = pretend.stub(project_name="ni")
- monkeypatch.setattr("pip._vendor.pkg_resources.working_set",
- [installed_package])
+ installed_package = mock.Mock(project_name="ni")
+ monkeypatch.setattr("pip._vendor.pkg_resources.working_set", [installed_package])
- dist = pretend.stub(version="1.0.0")
- get_dist = pretend.call_recorder(lambda x: dist)
- monkeypatch.setattr("pip._internal.commands.search.get_distribution",
- get_dist)
+ get_dist = mock.Mock()
+ get_dist.return_value = mock.Mock(version="1.0.0")
+ monkeypatch.setattr("pip._internal.commands.search.get_distribution", get_dist)
with caplog.at_level(logging.INFO):
print_results(hits)
message = caplog.records[-1].getMessage()
assert 'pre-release; install with "pip install --pre"' in message
- assert get_dist.calls == [pretend.call('ni')]
+ assert get_dist.call_args_list == [mock.call("ni")]
@pytest.mark.search
-def test_search_print_results_should_contain_latest_versions(caplog):
+def test_search_print_results_should_contain_latest_versions(
+ caplog: pytest.LogCaptureFixture,
+) -> None:
"""
Test that printed search results contain the latest package versions
"""
- hits = [
+ hits: List["TransformedHit"] = [
{
- 'name': 'testlib1',
- 'summary': 'Test library 1.',
- 'versions': ['1.0.5', '1.0.3']
+ "name": "testlib1",
+ "summary": "Test library 1.",
+ "versions": ["1.0.5", "1.0.3"],
},
{
- 'name': 'testlib2',
- 'summary': 'Test library 1.',
- 'versions': ['2.0.1', '2.0.3']
- }
+ "name": "testlib2",
+ "summary": "Test library 1.",
+ "versions": ["2.0.1", "2.0.3"],
+ },
]
with caplog.at_level(logging.INFO):
print_results(hits)
log_messages = sorted([r.getMessage() for r in caplog.records])
- assert log_messages[0].startswith('testlib1 (1.0.5)')
- assert log_messages[1].startswith('testlib2 (2.0.3)')
+ assert log_messages[0].startswith("testlib1 (1.0.5)")
+ assert log_messages[1].startswith("testlib2 (2.0.3)")
diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py
index fce369d32..2fc8ca242 100644
--- a/tests/functional/test_show.py
+++ b/tests/functional/test_show.py
@@ -1,4 +1,5 @@
import os
+import pathlib
import re
from pip import __version__
@@ -7,55 +8,57 @@ from pip._internal.operations.install.legacy import (
write_installed_files_from_setuptools_record,
)
from pip._internal.utils.unpacking import untar_file
-from tests.lib import create_test_package_with_setup
+from tests.lib import PipTestEnvironment, TestData, create_test_package_with_setup
-def test_basic_show(script):
+def test_basic_show(script: PipTestEnvironment) -> None:
"""
Test end to end test for show command.
"""
- result = script.pip('show', 'pip')
+ result = script.pip("show", "pip")
lines = result.stdout.splitlines()
assert len(lines) == 10
- assert 'Name: pip' in lines
- assert f'Version: {__version__}' in lines
- assert any(line.startswith('Location: ') for line in lines)
- assert 'Requires: ' in lines
+ assert "Name: pip" in lines
+ assert f"Version: {__version__}" in lines
+ assert any(line.startswith("Location: ") for line in lines)
+ assert "Requires: " in lines
-def test_show_with_files_not_found(script, data):
+def test_show_with_files_not_found(script: PipTestEnvironment, data: TestData) -> None:
"""
Test for show command with installed files listing enabled and
installed-files.txt not found.
"""
- editable = data.packages.joinpath('SetupPyUTF8')
- script.pip('install', '-e', editable)
- result = script.pip('show', '-f', 'SetupPyUTF8')
+ editable = data.packages.joinpath("SetupPyUTF8")
+ script.pip("install", "-e", editable)
+ result = script.pip("show", "-f", "SetupPyUTF8")
lines = result.stdout.splitlines()
assert len(lines) == 12
- assert 'Name: SetupPyUTF8' in lines
- assert 'Version: 0.0.0' in lines
- assert any(line.startswith('Location: ') for line in lines)
- assert 'Requires: ' in lines
- assert 'Files:' in lines
- assert 'Cannot locate RECORD or installed-files.txt' in lines
+ assert "Name: SetupPyUTF8" in lines
+ assert "Version: 0.0.0" in lines
+ assert any(line.startswith("Location: ") for line in lines)
+ assert "Requires: " in lines
+ assert "Files:" in lines
+ assert "Cannot locate RECORD or installed-files.txt" in lines
-def test_show_with_files_from_wheel(script, data):
+def test_show_with_files_from_wheel(script: PipTestEnvironment, data: TestData) -> None:
"""
Test that a wheel's files can be listed.
"""
- wheel_file = data.packages.joinpath('simple.dist-0.1-py2.py3-none-any.whl')
- script.pip('install', '--no-index', wheel_file)
- result = script.pip('show', '-f', 'simple.dist')
+ wheel_file = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl")
+ script.pip("install", "--no-index", wheel_file)
+ result = script.pip("show", "-f", "simple.dist")
lines = result.stdout.splitlines()
- assert 'Name: simple.dist' in lines
- assert 'Cannot locate RECORD or installed-files.txt' not in lines[6], lines[6]
+ assert "Name: simple.dist" in lines
+ assert "Cannot locate RECORD or installed-files.txt" not in lines[6], lines[6]
assert re.search(r"Files:\n( .+\n)+", result.stdout)
assert f" simpledist{os.sep}__init__.py" in lines[6:]
-def test_show_with_files_from_legacy(tmp_path, script, data):
+def test_show_with_files_from_legacy(
+ tmp_path: pathlib.Path, script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test listing files in the show command (legacy installed-files.txt).
"""
@@ -64,11 +67,14 @@ def test_show_with_files_from_legacy(tmp_path, script, data):
# 'setup.py install' plus installed-files.txt, which we manually generate.
source_dir = tmp_path.joinpath("unpacked-sdist")
setuptools_record = tmp_path.joinpath("installed-record.txt")
- untar_file(data.packages.joinpath("simple-1.0.tar.gz"), str(source_dir))
+ untar_file(os.fspath(data.packages.joinpath("simple-1.0.tar.gz")), str(source_dir))
script.run(
- "python", "setup.py", "install",
+ "python",
+ "setup.py",
+ "install",
"--single-version-externally-managed",
- "--record", str(setuptools_record),
+ "--record",
+ str(setuptools_record),
cwd=source_dir,
)
write_installed_files_from_setuptools_record(
@@ -77,30 +83,30 @@ def test_show_with_files_from_legacy(tmp_path, script, data):
req_description="simple==1.0",
)
- result = script.pip('show', '--files', 'simple')
+ result = script.pip("show", "--files", "simple")
lines = result.stdout.splitlines()
- assert 'Cannot locate RECORD or installed-files.txt' not in lines[6], lines[6]
+ assert "Cannot locate RECORD or installed-files.txt" not in lines[6], lines[6]
assert re.search(r"Files:\n( .+\n)+", result.stdout)
assert f" simple{os.sep}__init__.py" in lines[6:]
-def test_missing_argument(script):
+def test_missing_argument(script: PipTestEnvironment) -> None:
"""
Test show command with no arguments.
"""
- result = script.pip('show', expect_error=True)
- assert 'ERROR: Please provide a package name or names.' in result.stderr
+ result = script.pip("show", expect_error=True)
+ assert "ERROR: Please provide a package name or names." in result.stderr
-def test_find_package_not_found():
+def test_find_package_not_found() -> None:
"""
Test trying to get info about a nonexistent package.
"""
- result = search_packages_info(['abcd3'])
+ result = search_packages_info(["abcd3"])
assert len(list(result)) == 0
-def test_report_single_not_found(script):
+def test_report_single_not_found(script: PipTestEnvironment) -> None:
"""
Test passing one name and that isn't found.
"""
@@ -109,212 +115,225 @@ def test_report_single_not_found(script):
# Also, the following should report an error as there are no results
# to print. Consequently, there is no need to pass
# allow_stderr_warning=True since this is implied by expect_error=True.
- result = script.pip('show', 'Abcd-3', expect_error=True)
- assert 'WARNING: Package(s) not found: Abcd-3' in result.stderr
+ result = script.pip("show", "Abcd-3", expect_error=True)
+ assert "WARNING: Package(s) not found: Abcd-3" in result.stderr
assert not result.stdout.splitlines()
-def test_report_mixed_not_found(script):
+def test_report_mixed_not_found(script: PipTestEnvironment) -> None:
"""
Test passing a mixture of found and not-found names.
"""
# We test passing non-canonicalized names.
- result = script.pip(
- 'show', 'Abcd3', 'A-B-C', 'pip', allow_stderr_warning=True
- )
- assert 'WARNING: Package(s) not found: A-B-C, Abcd3' in result.stderr
+ result = script.pip("show", "Abcd3", "A-B-C", "pip", allow_stderr_warning=True)
+ assert "WARNING: Package(s) not found: A-B-C, Abcd3" in result.stderr
lines = result.stdout.splitlines()
assert len(lines) == 10
- assert 'Name: pip' in lines
+ assert "Name: pip" in lines
-def test_search_any_case():
+def test_search_any_case() -> None:
"""
Search for a package in any case.
"""
- result = list(search_packages_info(['PIP']))
+ result = list(search_packages_info(["PIP"]))
assert len(result) == 1
- assert result[0].name == 'pip'
+ assert result[0].name == "pip"
-def test_more_than_one_package():
+def test_more_than_one_package() -> None:
"""
Search for more than one package.
"""
- result = list(search_packages_info(['pIp', 'pytest', 'Virtualenv']))
+ result = list(search_packages_info(["pIp", "pytest", "Virtualenv"]))
assert len(result) == 3
-def test_show_verbose_with_classifiers(script):
+def test_show_verbose_with_classifiers(script: PipTestEnvironment) -> None:
"""
Test that classifiers can be listed
"""
- result = script.pip('show', 'pip', '--verbose')
+ result = script.pip("show", "pip", "--verbose")
lines = result.stdout.splitlines()
- assert 'Name: pip' in lines
+ assert "Name: pip" in lines
assert re.search(r"Classifiers:\n( .+\n)+", result.stdout)
assert "Intended Audience :: Developers" in result.stdout
-def test_show_verbose_installer(script, data):
+def test_show_verbose_installer(script: PipTestEnvironment, data: TestData) -> None:
"""
Test that the installer is shown (this currently needs a wheel install)
"""
- wheel_file = data.packages.joinpath('simple.dist-0.1-py2.py3-none-any.whl')
- script.pip('install', '--no-index', wheel_file)
- result = script.pip('show', '--verbose', 'simple.dist')
+ wheel_file = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl")
+ script.pip("install", "--no-index", wheel_file)
+ result = script.pip("show", "--verbose", "simple.dist")
+ lines = result.stdout.splitlines()
+ assert "Name: simple.dist" in lines
+ assert "Installer: pip" in lines
+
+
+def test_show_verbose_project_urls(script: PipTestEnvironment) -> None:
+ """
+ Test that project urls can be listed
+ """
+ result = script.pip("show", "pip", "--verbose")
lines = result.stdout.splitlines()
- assert 'Name: simple.dist' in lines
- assert 'Installer: pip' in lines
+ assert "Name: pip" in lines
+ assert re.search(r"Project-URLs:\n( .+\n)+", result.stdout)
+ assert "Source, https://github.com/pypa/pip" in result.stdout
-def test_show_verbose(script):
+def test_show_verbose(script: PipTestEnvironment) -> None:
"""
Test end to end test for verbose show command.
"""
- result = script.pip('show', '--verbose', 'pip')
+ result = script.pip("show", "--verbose", "pip")
lines = result.stdout.splitlines()
- assert any(line.startswith('Metadata-Version: ') for line in lines)
- assert any(line.startswith('Installer: ') for line in lines)
- assert 'Entry-points:' in lines
- assert 'Classifiers:' in lines
+ assert any(line.startswith("Metadata-Version: ") for line in lines)
+ assert any(line.startswith("Installer: ") for line in lines)
+ assert "Entry-points:" in lines
+ assert "Classifiers:" in lines
+ assert "Project-URLs:" in lines
-def test_all_fields(script):
+def test_all_fields(script: PipTestEnvironment) -> None:
"""
Test that all the fields are present
"""
- result = script.pip('show', 'pip')
+ result = script.pip("show", "pip")
lines = result.stdout.splitlines()
- expected = {'Name', 'Version', 'Summary', 'Home-page', 'Author',
- 'Author-email', 'License', 'Location', 'Requires',
- 'Required-by'}
- actual = {re.sub(':.*$', '', line) for line in lines}
+ expected = {
+ "Name",
+ "Version",
+ "Summary",
+ "Home-page",
+ "Author",
+ "Author-email",
+ "License",
+ "Location",
+ "Requires",
+ "Required-by",
+ }
+ actual = {re.sub(":.*$", "", line) for line in lines}
assert actual == expected
-def test_pip_show_is_short(script):
+def test_pip_show_is_short(script: PipTestEnvironment) -> None:
"""
Test that pip show stays short
"""
- result = script.pip('show', 'pip')
+ result = script.pip("show", "pip")
lines = result.stdout.splitlines()
assert len(lines) <= 10
-def test_pip_show_divider(script, data):
+def test_pip_show_divider(script: PipTestEnvironment, data: TestData) -> None:
"""
Expect a divider between packages
"""
- script.pip('install', 'pip-test-package', '--no-index',
- '-f', data.packages)
- result = script.pip('show', 'pip', 'pip-test-package')
+ script.pip("install", "pip-test-package", "--no-index", "-f", data.packages)
+ result = script.pip("show", "pip", "pip-test-package")
lines = result.stdout.splitlines()
assert "---" in lines
-def test_package_name_is_canonicalized(script, data):
- script.pip('install', 'pip-test-package', '--no-index', '-f',
- data.packages)
+def test_package_name_is_canonicalized(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ script.pip("install", "pip-test-package", "--no-index", "-f", data.packages)
- dash_show_result = script.pip('show', 'pip-test-package')
- underscore_upper_show_result = script.pip('show', 'pip-test_Package')
+ dash_show_result = script.pip("show", "pip-test-package")
+ underscore_upper_show_result = script.pip("show", "pip-test_Package")
assert underscore_upper_show_result.returncode == 0
assert underscore_upper_show_result.stdout == dash_show_result.stdout
-def test_show_required_by_packages_basic(script, data):
+def test_show_required_by_packages_basic(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test that installed packages that depend on this package are shown
"""
- editable_path = os.path.join(data.src, 'requires_simple')
- script.pip(
- 'install', '--no-index', '-f', data.find_links, editable_path
- )
+ editable_path = os.path.join(data.src, "requires_simple")
+ script.pip("install", "--no-index", "-f", data.find_links, editable_path)
- result = script.pip('show', 'simple')
+ result = script.pip("show", "simple")
lines = result.stdout.splitlines()
- assert 'Name: simple' in lines
- assert 'Required-by: requires-simple' in lines
+ assert "Name: simple" in lines
+ assert "Required-by: requires-simple" in lines
-def test_show_required_by_packages_capitalized(script, data):
+def test_show_required_by_packages_capitalized(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test that the installed packages which depend on a package are shown
where the package has a capital letter
"""
- editable_path = os.path.join(data.src, 'requires_capitalized')
- script.pip(
- 'install', '--no-index', '-f', data.find_links, editable_path
- )
+ editable_path = os.path.join(data.src, "requires_capitalized")
+ script.pip("install", "--no-index", "-f", data.find_links, editable_path)
- result = script.pip('show', 'simple')
+ result = script.pip("show", "simple")
lines = result.stdout.splitlines()
- assert 'Name: simple' in lines
- assert 'Required-by: Requires-Capitalized' in lines
+ assert "Name: simple" in lines
+ assert "Required-by: Requires-Capitalized" in lines
-def test_show_required_by_packages_requiring_capitalized(script, data):
+def test_show_required_by_packages_requiring_capitalized(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test that the installed packages which depend on a package are shown
where the package has a name with a mix of
lower and upper case letters
"""
- required_package_path = os.path.join(data.src, 'requires_capitalized')
- script.pip(
- 'install', '--no-index', '-f', data.find_links, required_package_path
- )
- editable_path = os.path.join(data.src, 'requires_requires_capitalized')
- script.pip(
- 'install', '--no-index', '-f', data.find_links, editable_path
- )
+ required_package_path = os.path.join(data.src, "requires_capitalized")
+ script.pip("install", "--no-index", "-f", data.find_links, required_package_path)
+ editable_path = os.path.join(data.src, "requires_requires_capitalized")
+ script.pip("install", "--no-index", "-f", data.find_links, editable_path)
- result = script.pip('show', 'Requires_Capitalized')
+ result = script.pip("show", "Requires_Capitalized")
lines = result.stdout.splitlines()
print(lines)
- assert 'Name: Requires-Capitalized' in lines
- assert 'Required-by: requires-requires-capitalized' in lines
+ assert "Name: Requires-Capitalized" in lines
+ assert "Required-by: requires-requires-capitalized" in lines
-def test_show_skip_work_dir_pkg(script):
+def test_show_skip_work_dir_pkg(script: PipTestEnvironment) -> None:
"""
Test that show should not include package
present in working directory
"""
# Create a test package and create .egg-info dir
- pkg_path = create_test_package_with_setup(
- script, name='simple', version='1.0')
- script.run('python', 'setup.py', 'egg_info',
- expect_stderr=True, cwd=pkg_path)
+ pkg_path = create_test_package_with_setup(script, name="simple", version="1.0")
+ script.run("python", "setup.py", "egg_info", expect_stderr=True, cwd=pkg_path)
# Show should not include package simple when run from package directory
- result = script.pip('show', 'simple', expect_error=True, cwd=pkg_path)
- assert 'WARNING: Package(s) not found: simple' in result.stderr
+ result = script.pip("show", "simple", expect_error=True, cwd=pkg_path)
+ assert "WARNING: Package(s) not found: simple" in result.stderr
-def test_show_include_work_dir_pkg(script):
+def test_show_include_work_dir_pkg(script: PipTestEnvironment) -> None:
"""
Test that show should include package in working directory
if working directory is added in PYTHONPATH
"""
# Create a test package and create .egg-info dir
- pkg_path = create_test_package_with_setup(
- script, name='simple', version='1.0')
- script.run('python', 'setup.py', 'egg_info',
- expect_stderr=True, cwd=pkg_path)
+ pkg_path = create_test_package_with_setup(script, name="simple", version="1.0")
+ script.run("python", "setup.py", "egg_info", expect_stderr=True, cwd=pkg_path)
- script.environ.update({'PYTHONPATH': pkg_path})
+ script.environ.update({"PYTHONPATH": pkg_path})
# Show should include package simple when run from package directory,
# when package directory is in PYTHONPATH
- result = script.pip('show', 'simple', cwd=pkg_path)
+ result = script.pip("show", "simple", cwd=pkg_path)
lines = result.stdout.splitlines()
- assert 'Name: simple' in lines
+ assert "Name: simple" in lines
diff --git a/tests/functional/test_truststore.py b/tests/functional/test_truststore.py
new file mode 100644
index 000000000..33153d0fb
--- /dev/null
+++ b/tests/functional/test_truststore.py
@@ -0,0 +1,61 @@
+import sys
+from typing import Any, Callable
+
+import pytest
+
+from tests.lib import PipTestEnvironment, TestPipResult
+
+PipRunner = Callable[..., TestPipResult]
+
+
+@pytest.fixture()
+def pip(script: PipTestEnvironment) -> PipRunner:
+ def pip(*args: str, **kwargs: Any) -> TestPipResult:
+ return script.pip(*args, "--use-feature=truststore", **kwargs)
+
+ return pip
+
+
+@pytest.mark.skipif(sys.version_info >= (3, 10), reason="3.10 can run truststore")
+def test_truststore_error_on_old_python(pip: PipRunner) -> None:
+ result = pip(
+ "install",
+ "--no-index",
+ "does-not-matter",
+ expect_error=True,
+ )
+ assert "The truststore feature is only available for Python 3.10+" in result.stderr
+
+
+@pytest.mark.skipif(sys.version_info < (3, 10), reason="3.10+ required for truststore")
+def test_truststore_error_without_preinstalled(pip: PipRunner) -> None:
+ result = pip(
+ "install",
+ "--no-index",
+ "does-not-matter",
+ expect_error=True,
+ )
+ assert (
+ "To use the truststore feature, 'truststore' must be installed into "
+ "pip's current environment."
+ ) in result.stderr
+
+
+@pytest.mark.skipif(sys.version_info < (3, 10), reason="3.10+ required for truststore")
+@pytest.mark.network
+@pytest.mark.parametrize(
+ "package",
+ [
+ "INITools",
+ "https://github.com/pypa/pip-test-package/archive/refs/heads/master.zip",
+ ],
+ ids=["PyPI", "GitHub"],
+)
+def test_trustore_can_install(
+ script: PipTestEnvironment,
+ pip: PipRunner,
+ package: str,
+) -> None:
+ script.pip("install", "truststore")
+ result = pip("install", package)
+ assert "Successfully installed" in result.stdout
diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py
index 4fea1e2f9..b0e12f6af 100644
--- a/tests/functional/test_uninstall.py
+++ b/tests/functional/test_uninstall.py
@@ -3,51 +3,64 @@ import os
import sys
import textwrap
from os.path import join, normpath
+from pathlib import Path
from tempfile import mkdtemp
+from typing import Any, Iterator
+from unittest.mock import Mock
-import pretend
import pytest
from pip._internal.req.constructors import install_req_from_line
from pip._internal.utils.misc import rmtree
-from tests.lib import assert_all_changes, create_test_package_with_setup, need_svn
+from tests.lib import (
+ PipTestEnvironment,
+ TestData,
+ assert_all_changes,
+ create_test_package_with_setup,
+ need_svn,
+)
from tests.lib.local_repos import local_checkout, local_repo
@pytest.mark.network
-def test_basic_uninstall(script):
+def test_basic_uninstall(script: PipTestEnvironment) -> None:
"""
Test basic install and uninstall.
"""
- result = script.pip('install', 'INITools==0.2')
- result.did_create(join(script.site_packages, 'initools'))
+ result = script.pip("install", "INITools==0.2")
+ result.did_create(join(script.site_packages, "initools"))
# the import forces the generation of __pycache__ if the version of python
# supports it
- script.run('python', '-c', "import initools")
- result2 = script.pip('uninstall', 'INITools', '-y')
- assert_all_changes(result, result2, [script.venv / 'build', 'cache'])
+ script.run("python", "-c", "import initools")
+ result2 = script.pip("uninstall", "INITools", "-y")
+ assert_all_changes(result, result2, [script.venv / "build", "cache"])
-def test_basic_uninstall_distutils(script):
+def test_basic_uninstall_distutils(script: PipTestEnvironment) -> None:
"""
Test basic install and uninstall.
"""
script.scratch_path.joinpath("distutils_install").mkdir()
- pkg_path = script.scratch_path / 'distutils_install'
- pkg_path.joinpath("setup.py").write_text(textwrap.dedent("""
+ pkg_path = script.scratch_path / "distutils_install"
+ pkg_path.joinpath("setup.py").write_text(
+ textwrap.dedent(
+ """
from distutils.core import setup
setup(
name='distutils-install',
version='0.1',
)
- """))
- result = script.run('python', pkg_path / 'setup.py', 'install')
- result = script.pip('list', '--format=json')
+ """
+ )
+ )
+ result = script.run("python", os.fspath(pkg_path / "setup.py"), "install")
+ result = script.pip("list", "--format=json")
script.assert_installed(distutils_install="0.1")
- result = script.pip('uninstall', 'distutils_install', '-y',
- expect_stderr=True, expect_error=True)
+ result = script.pip(
+ "uninstall", "distutils_install", "-y", expect_stderr=True, expect_error=True
+ )
assert (
"Cannot uninstall 'distutils-install'. It is a distutils installed "
"project and thus we cannot accurately determine which files belong "
@@ -56,7 +69,7 @@ def test_basic_uninstall_distutils(script):
@pytest.mark.network
-def test_basic_uninstall_with_scripts(script):
+def test_basic_uninstall_with_scripts(script: PipTestEnvironment) -> None:
"""
Uninstall an easy_installed package with scripts.
@@ -64,22 +77,22 @@ def test_basic_uninstall_with_scripts(script):
# setuptools 52 removed easy_install.
script.pip("install", "setuptools==51.3.3", use_module=True)
- result = script.easy_install('PyLogo', expect_stderr=True)
- easy_install_pth = script.site_packages / 'easy-install.pth'
- pylogo = sys.platform == 'win32' and 'pylogo' or 'PyLogo'
- assert(pylogo in result.files_updated[easy_install_pth].bytes)
- result2 = script.pip('uninstall', 'pylogo', '-y')
+ result = script.easy_install("PyLogo", expect_stderr=True)
+ easy_install_pth = script.site_packages / "easy-install.pth"
+ pylogo = sys.platform == "win32" and "pylogo" or "PyLogo"
+ assert pylogo in result.files_updated[os.fspath(easy_install_pth)].bytes
+ result2 = script.pip("uninstall", "pylogo", "-y")
assert_all_changes(
result,
result2,
- [script.venv / 'build', 'cache', easy_install_pth],
+ [script.venv / "build", "cache", easy_install_pth],
)
-@pytest.mark.parametrize("name",
- ["GTrolls.tar.gz",
- "https://guyto.com/archives/"])
-def test_uninstall_invalid_parameter(script, caplog, name):
+@pytest.mark.parametrize("name", ["GTrolls.tar.gz", "https://guyto.com/archives/"])
+def test_uninstall_invalid_parameter(
+ script: PipTestEnvironment, caplog: pytest.LogCaptureFixture, name: str
+) -> None:
result = script.pip("uninstall", name, "-y", expect_error=True)
expected_message = (
f"Invalid requirement: '{name}' ignored -"
@@ -89,7 +102,7 @@ def test_uninstall_invalid_parameter(script, caplog, name):
@pytest.mark.network
-def test_uninstall_easy_install_after_import(script):
+def test_uninstall_easy_install_after_import(script: PipTestEnvironment) -> None:
"""
Uninstall an easy_installed package after it's been imported
@@ -97,24 +110,24 @@ def test_uninstall_easy_install_after_import(script):
# setuptools 52 removed easy_install.
script.pip("install", "setuptools==51.3.3", use_module=True)
- result = script.easy_install('INITools==0.2', expect_stderr=True)
+ result = script.easy_install("INITools==0.2", expect_stderr=True)
# the import forces the generation of __pycache__ if the version of python
# supports it
- script.run('python', '-c', "import initools")
- result2 = script.pip('uninstall', 'INITools', '-y')
+ script.run("python", "-c", "import initools")
+ result2 = script.pip("uninstall", "INITools", "-y")
assert_all_changes(
result,
result2,
[
- script.venv / 'build',
- 'cache',
- script.site_packages / 'easy-install.pth',
- ]
+ script.venv / "build",
+ "cache",
+ script.site_packages / "easy-install.pth",
+ ],
)
@pytest.mark.network
-def test_uninstall_trailing_newline(script):
+def test_uninstall_trailing_newline(script: PipTestEnvironment) -> None:
"""
Uninstall behaves appropriately if easy-install.pth
lacks a trailing newline
@@ -123,26 +136,25 @@ def test_uninstall_trailing_newline(script):
# setuptools 52 removed easy_install.
script.pip("install", "setuptools==51.3.3", use_module=True)
- script.easy_install('INITools==0.2', expect_stderr=True)
- script.easy_install('PyLogo', expect_stderr=True)
- easy_install_pth = script.site_packages_path / 'easy-install.pth'
+ script.easy_install("INITools==0.2", expect_stderr=True)
+ script.easy_install("PyLogo", expect_stderr=True)
+ easy_install_pth = script.site_packages_path / "easy-install.pth"
# trim trailing newline from easy-install.pth
with open(easy_install_pth) as f:
pth_before = f.read()
- with open(easy_install_pth, 'w') as f:
+ with open(easy_install_pth, "w") as f:
f.write(pth_before.rstrip())
# uninstall initools
- script.pip('uninstall', 'INITools', '-y')
+ script.pip("uninstall", "INITools", "-y")
with open(easy_install_pth) as f:
pth_after = f.read()
# verify that only initools is removed
before_without_initools = [
- line for line in pth_before.splitlines()
- if 'initools' not in line.lower()
+ line for line in pth_before.splitlines() if "initools" not in line.lower()
]
lines_after = pth_after.splitlines()
@@ -150,24 +162,26 @@ def test_uninstall_trailing_newline(script):
@pytest.mark.network
-def test_basic_uninstall_namespace_package(script):
+def test_basic_uninstall_namespace_package(script: PipTestEnvironment) -> None:
"""
Uninstall a distribution with a namespace package without clobbering
the namespace and everything in it.
"""
- result = script.pip('install', 'pd.requires==0.0.3')
- result.did_create(join(script.site_packages, 'pd'))
- result2 = script.pip('uninstall', 'pd.find', '-y')
- assert join(script.site_packages, 'pd') not in result2.files_deleted, (
- sorted(result2.files_deleted.keys())
+ result = script.pip("install", "pd.requires==0.0.3")
+ result.did_create(join(script.site_packages, "pd"))
+ result2 = script.pip("uninstall", "pd.find", "-y")
+ assert join(script.site_packages, "pd") not in result2.files_deleted, sorted(
+ result2.files_deleted.keys()
)
- assert join(script.site_packages, 'pd', 'find') in result2.files_deleted, (
- sorted(result2.files_deleted.keys())
+ assert join(script.site_packages, "pd", "find") in result2.files_deleted, sorted(
+ result2.files_deleted.keys()
)
-def test_uninstall_overlapping_package(script, data):
+def test_uninstall_overlapping_package(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Uninstalling a distribution that adds modules to a pre-existing package
should only remove those added modules, not the rest of the existing
@@ -178,65 +192,70 @@ def test_uninstall_overlapping_package(script, data):
parent_pkg = data.packages.joinpath("parent-0.1.tar.gz")
child_pkg = data.packages.joinpath("child-0.1.tar.gz")
- result1 = script.pip('install', parent_pkg)
- result1.did_create(join(script.site_packages, 'parent'))
- result2 = script.pip('install', child_pkg)
- result2.did_create(join(script.site_packages, 'child'))
- result2.did_create(normpath(
- join(script.site_packages, 'parent/plugins/child_plugin.py')
- ))
+ result1 = script.pip("install", parent_pkg)
+ result1.did_create(join(script.site_packages, "parent"))
+ result2 = script.pip("install", child_pkg)
+ result2.did_create(join(script.site_packages, "child"))
+ result2.did_create(
+ normpath(join(script.site_packages, "parent/plugins/child_plugin.py"))
+ )
# The import forces the generation of __pycache__ if the version of python
# supports it
- script.run('python', '-c', "import parent.plugins.child_plugin, child")
- result3 = script.pip('uninstall', '-y', 'child')
- assert join(script.site_packages, 'child') in result3.files_deleted, (
- sorted(result3.files_created.keys())
+ script.run("python", "-c", "import parent.plugins.child_plugin, child")
+ result3 = script.pip("uninstall", "-y", "child")
+ assert join(script.site_packages, "child") in result3.files_deleted, sorted(
+ result3.files_created.keys()
)
- assert normpath(
- join(script.site_packages, 'parent/plugins/child_plugin.py')
- ) in result3.files_deleted, sorted(result3.files_deleted.keys())
- assert join(script.site_packages, 'parent') not in result3.files_deleted, (
- sorted(result3.files_deleted.keys())
+ assert (
+ normpath(join(script.site_packages, "parent/plugins/child_plugin.py"))
+ in result3.files_deleted
+ ), sorted(result3.files_deleted.keys())
+ assert join(script.site_packages, "parent") not in result3.files_deleted, sorted(
+ result3.files_deleted.keys()
)
# Additional check: uninstalling 'child' should return things to the
# previous state, without unintended side effects.
assert_all_changes(result2, result3, [])
-@pytest.mark.parametrize("console_scripts",
- ["test_ = distutils_install",
- "test_:test_ = distutils_install"])
-def test_uninstall_entry_point_colon_in_name(script, console_scripts):
+@pytest.mark.parametrize(
+ "console_scripts", ["test_ = distutils_install", "test_:test_ = distutils_install"]
+)
+def test_uninstall_entry_point_colon_in_name(
+ script: PipTestEnvironment, console_scripts: str
+) -> None:
"""
Test uninstall package with two or more entry points in the same section,
whose name contain a colon.
"""
- pkg_name = 'ep_install'
+ pkg_name = "ep_install"
pkg_path = create_test_package_with_setup(
script,
name=pkg_name,
- version='0.1',
- entry_points={"console_scripts": [console_scripts, ],
- "pip_test.ep":
- ["ep:name1 = distutils_install",
- "ep:name2 = distutils_install"]
- }
- )
- script_name = script.bin_path.joinpath(
- console_scripts.split('=')[0].strip()
+ version="0.1",
+ entry_points={
+ "console_scripts": [
+ console_scripts,
+ ],
+ "pip_test.ep": [
+ "ep:name1 = distutils_install",
+ "ep:name2 = distutils_install",
+ ],
+ },
)
- if sys.platform == 'win32':
- script_name += '.exe'
- script.pip('install', pkg_path)
+ script_name = script.bin_path.joinpath(console_scripts.split("=")[0].strip())
+ if sys.platform == "win32":
+ script_name = script_name.with_suffix(".exe")
+ script.pip("install", pkg_path)
assert script_name.exists()
script.assert_installed(ep_install="0.1")
- script.pip('uninstall', 'ep_install', '-y')
+ script.pip("uninstall", "ep_install", "-y")
assert not script_name.exists()
script.assert_not_installed("ep-install")
-def test_uninstall_gui_scripts(script):
+def test_uninstall_gui_scripts(script: PipTestEnvironment) -> None:
"""
Make sure that uninstall removes gui scripts
"""
@@ -244,60 +263,72 @@ def test_uninstall_gui_scripts(script):
pkg_path = create_test_package_with_setup(
script,
name=pkg_name,
- version='0.1',
- entry_points={"gui_scripts": ["test_ = distutils_install", ], }
+ version="0.1",
+ entry_points={
+ "gui_scripts": [
+ "test_ = distutils_install",
+ ],
+ },
)
- script_name = script.bin_path.joinpath('test_')
- if sys.platform == 'win32':
- script_name += '.exe'
- script.pip('install', pkg_path)
+ script_name = script.bin_path.joinpath("test_")
+ if sys.platform == "win32":
+ script_name = script_name.with_suffix(".exe")
+ script.pip("install", pkg_path)
assert script_name.exists()
- script.pip('uninstall', pkg_name, '-y')
+ script.pip("uninstall", pkg_name, "-y")
assert not script_name.exists()
-def test_uninstall_console_scripts(script):
+def test_uninstall_console_scripts(script: PipTestEnvironment) -> None:
"""
Test uninstalling a package with more files (console_script entry points,
extra directories).
"""
pkg_path = create_test_package_with_setup(
script,
- name='discover',
- version='0.1',
- entry_points={'console_scripts': ['discover = discover:main']},
+ name="discover",
+ version="0.1",
+ entry_points={"console_scripts": ["discover = discover:main"]},
+ )
+ result = script.pip("install", pkg_path)
+ result.did_create(script.bin / f"discover{script.exe}")
+ result2 = script.pip("uninstall", "discover", "-y")
+ assert_all_changes(
+ result,
+ result2,
+ [
+ os.path.join(script.venv, "build"),
+ "cache",
+ os.path.join("scratch", "discover", "discover.egg-info"),
+ ],
)
- result = script.pip('install', pkg_path)
- result.did_create(script.bin / 'discover' + script.exe)
- result2 = script.pip('uninstall', 'discover', '-y')
- assert_all_changes(result, result2, [script.venv / 'build', 'cache'])
-def test_uninstall_console_scripts_uppercase_name(script):
+def test_uninstall_console_scripts_uppercase_name(script: PipTestEnvironment) -> None:
"""
Test uninstalling console script with uppercase character.
"""
pkg_path = create_test_package_with_setup(
script,
- name='ep_install',
- version='0.1',
+ name="ep_install",
+ version="0.1",
entry_points={
"console_scripts": [
"Test = distutils_install",
],
},
)
- script_name = script.bin_path.joinpath('Test' + script.exe)
+ script_name = script.bin_path.joinpath("Test" + script.exe)
- script.pip('install', pkg_path)
+ script.pip("install", pkg_path)
assert script_name.exists()
- script.pip('uninstall', 'ep_install', '-y')
+ script.pip("uninstall", "ep_install", "-y")
assert not script_name.exists()
@pytest.mark.network
-def test_uninstall_easy_installed_console_scripts(script):
+def test_uninstall_easy_installed_console_scripts(script: PipTestEnvironment) -> None:
"""
Test uninstalling package with console_scripts that is easy_installed.
"""
@@ -305,56 +336,58 @@ def test_uninstall_easy_installed_console_scripts(script):
# the command is used.
script.pip("install", "setuptools==51.3.3", use_module=True)
- result = script.easy_install('discover', allow_stderr_warning=True)
- result.did_create(script.bin / 'discover' + script.exe)
- result2 = script.pip('uninstall', 'discover', '-y')
+ result = script.easy_install("discover", allow_stderr_warning=True)
+ result.did_create(script.bin / f"discover{script.exe}")
+ result2 = script.pip("uninstall", "discover", "-y")
assert_all_changes(
result,
result2,
[
- script.venv / 'build',
- 'cache',
- script.site_packages / 'easy-install.pth',
- ]
+ script.venv / "build",
+ "cache",
+ script.site_packages / "easy-install.pth",
+ ],
)
@pytest.mark.xfail
@pytest.mark.network
@need_svn
-def test_uninstall_editable_from_svn(script, tmpdir):
+def test_uninstall_editable_from_svn(script: PipTestEnvironment, tmpdir: Path) -> None:
"""
Test uninstalling an editable installation from svn.
"""
result = script.pip(
- 'install', '-e',
- '{checkout}#egg=initools'.format(
- checkout=local_checkout(
- 'svn+http://svn.colorstudy.com/INITools', tmpdir)
+ "install",
+ "-e",
+ "{checkout}#egg=initools".format(
+ checkout=local_checkout("svn+http://svn.colorstudy.com/INITools", tmpdir)
),
)
- result.assert_installed('INITools')
- result2 = script.pip('uninstall', '-y', 'initools')
- assert (script.venv / 'src' / 'initools' in result2.files_after)
+ result.assert_installed("INITools")
+ result2 = script.pip("uninstall", "-y", "initools")
+ assert script.venv / "src" / "initools" in result2.files_after
assert_all_changes(
result,
result2,
[
- script.venv / 'src',
- script.venv / 'build',
- script.site_packages / 'easy-install.pth'
+ script.venv / "src",
+ script.venv / "build",
+ script.site_packages / "easy-install.pth",
],
)
@pytest.mark.network
-def test_uninstall_editable_with_source_outside_venv(script, tmpdir):
+def test_uninstall_editable_with_source_outside_venv(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""
Test uninstalling editable install from existing source outside the venv.
"""
try:
temp = mkdtemp()
- temp_pkg_dir = join(temp, 'pip-test-package')
+ temp_pkg_dir = join(temp, "pip-test-package")
_test_uninstall_editable_with_source_outside_venv(
script,
tmpdir,
@@ -365,47 +398,52 @@ def test_uninstall_editable_with_source_outside_venv(script, tmpdir):
def _test_uninstall_editable_with_source_outside_venv(
- script, tmpdir, temp_pkg_dir,
-):
+ script: PipTestEnvironment,
+ tmpdir: Path,
+ temp_pkg_dir: str,
+) -> None:
result = script.run(
- 'git', 'clone',
- local_repo('git+git://github.com/pypa/pip-test-package', tmpdir),
+ "git",
+ "clone",
+ local_repo("git+https://github.com/pypa/pip-test-package", tmpdir),
temp_pkg_dir,
expect_stderr=True,
)
- result2 = script.pip('install', '-e', temp_pkg_dir)
- result2.did_create(join(
- script.site_packages, 'pip-test-package.egg-link'
- ))
- result3 = script.pip('uninstall', '-y', 'pip-test-package')
+ result2 = script.pip("install", "-e", temp_pkg_dir)
+ result2.did_create(join(script.site_packages, "pip-test-package.egg-link"))
+ result3 = script.pip("uninstall", "-y", "pip-test-package")
assert_all_changes(
result,
result3,
- [script.venv / 'build', script.site_packages / 'easy-install.pth'],
+ [script.venv / "build", script.site_packages / "easy-install.pth"],
)
@pytest.mark.xfail
@pytest.mark.network
@need_svn
-def test_uninstall_from_reqs_file(script, tmpdir):
+def test_uninstall_from_reqs_file(script: PipTestEnvironment, tmpdir: Path) -> None:
"""
Test uninstall from a requirements file.
"""
local_svn_url = local_checkout(
- 'svn+http://svn.colorstudy.com/INITools', tmpdir,
+ "svn+http://svn.colorstudy.com/INITools",
+ tmpdir,
)
script.scratch_path.joinpath("test-req.txt").write_text(
- textwrap.dedent("""
+ textwrap.dedent(
+ """
-e {url}#egg=initools
# and something else to test out:
PyLogo<0.4
- """).format(url=local_svn_url)
+ """
+ ).format(url=local_svn_url)
)
- result = script.pip('install', '-r', 'test-req.txt')
+ result = script.pip("install", "-r", "test-req.txt")
script.scratch_path.joinpath("test-req.txt").write_text(
- textwrap.dedent("""
+ textwrap.dedent(
+ """
# -f, -i, and --extra-index-url should all be ignored by uninstall
-f http://www.example.com
-i http://www.example.com
@@ -414,55 +452,55 @@ def test_uninstall_from_reqs_file(script, tmpdir):
-e {url}#egg=initools
# and something else to test out:
PyLogo<0.4
- """).format(url=local_svn_url)
+ """
+ ).format(url=local_svn_url)
)
- result2 = script.pip('uninstall', '-r', 'test-req.txt', '-y')
+ result2 = script.pip("uninstall", "-r", "test-req.txt", "-y")
assert_all_changes(
result,
result2,
[
- script.venv / 'build',
- script.venv / 'src',
- script.scratch / 'test-req.txt',
- script.site_packages / 'easy-install.pth',
+ script.venv / "build",
+ script.venv / "src",
+ script.scratch / "test-req.txt",
+ script.site_packages / "easy-install.pth",
],
)
-def test_uninstallpathset_no_paths(caplog):
+def test_uninstallpathset_no_paths(caplog: pytest.LogCaptureFixture) -> None:
"""
Test UninstallPathSet logs notification when there are no paths to
uninstall
"""
- from pkg_resources import get_distribution
-
+ from pip._internal.metadata import get_default_environment
from pip._internal.req.req_uninstall import UninstallPathSet
caplog.set_level(logging.INFO)
- test_dist = get_distribution('pip')
+ test_dist = get_default_environment().get_distribution("pip")
+ assert test_dist is not None, "pip not installed"
+
uninstall_set = UninstallPathSet(test_dist)
uninstall_set.remove() # with no files added to set
- assert (
- "Can't uninstall 'pip'. No files were found to uninstall."
- in caplog.text
- )
+ assert "Can't uninstall 'pip'. No files were found to uninstall." in caplog.text
-def test_uninstall_non_local_distutils(caplog, monkeypatch, tmpdir):
+def test_uninstall_non_local_distutils(
+ caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch, tmpdir: Path
+) -> None:
einfo = tmpdir.joinpath("thing-1.0.egg-info")
with open(einfo, "wb"):
pass
- dist = pretend.stub(
+ get_dist = Mock()
+ get_dist.return_value = Mock(
key="thing",
project_name="thing",
egg_info=einfo,
location=einfo,
- _provider=pretend.stub(),
)
- get_dist = pretend.call_recorder(lambda x: dist)
monkeypatch.setattr("pip._vendor.pkg_resources.get_distribution", get_dist)
req = install_req_from_line("thing")
@@ -471,37 +509,48 @@ def test_uninstall_non_local_distutils(caplog, monkeypatch, tmpdir):
assert os.path.exists(einfo)
-def test_uninstall_wheel(script, data):
+def test_uninstall_wheel(script: PipTestEnvironment, data: TestData) -> None:
"""
Test uninstalling a wheel
"""
package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl")
- result = script.pip('install', package, '--no-index')
- dist_info_folder = script.site_packages / 'simple.dist-0.1.dist-info'
+ result = script.pip("install", package, "--no-index")
+ dist_info_folder = script.site_packages / "simple.dist-0.1.dist-info"
result.did_create(dist_info_folder)
- result2 = script.pip('uninstall', 'simple.dist', '-y')
+ result2 = script.pip("uninstall", "simple.dist", "-y")
assert_all_changes(result, result2, [])
-@pytest.mark.parametrize('installer', [FileNotFoundError, IsADirectoryError,
- '', os.linesep, b'\xc0\xff\xee', 'pip',
- 'MegaCorp Cloud Install-O-Matic'])
-def test_uninstall_without_record_fails(script, data, installer):
+@pytest.mark.parametrize(
+ "installer",
+ [
+ FileNotFoundError,
+ IsADirectoryError,
+ "",
+ os.linesep,
+ b"\xc0\xff\xee",
+ "pip",
+ "MegaCorp Cloud Install-O-Matic",
+ ],
+)
+def test_uninstall_without_record_fails(
+ script: PipTestEnvironment, data: TestData, installer: Any
+) -> None:
"""
Test uninstalling a package installed without RECORD
"""
package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl")
- result = script.pip('install', package, '--no-index')
- dist_info_folder = script.site_packages / 'simple.dist-0.1.dist-info'
+ result = script.pip("install", package, "--no-index")
+ dist_info_folder = script.site_packages / "simple.dist-0.1.dist-info"
result.did_create(dist_info_folder)
# Remove RECORD
- record_path = dist_info_folder / 'RECORD'
+ record_path = dist_info_folder / "RECORD"
(script.base_path / record_path).unlink()
ignore_changes = [record_path]
# Populate, remove or otherwise break INSTALLER
- installer_path = dist_info_folder / 'INSTALLER'
+ installer_path = dist_info_folder / "INSTALLER"
ignore_changes += [installer_path]
installer_path = script.base_path / installer_path
if installer in (FileNotFoundError, IsADirectoryError):
@@ -514,106 +563,118 @@ def test_uninstall_without_record_fails(script, data, installer):
else:
installer_path.write_text(installer + os.linesep)
- result2 = script.pip('uninstall', 'simple.dist', '-y', expect_error=True)
- expected_error_message = ('ERROR: Cannot uninstall simple.dist 0.1, '
- 'RECORD file not found.')
- if not isinstance(installer, str) or not installer.strip() or installer == 'pip':
- expected_error_message += (" You might be able to recover from this via: "
- "'pip install --force-reinstall --no-deps "
- "simple.dist==0.1'.")
+ result2 = script.pip("uninstall", "simple.dist", "-y", expect_error=True)
+ expected_error_message = (
+ "ERROR: Cannot uninstall simple.dist 0.1, RECORD file not found."
+ )
+ if not isinstance(installer, str) or not installer.strip() or installer == "pip":
+ expected_error_message += (
+ " You might be able to recover from this via: "
+ "'pip install --force-reinstall --no-deps "
+ "simple.dist==0.1'."
+ )
elif installer:
- expected_error_message += (' Hint: The package was installed by '
- '{}.'.format(installer))
+ expected_error_message += " Hint: The package was installed by {}.".format(
+ installer
+ )
assert result2.stderr.rstrip() == expected_error_message
assert_all_changes(result.files_after, result2, ignore_changes)
@pytest.mark.skipif("sys.platform == 'win32'")
-def test_uninstall_with_symlink(script, data, tmpdir):
+def test_uninstall_with_symlink(
+ script: PipTestEnvironment, data: TestData, tmpdir: Path
+) -> None:
"""
Test uninstalling a wheel, with an additional symlink
https://github.com/pypa/pip/issues/6892
"""
package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl")
- script.pip('install', package, '--no-index')
+ script.pip("install", package, "--no-index")
symlink_target = tmpdir / "target"
symlink_target.mkdir()
symlink_source = script.site_packages / "symlink"
(script.base_path / symlink_source).symlink_to(symlink_target)
st_mode = symlink_target.stat().st_mode
- distinfo_path = script.site_packages_path / 'simple.dist-0.1.dist-info'
- record_path = distinfo_path / 'RECORD'
+ distinfo_path = script.site_packages_path / "simple.dist-0.1.dist-info"
+ record_path = distinfo_path / "RECORD"
with open(record_path, "a") as f:
f.write("symlink,,\n")
- uninstall_result = script.pip('uninstall', 'simple.dist', '-y')
+ uninstall_result = script.pip("uninstall", "simple.dist", "-y")
assert symlink_source in uninstall_result.files_deleted
assert symlink_target.stat().st_mode == st_mode
-def test_uninstall_setuptools_develop_install(script, data):
+def test_uninstall_setuptools_develop_install(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""Try uninstall after setup.py develop followed of setup.py install"""
pkg_path = data.packages.joinpath("FSPkg")
- script.run('python', 'setup.py', 'develop',
- expect_stderr=True, cwd=pkg_path)
- script.run('python', 'setup.py', 'install',
- expect_stderr=True, cwd=pkg_path)
+ script.run("python", "setup.py", "develop", expect_stderr=True, cwd=pkg_path)
+ script.run("python", "setup.py", "install", expect_stderr=True, cwd=pkg_path)
script.assert_installed(FSPkg="0.1.dev0")
# Uninstall both develop and install
- uninstall = script.pip('uninstall', 'FSPkg', '-y')
- assert any(filename.endswith('.egg')
- for filename in uninstall.files_deleted.keys())
- uninstall2 = script.pip('uninstall', 'FSPkg', '-y')
- assert join(
- script.site_packages, 'FSPkg.egg-link'
- ) in uninstall2.files_deleted, list(uninstall2.files_deleted.keys())
+ uninstall = script.pip("uninstall", "FSPkg", "-y")
+ assert any(p.suffix == ".egg" for p in uninstall.files_deleted), str(uninstall)
+ uninstall2 = script.pip("uninstall", "FSPkg", "-y")
+ assert (
+ join(script.site_packages, "FSPkg.egg-link") in uninstall2.files_deleted
+ ), str(uninstall2)
script.assert_not_installed("FSPkg")
-def test_uninstall_editable_and_pip_install(script, data):
+def test_uninstall_editable_and_pip_install(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""Try uninstall after pip install -e after pip install"""
# SETUPTOOLS_SYS_PATH_TECHNIQUE=raw removes the assumption that `-e`
# installs are always higher priority than regular installs.
# This becomes the default behavior in setuptools 25.
- script.environ['SETUPTOOLS_SYS_PATH_TECHNIQUE'] = 'raw'
+ script.environ["SETUPTOOLS_SYS_PATH_TECHNIQUE"] = "raw"
pkg_path = data.packages.joinpath("FSPkg")
- script.pip('install', '-e', '.',
- expect_stderr=True, cwd=pkg_path)
+ script.pip("install", "-e", ".", expect_stderr=True, cwd=pkg_path)
# ensure both are installed with --ignore-installed:
- script.pip('install', '--ignore-installed', '.',
- expect_stderr=True, cwd=pkg_path)
+ script.pip("install", "--ignore-installed", ".", expect_stderr=True, cwd=pkg_path)
script.assert_installed(FSPkg="0.1.dev0")
# Uninstall both develop and install
- uninstall = script.pip('uninstall', 'FSPkg', '-y')
- assert not any(filename.endswith('.egg-link')
- for filename in uninstall.files_deleted.keys())
- uninstall2 = script.pip('uninstall', 'FSPkg', '-y')
- assert join(
- script.site_packages, 'FSPkg.egg-link'
- ) in uninstall2.files_deleted, list(uninstall2.files_deleted.keys())
+ uninstall = script.pip("uninstall", "FSPkg", "-y")
+ assert not any(p.suffix == ".egg-link" for p in uninstall.files_deleted)
+ uninstall2 = script.pip("uninstall", "FSPkg", "-y")
+ assert (
+ join(script.site_packages, "FSPkg.egg-link") in uninstall2.files_deleted
+ ), list(uninstall2.files_deleted.keys())
script.assert_not_installed("FSPkg")
-def test_uninstall_editable_and_pip_install_easy_install_remove(script, data):
+@pytest.fixture()
+def move_easy_install_pth(script: PipTestEnvironment) -> Iterator[None]:
+ """Move easy-install.pth out of the way for testing easy_install."""
+ easy_install_pth = join(script.site_packages_path, "easy-install.pth")
+ pip_test_pth = join(script.site_packages_path, "pip-test.pth")
+ os.rename(easy_install_pth, pip_test_pth)
+ yield
+ os.rename(pip_test_pth, easy_install_pth)
+
+
+@pytest.mark.usefixtures("move_easy_install_pth")
+def test_uninstall_editable_and_pip_install_easy_install_remove(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""Try uninstall after pip install -e after pip install
and removing easy-install.pth"""
# SETUPTOOLS_SYS_PATH_TECHNIQUE=raw removes the assumption that `-e`
# installs are always higher priority than regular installs.
# This becomes the default behavior in setuptools 25.
- script.environ['SETUPTOOLS_SYS_PATH_TECHNIQUE'] = 'raw'
-
- # Rename easy-install.pth to pip-test.pth
- easy_install_pth = join(script.site_packages_path, 'easy-install.pth')
- pip_test_pth = join(script.site_packages_path, 'pip-test.pth')
- os.rename(easy_install_pth, pip_test_pth)
+ script.environ["SETUPTOOLS_SYS_PATH_TECHNIQUE"] = "raw"
# Install FSPkg
pkg_path = data.packages.joinpath("FSPkg")
- script.pip('install', '-e', '.',
- expect_stderr=True, cwd=pkg_path)
+ script.pip("install", "-e", ".", expect_stderr=True, cwd=pkg_path)
# Rename easy-install.pth to pip-test-fspkg.pth
- pip_test_fspkg_pth = join(script.site_packages_path, 'pip-test-fspkg.pth')
+ easy_install_pth = join(script.site_packages_path, "easy-install.pth")
+ pip_test_fspkg_pth = join(script.site_packages_path, "pip-test-fspkg.pth")
os.rename(easy_install_pth, pip_test_fspkg_pth)
# Confirm that FSPkg is installed
@@ -623,35 +684,42 @@ def test_uninstall_editable_and_pip_install_easy_install_remove(script, data):
os.remove(pip_test_fspkg_pth)
# Uninstall will fail with given warning
- uninstall = script.pip('uninstall', 'FSPkg', '-y')
+ uninstall = script.pip("uninstall", "FSPkg", "-y", allow_stderr_warning=True)
assert "Cannot remove entries from nonexistent file" in uninstall.stderr
- assert join(
- script.site_packages, 'FSPkg.egg-link'
- ) in uninstall.files_deleted, list(uninstall.files_deleted.keys())
+ assert (
+ join(script.site_packages, "FSPkg.egg-link") in uninstall.files_deleted
+ ), list(uninstall.files_deleted.keys())
# Confirm that FSPkg is uninstalled
script.assert_not_installed("FSPkg")
- # Rename pip-test.pth back to easy-install.pth
- os.rename(pip_test_pth, easy_install_pth)
-
-def test_uninstall_ignores_missing_packages(script, data):
- """Uninstall of a non existent package prints a warning and exits cleanly
- """
+def test_uninstall_ignores_missing_packages(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ """Uninstall of a non existent package prints a warning and exits cleanly"""
result = script.pip(
- 'uninstall', '-y', 'non-existent-pkg', expect_stderr=True,
+ "uninstall",
+ "-y",
+ "non-existent-pkg",
+ expect_stderr=True,
)
assert "Skipping non-existent-pkg as it is not installed." in result.stderr
assert result.returncode == 0, "Expected clean exit"
-def test_uninstall_ignores_missing_packages_and_uninstalls_rest(script, data):
- script.pip_install_local('simple')
+def test_uninstall_ignores_missing_packages_and_uninstalls_rest(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ script.pip_install_local("simple")
result = script.pip(
- 'uninstall', '-y', 'non-existent-pkg', 'simple', expect_stderr=True,
+ "uninstall",
+ "-y",
+ "non-existent-pkg",
+ "simple",
+ expect_stderr=True,
)
assert "Skipping non-existent-pkg as it is not installed." in result.stderr
diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py
index 7a0006d47..6d48fe162 100644
--- a/tests/functional/test_uninstall_user.py
+++ b/tests/functional/test_uninstall_user.py
@@ -7,50 +7,55 @@ import pytest
from tests.functional.test_install_user import _patch_dist_in_site_packages
from tests.lib import pyversion # noqa: F401
-from tests.lib import assert_all_changes
+from tests.lib import PipTestEnvironment, TestData, assert_all_changes
+from tests.lib.venv import VirtualEnvironment
@pytest.mark.incompatible_with_test_venv
class Tests_UninstallUserSite:
-
@pytest.mark.network
- def test_uninstall_from_usersite(self, script):
+ def test_uninstall_from_usersite(self, script: PipTestEnvironment) -> None:
"""
Test uninstall from usersite
"""
- result1 = script.pip('install', '--user', 'INITools==0.3')
- result2 = script.pip('uninstall', '-y', 'INITools')
- assert_all_changes(result1, result2, [script.venv / 'build', 'cache'])
+ result1 = script.pip("install", "--user", "INITools==0.3")
+ result2 = script.pip("uninstall", "-y", "INITools")
+ assert_all_changes(result1, result2, [script.venv / "build", "cache"])
def test_uninstall_from_usersite_with_dist_in_global_site(
- self, virtualenv, script):
+ self, virtualenv: VirtualEnvironment, script: PipTestEnvironment
+ ) -> None:
"""
Test uninstall from usersite (with same dist in global site)
"""
_patch_dist_in_site_packages(virtualenv)
- script.pip_install_local('pip-test-package==0.1', '--no-binary=:all:')
+ script.pip_install_local("pip-test-package==0.1", "--no-binary=:all:")
result2 = script.pip_install_local(
- '--user', 'pip-test-package==0.1.1', '--no-binary=:all:')
- result3 = script.pip('uninstall', '-vy', 'pip-test-package')
+ "--user", "pip-test-package==0.1.1", "--no-binary=:all:"
+ )
+ result3 = script.pip("uninstall", "-vy", "pip-test-package")
# uninstall console is mentioning user scripts, but not global scripts
assert normcase(script.user_bin_path) in result3.stdout, str(result3)
assert normcase(script.bin_path) not in result3.stdout, str(result3)
# uninstall worked
- assert_all_changes(result2, result3, [script.venv / 'build', 'cache'])
+ assert_all_changes(result2, result3, [script.venv / "build", "cache"])
# site still has 0.2 (can't look in result1; have to check)
# keep checking for egg-info because no-binary implies setup.py install
egg_info_folder = (
- script.base_path / script.site_packages /
- f'pip_test_package-0.1-py{pyversion}.egg-info'
+ script.base_path
+ / script.site_packages
+ / f"pip_test_package-0.1-py{pyversion}.egg-info"
)
assert isdir(egg_info_folder)
- def test_uninstall_editable_from_usersite(self, script, data):
+ def test_uninstall_editable_from_usersite(
+ self, script: PipTestEnvironment, data: TestData
+ ) -> None:
"""
Test uninstall editable local user install
"""
@@ -58,22 +63,20 @@ class Tests_UninstallUserSite:
# install
to_install = data.packages.joinpath("FSPkg")
- result1 = script.pip(
- 'install', '--user', '-e', to_install
- )
- egg_link = script.user_site / 'FSPkg.egg-link'
+ result1 = script.pip("install", "--user", "-e", to_install)
+ egg_link = script.user_site / "FSPkg.egg-link"
result1.did_create(egg_link)
# uninstall
- result2 = script.pip('uninstall', '-y', 'FSPkg')
+ result2 = script.pip("uninstall", "-y", "FSPkg")
assert not isfile(script.base_path / egg_link)
assert_all_changes(
result1,
result2,
[
- script.venv / 'build',
- 'cache',
- script.user_site / 'easy-install.pth',
- ]
+ script.venv / "build",
+ "cache",
+ script.user_site / "easy-install.pth",
+ ],
)
diff --git a/tests/functional/test_vcs_bazaar.py b/tests/functional/test_vcs_bazaar.py
index 0e598382a..63955d6e7 100644
--- a/tests/functional/test_vcs_bazaar.py
+++ b/tests/functional/test_vcs_bazaar.py
@@ -3,29 +3,31 @@ Contains functional tests of the Bazaar class.
"""
import os
+import sys
+from pathlib import Path
import pytest
from pip._internal.vcs.bazaar import Bazaar
from pip._internal.vcs.versioncontrol import RemoteNotFoundError
-from tests.lib import is_bzr_installed, need_bzr
+from tests.lib import PipTestEnvironment, is_bzr_installed, need_bzr
@pytest.mark.skipif(
- 'TRAVIS' not in os.environ,
- reason='Bazaar is only required under Travis')
-def test_ensure_bzr_available():
- """Make sure that bzr is available when running in Travis."""
+ sys.platform == "win32" or "CI" not in os.environ,
+ reason="Bazaar is only required under CI",
+)
+def test_ensure_bzr_available() -> None:
+ """Make sure that bzr is available when running in CI."""
assert is_bzr_installed()
@need_bzr
-def test_get_remote_url__no_remote(script, tmpdir):
- repo_dir = tmpdir / 'temp-repo'
+def test_get_remote_url__no_remote(script: PipTestEnvironment, tmpdir: Path) -> None:
+ repo_dir = tmpdir / "temp-repo"
repo_dir.mkdir()
- repo_dir = str(repo_dir)
- script.run('bzr', 'init', repo_dir)
+ script.run("bzr", "init", os.fspath(repo_dir))
with pytest.raises(RemoteNotFoundError):
- Bazaar().get_remote_url(repo_dir)
+ Bazaar().get_remote_url(os.fspath(repo_dir))
diff --git a/tests/functional/test_vcs_git.py b/tests/functional/test_vcs_git.py
index d5de1a2fd..da4d9583f 100644
--- a/tests/functional/test_vcs_git.py
+++ b/tests/functional/test_vcs_git.py
@@ -1,44 +1,52 @@
"""
Contains functional tests of the Git class.
"""
-
+import logging
import os
+import pathlib
+from typing import List, Optional, Tuple
+from unittest.mock import Mock, patch
import pytest
+from pip._internal.utils.misc import HiddenText
from pip._internal.vcs import vcs
from pip._internal.vcs.git import Git, RemoteNotFoundError
-from tests.lib import _create_test_package, _git_commit, _test_path_to_file_url
+from tests.lib import PipTestEnvironment, _create_test_package, _git_commit
-def test_get_backend_for_scheme():
+def test_get_backend_for_scheme() -> None:
assert vcs.get_backend_for_scheme("git+https") is vcs.get_backend("Git")
-def get_head_sha(script, dest):
+def get_head_sha(script: PipTestEnvironment, dest: str) -> str:
"""Return the HEAD sha."""
- result = script.run('git', 'rev-parse', 'HEAD', cwd=dest)
+ result = script.run("git", "rev-parse", "HEAD", cwd=dest)
sha = result.stdout.strip()
return sha
-def checkout_ref(script, repo_dir, ref):
- script.run('git', 'checkout', ref, cwd=repo_dir)
+def checkout_ref(script: PipTestEnvironment, repo_dir: str, ref: str) -> None:
+ script.run("git", "checkout", ref, cwd=repo_dir)
-def checkout_new_branch(script, repo_dir, branch):
+def checkout_new_branch(script: PipTestEnvironment, repo_dir: str, branch: str) -> None:
script.run(
- 'git', 'checkout', '-b', branch, cwd=repo_dir,
+ "git",
+ "checkout",
+ "-b",
+ branch,
+ cwd=repo_dir,
)
-def do_commit(script, dest):
- _git_commit(script, dest, message='test commit', allow_empty=True)
+def do_commit(script: PipTestEnvironment, dest: str) -> str:
+ _git_commit(script, dest, message="test commit", allow_empty=True)
return get_head_sha(script, dest)
-def add_commits(script, dest, count):
+def add_commits(script: PipTestEnvironment, dest: str, count: int) -> List[str]:
"""Return a list of the commit hashes from oldest to newest."""
shas = []
for _ in range(count):
@@ -48,111 +56,115 @@ def add_commits(script, dest, count):
return shas
-def check_rev(repo_dir, rev, expected):
+def check_rev(repo_dir: str, rev: str, expected: Tuple[Optional[str], bool]) -> None:
assert Git.get_revision_sha(repo_dir, rev) == expected
-def test_git_dir_ignored(tmpdir):
+def test_git_dir_ignored(tmpdir: pathlib.Path) -> None:
"""
Test that a GIT_DIR environment variable is ignored.
"""
- repo_path = tmpdir / 'test-repo'
+ repo_path = tmpdir / "test-repo"
repo_path.mkdir()
repo_dir = str(repo_path)
- env = {'GIT_DIR': 'foo'}
+ env = {"GIT_DIR": "foo"}
# If GIT_DIR is not ignored, then os.listdir() will return ['foo'].
- Git.run_command(['init', repo_dir], cwd=repo_dir, extra_environ=env)
- assert os.listdir(repo_dir) == ['.git']
+ Git.run_command(["init", repo_dir], cwd=repo_dir, extra_environ=env)
+ assert os.listdir(repo_dir) == [".git"]
-def test_git_work_tree_ignored(tmpdir):
+def test_git_work_tree_ignored(tmpdir: pathlib.Path) -> None:
"""
Test that a GIT_WORK_TREE environment variable is ignored.
"""
- repo_path = tmpdir / 'test-repo'
+ repo_path = tmpdir / "test-repo"
repo_path.mkdir()
repo_dir = str(repo_path)
- Git.run_command(['init', repo_dir], cwd=repo_dir)
+ Git.run_command(["init", repo_dir], cwd=repo_dir)
# Choose a directory relative to the cwd that does not exist.
# If GIT_WORK_TREE is not ignored, then the command will error out
# with: "fatal: This operation must be run in a work tree".
- env = {'GIT_WORK_TREE': 'foo'}
- Git.run_command(['status', repo_dir], extra_environ=env, cwd=repo_dir)
+ env = {"GIT_WORK_TREE": "foo"}
+ Git.run_command(["status", repo_dir], extra_environ=env, cwd=repo_dir)
-def test_get_remote_url(script, tmpdir):
- source_dir = tmpdir / 'source'
- source_dir.mkdir()
- source_url = _test_path_to_file_url(source_dir)
+def test_get_remote_url(script: PipTestEnvironment, tmpdir: pathlib.Path) -> None:
+ source_path = tmpdir / "source"
+ source_path.mkdir()
+ source_url = source_path.as_uri()
- source_dir = str(source_dir)
- script.run('git', 'init', cwd=source_dir)
+ source_dir = str(source_path)
+ script.run("git", "init", cwd=source_dir)
do_commit(script, source_dir)
- repo_dir = str(tmpdir / 'repo')
- script.run('git', 'clone', source_url, repo_dir)
+ repo_dir = str(tmpdir / "repo")
+ script.run("git", "clone", source_url, repo_dir)
remote_url = Git.get_remote_url(repo_dir)
assert remote_url == source_url
-def test_get_remote_url__no_remote(script, tmpdir):
+def test_get_remote_url__no_remote(
+ script: PipTestEnvironment, tmpdir: pathlib.Path
+) -> None:
"""
Test a repo with no remote.
"""
- repo_dir = tmpdir / 'temp-repo'
- repo_dir.mkdir()
- repo_dir = str(repo_dir)
+ repo_path = tmpdir / "temp-repo"
+ repo_path.mkdir()
+ repo_dir = str(repo_path)
- script.run('git', 'init', cwd=repo_dir)
+ script.run("git", "init", cwd=repo_dir)
with pytest.raises(RemoteNotFoundError):
Git.get_remote_url(repo_dir)
-def test_get_current_branch(script):
+def test_get_current_branch(script: PipTestEnvironment) -> None:
repo_dir = str(script.scratch_path)
- script.run('git', 'init', cwd=repo_dir)
+ script.run("git", "init", cwd=repo_dir)
sha = do_commit(script, repo_dir)
- assert Git.get_current_branch(repo_dir) == 'master'
+ assert Git.get_current_branch(repo_dir) == "master"
# Switch to a branch with the same SHA as "master" but whose name
# is alphabetically after.
- checkout_new_branch(script, repo_dir, 'release')
- assert Git.get_current_branch(repo_dir) == 'release'
+ checkout_new_branch(script, repo_dir, "release")
+ assert Git.get_current_branch(repo_dir) == "release"
# Also test the detached HEAD case.
checkout_ref(script, repo_dir, sha)
assert Git.get_current_branch(repo_dir) is None
-def test_get_current_branch__branch_and_tag_same_name(script, tmpdir):
+def test_get_current_branch__branch_and_tag_same_name(
+ script: PipTestEnvironment, tmpdir: pathlib.Path
+) -> None:
"""
Check calling get_current_branch() from a branch or tag when the branch
and tag have the same name.
"""
repo_dir = str(tmpdir)
- script.run('git', 'init', cwd=repo_dir)
+ script.run("git", "init", cwd=repo_dir)
do_commit(script, repo_dir)
- checkout_new_branch(script, repo_dir, 'dev')
+ checkout_new_branch(script, repo_dir, "dev")
# Create a tag with the same name as the branch.
- script.run('git', 'tag', 'dev', cwd=repo_dir)
+ script.run("git", "tag", "dev", cwd=repo_dir)
- assert Git.get_current_branch(repo_dir) == 'dev'
+ assert Git.get_current_branch(repo_dir) == "dev"
# Now try with the tag checked out.
- checkout_ref(script, repo_dir, 'refs/tags/dev')
+ checkout_ref(script, repo_dir, "refs/tags/dev")
assert Git.get_current_branch(repo_dir) is None
-def test_get_revision_sha(script):
+def test_get_revision_sha(script: PipTestEnvironment) -> None:
repo_dir = str(script.scratch_path)
- script.run('git', 'init', cwd=repo_dir)
+ script.run("git", "init", cwd=repo_dir)
shas = add_commits(script, repo_dir, count=3)
tag_sha = shas[0]
@@ -160,99 +172,96 @@ def test_get_revision_sha(script):
head_sha = shas[2]
assert head_sha == shas[-1]
- origin_ref = 'refs/remotes/origin/origin-branch'
- generic_ref = 'refs/generic-ref'
+ origin_ref = "refs/remotes/origin/origin-branch"
+ generic_ref = "refs/generic-ref"
+ script.run("git", "branch", "local-branch", head_sha, cwd=repo_dir)
+ script.run("git", "tag", "v1.0", tag_sha, cwd=repo_dir)
+ script.run("git", "update-ref", origin_ref, origin_sha, cwd=repo_dir)
script.run(
- 'git', 'branch', 'local-branch', head_sha, cwd=repo_dir
+ "git",
+ "update-ref",
+ "refs/remotes/upstream/upstream-branch",
+ head_sha,
+ cwd=repo_dir,
)
- script.run('git', 'tag', 'v1.0', tag_sha, cwd=repo_dir)
- script.run('git', 'update-ref', origin_ref, origin_sha, cwd=repo_dir)
- script.run(
- 'git', 'update-ref', 'refs/remotes/upstream/upstream-branch',
- head_sha, cwd=repo_dir
- )
- script.run('git', 'update-ref', generic_ref, head_sha, cwd=repo_dir)
+ script.run("git", "update-ref", generic_ref, head_sha, cwd=repo_dir)
# Test two tags pointing to the same sha.
- script.run('git', 'tag', 'v2.0', tag_sha, cwd=repo_dir)
+ script.run("git", "tag", "v2.0", tag_sha, cwd=repo_dir)
# Test tags sharing the same suffix as another tag, both before and
# after the suffix alphabetically.
- script.run('git', 'tag', 'aaa/v1.0', head_sha, cwd=repo_dir)
- script.run('git', 'tag', 'zzz/v1.0', head_sha, cwd=repo_dir)
+ script.run("git", "tag", "aaa/v1.0", head_sha, cwd=repo_dir)
+ script.run("git", "tag", "zzz/v1.0", head_sha, cwd=repo_dir)
- check_rev(repo_dir, 'v1.0', (tag_sha, False))
- check_rev(repo_dir, 'v2.0', (tag_sha, False))
- check_rev(repo_dir, 'origin-branch', (origin_sha, True))
+ check_rev(repo_dir, "v1.0", (tag_sha, False))
+ check_rev(repo_dir, "v2.0", (tag_sha, False))
+ check_rev(repo_dir, "origin-branch", (origin_sha, True))
ignored_names = [
# Local branches should be ignored.
- 'local-branch',
+ "local-branch",
# Non-origin remote branches should be ignored.
- 'upstream-branch',
+ "upstream-branch",
# Generic refs should be ignored.
- 'generic-ref',
+ "generic-ref",
# Fully spelled-out refs should be ignored.
origin_ref,
generic_ref,
# Test passing a valid commit hash.
tag_sha,
# Test passing a non-existent name.
- 'does-not-exist',
+ "does-not-exist",
]
for name in ignored_names:
check_rev(repo_dir, name, (None, False))
-def test_is_commit_id_equal(script):
+def test_is_commit_id_equal(script: PipTestEnvironment) -> None:
"""
Test Git.is_commit_id_equal().
"""
- version_pkg_path = _create_test_package(script)
- script.run('git', 'branch', 'branch0.1', cwd=version_pkg_path)
- commit = script.run(
- 'git', 'rev-parse', 'HEAD',
- cwd=version_pkg_path
- ).stdout.strip()
+ version_pkg_path = os.fspath(_create_test_package(script.scratch_path))
+ script.run("git", "branch", "branch0.1", cwd=version_pkg_path)
+ commit = script.run("git", "rev-parse", "HEAD", cwd=version_pkg_path).stdout.strip()
assert Git.is_commit_id_equal(version_pkg_path, commit)
assert not Git.is_commit_id_equal(version_pkg_path, commit[:7])
- assert not Git.is_commit_id_equal(version_pkg_path, 'branch0.1')
- assert not Git.is_commit_id_equal(version_pkg_path, 'abc123')
+ assert not Git.is_commit_id_equal(version_pkg_path, "branch0.1")
+ assert not Git.is_commit_id_equal(version_pkg_path, "abc123")
# Also check passing a None value.
assert not Git.is_commit_id_equal(version_pkg_path, None)
-def test_is_immutable_rev_checkout(script):
- version_pkg_path = _create_test_package(script)
- commit = script.run(
- 'git', 'rev-parse', 'HEAD',
- cwd=version_pkg_path
- ).stdout.strip()
+def test_is_immutable_rev_checkout(script: PipTestEnvironment) -> None:
+ version_pkg_path = os.fspath(_create_test_package(script.scratch_path))
+ commit = script.run("git", "rev-parse", "HEAD", cwd=version_pkg_path).stdout.strip()
assert Git().is_immutable_rev_checkout(
"git+https://g.c/o/r@" + commit, version_pkg_path
)
- assert not Git().is_immutable_rev_checkout(
- "git+https://g.c/o/r", version_pkg_path
- )
+ assert not Git().is_immutable_rev_checkout("git+https://g.c/o/r", version_pkg_path)
assert not Git().is_immutable_rev_checkout(
"git+https://g.c/o/r@master", version_pkg_path
)
-def test_get_repository_root(script):
- version_pkg_path = _create_test_package(script)
+def test_get_repository_root(script: PipTestEnvironment) -> None:
+ version_pkg_path = _create_test_package(script.scratch_path)
tests_path = version_pkg_path.joinpath("tests")
tests_path.mkdir()
- root1 = Git.get_repository_root(version_pkg_path)
+ root1 = Git.get_repository_root(os.fspath(version_pkg_path))
+ assert root1 is not None
assert os.path.normcase(root1) == os.path.normcase(version_pkg_path)
- root2 = Git.get_repository_root(version_pkg_path.joinpath("tests"))
+ root2 = Git.get_repository_root(os.fspath(tests_path))
+ assert root2 is not None
assert os.path.normcase(root2) == os.path.normcase(version_pkg_path)
-def test_resolve_commit_not_on_branch(script, tmp_path):
+def test_resolve_commit_not_on_branch(
+ script: PipTestEnvironment, tmp_path: pathlib.Path
+) -> None:
repo_path = tmp_path / "repo"
repo_file = repo_path / "file.txt"
clone_path = repo_path / "clone"
@@ -267,9 +276,7 @@ def test_resolve_commit_not_on_branch(script, tmp_path):
# create a commit
repo_file.write_text("..")
script.run("git", "commit", "-a", "-m", "commit 1", cwd=str(repo_path))
- commit = script.run(
- "git", "rev-parse", "HEAD", cwd=str(repo_path)
- ).stdout.strip()
+ commit = script.run("git", "rev-parse", "HEAD", cwd=str(repo_path)).stdout.strip()
# make sure our commit is not on a branch
script.run("git", "checkout", "master", cwd=str(repo_path))
@@ -281,4 +288,172 @@ def test_resolve_commit_not_on_branch(script, tmp_path):
# check we can fetch our commit
rev_options = Git.make_rev_options(commit)
- Git().fetch_new(str(clone_path), repo_path.as_uri(), rev_options)
+ Git().fetch_new(
+ str(clone_path),
+ HiddenText(repo_path.as_uri(), redacted="*"),
+ rev_options,
+ verbosity=0,
+ )
+
+
+def _initialize_clonetest_server(
+ repo_path: pathlib.Path, script: PipTestEnvironment, enable_partial_clone: bool
+) -> pathlib.Path:
+ repo_path.mkdir()
+ script.run("git", "init", cwd=str(repo_path))
+ repo_file = repo_path / "file.txt"
+ repo_file.write_text(".")
+ script.run("git", "add", "file.txt", cwd=str(repo_path))
+ script.run("git", "commit", "-m", "initial commit", cwd=str(repo_path))
+
+ # Enable filtering support on server
+ if enable_partial_clone:
+ script.run("git", "config", "uploadpack.allowFilter", "true", cwd=repo_path)
+ script.run(
+ "git", "config", "uploadpack.allowanysha1inwant", "true", cwd=repo_path
+ )
+
+ return repo_file
+
+
+@pytest.mark.parametrize(
+ "version_out, expected_message",
+ (
+ ("git version -2.25.1", "Can't parse git version: git version -2.25.1"),
+ ("git version 2.a.1", "Can't parse git version: git version 2.a.1"),
+ ("git ver. 2.25.1", "Can't parse git version: git ver. 2.25.1"),
+ ),
+)
+@patch("pip._internal.vcs.versioncontrol.VersionControl.run_command")
+def test_git_parse_fail_warning(
+ mock_run_command: Mock,
+ caplog: pytest.LogCaptureFixture,
+ version_out: str,
+ expected_message: str,
+) -> None:
+ """Test invalid git version logs adds an explicit warning log."""
+ mock_run_command.return_value = version_out
+
+ caplog.set_level(logging.WARNING)
+
+ git_tuple = Git().get_git_version()
+ # Returns an empty tuple if it is an invalid git version
+ assert git_tuple == ()
+
+ # Check for warning log
+ assert expected_message in caplog.text.strip()
+
+
+@pytest.mark.skipif(Git().get_git_version() < (2, 17), reason="git too old")
+def test_partial_clone(script: PipTestEnvironment, tmp_path: pathlib.Path) -> None:
+ """Test partial clone w/ a git-server that supports it"""
+ repo_path = tmp_path / "repo"
+ repo_file = _initialize_clonetest_server(
+ repo_path, script, enable_partial_clone=True
+ )
+ clone_path1 = repo_path / "clone1"
+ clone_path2 = repo_path / "clone2"
+
+ commit = script.run("git", "rev-parse", "HEAD", cwd=str(repo_path)).stdout.strip()
+
+ # Check that we can clone at HEAD
+ Git().fetch_new(
+ str(clone_path1),
+ HiddenText(repo_path.as_uri(), redacted="*"),
+ Git.make_rev_options(),
+ verbosity=0,
+ )
+ # Check that we can clone to commit
+ Git().fetch_new(
+ str(clone_path2),
+ HiddenText(repo_path.as_uri(), redacted="*"),
+ Git.make_rev_options(commit),
+ verbosity=0,
+ )
+
+ # Write some additional stuff to git pull
+ repo_file.write_text("..")
+ script.run("git", "commit", "-am", "second commit", cwd=str(repo_path))
+
+ # Make sure git pull works - with server supporting filtering
+ assert (
+ "warning: filtering not recognized by server, ignoring"
+ not in script.run("git", "pull", cwd=clone_path1).stderr
+ )
+ assert (
+ "warning: filtering not recognized by server, ignoring"
+ not in script.run("git", "pull", cwd=clone_path2).stderr
+ )
+
+
+@pytest.mark.skipif(Git().get_git_version() < (2, 17), reason="git too old")
+def test_partial_clone_without_server_support(
+ script: PipTestEnvironment, tmp_path: pathlib.Path
+) -> None:
+ """Test partial clone w/ a git-server that does not support it"""
+ repo_path = tmp_path / "repo"
+ repo_file = _initialize_clonetest_server(
+ repo_path, script, enable_partial_clone=False
+ )
+ clone_path1 = repo_path / "clone1"
+ clone_path2 = repo_path / "clone2"
+
+ commit = script.run("git", "rev-parse", "HEAD", cwd=str(repo_path)).stdout.strip()
+
+ # Check that we can clone at HEAD
+ Git().fetch_new(
+ str(clone_path1),
+ HiddenText(repo_path.as_uri(), redacted="*"),
+ Git.make_rev_options(),
+ verbosity=0,
+ )
+ # Check that we can clone to commit
+ Git().fetch_new(
+ str(clone_path2),
+ HiddenText(repo_path.as_uri(), redacted="*"),
+ Git.make_rev_options(commit),
+ verbosity=0,
+ )
+
+ # Write some additional stuff to git pull
+ repo_file.write_text("..")
+ script.run("git", "commit", "-am", "second commit", cwd=str(repo_path))
+
+ # Make sure git pull works - even though server doesn't support filtering
+ assert (
+ "warning: filtering not recognized by server, ignoring"
+ in script.run("git", "pull", cwd=clone_path1).stderr
+ )
+ assert (
+ "warning: filtering not recognized by server, ignoring"
+ in script.run("git", "pull", cwd=clone_path2).stderr
+ )
+
+
+def test_clone_without_partial_clone_support(
+ script: PipTestEnvironment, tmp_path: pathlib.Path
+) -> None:
+ """Older git clients don't support partial clone. Test the fallback path"""
+ repo_path = tmp_path / "repo"
+ repo_file = _initialize_clonetest_server(
+ repo_path, script, enable_partial_clone=True
+ )
+ clone_path = repo_path / "clone1"
+
+ # Check that we can clone w/ old version of git w/o --filter
+ with patch("pip._internal.vcs.git.Git.get_git_version", return_value=(2, 16)):
+ Git().fetch_new(
+ str(clone_path),
+ HiddenText(repo_path.as_uri(), redacted="*"),
+ Git.make_rev_options(),
+ verbosity=0,
+ )
+
+ repo_file.write_text("...")
+ script.run("git", "commit", "-am", "third commit", cwd=str(repo_path))
+
+ # Should work fine w/o attempting to use `--filter` args
+ assert (
+ "warning: filtering not recognized by server, ignoring"
+ not in script.run("git", "pull", cwd=clone_path).stderr
+ )
diff --git a/tests/functional/test_vcs_mercurial.py b/tests/functional/test_vcs_mercurial.py
index 841c4d821..9a909e71f 100644
--- a/tests/functional/test_vcs_mercurial.py
+++ b/tests/functional/test_vcs_mercurial.py
@@ -1,17 +1,19 @@
import os
from pip._internal.vcs.mercurial import Mercurial
-from tests.lib import _create_test_package, need_mercurial
+from tests.lib import PipTestEnvironment, _create_test_package, need_mercurial
@need_mercurial
-def test_get_repository_root(script):
- version_pkg_path = _create_test_package(script, vcs="hg")
+def test_get_repository_root(script: PipTestEnvironment) -> None:
+ version_pkg_path = _create_test_package(script.scratch_path, vcs="hg")
tests_path = version_pkg_path.joinpath("tests")
tests_path.mkdir()
- root1 = Mercurial.get_repository_root(version_pkg_path)
+ root1 = Mercurial.get_repository_root(os.fspath(version_pkg_path))
+ assert root1 is not None
assert os.path.normcase(root1) == os.path.normcase(version_pkg_path)
- root2 = Mercurial.get_repository_root(version_pkg_path.joinpath("tests"))
+ root2 = Mercurial.get_repository_root(os.fspath(tests_path))
+ assert root2 is not None
assert os.path.normcase(root2) == os.path.normcase(version_pkg_path)
diff --git a/tests/functional/test_vcs_subversion.py b/tests/functional/test_vcs_subversion.py
index 194019da9..05c20c7c1 100644
--- a/tests/functional/test_vcs_subversion.py
+++ b/tests/functional/test_vcs_subversion.py
@@ -1,31 +1,35 @@
+from pathlib import Path
+
import pytest
from pip._internal.vcs.subversion import Subversion
from pip._internal.vcs.versioncontrol import RemoteNotFoundError
-from tests.lib import _create_svn_repo, need_svn
+from tests.lib import PipTestEnvironment, _create_svn_repo, need_svn
@need_svn
-def test_get_remote_url__no_remote(script, tmpdir):
- repo_dir = tmpdir / 'temp-repo'
- repo_dir.mkdir()
- repo_dir = str(repo_dir)
+def test_get_remote_url__no_remote(script: PipTestEnvironment, tmpdir: Path) -> None:
+ repo_path = tmpdir / "temp-repo"
+ repo_path.mkdir()
+ repo_dir = str(repo_path)
- _create_svn_repo(script, repo_dir)
+ _create_svn_repo(script.scratch_path, repo_dir)
with pytest.raises(RemoteNotFoundError):
Subversion().get_remote_url(repo_dir)
@need_svn
-def test_get_remote_url__no_remote_with_setup(script, tmpdir):
- repo_dir = tmpdir / 'temp-repo'
- repo_dir.mkdir()
- setup = repo_dir / "setup.py"
+def test_get_remote_url__no_remote_with_setup(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
+ repo_path = tmpdir / "temp-repo"
+ repo_path.mkdir()
+ setup = repo_path / "setup.py"
setup.touch()
- repo_dir = str(repo_dir)
+ repo_dir = str(repo_path)
- _create_svn_repo(script, repo_dir)
+ _create_svn_repo(script.scratch_path, repo_dir)
with pytest.raises(RemoteNotFoundError):
Subversion().get_remote_url(repo_dir)
diff --git a/tests/functional/test_warning.py b/tests/functional/test_warning.py
index 5c5b1a201..c47443ca9 100644
--- a/tests/functional/test_warning.py
+++ b/tests/functional/test_warning.py
@@ -1,49 +1,69 @@
+import os
+import sys
import textwrap
+from pathlib import Path
import pytest
+from tests.lib import PipTestEnvironment
+
@pytest.fixture
-def warnings_demo(tmpdir):
- demo = tmpdir.joinpath('warnings_demo.py')
- demo.write_text(textwrap.dedent('''
+def warnings_demo(tmpdir: Path) -> Path:
+ demo = tmpdir.joinpath("warnings_demo.py")
+ demo.write_text(
+ textwrap.dedent(
+ """
from logging import basicConfig
from pip._internal.utils import deprecation
deprecation.install_warning_logger()
basicConfig()
- deprecation.deprecated("deprecated!", replacement=None, gone_in=None)
- '''))
+ deprecation.deprecated(reason="deprecated!", replacement=None, gone_in=None)
+ """
+ )
+ )
return demo
-def test_deprecation_warnings_are_correct(script, warnings_demo):
- result = script.run('python', warnings_demo, expect_stderr=True)
- expected = 'WARNING:pip._internal.deprecations:DEPRECATION: deprecated!\n'
+def test_deprecation_warnings_are_correct(
+ script: PipTestEnvironment, warnings_demo: Path
+) -> None:
+ result = script.run("python", os.fspath(warnings_demo), expect_stderr=True)
+ expected = "WARNING:pip._internal.deprecations:DEPRECATION: deprecated!\n"
assert result.stderr == expected
-def test_deprecation_warnings_can_be_silenced(script, warnings_demo):
- script.environ['PYTHONWARNINGS'] = 'ignore'
- result = script.run('python', warnings_demo)
- assert result.stderr == ''
+def test_deprecation_warnings_can_be_silenced(
+ script: PipTestEnvironment, warnings_demo: Path
+) -> None:
+ script.environ["PYTHONWARNINGS"] = "ignore"
+ result = script.run("python", os.fspath(warnings_demo))
+ assert result.stderr == ""
DEPRECATION_TEXT = "drop support for Python 2.7"
CPYTHON_DEPRECATION_TEXT = "January 1st, 2020"
-def test_version_warning_is_not_shown_if_python_version_is_not_2(script):
+def test_version_warning_is_not_shown_if_python_version_is_not_2(
+ script: PipTestEnvironment,
+) -> None:
result = script.pip("debug", allow_stderr_warning=True)
assert DEPRECATION_TEXT not in result.stderr, str(result)
assert CPYTHON_DEPRECATION_TEXT not in result.stderr, str(result)
-def test_flag_does_nothing_if_python_version_is_not_2(script):
+def test_flag_does_nothing_if_python_version_is_not_2(
+ script: PipTestEnvironment,
+) -> None:
script.pip("list", "--no-python-version-warning")
-def test_pip_works_with_warnings_as_errors(script):
- script.environ['PYTHONWARNINGS'] = 'error'
+@pytest.mark.skipif(
+ sys.version_info >= (3, 10), reason="distutils is deprecated in 3.10+"
+)
+def test_pip_works_with_warnings_as_errors(script: PipTestEnvironment) -> None:
+ script.environ["PYTHONWARNINGS"] = "error"
script.pip("--version")
diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py
index da040c307..1894b37a6 100644
--- a/tests/functional/test_wheel.py
+++ b/tests/functional/test_wheel.py
@@ -2,74 +2,84 @@
import os
import re
import sys
-from os.path import exists
+from pathlib import Path
import pytest
from pip._internal.cli.status_codes import ERROR
from tests.lib import pyversion # noqa: F401
+from tests.lib import PipTestEnvironment, TestData
+pytestmark = pytest.mark.usefixtures("with_wheel")
-@pytest.fixture(autouse=True)
-def auto_with_wheel(with_wheel):
- pass
-
-def add_files_to_dist_directory(folder):
- (folder / 'dist').mkdir(parents=True)
- (folder / 'dist' / 'a_name-0.0.1.tar.gz').write_text("hello")
+def add_files_to_dist_directory(folder: Path) -> None:
+ (folder / "dist").mkdir(parents=True)
+ (folder / "dist" / "a_name-0.0.1.tar.gz").write_text("hello")
# Not adding a wheel file since that confuses setuptools' backend.
# (folder / 'dist' / 'a_name-0.0.1-py2.py3-none-any.whl').write_text(
# "hello"
# )
-def test_wheel_exit_status_code_when_no_requirements(script):
+def test_wheel_exit_status_code_when_no_requirements(
+ script: PipTestEnvironment,
+) -> None:
"""
Test wheel exit status code when no requirements specified
"""
- result = script.pip('wheel', expect_error=True)
+ result = script.pip("wheel", expect_error=True)
assert "You must give at least one requirement to wheel" in result.stderr
assert result.returncode == ERROR
-def test_wheel_exit_status_code_when_blank_requirements_file(script):
+def test_wheel_exit_status_code_when_blank_requirements_file(
+ script: PipTestEnvironment,
+) -> None:
"""
Test wheel exit status code when blank requirements file specified
"""
script.scratch_path.joinpath("blank.txt").write_text("\n")
- script.pip('wheel', '-r', 'blank.txt')
+ script.pip("wheel", "-r", "blank.txt")
-def test_pip_wheel_success(script, data):
+def test_pip_wheel_success(script: PipTestEnvironment, data: TestData) -> None:
"""
Test 'pip wheel' success.
"""
result = script.pip(
- 'wheel', '--no-index', '-f', data.find_links,
- 'simple==3.0',
+ "wheel",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "simple==3.0",
)
- wheel_file_name = f'simple-3.0-py{pyversion[0]}-none-any.whl'
+ wheel_file_name = f"simple-3.0-py{pyversion[0]}-none-any.whl"
wheel_file_path = script.scratch / wheel_file_name
assert re.search(
r"Created wheel for simple: "
- r"filename={filename} size=\d+ sha256=[A-Fa-f0-9]{{64}}"
- .format(filename=re.escape(wheel_file_name)), result.stdout)
- assert re.search(
- r"^\s+Stored in directory: ", result.stdout, re.M)
+ r"filename={filename} size=\d+ sha256=[A-Fa-f0-9]{{64}}".format(
+ filename=re.escape(wheel_file_name)
+ ),
+ result.stdout,
+ )
+ assert re.search(r"^\s+Stored in directory: ", result.stdout, re.M)
result.did_create(wheel_file_path)
assert "Successfully built simple" in result.stdout, result.stdout
-def test_pip_wheel_build_cache(script, data):
+def test_pip_wheel_build_cache(script: PipTestEnvironment, data: TestData) -> None:
"""
Test 'pip wheel' builds and caches.
"""
result = script.pip(
- 'wheel', '--no-index', '-f', data.find_links,
- 'simple==3.0',
+ "wheel",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "simple==3.0",
)
- wheel_file_name = f'simple-3.0-py{pyversion[0]}-none-any.whl'
+ wheel_file_name = f"simple-3.0-py{pyversion[0]}-none-any.whl"
wheel_file_path = script.scratch / wheel_file_name
result.did_create(wheel_file_path)
assert "Successfully built simple" in result.stdout, result.stdout
@@ -78,138 +88,170 @@ def test_pip_wheel_build_cache(script, data):
# pip wheel again and test that no build occurs since
# we get the wheel from cache
result = script.pip(
- 'wheel', '--no-index', '-f', data.find_links,
- 'simple==3.0',
+ "wheel",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "simple==3.0",
)
result.did_create(wheel_file_path)
assert "Successfully built simple" not in result.stdout, result.stdout
-def test_basic_pip_wheel_downloads_wheels(script, data):
+def test_basic_pip_wheel_downloads_wheels(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test 'pip wheel' downloads wheels
"""
result = script.pip(
- 'wheel', '--no-index', '-f', data.find_links, 'simple.dist',
+ "wheel",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "simple.dist",
)
- wheel_file_name = 'simple.dist-0.1-py2.py3-none-any.whl'
+ wheel_file_name = "simple.dist-0.1-py2.py3-none-any.whl"
wheel_file_path = script.scratch / wheel_file_name
result.did_create(wheel_file_path)
assert "Saved" in result.stdout, result.stdout
-def test_pip_wheel_build_relative_cachedir(script, data):
+def test_pip_wheel_build_relative_cachedir(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test 'pip wheel' builds and caches with a non-absolute cache directory.
"""
result = script.pip(
- 'wheel', '--no-index', '-f', data.find_links,
- '--cache-dir', './cache',
- 'simple==3.0',
+ "wheel",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "--cache-dir",
+ "./cache",
+ "simple==3.0",
)
assert result.returncode == 0
-def test_pip_wheel_builds_when_no_binary_set(script, data):
- data.packages.joinpath('simple-3.0-py2.py3-none-any.whl').touch()
+def test_pip_wheel_builds_when_no_binary_set(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ data.packages.joinpath("simple-3.0-py2.py3-none-any.whl").touch()
# Check that the wheel package is ignored
res = script.pip(
- 'wheel', '--no-index', '--no-binary', ':all:',
- '-f', data.find_links,
- 'simple==3.0')
+ "wheel",
+ "--no-index",
+ "--no-binary",
+ ":all:",
+ "-f",
+ data.find_links,
+ "simple==3.0",
+ )
assert "Building wheel for simple" in str(res), str(res)
@pytest.mark.skipif("sys.platform == 'win32'")
-def test_pip_wheel_readonly_cache(script, data, tmpdir):
+def test_pip_wheel_readonly_cache(
+ script: PipTestEnvironment, data: TestData, tmpdir: Path
+) -> None:
cache_dir = tmpdir / "cache"
cache_dir.mkdir()
os.chmod(cache_dir, 0o400) # read-only cache
# Check that the wheel package is ignored
res = script.pip(
- 'wheel', '--no-index',
- '-f', data.find_links,
- '--cache-dir', cache_dir,
- 'simple==3.0',
+ "wheel",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "--cache-dir",
+ cache_dir,
+ "simple==3.0",
allow_stderr_warning=True,
)
assert res.returncode == 0
assert "The cache has been disabled." in str(res), str(res)
-def test_pip_wheel_builds_editable_deps(script, data):
+def test_pip_wheel_builds_editable_deps(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""
Test 'pip wheel' finds and builds dependencies of editables
"""
- editable_path = os.path.join(data.src, 'requires_simple')
+ editable_path = os.path.join(data.src, "requires_simple")
result = script.pip(
- 'wheel', '--no-index', '-f', data.find_links,
- '-e', editable_path
+ "wheel", "--no-index", "-f", data.find_links, "-e", editable_path
)
- wheel_file_name = f'simple-1.0-py{pyversion[0]}-none-any.whl'
+ wheel_file_name = f"simple-1.0-py{pyversion[0]}-none-any.whl"
wheel_file_path = script.scratch / wheel_file_name
result.did_create(wheel_file_path)
-def test_pip_wheel_builds_editable(script, data):
+def test_pip_wheel_builds_editable(script: PipTestEnvironment, data: TestData) -> None:
"""
Test 'pip wheel' builds an editable package
"""
- editable_path = os.path.join(data.src, 'simplewheel-1.0')
+ editable_path = os.path.join(data.src, "simplewheel-1.0")
result = script.pip(
- 'wheel', '--no-index', '-f', data.find_links,
- '-e', editable_path
+ "wheel", "--no-index", "-f", data.find_links, "-e", editable_path
)
- wheel_file_name = f'simplewheel-1.0-py{pyversion[0]}-none-any.whl'
+ wheel_file_name = f"simplewheel-1.0-py{pyversion[0]}-none-any.whl"
wheel_file_path = script.scratch / wheel_file_name
result.did_create(wheel_file_path)
@pytest.mark.network
-def test_pip_wheel_git_editable_keeps_clone(script, tmpdir):
+def test_pip_wheel_git_editable_keeps_clone(
+ script: PipTestEnvironment, tmpdir: Path
+) -> None:
"""
Test that `pip wheel -e giturl` preserves a git clone in src.
"""
script.pip(
- 'wheel',
- '--no-deps',
- '-e',
- 'git+https://github.com/pypa/pip-test-package#egg=pip-test-package',
- '--src',
- tmpdir / 'src',
- '--wheel-dir',
+ "wheel",
+ "--no-deps",
+ "-e",
+ "git+https://github.com/pypa/pip-test-package#egg=pip-test-package",
+ "--src",
+ tmpdir / "src",
+ "--wheel-dir",
tmpdir,
)
- assert (tmpdir / 'src' / 'pip-test-package').exists()
- assert (tmpdir / 'src' / 'pip-test-package' / '.git').exists()
+ assert (tmpdir / "src" / "pip-test-package").exists()
+ assert (tmpdir / "src" / "pip-test-package" / ".git").exists()
-def test_pip_wheel_builds_editable_does_not_create_zip(script, data, tmpdir):
+def test_pip_wheel_builds_editable_does_not_create_zip(
+ script: PipTestEnvironment, data: TestData, tmpdir: Path
+) -> None:
"""
Test 'pip wheel' of editables does not create zip files
(regression test for issue #9122)
"""
wheel_dir = tmpdir / "wheel_dir"
wheel_dir.mkdir()
- editable_path = os.path.join(data.src, 'simplewheel-1.0')
- script.pip(
- 'wheel', '--no-deps', '-e', editable_path, '-w', wheel_dir
- )
+ editable_path = os.path.join(data.src, "simplewheel-1.0")
+ script.pip("wheel", "--no-deps", "-e", editable_path, "-w", wheel_dir)
wheels = os.listdir(wheel_dir)
assert len(wheels) == 1
assert wheels[0].endswith(".whl")
-def test_pip_wheel_fail(script, data):
+def test_pip_wheel_fail(script: PipTestEnvironment, data: TestData) -> None:
"""
Test 'pip wheel' failure.
"""
result = script.pip(
- 'wheel', '--no-index', '-f', data.find_links,
- 'wheelbroken==0.1',
+ "wheel",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "wheelbroken==0.1",
expect_error=True,
)
- wheel_file_name = f'wheelbroken-0.1-py{pyversion[0]}-none-any.whl'
+ wheel_file_name = f"wheelbroken-0.1-py{pyversion[0]}-none-any.whl"
wheel_file_path = script.scratch / wheel_file_name
result.did_not_create(wheel_file_path)
assert "FakeError" in result.stderr, result.stderr
@@ -217,134 +259,132 @@ def test_pip_wheel_fail(script, data):
assert result.returncode != 0
-@pytest.mark.xfail(
- reason="The --build option was removed"
-)
-def test_no_clean_option_blocks_cleaning_after_wheel(
- script,
- data,
- resolver_variant,
-):
- """
- Test --no-clean option blocks cleaning after wheel build
- """
- build = script.venv_path / 'build'
- result = script.pip(
- 'wheel', '--no-clean', '--no-index', '--build', build,
- f'--find-links={data.find_links}',
- 'simple',
- expect_temp=True,
- # TODO: allow_stderr_warning is used for the --build deprecation,
- # remove it when removing support for --build
- allow_stderr_warning=True,
- )
-
- if resolver_variant == "legacy":
- build = build / 'simple'
- message = f"build/simple should still exist {result}"
- assert exists(build), message
-
-
-def test_pip_wheel_source_deps(script, data):
+def test_pip_wheel_source_deps(script: PipTestEnvironment, data: TestData) -> None:
"""
Test 'pip wheel' finds and builds source archive dependencies
of wheels
"""
# 'requires_source' is a wheel that depends on the 'source' project
result = script.pip(
- 'wheel', '--no-index', '-f', data.find_links,
- 'requires_source',
+ "wheel",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "requires_source",
)
- wheel_file_name = f'source-1.0-py{pyversion[0]}-none-any.whl'
+ wheel_file_name = f"source-1.0-py{pyversion[0]}-none-any.whl"
wheel_file_path = script.scratch / wheel_file_name
result.did_create(wheel_file_path)
assert "Successfully built source" in result.stdout, result.stdout
-def test_wheel_package_with_latin1_setup(script, data):
+def test_wheel_package_with_latin1_setup(
+ script: PipTestEnvironment, data: TestData
+) -> None:
"""Create a wheel from a package with latin-1 encoded setup.py."""
pkg_to_wheel = data.packages.joinpath("SetupPyLatin1")
- result = script.pip('wheel', pkg_to_wheel)
- assert 'Successfully built SetupPyUTF8' in result.stdout
+ result = script.pip("wheel", pkg_to_wheel)
+ assert "Successfully built SetupPyUTF8" in result.stdout
-def test_pip_wheel_with_pep518_build_reqs(script, data, common_wheels):
- result = script.pip('wheel', '--no-index', '-f', data.find_links,
- '-f', common_wheels, 'pep518==3.0',)
- wheel_file_name = f'pep518-3.0-py{pyversion[0]}-none-any.whl'
+def test_pip_wheel_with_pep518_build_reqs(
+ script: PipTestEnvironment, data: TestData, common_wheels: Path
+) -> None:
+ result = script.pip(
+ "wheel",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "-f",
+ common_wheels,
+ "pep518==3.0",
+ )
+ wheel_file_name = f"pep518-3.0-py{pyversion[0]}-none-any.whl"
wheel_file_path = script.scratch / wheel_file_name
result.did_create(wheel_file_path)
assert "Successfully built pep518" in result.stdout, result.stdout
assert "Installing build dependencies" in result.stdout, result.stdout
-def test_pip_wheel_with_pep518_build_reqs_no_isolation(script, data):
- script.pip_install_local('simplewheel==2.0')
+def test_pip_wheel_with_pep518_build_reqs_no_isolation(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ script.pip_install_local("simplewheel==2.0")
result = script.pip(
- 'wheel', '--no-index', '-f', data.find_links,
- '--no-build-isolation', 'pep518==3.0',
+ "wheel",
+ "--no-index",
+ "-f",
+ data.find_links,
+ "--no-build-isolation",
+ "pep518==3.0",
)
- wheel_file_name = f'pep518-3.0-py{pyversion[0]}-none-any.whl'
+ wheel_file_name = f"pep518-3.0-py{pyversion[0]}-none-any.whl"
wheel_file_path = script.scratch / wheel_file_name
result.did_create(wheel_file_path)
assert "Successfully built pep518" in result.stdout, result.stdout
assert "Installing build dependencies" not in result.stdout, result.stdout
-def test_pip_wheel_with_user_set_in_config(script, data, common_wheels):
- config_file = script.scratch_path / 'pip.conf'
- script.environ['PIP_CONFIG_FILE'] = str(config_file)
+def test_pip_wheel_with_user_set_in_config(
+ script: PipTestEnvironment, data: TestData, common_wheels: Path
+) -> None:
+ config_file = script.scratch_path / "pip.conf"
+ script.environ["PIP_CONFIG_FILE"] = str(config_file)
config_file.write_text("[install]\nuser = true")
result = script.pip(
- 'wheel', data.src / 'withpyproject',
- '--no-index', '-f', common_wheels
+ "wheel", data.src / "withpyproject", "--no-index", "-f", common_wheels
)
assert "Successfully built withpyproject" in result.stdout, result.stdout
-@pytest.mark.skipif(sys.platform.startswith('win'),
- reason='The empty extension module does not work on Win')
-def test_pip_wheel_ext_module_with_tmpdir_inside(script, data, common_wheels):
- tmpdir = data.src / 'extension/tmp'
+@pytest.mark.skipif(
+ sys.platform.startswith("win"),
+ reason="The empty extension module does not work on Win",
+)
+def test_pip_wheel_ext_module_with_tmpdir_inside(
+ script: PipTestEnvironment, data: TestData, common_wheels: Path
+) -> None:
+ tmpdir = data.src / "extension/tmp"
tmpdir.mkdir()
- script.environ['TMPDIR'] = str(tmpdir)
+ script.environ["TMPDIR"] = str(tmpdir)
# To avoid a test dependency on a C compiler, we set the env vars to "noop"
# The .c source is empty anyway
- script.environ['CC'] = script.environ['LDSHARED'] = 'true'
+ script.environ["CC"] = script.environ["LDSHARED"] = "true"
result = script.pip(
- 'wheel', data.src / 'extension',
- '--no-index', '-f', common_wheels
+ "wheel", data.src / "extension", "--no-index", "-f", common_wheels
)
assert "Successfully built extension" in result.stdout, result.stdout
@pytest.mark.network
-def test_pep517_wheels_are_not_confused_with_other_files(script, tmpdir, data):
- """Check correct wheels are copied. (#6196)
- """
- pkg_to_wheel = data.src / 'withpyproject'
+def test_pep517_wheels_are_not_confused_with_other_files(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ """Check correct wheels are copied. (#6196)"""
+ pkg_to_wheel = data.src / "withpyproject"
add_files_to_dist_directory(pkg_to_wheel)
- result = script.pip('wheel', pkg_to_wheel, '-w', script.scratch_path)
+ result = script.pip("wheel", pkg_to_wheel, "-w", script.scratch_path)
assert "Installing build dependencies" in result.stdout, result.stdout
- wheel_file_name = f'withpyproject-0.0.1-py{pyversion[0]}-none-any.whl'
+ wheel_file_name = f"withpyproject-0.0.1-py{pyversion[0]}-none-any.whl"
wheel_file_path = script.scratch / wheel_file_name
result.did_create(wheel_file_path)
-def test_legacy_wheels_are_not_confused_with_other_files(script, tmpdir, data):
- """Check correct wheels are copied. (#6196)
- """
- pkg_to_wheel = data.src / 'simplewheel-1.0'
+def test_legacy_wheels_are_not_confused_with_other_files(
+ script: PipTestEnvironment, data: TestData
+) -> None:
+ """Check correct wheels are copied. (#6196)"""
+ pkg_to_wheel = data.src / "simplewheel-1.0"
add_files_to_dist_directory(pkg_to_wheel)
- result = script.pip('wheel', pkg_to_wheel, '-w', script.scratch_path)
+ result = script.pip("wheel", pkg_to_wheel, "-w", script.scratch_path)
assert "Installing build dependencies" not in result.stdout, result.stdout
- wheel_file_name = f'simplewheel-1.0-py{pyversion[0]}-none-any.whl'
+ wheel_file_name = f"simplewheel-1.0-py{pyversion[0]}-none-any.whl"
wheel_file_path = script.scratch / wheel_file_name
result.did_create(wheel_file_path)
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index ddb4bfe4c..7410072f5 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -1,5 +1,6 @@
import json
import os
+import pathlib
import re
import shutil
import site
@@ -11,12 +12,25 @@ from contextlib import contextmanager
from hashlib import sha256
from io import BytesIO
from textwrap import dedent
-from typing import List, Optional
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Mapping,
+ Optional,
+ Tuple,
+ Union,
+ cast,
+)
from zipfile import ZipFile
import pytest
from pip._vendor.packaging.utils import canonicalize_name
-from scripttest import FoundDir, TestFileEnvironment
+from scripttest import FoundDir, FoundFile, ProcResult, TestFileEnvironment
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
@@ -25,50 +39,33 @@ from pip._internal.models.search_scope import SearchScope
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.models.target_python import TargetPython
from pip._internal.network.session import PipSession
-from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX
-from tests.lib.path import Path, curdir
+from tests.lib.venv import VirtualEnvironment
from tests.lib.wheel import make_wheel
-DATA_DIR = Path(__file__).parent.parent.joinpath("data").resolve()
-SRC_DIR = Path(__file__).resolve().parent.parent.parent
+if TYPE_CHECKING:
+ # Literal was introduced in Python 3.8.
+ from typing import Literal
-pyversion = get_major_minor_version()
+ ResolverVariant = Literal["resolvelib", "legacy"]
+else:
+ ResolverVariant = str
-CURRENT_PY_VERSION_INFO = sys.version_info[:3]
+DATA_DIR = pathlib.Path(__file__).parent.parent.joinpath("data").resolve()
+SRC_DIR = pathlib.Path(__file__).resolve().parent.parent.parent
+pyversion = get_major_minor_version()
-def assert_paths_equal(actual, expected):
- assert os.path.normpath(actual) == os.path.normpath(expected)
+CURRENT_PY_VERSION_INFO = sys.version_info[:3]
+_Test = Callable[..., None]
+_FilesState = Dict[str, Union[FoundDir, FoundFile]]
-def path_to_url(path):
- """
- Convert a path to URI. The path will be made absolute and
- will not have quoted path parts.
- (adapted from pip.util)
- """
- path = os.path.normpath(os.path.abspath(path))
- drive, path = os.path.splitdrive(path)
- filepath = path.split(os.path.sep)
- url = "/".join(filepath)
- if drive:
- # Note: match urllib.request.pathname2url's
- # behavior: uppercase the drive letter.
- return "file:///" + drive.upper() + url
- return "file://" + url
-
-
-def _test_path_to_file_url(path):
- """
- Convert a test Path to a "file://" URL.
- Args:
- path: a tests.lib.path.Path object.
- """
- return "file://" + path.resolve().replace("\\", "/")
+def assert_paths_equal(actual: str, expected: str) -> None:
+ assert os.path.normpath(actual) == os.path.normpath(expected)
-def create_file(path, contents=None):
+def create_file(path: str, contents: Optional[str] = None) -> None:
"""Create a file on the path, with the given contents"""
from pip._internal.utils.misc import ensure_dir
@@ -81,23 +78,26 @@ def create_file(path, contents=None):
def make_test_search_scope(
- find_links=None, # type: Optional[List[str]]
- index_urls=None, # type: Optional[List[str]]
-):
+ find_links: Optional[List[str]] = None,
+ index_urls: Optional[List[str]] = None,
+) -> SearchScope:
if find_links is None:
find_links = []
if index_urls is None:
index_urls = []
- return SearchScope.create(find_links=find_links, index_urls=index_urls)
+ return SearchScope.create(
+ find_links=find_links,
+ index_urls=index_urls,
+ no_index=False,
+ )
def make_test_link_collector(
- find_links=None, # type: Optional[List[str]]
- index_urls=None, # type: Optional[List[str]]
- session=None, # type: Optional[PipSession]
-):
- # type: (...) -> LinkCollector
+ find_links: Optional[List[str]] = None,
+ index_urls: Optional[List[str]] = None,
+ session: Optional[PipSession] = None,
+) -> LinkCollector:
"""
Create a LinkCollector object for testing purposes.
"""
@@ -113,13 +113,12 @@ def make_test_link_collector(
def make_test_finder(
- find_links=None, # type: Optional[List[str]]
- index_urls=None, # type: Optional[List[str]]
- allow_all_prereleases=False, # type: bool
- session=None, # type: Optional[PipSession]
- target_python=None, # type: Optional[TargetPython]
-):
- # type: (...) -> PackageFinder
+ find_links: Optional[List[str]] = None,
+ index_urls: Optional[List[str]] = None,
+ allow_all_prereleases: bool = False,
+ session: Optional[PipSession] = None,
+ target_python: Optional[TargetPython] = None,
+) -> PackageFinder:
"""
Create a PackageFinder for testing purposes.
"""
@@ -152,17 +151,23 @@ class TestData:
data into a directory and operating on the copied data.
"""
- def __init__(self, root, source=None):
+ __test__ = False
+
+ def __init__(
+ self,
+ root: pathlib.Path,
+ source: Optional[pathlib.Path] = None,
+ ) -> None:
self.source = source or DATA_DIR
- self.root = Path(root).resolve()
+ self.root = root.resolve()
@classmethod
- def copy(cls, root):
+ def copy(cls, root: pathlib.Path) -> "TestData":
obj = cls(root)
obj.reset()
return obj
- def reset(self):
+ def reset(self) -> None:
# Check explicitly for the target directory to avoid overly-broad
# try/except.
if self.root.exists():
@@ -170,51 +175,51 @@ class TestData:
shutil.copytree(self.source, self.root, symlinks=True)
@property
- def packages(self):
+ def packages(self) -> pathlib.Path:
return self.root.joinpath("packages")
@property
- def packages2(self):
+ def packages2(self) -> pathlib.Path:
return self.root.joinpath("packages2")
@property
- def packages3(self):
+ def packages3(self) -> pathlib.Path:
return self.root.joinpath("packages3")
@property
- def src(self):
+ def src(self) -> pathlib.Path:
return self.root.joinpath("src")
@property
- def indexes(self):
+ def indexes(self) -> pathlib.Path:
return self.root.joinpath("indexes")
@property
- def reqfiles(self):
+ def reqfiles(self) -> pathlib.Path:
return self.root.joinpath("reqfiles")
@property
- def completion_paths(self):
+ def completion_paths(self) -> pathlib.Path:
return self.root.joinpath("completion_paths")
@property
- def find_links(self):
- return path_to_url(self.packages)
+ def find_links(self) -> str:
+ return self.packages.as_uri()
@property
- def find_links2(self):
- return path_to_url(self.packages2)
+ def find_links2(self) -> str:
+ return self.packages2.as_uri()
@property
- def find_links3(self):
- return path_to_url(self.packages3)
+ def find_links3(self) -> str:
+ return self.packages3.as_uri()
@property
- def backends(self):
- return path_to_url(self.root.joinpath("backends"))
+ def backends(self) -> str:
+ return self.root.joinpath("backends").as_uri()
- def index_url(self, index="simple"):
- return path_to_url(self.root.joinpath("indexes", index))
+ def index_url(self, index: str = "simple") -> str:
+ return self.root.joinpath("indexes", index).as_uri()
class TestFailure(AssertionError):
@@ -222,11 +227,39 @@ class TestFailure(AssertionError):
An "assertion" failed during testing.
"""
- pass
+
+StrPath = Union[str, pathlib.Path]
+
+
+class FoundFiles(Mapping[StrPath, FoundFile]):
+ def __init__(self, paths: Mapping[str, FoundFile]) -> None:
+ self._paths = {pathlib.Path(k): v for k, v in paths.items()}
+
+ def __contains__(self, o: object) -> bool:
+ if isinstance(o, pathlib.Path):
+ return o in self._paths
+ elif isinstance(o, str):
+ return pathlib.Path(o) in self._paths
+ return False
+
+ def __len__(self) -> int:
+ return len(self._paths)
+
+ def __getitem__(self, k: StrPath) -> FoundFile:
+ if isinstance(k, pathlib.Path):
+ return self._paths[k]
+ elif isinstance(k, str):
+ return self._paths[pathlib.Path(k)]
+ raise KeyError(k)
+
+ def __iter__(self) -> Iterator[pathlib.Path]:
+ return iter(self._paths)
class TestPipResult:
- def __init__(self, impl, verbose=False):
+ __test__ = False
+
+ def __init__(self, impl: ProcResult, verbose: bool = False) -> None:
self._impl = impl
if verbose:
@@ -236,38 +269,50 @@ class TestPipResult:
print(self.stderr)
print("=======================")
- def __getattr__(self, attr):
+ def __getattr__(self, attr: str) -> Any:
return getattr(self._impl, attr)
if sys.platform == "win32":
@property
- def stdout(self):
+ def stdout(self) -> str:
return self._impl.stdout.replace("\r\n", "\n")
@property
- def stderr(self):
+ def stderr(self) -> str:
return self._impl.stderr.replace("\r\n", "\n")
- def __str__(self):
+ def __str__(self) -> str:
return str(self._impl).replace("\r\n", "\n")
else:
# Python doesn't automatically forward __str__ through __getattr__
- def __str__(self):
+ def __str__(self) -> str:
return str(self._impl)
+ @property
+ def files_created(self) -> FoundFiles:
+ return FoundFiles(self._impl.files_created)
+
+ @property
+ def files_updated(self) -> FoundFiles:
+ return FoundFiles(self._impl.files_updated)
+
+ @property
+ def files_deleted(self) -> FoundFiles:
+ return FoundFiles(self._impl.files_deleted)
+
def assert_installed(
self,
- pkg_name,
- editable=True,
- with_files=None,
- without_files=None,
- without_egg_link=False,
- use_user_site=False,
- sub_dir=False,
- ):
+ pkg_name: str,
+ editable: bool = True,
+ with_files: Optional[List[str]] = None,
+ without_files: Optional[List[str]] = None,
+ without_egg_link: bool = False,
+ use_user_site: bool = False,
+ sub_dir: Optional[str] = None,
+ ) -> None:
with_files = with_files or []
without_files = without_files or []
e = self.test_env
@@ -282,9 +327,9 @@ class TestPipResult:
pkg_dir = e.site_packages / pkg_name
if use_user_site:
- egg_link_path = e.user_site / pkg_name + ".egg-link"
+ egg_link_path = e.user_site / f"{pkg_name}.egg-link"
else:
- egg_link_path = e.site_packages / pkg_name + ".egg-link"
+ egg_link_path = e.site_packages / f"{pkg_name}.egg-link"
if without_egg_link:
if egg_link_path in self.files_created:
@@ -303,9 +348,9 @@ class TestPipResult:
# FIXME: I don't understand why there's a trailing . here
if not (
egg_link_contents.endswith("\n.")
- and egg_link_contents[:-2].endswith(pkg_dir)
+ and egg_link_contents[:-2].endswith(os.fspath(pkg_dir))
):
- expected_ending = pkg_dir + "\n."
+ expected_ending = f"{pkg_dir}\n."
raise TestFailure(
textwrap.dedent(
f"""
@@ -327,9 +372,9 @@ class TestPipResult:
maybe = "" if without_egg_link else "not "
raise TestFailure(f"{pth_file} unexpectedly {maybe}updated by install")
- if (pkg_dir in self.files_created) == (curdir in without_files):
- maybe = "not " if curdir in without_files else ""
- files = sorted(self.files_created)
+ if (pkg_dir in self.files_created) == (os.curdir in without_files):
+ maybe = "not " if os.curdir in without_files else ""
+ files = sorted(p.as_posix() for p in self.files_created)
raise TestFailure(
textwrap.dedent(
f"""
@@ -354,20 +399,20 @@ class TestPipResult:
f"Package directory {pkg_dir!r} has unexpected content {f}"
)
- def did_create(self, path, message=None):
- assert str(path) in self.files_created, _one_or_both(message, self)
+ def did_create(self, path: StrPath, message: Optional[str] = None) -> None:
+ assert path in self.files_created, _one_or_both(message, self)
- def did_not_create(self, path, message=None):
- assert str(path) not in self.files_created, _one_or_both(message, self)
+ def did_not_create(self, p: StrPath, message: Optional[str] = None) -> None:
+ assert p not in self.files_created, _one_or_both(message, self)
- def did_update(self, path, message=None):
- assert str(path) in self.files_updated, _one_or_both(message, self)
+ def did_update(self, path: StrPath, message: Optional[str] = None) -> None:
+ assert path in self.files_updated, _one_or_both(message, self)
- def did_not_update(self, path, message=None):
- assert str(path) not in self.files_updated, _one_or_both(message, self)
+ def did_not_update(self, p: StrPath, message: Optional[str] = None) -> None:
+ assert p not in self.files_updated, _one_or_both(message, self)
-def _one_or_both(a, b):
+def _one_or_both(a: Optional[str], b: Any) -> str:
"""Returns f"{a}\n{b}" if a is truthy, else returns str(b)."""
if not a:
return str(b)
@@ -375,7 +420,7 @@ def _one_or_both(a, b):
return f"{a}\n{b}"
-def make_check_stderr_message(stderr, line, reason):
+def make_check_stderr_message(stderr: str, line: str, reason: str) -> str:
"""
Create an exception message to use inside check_stderr().
"""
@@ -389,10 +434,10 @@ def make_check_stderr_message(stderr, line, reason):
def _check_stderr(
- stderr,
- allow_stderr_warning,
- allow_stderr_error,
-):
+ stderr: str,
+ allow_stderr_warning: bool,
+ allow_stderr_error: bool,
+) -> None:
"""
Check the given stderr for logged warnings and errors.
@@ -405,6 +450,7 @@ def _check_stderr(
lines = stderr.splitlines()
for line in lines:
+ line = line.lstrip()
# First check for logging errors, which we don't allow during
# tests even if allow_stderr_error=True (since a logging error
# would signal a bug in pip's code).
@@ -431,7 +477,7 @@ def _check_stderr(
if allow_stderr_warning:
continue
- if line.startswith("WARNING: ") or line.startswith(DEPRECATION_MSG_PREFIX):
+ if line.startswith("WARNING: "):
reason = (
"stderr has an unexpected warning "
"(pass allow_stderr_warning=True to permit this)"
@@ -457,28 +503,31 @@ class PipTestEnvironment(TestFileEnvironment):
exe = sys.platform == "win32" and ".exe" or ""
verbose = False
- def __init__(self, base_path, *args, virtualenv, pip_expect_warning=None, **kwargs):
- # Make our base_path a test.lib.path.Path object
- base_path = Path(base_path)
-
+ def __init__(
+ self,
+ base_path: pathlib.Path,
+ *args: Any,
+ virtualenv: VirtualEnvironment,
+ pip_expect_warning: bool = False,
+ zipapp: Optional[str] = None,
+ **kwargs: Any,
+ ) -> None:
# Store paths related to the virtual environment
self.venv_path = virtualenv.location
self.lib_path = virtualenv.lib
self.site_packages_path = virtualenv.site
self.bin_path = virtualenv.bin
+ assert site.USER_BASE is not None
+ assert site.USER_SITE is not None
+
self.user_base_path = self.venv_path.joinpath("user")
self.user_site_path = self.venv_path.joinpath(
"user",
site.USER_SITE[len(site.USER_BASE) + 1 :],
)
if sys.platform == "win32":
- if sys.version_info >= (3, 5):
- scripts_base = Path(
- os.path.normpath(self.user_site_path.joinpath(".."))
- )
- else:
- scripts_base = self.user_base_path
+ scripts_base = self.user_site_path.joinpath("..").resolve()
self.user_bin_path = scripts_base.joinpath("Scripts")
else:
self.user_bin_path = self.user_base_path.joinpath(
@@ -494,8 +543,8 @@ class PipTestEnvironment(TestFileEnvironment):
# Setup our environment
environ = kwargs.setdefault("environ", os.environ.copy())
- environ["PATH"] = Path.pathsep.join(
- [self.bin_path] + [environ.get("PATH", [])],
+ environ["PATH"] = os.pathsep.join(
+ [os.fspath(self.bin_path), environ.get("PATH", "")],
)
environ["PYTHONUSERBASE"] = self.user_base_path
# Writing bytecode can mess up updated file detection
@@ -507,6 +556,9 @@ class PipTestEnvironment(TestFileEnvironment):
# (useful for Python version deprecation)
self.pip_expect_warning = pip_expect_warning
+ # The name of an (optional) zipapp to use when running pip
+ self.zipapp = zipapp
+
# Call the TestFileEnvironment __init__
super().__init__(base_path, *args, **kwargs)
@@ -523,13 +575,13 @@ class PipTestEnvironment(TestFileEnvironment):
"scratch",
]:
real_name = f"{name}_path"
- relative_path = Path(
+ relative_path = pathlib.Path(
os.path.relpath(getattr(self, real_name), self.base_path)
)
setattr(self, name, relative_path)
# Make sure temp_path is a Path object
- self.temp_path = Path(self.temp_path)
+ self.temp_path: pathlib.Path = pathlib.Path(self.temp_path)
# Ensure the tmp dir exists, things break horribly if it doesn't
self.temp_path.mkdir()
@@ -538,14 +590,18 @@ class PipTestEnvironment(TestFileEnvironment):
self.user_site_path.mkdir(parents=True)
self.user_site_path.joinpath("easy-install.pth").touch()
- def _ignore_file(self, fn):
+ def _ignore_file(self, fn: str) -> bool:
if fn.endswith("__pycache__") or fn.endswith(".pyc"):
result = True
+ elif self.zipapp and fn.endswith("cacert.pem"):
+ # Temporary copies of cacert.pem are extracted
+ # when running from a zipapp
+ result = True
else:
result = super()._ignore_file(fn)
return result
- def _find_traverse(self, path, result):
+ def _find_traverse(self, path: str, result: Dict[str, FoundDir]) -> None:
# Ignore symlinked directories to avoid duplicates in `run()`
# results because of venv `lib64 -> lib/` symlink on Linux.
full = os.path.join(self.base_path, path)
@@ -557,14 +613,13 @@ class PipTestEnvironment(TestFileEnvironment):
def run(
self,
- *args,
- cwd=None,
- run_from=None,
- allow_stderr_error=None,
- allow_stderr_warning=None,
- allow_error=None,
- **kw,
- ):
+ *args: str,
+ cwd: Optional[StrPath] = None,
+ allow_stderr_error: Optional[bool] = None,
+ allow_stderr_warning: Optional[bool] = None,
+ allow_error: bool = False,
+ **kw: Any,
+ ) -> TestPipResult:
"""
:param allow_stderr_error: whether a logged error is allowed in
stderr. Passing True for this argument implies
@@ -587,11 +642,10 @@ class PipTestEnvironment(TestFileEnvironment):
if self.verbose:
print(f">> running {args} {kw}")
- assert not cwd or not run_from, "Don't use run_from; it's going away"
- cwd = cwd or run_from or self.cwd
+ cwd = cwd or self.cwd
if sys.platform == "win32":
# Partial fix for ScriptTest.run using `shell=True` on Windows.
- args = [str(a).replace("^", "^^").replace("&", "^&") for a in args]
+ args = tuple(str(a).replace("^", "^^").replace("&", "^&") for a in args)
if allow_error:
kw["expect_error"] = True
@@ -635,7 +689,7 @@ class PipTestEnvironment(TestFileEnvironment):
if expect_error and not allow_error:
if result.returncode == 0:
__tracebackhide__ = True
- raise AssertionError("Script passed unexpectedly.")
+ raise AssertionError(f"Script passed unexpectedly:\n{result}")
_check_stderr(
result.stderr,
@@ -645,32 +699,44 @@ class PipTestEnvironment(TestFileEnvironment):
return TestPipResult(result, verbose=self.verbose)
- def pip(self, *args, use_module=True, **kwargs):
+ def pip(
+ self,
+ *args: StrPath,
+ use_module: bool = True,
+ **kwargs: Any,
+ ) -> TestPipResult:
__tracebackhide__ = True
if self.pip_expect_warning:
kwargs["allow_stderr_warning"] = True
- if use_module:
+ if self.zipapp:
+ exe = "python"
+ args = (self.zipapp,) + args
+ elif use_module:
exe = "python"
args = ("-m", "pip") + args
else:
exe = "pip"
- return self.run(exe, *args, **kwargs)
+ return self.run(exe, *(os.fspath(a) for a in args), **kwargs)
- def pip_install_local(self, *args, **kwargs):
+ def pip_install_local(
+ self,
+ *args: StrPath,
+ **kwargs: Any,
+ ) -> TestPipResult:
return self.pip(
"install",
"--no-index",
"--find-links",
- path_to_url(os.path.join(DATA_DIR, "packages")),
+ pathlib.Path(DATA_DIR, "packages").as_uri(),
*args,
**kwargs,
)
- def easy_install(self, *args, **kwargs):
+ def easy_install(self, *args: str, **kwargs: Any) -> TestPipResult:
args = ("-m", "easy_install") + args
return self.run("python", *args, **kwargs)
- def assert_installed(self, **kwargs):
+ def assert_installed(self, **kwargs: str) -> None:
ret = self.pip("list", "--format=json")
installed = set(
(canonicalize_name(val["name"]), val["version"])
@@ -679,7 +745,7 @@ class PipTestEnvironment(TestFileEnvironment):
expected = set((canonicalize_name(k), v) for k, v in kwargs.items())
assert expected <= installed, "{!r} not all in {!r}".format(expected, installed)
- def assert_not_installed(self, *args):
+ def assert_not_installed(self, *args: str) -> None:
ret = self.pip("list", "--format=json")
installed = set(
canonicalize_name(val["name"]) for val in json.loads(ret.stdout)
@@ -695,7 +761,9 @@ class PipTestEnvironment(TestFileEnvironment):
# FIXME ScriptTest does something similar, but only within a single
# ProcResult; this generalizes it so states can be compared across
# multiple commands. Maybe should be rolled into ScriptTest?
-def diff_states(start, end, ignore=None):
+def diff_states(
+ start: _FilesState, end: _FilesState, ignore: Iterable[StrPath] = ()
+) -> Dict[str, _FilesState]:
"""
Differences two "filesystem states" as represented by dictionaries
of FoundFile and FoundDir objects.
@@ -719,9 +787,9 @@ def diff_states(start, end, ignore=None):
size are considered.
"""
- ignore = ignore or []
- def prefix_match(path, prefix):
+ def prefix_match(path: str, prefix_path: StrPath) -> bool:
+ prefix = os.fspath(prefix_path)
if path == prefix:
return True
prefix = prefix.rstrip(os.path.sep) + os.path.sep
@@ -740,7 +808,11 @@ def diff_states(start, end, ignore=None):
return dict(deleted=deleted, created=created, updated=updated)
-def assert_all_changes(start_state, end_state, expected_changes):
+def assert_all_changes(
+ start_state: Union[_FilesState, TestPipResult],
+ end_state: Union[_FilesState, TestPipResult],
+ expected_changes: List[StrPath],
+) -> Dict[str, _FilesState]:
"""
Fails if anything changed that isn't listed in the
expected_changes.
@@ -761,6 +833,8 @@ def assert_all_changes(start_state, end_state, expected_changes):
start_files = start_state.files_before
if isinstance(end_state, TestPipResult):
end_files = end_state.files_after
+ start_files = cast(_FilesState, start_files)
+ end_files = cast(_FilesState, end_files)
diff = diff_states(start_files, end_files, ignore=expected_changes)
if list(diff.values()) != [{}, {}, {}]:
@@ -773,7 +847,11 @@ def assert_all_changes(start_state, end_state, expected_changes):
return diff
-def _create_main_file(dir_path, name=None, output=None):
+def _create_main_file(
+ dir_path: pathlib.Path,
+ name: Optional[str] = None,
+ output: Optional[str] = None,
+) -> None:
"""
Create a module with a main() function that prints the given output.
"""
@@ -792,12 +870,12 @@ def _create_main_file(dir_path, name=None, output=None):
def _git_commit(
- env_or_script,
- repo_dir,
- message=None,
- allow_empty=False,
- stage_modified=False,
-):
+ env_or_script: PipTestEnvironment,
+ repo_dir: StrPath,
+ message: Optional[str] = None,
+ allow_empty: bool = False,
+ stage_modified: bool = False,
+) -> None:
"""
Run git-commit.
@@ -829,57 +907,67 @@ def _git_commit(
env_or_script.run(*new_args, cwd=repo_dir)
-def _vcs_add(script, version_pkg_path, vcs="git"):
+def _vcs_add(
+ location: pathlib.Path,
+ version_pkg_path: pathlib.Path,
+ vcs: str = "git",
+) -> pathlib.Path:
if vcs == "git":
- script.run("git", "init", cwd=version_pkg_path)
- script.run("git", "add", ".", cwd=version_pkg_path)
- _git_commit(script, version_pkg_path, message="initial version")
+ subprocess.check_call(["git", "init"], cwd=os.fspath(version_pkg_path))
+ subprocess.check_call(["git", "add", "."], cwd=os.fspath(version_pkg_path))
+ subprocess.check_call(
+ ["git", "commit", "-m", "initial version"], cwd=os.fspath(version_pkg_path)
+ )
elif vcs == "hg":
- script.run("hg", "init", cwd=version_pkg_path)
- script.run("hg", "add", ".", cwd=version_pkg_path)
- script.run(
- "hg",
- "commit",
- "-q",
- "--user",
- "pip <distutils-sig@python.org>",
- "-m",
- "initial version",
- cwd=version_pkg_path,
+ subprocess.check_call(["hg", "init"], cwd=os.fspath(version_pkg_path))
+ subprocess.check_call(["hg", "add", "."], cwd=os.fspath(version_pkg_path))
+ subprocess.check_call(
+ [
+ "hg",
+ "commit",
+ "-q",
+ "--user",
+ "pip <distutils-sig@python.org>",
+ "-m",
+ "initial version",
+ ],
+ cwd=os.fspath(version_pkg_path),
)
elif vcs == "svn":
- repo_url = _create_svn_repo(script, version_pkg_path)
- script.run(
- "svn", "checkout", repo_url, "pip-test-package", cwd=script.scratch_path
+ repo_url = _create_svn_repo(location, version_pkg_path)
+ subprocess.check_call(
+ ["svn", "checkout", repo_url, "pip-test-package"], cwd=os.fspath(location)
)
- checkout_path = script.scratch_path / "pip-test-package"
-
- # svn internally stores windows drives as uppercase; we'll match that.
- checkout_path = checkout_path.replace("c:", "C:")
+ checkout_path = location / "pip-test-package"
version_pkg_path = checkout_path
elif vcs == "bazaar":
- script.run("bzr", "init", cwd=version_pkg_path)
- script.run("bzr", "add", ".", cwd=version_pkg_path)
- script.run(
- "bzr", "whoami", "pip <distutils-sig@python.org>", cwd=version_pkg_path
+ subprocess.check_call(["bzr", "init"], cwd=os.fspath(version_pkg_path))
+ subprocess.check_call(["bzr", "add", "."], cwd=os.fspath(version_pkg_path))
+ subprocess.check_call(
+ ["bzr", "whoami", "pip <distutils-sig@python.org>"],
+ cwd=os.fspath(version_pkg_path),
)
- script.run(
- "bzr",
- "commit",
- "-q",
- "--author",
- "pip <distutils-sig@python.org>",
- "-m",
- "initial version",
- cwd=version_pkg_path,
+ subprocess.check_call(
+ [
+ "bzr",
+ "commit",
+ "-q",
+ "--author",
+ "pip <distutils-sig@python.org>",
+ "-m",
+ "initial version",
+ ],
+ cwd=os.fspath(version_pkg_path),
)
else:
raise ValueError(f"Unknown vcs: {vcs}")
return version_pkg_path
-def _create_test_package_with_subdirectory(script, subdirectory):
+def _create_test_package_with_subdirectory(
+ script: PipTestEnvironment, subdirectory: str
+) -> pathlib.Path:
script.scratch_path.joinpath("version_pkg").mkdir()
version_pkg_path = script.scratch_path / "version_pkg"
_create_main_file(version_pkg_path, name="version_pkg", output="0.1")
@@ -926,9 +1014,11 @@ def _create_test_package_with_subdirectory(script, subdirectory):
return version_pkg_path
-def _create_test_package_with_srcdir(script, name="version_pkg", vcs="git"):
- script.scratch_path.joinpath(name).mkdir()
- version_pkg_path = script.scratch_path / name
+def _create_test_package_with_srcdir(
+ dir_path: pathlib.Path, name: str = "version_pkg", vcs: str = "git"
+) -> pathlib.Path:
+ dir_path.joinpath(name).mkdir()
+ version_pkg_path = dir_path / name
subdir_path = version_pkg_path.joinpath("subdir")
subdir_path.mkdir()
src_path = subdir_path.joinpath("src")
@@ -951,12 +1041,14 @@ def _create_test_package_with_srcdir(script, name="version_pkg", vcs="git"):
)
)
)
- return _vcs_add(script, version_pkg_path, vcs)
+ return _vcs_add(dir_path, version_pkg_path, vcs)
-def _create_test_package(script, name="version_pkg", vcs="git"):
- script.scratch_path.joinpath(name).mkdir()
- version_pkg_path = script.scratch_path / name
+def _create_test_package(
+ dir_path: pathlib.Path, name: str = "version_pkg", vcs: str = "git"
+) -> pathlib.Path:
+ dir_path.joinpath(name).mkdir()
+ version_pkg_path = dir_path / name
_create_main_file(version_pkg_path, name=name, output="0.1")
version_pkg_path.joinpath("setup.py").write_text(
textwrap.dedent(
@@ -974,25 +1066,31 @@ def _create_test_package(script, name="version_pkg", vcs="git"):
)
)
)
- return _vcs_add(script, version_pkg_path, vcs)
-
-
-def _create_svn_repo(script, version_pkg_path):
- repo_url = path_to_url(script.scratch_path / "pip-test-package-repo" / "trunk")
- script.run("svnadmin", "create", "pip-test-package-repo", cwd=script.scratch_path)
- script.run(
- "svn",
- "import",
- version_pkg_path,
- repo_url,
- "-m",
- "Initial import of pip-test-package",
- cwd=script.scratch_path,
+ return _vcs_add(dir_path, version_pkg_path, vcs)
+
+
+def _create_svn_repo(repo_path: pathlib.Path, version_pkg_path: StrPath) -> str:
+ repo_url = repo_path.joinpath("pip-test-package-repo", "trunk").as_uri()
+ subprocess.check_call(
+ "svnadmin create pip-test-package-repo".split(), cwd=repo_path
+ )
+ subprocess.check_call(
+ [
+ "svn",
+ "import",
+ os.fspath(version_pkg_path),
+ repo_url,
+ "-m",
+ "Initial import of pip-test-package",
+ ],
+ cwd=os.fspath(repo_path),
)
return repo_url
-def _change_test_package_version(script, version_pkg_path):
+def _change_test_package_version(
+ script: PipTestEnvironment, version_pkg_path: pathlib.Path
+) -> None:
_create_main_file(
version_pkg_path, name="version_pkg", output="some different version"
)
@@ -1000,21 +1098,8 @@ def _change_test_package_version(script, version_pkg_path):
_git_commit(script, version_pkg_path, message="messed version", stage_modified=True)
-def assert_raises_regexp(exception, reg, run, *args, **kwargs):
- """Like assertRaisesRegexp in unittest"""
- __tracebackhide__ = True
-
- try:
- run(*args, **kwargs)
- assert False, f"{exception} should have been thrown"
- except exception:
- e = sys.exc_info()[1]
- p = re.compile(reg)
- assert p.search(str(e)), str(e)
-
-
@contextmanager
-def requirements_file(contents, tmpdir):
+def requirements_file(contents: str, tmpdir: pathlib.Path) -> Iterator[pathlib.Path]:
"""Return a Path to a requirements file of given contents.
As long as the context manager is open, the requirements file will exist.
@@ -1028,7 +1113,9 @@ def requirements_file(contents, tmpdir):
path.unlink()
-def create_test_package_with_setup(script, **setup_kwargs):
+def create_test_package_with_setup(
+ script: PipTestEnvironment, **setup_kwargs: Any
+) -> pathlib.Path:
assert "name" in setup_kwargs, setup_kwargs
pkg_path = script.scratch_path / setup_kwargs["name"]
pkg_path.mkdir()
@@ -1044,17 +1131,15 @@ def create_test_package_with_setup(script, **setup_kwargs):
return pkg_path
-def urlsafe_b64encode_nopad(data):
- # type: (bytes) -> str
+def urlsafe_b64encode_nopad(data: bytes) -> str:
return urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
-def create_really_basic_wheel(name, version):
- # type: (str, str) -> bytes
- def digest(contents):
+def create_really_basic_wheel(name: str, version: str) -> bytes:
+ def digest(contents: bytes) -> str:
return "sha256={}".format(urlsafe_b64encode_nopad(sha256(contents).digest()))
- def add_file(path, text):
+ def add_file(path: str, text: str) -> None:
contents = text.encode("utf-8")
z.writestr(path, contents)
records.append((path, digest(contents), str(len(contents))))
@@ -1083,14 +1168,14 @@ def create_really_basic_wheel(name, version):
def create_basic_wheel_for_package(
- script,
- name,
- version,
- depends=None,
- extras=None,
- requires_python=None,
- extra_files=None,
-):
+ script: PipTestEnvironment,
+ name: str,
+ version: str,
+ depends: Optional[List[str]] = None,
+ extras: Optional[Dict[str, List[str]]] = None,
+ requires_python: Optional[str] = None,
+ extra_files: Optional[Dict[str, Union[bytes, str]]] = None,
+) -> pathlib.Path:
if depends is None:
depends = []
if extras is None:
@@ -1120,7 +1205,7 @@ def create_basic_wheel_for_package(
for package in packages
]
- metadata_updates = {
+ metadata_updates: Dict[str, Any] = {
"Provides-Extra": list(extras),
"Requires-Dist": requires_dist,
}
@@ -1142,22 +1227,49 @@ def create_basic_wheel_for_package(
return archive_path
-def create_basic_sdist_for_package(script, name, version, extra_files=None):
+def create_basic_sdist_for_package(
+ script: PipTestEnvironment,
+ name: str,
+ version: str,
+ extra_files: Optional[Dict[str, str]] = None,
+ *,
+ fails_egg_info: bool = False,
+ fails_bdist_wheel: bool = False,
+ depends: Optional[List[str]] = None,
+ setup_py_prelude: str = "",
+) -> pathlib.Path:
files = {
- "setup.py": """
+ "setup.py": textwrap.dedent(
+ """\
+ import sys
from setuptools import find_packages, setup
- setup(name={name!r}, version={version!r})
- """,
+
+ {setup_py_prelude}
+
+ fails_bdist_wheel = {fails_bdist_wheel!r}
+ fails_egg_info = {fails_egg_info!r}
+
+ if fails_egg_info and "egg_info" in sys.argv:
+ raise Exception("Simulated failure for generating metadata.")
+
+ if fails_bdist_wheel and "bdist_wheel" in sys.argv:
+ raise Exception("Simulated failure for building a wheel.")
+
+ setup(name={name!r}, version={version!r},
+ install_requires={depends!r})
+ """
+ ).format(
+ name=name,
+ version=version,
+ depends=depends or [],
+ setup_py_prelude=setup_py_prelude,
+ fails_bdist_wheel=fails_bdist_wheel,
+ fails_egg_info=fails_egg_info,
+ ),
}
# Some useful shorthands
- archive_name = "{name}-{version}.tar.gz".format(name=name, version=version)
-
- # Replace key-values with formatted values
- for key, value in list(files.items()):
- del files[key]
- key = key.format(name=name)
- files[key] = textwrap.dedent(value).format(name=name, version=version).strip()
+ archive_name = f"{name}-{version}.tar.gz"
# Add new files after formatting
if extra_files:
@@ -1170,7 +1282,7 @@ def create_basic_sdist_for_package(script, name, version, extra_files=None):
retval = script.scratch_path / archive_name
generated = shutil.make_archive(
- retval,
+ os.fspath(retval),
"gztar",
root_dir=script.temp_path,
base_dir=os.curdir,
@@ -1183,8 +1295,8 @@ def create_basic_sdist_for_package(script, name, version, extra_files=None):
return retval
-def need_executable(name, check_cmd):
- def wrapper(fn):
+def need_executable(name: str, check_cmd: Tuple[str, ...]) -> Callable[[_Test], _Test]:
+ def wrapper(fn: _Test) -> _Test:
try:
subprocess.check_output(check_cmd)
except (OSError, subprocess.CalledProcessError):
@@ -1194,7 +1306,7 @@ def need_executable(name, check_cmd):
return wrapper
-def is_bzr_installed():
+def is_bzr_installed() -> bool:
try:
subprocess.check_output(("bzr", "version", "--short"))
except OSError:
@@ -1202,7 +1314,7 @@ def is_bzr_installed():
return True
-def is_svn_installed():
+def is_svn_installed() -> bool:
try:
subprocess.check_output(("svn", "--version"))
except OSError:
@@ -1210,11 +1322,11 @@ def is_svn_installed():
return True
-def need_bzr(fn):
+def need_bzr(fn: _Test) -> _Test:
return pytest.mark.bzr(need_executable("Bazaar", ("bzr", "version", "--short"))(fn))
-def need_svn(fn):
+def need_svn(fn: _Test) -> _Test:
return pytest.mark.svn(
need_executable("Subversion", ("svn", "--version"))(
need_executable("Subversion Admin", ("svnadmin", "--version"))(fn)
@@ -1222,5 +1334,5 @@ def need_svn(fn):
)
-def need_mercurial(fn):
+def need_mercurial(fn: _Test) -> _Test:
return pytest.mark.mercurial(need_executable("Mercurial", ("hg", "version"))(fn))
diff --git a/tests/lib/certs.py b/tests/lib/certs.py
index 6d69395b2..54b484ac0 100644
--- a/tests/lib/certs.py
+++ b/tests/lib/certs.py
@@ -8,8 +8,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
-def make_tls_cert(hostname):
- # type: (str) -> Tuple[x509.Certificate, rsa.RSAPrivateKey]
+def make_tls_cert(hostname: str) -> Tuple[x509.Certificate, rsa.RSAPrivateKey]:
key = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend()
)
@@ -35,8 +34,7 @@ def make_tls_cert(hostname):
return cert, key
-def serialize_key(key):
- # type: (rsa.RSAPrivateKey) -> bytes
+def serialize_key(key: rsa.RSAPrivateKey) -> bytes:
return key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
@@ -44,6 +42,5 @@ def serialize_key(key):
)
-def serialize_cert(cert):
- # type: (x509.Certificate) -> bytes
+def serialize_cert(cert: x509.Certificate) -> bytes:
return cert.public_bytes(serialization.Encoding.PEM)
diff --git a/tests/lib/compat.py b/tests/lib/compat.py
index ab0ba287d..4d44cbddb 100644
--- a/tests/lib/compat.py
+++ b/tests/lib/compat.py
@@ -1,10 +1,12 @@
+# mypy: no-warn-unused-ignores
+
import contextlib
-from typing import Iterator
+import signal
+from typing import Iterable, Iterator
@contextlib.contextmanager
-def nullcontext():
- # type: () -> Iterator[None]
+def nullcontext() -> Iterator[None]:
"""
Context manager that does no additional processing.
@@ -19,3 +21,34 @@ def nullcontext():
support.
"""
yield
+
+
+# Applies on Windows.
+if not hasattr(signal, "pthread_sigmask"):
+ # We're not relying on this behavior anywhere currently, it's just best
+ # practice.
+ blocked_signals = nullcontext
+else:
+
+ @contextlib.contextmanager
+ def blocked_signals() -> Iterator[None]:
+ """Block all signals for e.g. starting a worker thread."""
+ # valid_signals() was added in Python 3.8 (and not using it results
+ # in a warning on pthread_sigmask() call)
+ mask: Iterable[int]
+ try:
+ mask = signal.valid_signals()
+ except AttributeError:
+ mask = set(range(1, signal.NSIG))
+
+ old_mask = signal.pthread_sigmask( # type: ignore[attr-defined]
+ signal.SIG_SETMASK, # type: ignore[attr-defined]
+ mask,
+ )
+ try:
+ yield
+ finally:
+ signal.pthread_sigmask( # type: ignore[attr-defined]
+ signal.SIG_SETMASK, # type: ignore[attr-defined]
+ old_mask,
+ )
diff --git a/tests/lib/configuration_helpers.py b/tests/lib/configuration_helpers.py
index 5b20aafa1..67f75e8e7 100644
--- a/tests/lib/configuration_helpers.py
+++ b/tests/lib/configuration_helpers.py
@@ -6,39 +6,42 @@ import functools
import os
import tempfile
import textwrap
+from typing import Any, Dict, Iterator
import pip._internal.configuration
from pip._internal.utils.misc import ensure_dir
# This is so that tests don't need to import pip._internal.configuration.
+Kind = pip._internal.configuration.Kind
kinds = pip._internal.configuration.kinds
class ConfigurationMixin:
- def setup(self):
+ def setup(self) -> None:
self.configuration = pip._internal.configuration.Configuration(
isolated=False,
)
- self._files_to_clear = []
- def teardown(self):
- for fname in self._files_to_clear:
- fname.stop()
-
- def patch_configuration(self, variant, di):
+ def patch_configuration(self, variant: Kind, di: Dict[str, Any]) -> None:
old = self.configuration._load_config_files
@functools.wraps(old)
- def overridden():
+ def overridden() -> None:
# Manual Overload
self.configuration._config[variant].update(di)
- self.configuration._parsers[variant].append((None, None))
- return old()
+ # Configuration._parsers has type:
+ # Dict[Kind, List[Tuple[str, RawConfigParser]]].
+ # As a testing convenience, pass a special value.
+ self.configuration._parsers[variant].append(
+ (None, None), # type: ignore[arg-type]
+ )
+ old()
- self.configuration._load_config_files = overridden
+ # https://github.com/python/mypy/issues/2427
+ self.configuration._load_config_files = overridden # type: ignore[assignment]
@contextlib.contextmanager
- def tmpfile(self, contents):
+ def tmpfile(self, contents: str) -> Iterator[str]:
# Create a temporary file
fd, path = tempfile.mkstemp(prefix="pip_", suffix="_config.ini", text=True)
os.close(fd)
@@ -51,8 +54,3 @@ class ConfigurationMixin:
yield path
os.remove(path)
-
- @staticmethod
- def get_file_contents(path):
- with open(path) as f:
- return f.read()
diff --git a/tests/lib/direct_url.py b/tests/lib/direct_url.py
index 497e10c6b..e0dac0320 100644
--- a/tests/lib/direct_url.py
+++ b/tests/lib/direct_url.py
@@ -1,15 +1,25 @@
+import os
import re
+from pathlib import Path
+from typing import Optional
from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl
+from tests.lib import TestPipResult
-def get_created_direct_url(result, pkg):
+def get_created_direct_url_path(result: TestPipResult, pkg: str) -> Optional[Path]:
direct_url_metadata_re = re.compile(
pkg + r"-[\d\.]+\.dist-info." + DIRECT_URL_METADATA_NAME + r"$"
)
for filename in result.files_created:
- if direct_url_metadata_re.search(filename):
- direct_url_path = result.test_env.base_path / filename
- with open(direct_url_path) as f:
- return DirectUrl.from_json(f.read())
+ if direct_url_metadata_re.search(os.fspath(filename)):
+ return result.test_env.base_path / filename
+ return None
+
+
+def get_created_direct_url(result: TestPipResult, pkg: str) -> Optional[DirectUrl]:
+ direct_url_path = get_created_direct_url_path(result, pkg)
+ if direct_url_path:
+ with open(direct_url_path) as f:
+ return DirectUrl.from_json(f.read())
return None
diff --git a/tests/lib/filesystem.py b/tests/lib/filesystem.py
index 05e2db62c..5f8fe519d 100644
--- a/tests/lib/filesystem.py
+++ b/tests/lib/filesystem.py
@@ -1,41 +1,13 @@
"""Helpers for filesystem-dependent tests.
"""
import os
-import socket
-import subprocess
-import sys
from functools import partial
from itertools import chain
+from typing import Iterator, List, Set
-from .path import Path
-
-def make_socket_file(path):
- # Socket paths are limited to 108 characters (sometimes less) so we
- # chdir before creating it and use a relative path name.
- cwd = os.getcwd()
- os.chdir(os.path.dirname(path))
- try:
- sock = socket.socket(socket.AF_UNIX)
- sock.bind(os.path.basename(path))
- finally:
- os.chdir(cwd)
-
-
-def make_unreadable_file(path):
- Path(path).touch()
- os.chmod(path, 0o000)
- if sys.platform == "win32":
- # Once we drop PY2 we can use `os.getlogin()` instead.
- username = os.environ["USERNAME"]
- # Remove "Read Data/List Directory" permission for current user, but
- # leave everything else.
- args = ["icacls", path, "/deny", username + ":(RD)"]
- subprocess.check_call(args)
-
-
-def get_filelist(base):
- def join(dirpath, dirnames, filenames):
+def get_filelist(base: str) -> Set[str]:
+ def join(dirpath: str, dirnames: List[str], filenames: List[str]) -> Iterator[str]:
relative_dirpath = os.path.relpath(dirpath, base)
join_dirpath = partial(os.path.join, relative_dirpath)
return chain(
diff --git a/tests/lib/git_submodule_helpers.py b/tests/lib/git_submodule_helpers.py
index 220a926b5..12b40c607 100644
--- a/tests/lib/git_submodule_helpers.py
+++ b/tests/lib/git_submodule_helpers.py
@@ -1,9 +1,12 @@
+import os
import textwrap
+from pathlib import Path
+from typing import Tuple
-from tests.lib import _create_main_file, _git_commit
+from tests.lib import PipTestEnvironment, _create_main_file, _git_commit
-def _create_test_package_submodule(env):
+def _create_test_package_submodule(env: PipTestEnvironment) -> Path:
env.scratch_path.joinpath("version_pkg_submodule").mkdir()
submodule_path = env.scratch_path / "version_pkg_submodule"
env.run("touch", "testfile", cwd=submodule_path)
@@ -14,14 +17,18 @@ def _create_test_package_submodule(env):
return submodule_path
-def _change_test_package_submodule(env, submodule_path):
+def _change_test_package_submodule(
+ env: PipTestEnvironment, submodule_path: Path
+) -> None:
submodule_path.joinpath("testfile").write_text("this is a changed file")
submodule_path.joinpath("testfile2").write_text("this is an added file")
env.run("git", "add", ".", cwd=submodule_path)
_git_commit(env, submodule_path, message="submodule change")
-def _pull_in_submodule_changes_to_module(env, module_path, rel_path):
+def _pull_in_submodule_changes_to_module(
+ env: PipTestEnvironment, module_path: Path, rel_path: str
+) -> None:
"""
Args:
rel_path: the location of the submodule relative to the superproject.
@@ -32,7 +39,9 @@ def _pull_in_submodule_changes_to_module(env, module_path, rel_path):
_git_commit(env, module_path, message="submodule change", stage_modified=True)
-def _create_test_package_with_submodule(env, rel_path):
+def _create_test_package_with_submodule(
+ env: PipTestEnvironment, rel_path: str
+) -> Tuple[Path, Path]:
"""
Args:
rel_path: the location of the submodule relative to the superproject.
@@ -65,7 +74,7 @@ def _create_test_package_with_submodule(env, rel_path):
"git",
"submodule",
"add",
- submodule_path,
+ os.fspath(submodule_path),
rel_path,
cwd=version_pkg_path,
)
diff --git a/tests/lib/index.py b/tests/lib/index.py
index dff0ac103..17282bc56 100644
--- a/tests/lib/index.py
+++ b/tests/lib/index.py
@@ -1,8 +1,12 @@
+from typing import Optional
+
from pip._internal.models.candidate import InstallationCandidate
from pip._internal.models.link import Link
-def make_mock_candidate(version, yanked_reason=None, hex_digest=None):
+def make_mock_candidate(
+ version: str, yanked_reason: Optional[str] = None, hex_digest: Optional[str] = None
+) -> InstallationCandidate:
url = f"https://example.com/pkg-{version}.tar.gz"
if hex_digest is not None:
assert len(hex_digest) == 64
diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py
index c57ab16f8..a04d1d0fe 100644
--- a/tests/lib/local_repos.py
+++ b/tests/lib/local_repos.py
@@ -1,14 +1,13 @@
import os
import subprocess
import urllib.request
+from pathlib import Path
from pip._internal.utils.misc import hide_url
from pip._internal.vcs import vcs
-from tests.lib import path_to_url
-from tests.lib.path import Path
-def _create_svn_initools_repo(initools_dir):
+def _create_svn_initools_repo(initools_dir: str) -> None:
"""
Create the SVN INITools repo.
"""
@@ -29,10 +28,9 @@ def _create_svn_initools_repo(initools_dir):
def local_checkout(
- remote_repo, # type: str
- temp_path, # type: Path
-):
- # type: (...) -> str
+ remote_repo: str,
+ temp_path: Path,
+) -> str:
"""
:param temp_path: the return value of the tmpdir fixture, which is a
temp directory Path object unique to each test function invocation,
@@ -55,10 +53,11 @@ def local_checkout(
repo_url_path = os.path.join(repo_url_path, "trunk")
else:
vcs_backend = vcs.get_backend(vcs_name)
- vcs_backend.obtain(repo_url_path, url=hide_url(remote_repo))
+ assert vcs_backend is not None
+ vcs_backend.obtain(repo_url_path, url=hide_url(remote_repo), verbosity=0)
- return "{}+{}".format(vcs_name, path_to_url(repo_url_path))
+ return "{}+{}".format(vcs_name, Path(repo_url_path).as_uri())
-def local_repo(remote_repo, temp_path):
+def local_repo(remote_repo: str, temp_path: Path) -> str:
return local_checkout(remote_repo, temp_path).split("+", 1)[1]
diff --git a/tests/lib/options_helpers.py b/tests/lib/options_helpers.py
index 8aa105b96..31f650035 100644
--- a/tests/lib/options_helpers.py
+++ b/tests/lib/options_helpers.py
@@ -1,13 +1,18 @@
"""Provides helper classes for testing option handling in pip
"""
+from optparse import Values
+from typing import List, Tuple
+
from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import Command
from pip._internal.commands import CommandInfo, commands_dict
class FakeCommand(Command):
- def main(self, args):
+ def main( # type: ignore[override]
+ self, args: List[str]
+ ) -> Tuple[Values, List[str]]:
index_opts = cmdoptions.make_option_group(
cmdoptions.index_group,
self.parser,
@@ -17,12 +22,12 @@ class FakeCommand(Command):
class AddFakeCommandMixin:
- def setup(self):
+ def setup(self) -> None:
commands_dict["fake"] = CommandInfo(
"tests.lib.options_helpers",
"FakeCommand",
"fake summary",
)
- def teardown(self):
+ def teardown(self) -> None:
commands_dict.pop("fake")
diff --git a/tests/lib/path.py b/tests/lib/path.py
deleted file mode 100644
index 77d78cce5..000000000
--- a/tests/lib/path.py
+++ /dev/null
@@ -1,192 +0,0 @@
-# flake8: noqa
-# Author: Aziz Köksal
-import glob
-import os
-
-try:
- from os import supports_fd
-except ImportError:
- supports_fd = set()
-
-
-class Path(str):
- """
- Models a path in an object oriented way.
- """
-
- # File system path separator: '/' or '\'.
- sep = os.sep
-
- # Separator in the PATH environment variable.
- pathsep = os.pathsep
-
- def __new__(cls, *paths):
- if len(paths):
- return super().__new__(cls, os.path.join(*paths))
- return super().__new__(cls)
-
- def __div__(self, path):
- """
- Joins this path with another path.
-
- >>> path_obj / 'bc.d'
- >>> path_obj / path_obj2
- """
- return Path(self, path)
-
- __truediv__ = __div__
-
- def __rdiv__(self, path):
- """
- Joins this path with another path.
-
- >>> "/home/a" / path_obj
- """
- return Path(path, self)
-
- __rtruediv__ = __rdiv__
-
- def __idiv__(self, path):
- """
- Like __div__ but also assigns to the variable.
-
- >>> path_obj /= 'bc.d'
- """
- return Path(self, path)
-
- __itruediv__ = __idiv__
-
- def __add__(self, path):
- """
- >>> Path('/home/a') + 'bc.d'
- '/home/abc.d'
- """
- return Path(str(self) + path)
-
- def __radd__(self, path):
- """
- >>> '/home/a' + Path('bc.d')
- '/home/abc.d'
- """
- return Path(path + str(self))
-
- def __repr__(self):
- return "Path({inner})".format(inner=str.__repr__(self))
-
- @property
- def name(self):
- """
- '/home/a/bc.d' -> 'bc.d'
- """
- return os.path.basename(self)
-
- @property
- def stem(self):
- """
- '/home/a/bc.d' -> 'bc'
- """
- return Path(os.path.splitext(self)[0]).name
-
- @property
- def suffix(self):
- """
- '/home/a/bc.d' -> '.d'
- """
- return Path(os.path.splitext(self)[1])
-
- def resolve(self):
- """
- Resolves symbolic links.
- """
- return Path(os.path.realpath(self))
-
- @property
- def parent(self):
- """
- Returns the parent directory of this path.
-
- '/home/a/bc.d' -> '/home/a'
- '/home/a/' -> '/home/a'
- '/home/a' -> '/home'
- """
- return Path(os.path.dirname(self))
-
- def exists(self):
- """
- Returns True if the path exists.
- """
- return os.path.exists(self)
-
- def mkdir(self, mode=0x1FF, exist_ok=False, parents=False): # 0o777
- """
- Creates a directory, if it doesn't exist already.
-
- :param parents: Whether to create parent directories.
- """
-
- maker_func = os.makedirs if parents else os.mkdir
- try:
- maker_func(self, mode)
- except OSError:
- if not exist_ok or not os.path.isdir(self):
- raise
-
- def unlink(self):
- """
- Removes a file.
- """
- return os.remove(self)
-
- def rmdir(self):
- """
- Removes a directory.
- """
- return os.rmdir(self)
-
- def rename(self, to):
- """
- Renames a file or directory. May throw an OSError.
- """
- return os.rename(self, to)
-
- def glob(self, pattern):
- return (Path(i) for i in glob.iglob(self.joinpath(pattern)))
-
- def joinpath(self, *parts):
- return Path(self, *parts)
-
- # TODO: Remove after removing inheritance from str.
- def join(self, *parts):
- raise RuntimeError("Path.join is invalid, use joinpath instead.")
-
- def read_bytes(self):
- # type: () -> bytes
- with open(self, "rb") as fp:
- return fp.read()
-
- def write_bytes(self, content):
- # type: (bytes) -> None
- with open(self, "wb") as f:
- f.write(content)
-
- def read_text(self):
- with open(self, "r") as fp:
- return fp.read()
-
- def write_text(self, content):
- with open(self, "w") as fp:
- fp.write(content)
-
- def touch(self):
- with open(self, "a") as fp:
- path = fp.fileno() if os.utime in supports_fd else self
- os.utime(path, None) # times is not optional on Python 2.7
-
- def symlink_to(self, target):
- os.symlink(target, self)
-
- def stat(self):
- return os.stat(self)
-
-
-curdir = Path(os.path.curdir)
diff --git a/tests/lib/requests_mocks.py b/tests/lib/requests_mocks.py
index 5db3970cb..a70a9b2b0 100644
--- a/tests/lib/requests_mocks.py
+++ b/tests/lib/requests_mocks.py
@@ -2,40 +2,47 @@
"""
from io import BytesIO
+from typing import Any, Callable, Dict, Iterator, List, Optional
+
+_Hook = Callable[["MockResponse"], None]
class FakeStream:
- def __init__(self, contents):
+ def __init__(self, contents: bytes) -> None:
self._io = BytesIO(contents)
- def read(self, size, decode_content=None):
+ def read(self, size: int, decode_content: Optional[bool] = None) -> bytes:
return self._io.read(size)
- def stream(self, size, decode_content=None):
+ def stream(
+ self, size: int, decode_content: Optional[bool] = None
+ ) -> Iterator[bytes]:
yield self._io.read(size)
- def release_conn(self):
+ def release_conn(self) -> None:
pass
class MockResponse:
- def __init__(self, contents):
+ request: "MockRequest"
+ connection: "MockConnection"
+ url: str
+
+ def __init__(self, contents: bytes) -> None:
self.raw = FakeStream(contents)
self.content = contents
- self.request = None
- self.reason = None
+ self.reason = "OK"
self.status_code = 200
- self.connection = None
- self.url = None
- self.headers = {"Content-Length": len(contents)}
- self.history = []
+ self.headers = {"Content-Length": str(len(contents))}
+ self.history: List[MockResponse] = []
+ self.from_cache = False
class MockConnection:
- def _send(self, req, **kwargs):
+ def _send(self, req: "MockRequest", **kwargs: Any) -> MockResponse:
raise NotImplementedError("_send must be overridden for tests")
- def send(self, req, **kwargs):
+ def send(self, req: "MockRequest", **kwargs: Any) -> MockResponse:
resp = self._send(req, **kwargs)
for cb in req.hooks.get("response", []):
cb(resp)
@@ -43,10 +50,10 @@ class MockConnection:
class MockRequest:
- def __init__(self, url):
+ def __init__(self, url: str) -> None:
self.url = url
- self.headers = {}
- self.hooks = {}
+ self.headers: Dict[str, str] = {}
+ self.hooks: Dict[str, List[_Hook]] = {}
- def register_hook(self, event_name, callback):
+ def register_hook(self, event_name: str, callback: _Hook) -> None:
self.hooks.setdefault(event_name, []).append(callback)
diff --git a/tests/lib/server.py b/tests/lib/server.py
index 6db46d166..4cc18452c 100644
--- a/tests/lib/server.py
+++ b/tests/lib/server.py
@@ -1,59 +1,29 @@
-import os
-import signal
+import pathlib
import ssl
import threading
from base64 import b64encode
from contextlib import contextmanager
from textwrap import dedent
-from types import TracebackType
-from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type
+from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator
from unittest.mock import Mock
from werkzeug.serving import BaseWSGIServer, WSGIRequestHandler
from werkzeug.serving import make_server as _make_server
-from .compat import nullcontext
+from .compat import blocked_signals
-Environ = Dict[str, str]
-Status = str
-Headers = Iterable[Tuple[str, str]]
-ExcInfo = Tuple[Type[BaseException], BaseException, TracebackType]
-Write = Callable[[bytes], None]
-StartResponse = Callable[[Status, Headers, Optional[ExcInfo]], Write]
-Body = List[bytes]
-Responder = Callable[[Environ, StartResponse], Body]
+if TYPE_CHECKING:
+ from _typeshed.wsgi import StartResponse, WSGIApplication, WSGIEnvironment
+Body = Iterable[bytes]
-class MockServer(BaseWSGIServer):
- mock = Mock() # type: Mock
-
-
-# Applies on Python 2 and Windows.
-if not hasattr(signal, "pthread_sigmask"):
- # We're not relying on this behavior anywhere currently, it's just best
- # practice.
- blocked_signals = nullcontext
-else:
-
- @contextmanager
- def blocked_signals():
- """Block all signals for e.g. starting a worker thread."""
- # valid_signals() was added in Python 3.8 (and not using it results
- # in a warning on pthread_sigmask() call)
- try:
- mask = signal.valid_signals()
- except AttributeError:
- mask = set(range(1, signal.NSIG))
- old_mask = signal.pthread_sigmask(signal.SIG_SETMASK, mask)
- try:
- yield
- finally:
- signal.pthread_sigmask(signal.SIG_SETMASK, old_mask)
+class MockServer(BaseWSGIServer):
+ mock: Mock = Mock()
class _RequestHandler(WSGIRequestHandler):
- def make_environ(self):
+ def make_environ(self) -> Dict[str, Any]:
environ = super().make_environ()
# From pallets/werkzeug#1469, will probably be in release after
@@ -77,14 +47,14 @@ class _RequestHandler(WSGIRequestHandler):
return environ
-def _mock_wsgi_adapter(mock):
- # type: (Callable[[Environ, StartResponse], Responder]) -> Responder
+def _mock_wsgi_adapter(
+ mock: Callable[["WSGIEnvironment", "StartResponse"], "WSGIApplication"]
+) -> "WSGIApplication":
"""Uses a mock to record function arguments and provide
the actual function that should respond.
"""
- def adapter(environ, start_response):
- # type: (Environ, StartResponse) -> Body
+ def adapter(environ: "WSGIEnvironment", start_response: "StartResponse") -> Body:
try:
responder = mock(environ, start_response)
except StopIteration:
@@ -94,8 +64,7 @@ def _mock_wsgi_adapter(mock):
return adapter
-def make_mock_server(**kwargs):
- # type: (Any) -> MockServer
+def make_mock_server(**kwargs: Any) -> MockServer:
"""Creates a mock HTTP(S) server listening on a random port on localhost.
The `mock` property of the returned server provides and records all WSGI
@@ -135,8 +104,7 @@ def make_mock_server(**kwargs):
@contextmanager
-def server_running(server):
- # type: (BaseWSGIServer) -> None
+def server_running(server: BaseWSGIServer) -> Iterator[None]:
"""Context manager for running the provided server in a separate thread."""
thread = threading.Thread(target=server.serve_forever)
thread.daemon = True
@@ -152,10 +120,8 @@ def server_running(server):
# Helper functions for making responses in a declarative way.
-def text_html_response(text):
- # type: (str) -> Responder
- def responder(environ, start_response):
- # type: (Environ, StartResponse) -> Body
+def text_html_response(text: str) -> "WSGIApplication":
+ def responder(environ: "WSGIEnvironment", start_response: "StartResponse") -> Body:
start_response(
"200 OK",
[
@@ -167,8 +133,7 @@ def text_html_response(text):
return responder
-def html5_page(text):
- # type: (str) -> str
+def html5_page(text: str) -> str:
return (
dedent(
"""
@@ -185,68 +150,42 @@ def html5_page(text):
)
-def index_page(spec):
- # type: (Dict[str, str]) -> Responder
- def link(name, value):
+def package_page(spec: Dict[str, str]) -> "WSGIApplication":
+ def link(name: str, value: str) -> str:
return '<a href="{}">{}</a>'.format(value, name)
links = "".join(link(*kv) for kv in spec.items())
return text_html_response(html5_page(links))
-def package_page(spec):
- # type: (Dict[str, str]) -> Responder
- def link(name, value):
- return '<a href="{}">{}</a>'.format(value, name)
-
- links = "".join(link(*kv) for kv in spec.items())
- return text_html_response(html5_page(links))
-
-
-def file_response(path):
- # type: (str) -> Responder
- def responder(environ, start_response):
- # type: (Environ, StartResponse) -> Body
- size = os.stat(path).st_size
+def file_response(path: pathlib.Path) -> "WSGIApplication":
+ def responder(environ: "WSGIEnvironment", start_response: "StartResponse") -> Body:
start_response(
"200 OK",
[
("Content-Type", "application/octet-stream"),
- ("Content-Length", str(size)),
+ ("Content-Length", str(path.stat().st_size)),
],
)
-
- with open(path, "rb") as f:
- return [f.read()]
+ return [path.read_bytes()]
return responder
-def authorization_response(path):
- # type: (str) -> Responder
+def authorization_response(path: pathlib.Path) -> "WSGIApplication":
correct_auth = "Basic " + b64encode(b"USERNAME:PASSWORD").decode("ascii")
- def responder(environ, start_response):
- # type: (Environ, StartResponse) -> Body
-
- if environ.get("HTTP_AUTHORIZATION") == correct_auth:
- size = os.stat(path).st_size
- start_response(
- "200 OK",
- [
- ("Content-Type", "application/octet-stream"),
- ("Content-Length", str(size)),
- ],
- )
- else:
- start_response(
- "401 Unauthorized",
- [
- ("WWW-Authenticate", "Basic"),
- ],
- )
-
- with open(path, "rb") as f:
- return [f.read()]
+ def responder(environ: "WSGIEnvironment", start_response: "StartResponse") -> Body:
+ if environ.get("HTTP_AUTHORIZATION") != correct_auth:
+ start_response("401 Unauthorized", [("WWW-Authenticate", "Basic")])
+ return ()
+ start_response(
+ "200 OK",
+ [
+ ("Content-Type", "application/octet-stream"),
+ ("Content-Length", str(path.stat().st_size)),
+ ],
+ )
+ return [path.read_bytes()]
return responder
diff --git a/tests/lib/test_lib.py b/tests/lib/test_lib.py
index 29b76f58c..a541a0a20 100644
--- a/tests/lib/test_lib.py
+++ b/tests/lib/test_lib.py
@@ -1,17 +1,21 @@
"""Test the test support."""
import filecmp
+import pathlib
import re
import sys
from contextlib import contextmanager
from os.path import isdir, join
+from typing import Any, Dict, Iterator, Type
import pytest
-from tests.lib import SRC_DIR
+from tests.lib import SRC_DIR, PipTestEnvironment
@contextmanager
-def assert_error_startswith(exc_type, expected_start):
+def assert_error_startswith(
+ exc_type: Type[Exception], expected_start: str
+) -> Iterator[None]:
"""
Assert that an exception is raised starting with a certain message.
"""
@@ -21,7 +25,7 @@ def assert_error_startswith(exc_type, expected_start):
assert str(err.value).startswith(expected_start), f"full message: {err.value}"
-def test_tmp_dir_exists_in_env(script):
+def test_tmp_dir_exists_in_env(script: PipTestEnvironment) -> None:
"""
Test that $TMPDIR == env.temp_path and path exists and env.assert_no_temp()
passes (in fast env)
@@ -29,25 +33,31 @@ def test_tmp_dir_exists_in_env(script):
# need these tests to ensure the assert_no_temp feature of scripttest is
# working
script.assert_no_temp() # this fails if env.tmp_path doesn't exist
- assert script.environ["TMPDIR"] == script.temp_path
+ assert pathlib.Path(script.environ["TMPDIR"]) == script.temp_path
assert isdir(script.temp_path)
-def test_correct_pip_version(script):
+def test_correct_pip_version(script: PipTestEnvironment) -> None:
"""
Check we are running proper version of pip in run_pip.
"""
+
+ if script.zipapp:
+ pytest.skip("Test relies on the pip under test being in the filesystem")
+
# output is like:
# pip PIPVERSION from PIPDIRECTORY (python PYVERSION)
result = script.pip("--version")
# compare the directory tree of the invoked pip with that of this source
# distribution
- pip_folder_outputed = re.match(
+ match = re.match(
r"pip \d+(\.[\d]+)+(\.?(b|rc|dev|pre|post)\d+)? from (.*) "
- r"\(python \d(.[\d])+\)$",
+ r"\(python \d+(\.[\d]+)+\)$",
result.stdout,
- ).group(4)
+ )
+ assert match is not None
+ pip_folder_outputed = match.group(4)
pip_folder = join(SRC_DIR, "src", "pip")
diffs = filecmp.dircmp(pip_folder, pip_folder_outputed)
@@ -67,7 +77,7 @@ def test_correct_pip_version(script):
)
-def test_as_import(script):
+def test_as_import(script: PipTestEnvironment) -> None:
"""test that pip.__init__.py does not shadow
the command submodule with a dictionary
"""
@@ -77,7 +87,9 @@ def test_as_import(script):
class TestPipTestEnvironment:
- def run_stderr_with_prefix(self, script, prefix, **kwargs):
+ def run_stderr_with_prefix(
+ self, script: PipTestEnvironment, prefix: str, **kwargs: Any
+ ) -> None:
"""
Call run() that prints stderr with the given prefix.
"""
@@ -86,7 +98,9 @@ class TestPipTestEnvironment:
args = [sys.executable, "-c", command]
script.run(*args, **kwargs)
- def run_with_log_command(self, script, sub_string, **kwargs):
+ def run_with_log_command(
+ self, script: PipTestEnvironment, sub_string: str, **kwargs: Any
+ ) -> None:
"""
Call run() on a command that logs a "%"-style format string using
the given substring as the string's replacement field.
@@ -106,14 +120,14 @@ class TestPipTestEnvironment:
"FOO",
),
)
- def test_run__allowed_stderr(self, script, prefix):
+ def test_run__allowed_stderr(self, script: PipTestEnvironment, prefix: str) -> None:
"""
Test calling run() with allowed stderr.
"""
# Check that no error happens.
self.run_stderr_with_prefix(script, prefix)
- def test_run__allow_stderr_warning(self, script):
+ def test_run__allow_stderr_warning(self, script: PipTestEnvironment) -> None:
"""
Test passing allow_stderr_warning=True.
"""
@@ -136,12 +150,13 @@ class TestPipTestEnvironment:
@pytest.mark.parametrize(
"prefix",
(
- "DEPRECATION",
"WARNING",
"ERROR",
),
)
- def test_run__allow_stderr_error(self, script, prefix):
+ def test_run__allow_stderr_error(
+ self, script: PipTestEnvironment, prefix: str
+ ) -> None:
"""
Test passing allow_stderr_error=True.
"""
@@ -151,19 +166,20 @@ class TestPipTestEnvironment:
@pytest.mark.parametrize(
"prefix, expected_start",
(
- ("DEPRECATION", "stderr has an unexpected warning"),
("WARNING", "stderr has an unexpected warning"),
("ERROR", "stderr has an unexpected error"),
),
)
- def test_run__unexpected_stderr(self, script, prefix, expected_start):
+ def test_run__unexpected_stderr(
+ self, script: PipTestEnvironment, prefix: str, expected_start: str
+ ) -> None:
"""
Test calling run() with unexpected stderr output.
"""
with assert_error_startswith(RuntimeError, expected_start):
self.run_stderr_with_prefix(script, prefix)
- def test_run__logging_error(self, script):
+ def test_run__logging_error(self, script: PipTestEnvironment) -> None:
"""
Test calling run() with an unexpected logging error.
"""
@@ -183,9 +199,8 @@ class TestPipTestEnvironment:
)
def test_run__allow_stderr_error_false_error_with_expect_error(
- self,
- script,
- ):
+ self, script: PipTestEnvironment
+ ) -> None:
"""
Test passing allow_stderr_error=False with expect_error=True.
"""
@@ -194,9 +209,8 @@ class TestPipTestEnvironment:
script.run("python", allow_stderr_error=False, expect_error=True)
def test_run__allow_stderr_warning_false_error_with_expect_stderr(
- self,
- script,
- ):
+ self, script: PipTestEnvironment
+ ) -> None:
"""
Test passing allow_stderr_warning=False with expect_stderr=True.
"""
@@ -217,23 +231,29 @@ class TestPipTestEnvironment:
"allow_stderr_error",
),
)
- def test_run__allow_stderr_warning_false_error(self, script, arg_name):
+ def test_run__allow_stderr_warning_false_error(
+ self, script: PipTestEnvironment, arg_name: str
+ ) -> None:
"""
Test passing allow_stderr_warning=False when it is not allowed.
"""
- kwargs = {"allow_stderr_warning": False, arg_name: True}
+ kwargs: Dict[str, Any] = {"allow_stderr_warning": False, arg_name: True}
expected_start = (
"cannot pass allow_stderr_warning=False with allow_stderr_error=True"
)
with assert_error_startswith(RuntimeError, expected_start):
script.run("python", **kwargs)
- def test_run__expect_error_fails_when_zero_returncode(self, script):
+ def test_run__expect_error_fails_when_zero_returncode(
+ self, script: PipTestEnvironment
+ ) -> None:
expected_start = "Script passed unexpectedly"
with assert_error_startswith(AssertionError, expected_start):
script.run("python", expect_error=True)
- def test_run__no_expect_error_fails_when_nonzero_returncode(self, script):
+ def test_run__no_expect_error_fails_when_nonzero_returncode(
+ self, script: PipTestEnvironment
+ ) -> None:
expected_start = "Script returned code: 1"
with assert_error_startswith(AssertionError, expected_start):
script.run("python", "-c", "import sys; sys.exit(1)")
diff --git a/tests/lib/test_wheel.py b/tests/lib/test_wheel.py
index 579d48660..86994c28e 100644
--- a/tests/lib/test_wheel.py
+++ b/tests/lib/test_wheel.py
@@ -4,9 +4,11 @@ import csv
from email import message_from_string
from email.message import Message
from functools import partial
+from pathlib import Path
from zipfile import ZipFile
from tests.lib.wheel import (
+ File,
_default,
make_metadata_file,
make_wheel,
@@ -15,18 +17,17 @@ from tests.lib.wheel import (
)
-def test_message_from_dict_one_value():
+def test_message_from_dict_one_value() -> None:
message = message_from_dict({"a": "1"})
assert set(message.get_all("a")) == {"1"}
-def test_message_from_dict_multiple_values():
+def test_message_from_dict_multiple_values() -> None:
message = message_from_dict({"a": ["1", "2"]})
assert set(message.get_all("a")) == {"1", "2"}
-def message_from_bytes(contents):
- # type: (bytes) -> Message
+def message_from_bytes(contents: bytes) -> Message:
return message_from_string(contents.decode("utf-8"))
@@ -40,7 +41,7 @@ default_make_metadata = partial(
)
-def default_metadata_checks(f):
+def default_metadata_checks(f: File) -> Message:
assert f.name == "simple-0.1.0.dist-info/METADATA"
message = message_from_bytes(f.contents)
assert message.get_all("Metadata-Version") == ["2.1"]
@@ -49,32 +50,37 @@ def default_metadata_checks(f):
return message
-def test_make_metadata_file_defaults():
+def test_make_metadata_file_defaults() -> None:
f = default_make_metadata()
+ assert f is not None
default_metadata_checks(f)
-def test_make_metadata_file_custom_value():
+def test_make_metadata_file_custom_value() -> None:
f = default_make_metadata(updates={"a": "1"})
+ assert f is not None
message = default_metadata_checks(f)
assert message.get_all("a") == ["1"]
-def test_make_metadata_file_custom_value_list():
+def test_make_metadata_file_custom_value_list() -> None:
f = default_make_metadata(updates={"a": ["1", "2"]})
+ assert f is not None
message = default_metadata_checks(f)
assert set(message.get_all("a")) == {"1", "2"}
-def test_make_metadata_file_custom_value_overrides():
+def test_make_metadata_file_custom_value_overrides() -> None:
f = default_make_metadata(updates={"Metadata-Version": "2.2"})
+ assert f is not None
message = message_from_bytes(f.contents)
assert message.get_all("Metadata-Version") == ["2.2"]
-def test_make_metadata_file_custom_contents():
+def test_make_metadata_file_custom_contents() -> None:
value = b"hello"
f = default_make_metadata(value=value)
+ assert f is not None
assert f.contents == value
@@ -89,7 +95,7 @@ default_make_wheel_metadata = partial(
)
-def default_wheel_metadata_checks(f):
+def default_wheel_metadata_checks(f: File) -> Message:
assert f.name == "simple-0.1.0.dist-info/WHEEL"
message = message_from_bytes(f.contents)
assert message.get_all("Wheel-Version") == ["1.0"]
@@ -99,43 +105,47 @@ def default_wheel_metadata_checks(f):
return message
-def test_make_wheel_metadata_file_defaults():
+def test_make_wheel_metadata_file_defaults() -> None:
f = default_make_wheel_metadata()
+ assert f is not None
default_wheel_metadata_checks(f)
-def test_make_wheel_metadata_file_custom_value():
+def test_make_wheel_metadata_file_custom_value() -> None:
f = default_make_wheel_metadata(updates={"a": "1"})
+ assert f is not None
message = default_wheel_metadata_checks(f)
assert message.get_all("a") == ["1"]
-def test_make_wheel_metadata_file_custom_value_list():
+def test_make_wheel_metadata_file_custom_value_list() -> None:
f = default_make_wheel_metadata(updates={"a": ["1", "2"]})
+ assert f is not None
message = default_wheel_metadata_checks(f)
assert set(message.get_all("a")) == {"1", "2"}
-def test_make_wheel_metadata_file_custom_value_override():
+def test_make_wheel_metadata_file_custom_value_override() -> None:
f = default_make_wheel_metadata(updates={"Wheel-Version": "1.1"})
+ assert f is not None
message = message_from_bytes(f.contents)
assert message.get_all("Wheel-Version") == ["1.1"]
-def test_make_wheel_metadata_file_custom_contents():
+def test_make_wheel_metadata_file_custom_contents() -> None:
value = b"hello"
f = default_make_wheel_metadata(value=value)
-
+ assert f is not None
assert f.name == "simple-0.1.0.dist-info/WHEEL"
assert f.contents == value
-def test_make_wheel_metadata_file_no_contents():
+def test_make_wheel_metadata_file_no_contents() -> None:
f = default_make_wheel_metadata(value=None)
assert f is None
-def test_make_wheel_basics(tmpdir):
+def test_make_wheel_basics(tmpdir: Path) -> None:
make_wheel(name="simple", version="0.1.0").save_to_dir(tmpdir)
expected_wheel_path = tmpdir / "simple-0.1.0-py2.py3-none-any.whl"
@@ -150,7 +160,7 @@ def test_make_wheel_basics(tmpdir):
}
-def test_make_wheel_default_record():
+def test_make_wheel_default_record() -> None:
with make_wheel(
name="simple",
version="0.1.0",
@@ -192,7 +202,7 @@ def test_make_wheel_default_record():
assert records[name][1] == length, name
-def test_make_wheel_extra_files():
+def test_make_wheel_extra_files() -> None:
with make_wheel(
name="simple",
version="0.1.0",
@@ -215,7 +225,7 @@ def test_make_wheel_extra_files():
assert z.read("simple-0.1.0.data/info.txt") == b"c"
-def test_make_wheel_no_files():
+def test_make_wheel_no_files() -> None:
with make_wheel(
name="simple",
version="0.1.0",
@@ -226,7 +236,7 @@ def test_make_wheel_no_files():
assert not z.namelist()
-def test_make_wheel_custom_files():
+def test_make_wheel_custom_files() -> None:
with make_wheel(
name="simple",
version="0.1.0",
diff --git a/tests/lib/venv.py b/tests/lib/venv.py
index e297d6462..ab6644bc9 100644
--- a/tests/lib/venv.py
+++ b/tests/lib/venv.py
@@ -1,13 +1,22 @@
import compileall
+import os
import shutil
import subprocess
import sys
import textwrap
import venv as _venv
+from pathlib import Path
+from typing import TYPE_CHECKING, Optional, Union
import virtualenv as _virtualenv
-from .path import Path
+if TYPE_CHECKING:
+ # Literal was introduced in Python 3.8.
+ from typing import Literal
+
+ VirtualEnvironmentType = Literal["virtualenv", "venv"]
+else:
+ VirtualEnvironmentType = str
class VirtualEnvironment:
@@ -16,18 +25,28 @@ class VirtualEnvironment:
virtualenv but in the future it could use pyvenv.
"""
- def __init__(self, location, template=None, venv_type=None):
+ def __init__(
+ self,
+ location: Path,
+ template: Optional["VirtualEnvironment"] = None,
+ venv_type: Optional[VirtualEnvironmentType] = None,
+ ):
+ self.location = location
assert template is None or venv_type is None
- assert venv_type in (None, "virtualenv", "venv")
- self.location = Path(location)
- self._venv_type = venv_type or template._venv_type or "virtualenv"
+ self._venv_type: VirtualEnvironmentType
+ if template is not None:
+ self._venv_type = template._venv_type
+ elif venv_type is not None:
+ self._venv_type = venv_type
+ else:
+ self._venv_type = "virtualenv"
self._user_site_packages = False
self._template = template
- self._sitecustomize = None
+ self._sitecustomize: Optional[str] = None
self._update_paths()
self._create()
- def _update_paths(self):
+ def _update_paths(self) -> None:
home, lib, inc, bin = _virtualenv.path_locations(self.location)
self.bin = Path(bin)
self.site = Path(lib) / "site-packages"
@@ -38,10 +57,10 @@ class VirtualEnvironment:
else:
self.lib = Path(lib)
- def __repr__(self):
+ def __repr__(self) -> str:
return f"<VirtualEnvironment {self.location}>"
- def _create(self, clear=False):
+ def _create(self, clear: bool = False) -> None:
if clear:
shutil.rmtree(self.location)
if self._template:
@@ -77,7 +96,7 @@ class VirtualEnvironment:
self.sitecustomize = self._sitecustomize
self.user_site_packages = self._user_site_packages
- def _fix_virtualenv_site_module(self):
+ def _fix_virtualenv_site_module(self) -> None:
# Patch `site.py` so user site work as expected.
site_py = self.lib / "site.py"
with open(site_py) as fp:
@@ -111,7 +130,7 @@ class VirtualEnvironment:
# Make sure bytecode is up-to-date too.
assert compileall.compile_file(str(site_py), quiet=1, force=True)
- def _customize_site(self):
+ def _customize_site(self) -> None:
contents = ""
if self._venv_type == "venv":
# Enable user site (before system).
@@ -149,29 +168,29 @@ class VirtualEnvironment:
# Make sure bytecode is up-to-date too.
assert compileall.compile_file(str(sitecustomize), quiet=1, force=True)
- def clear(self):
+ def clear(self) -> None:
self._create(clear=True)
- def move(self, location):
- shutil.move(self.location, location)
+ def move(self, location: Union[Path, str]) -> None:
+ shutil.move(os.fspath(self.location), location)
self.location = Path(location)
self._update_paths()
@property
- def sitecustomize(self):
+ def sitecustomize(self) -> Optional[str]:
return self._sitecustomize
@sitecustomize.setter
- def sitecustomize(self, value):
+ def sitecustomize(self, value: str) -> None:
self._sitecustomize = value
self._customize_site()
@property
- def user_site_packages(self):
+ def user_site_packages(self) -> bool:
return self._user_site_packages
@user_site_packages.setter
- def user_site_packages(self, value):
+ def user_site_packages(self, value: bool) -> None:
self._user_site_packages = value
if self._venv_type == "virtualenv":
marker = self.lib / "no-global-site-packages.txt"
diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py
index bfcdc9d27..f2ddfd3b7 100644
--- a/tests/lib/wheel.py
+++ b/tests/lib/wheel.py
@@ -10,9 +10,9 @@ from enum import Enum
from functools import partial
from hashlib import sha256
from io import BytesIO, StringIO
+from pathlib import Path
from typing import (
AnyStr,
- Callable,
Dict,
Iterable,
List,
@@ -26,11 +26,8 @@ from zipfile import ZipFile
from pip._vendor.requests.structures import CaseInsensitiveDict
-from tests.lib.path import Path
+from pip._internal.metadata import BaseDistribution, MemoryWheel, get_wheel_distribution
-# path, digest, size
-RecordLike = Tuple[str, str, str]
-RecordCallback = Callable[[List["Record"]], Union[str, bytes, List[RecordLike]]]
# As would be used in metadata
HeaderValue = Union[str, List[str]]
@@ -51,15 +48,13 @@ T = TypeVar("T")
Defaulted = Union[Default, T]
-def ensure_binary(value):
- # type: (AnyStr) -> bytes
+def ensure_binary(value: Union[bytes, str]) -> bytes:
if isinstance(value, bytes):
return value
return value.encode()
-def message_from_dict(headers):
- # type: (Dict[str, HeaderValue]) -> Message
+def message_from_dict(headers: Dict[str, HeaderValue]) -> Message:
"""Plain key-value pairs are set in the returned message.
List values are converted into repeated headers in the result.
@@ -74,19 +69,17 @@ def message_from_dict(headers):
return message
-def dist_info_path(name, version, path):
- # type: (str, str, str) -> str
+def dist_info_path(name: str, version: str, path: str) -> str:
return f"{name}-{version}.dist-info/{path}"
def make_metadata_file(
- name, # type: str
- version, # type: str
- value, # type: Defaulted[Optional[AnyStr]]
- updates, # type: Defaulted[Dict[str, HeaderValue]]
- body, # type: Defaulted[AnyStr]
-):
- # type: () -> File
+ name: str,
+ version: str,
+ value: Defaulted[Optional[AnyStr]],
+ updates: Defaulted[Dict[str, HeaderValue]],
+ body: Defaulted[AnyStr],
+) -> Optional[File]:
if value is None:
return None
@@ -113,13 +106,12 @@ def make_metadata_file(
def make_wheel_metadata_file(
- name, # type: str
- version, # type: str
- value, # type: Defaulted[Optional[AnyStr]]
- tags, # type: Sequence[Tuple[str, str, str]]
- updates, # type: Defaulted[Dict[str, HeaderValue]]
-):
- # type: (...) -> Optional[File]
+ name: str,
+ version: str,
+ value: Defaulted[Optional[AnyStr]],
+ tags: Sequence[Tuple[str, str, str]],
+ updates: Defaulted[Dict[str, HeaderValue]],
+) -> Optional[File]:
if value is None:
return None
@@ -144,12 +136,11 @@ def make_wheel_metadata_file(
def make_entry_points_file(
- name, # type: str
- version, # type: str
- entry_points, # type: Defaulted[Dict[str, List[str]]]
- console_scripts, # type: Defaulted[List[str]]
-):
- # type: (...) -> Optional[File]
+ name: str,
+ version: str,
+ entry_points: Defaulted[Dict[str, List[str]]],
+ console_scripts: Defaulted[List[str]],
+) -> Optional[File]:
if entry_points is _default and console_scripts is _default:
return None
@@ -172,13 +163,13 @@ def make_entry_points_file(
)
-def make_files(files):
- # type: (Dict[str, AnyStr]) -> List[File]
+def make_files(files: Dict[str, Union[bytes, str]]) -> List[File]:
return [File(name, ensure_binary(contents)) for name, contents in files.items()]
-def make_metadata_files(name, version, files):
- # type: (str, str, Dict[str, AnyStr]) -> List[File]
+def make_metadata_files(
+ name: str, version: str, files: Dict[str, AnyStr]
+) -> List[File]:
get_path = partial(dist_info_path, name, version)
return [
File(get_path(name), ensure_binary(contents))
@@ -186,8 +177,7 @@ def make_metadata_files(name, version, files):
]
-def make_data_files(name, version, files):
- # type: (str, str, Dict[str, AnyStr]) -> List[File]
+def make_data_files(name: str, version: str, files: Dict[str, AnyStr]) -> List[File]:
data_dir = f"{name}-{version}.data"
return [
File(f"{data_dir}/{name}", ensure_binary(contents))
@@ -195,25 +185,21 @@ def make_data_files(name, version, files):
]
-def urlsafe_b64encode_nopad(data):
- # type: (bytes) -> str
+def urlsafe_b64encode_nopad(data: bytes) -> str:
return urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
-def digest(contents):
- # type: (bytes) -> str
+def digest(contents: bytes) -> str:
return "sha256={}".format(urlsafe_b64encode_nopad(sha256(contents).digest()))
def record_file_maker_wrapper(
- name, # type: str
- version, # type: str
- files, # type: List[File]
- record, # type: Defaulted[Optional[AnyStr]]
- record_callback, # type: Defaulted[RecordCallback]
-):
- # type: (...) -> Iterable[File]
- records = [] # type: List[Record]
+ name: str,
+ version: str,
+ files: Iterable[File],
+ record: Defaulted[Optional[AnyStr]],
+) -> Iterable[File]:
+ records: List[Record] = []
for file in files:
records.append(
Record(file.name, digest(file.contents), str(len(file.contents)))
@@ -231,20 +217,22 @@ def record_file_maker_wrapper(
records.append(Record(record_path, "", ""))
- if record_callback is not _default:
- records = record_callback(records)
-
with StringIO(newline="") as buf:
writer = csv.writer(buf)
- for record in records:
- writer.writerow(record)
+ for r in records:
+ writer.writerow(r)
contents = buf.getvalue().encode("utf-8")
yield File(record_path, contents)
-def wheel_name(name, version, pythons, abis, platforms):
- # type: (str, str, str, str, str) -> str
+def wheel_name(
+ name: str,
+ version: str,
+ pythons: Iterable[str],
+ abis: Iterable[str],
+ platforms: Iterable[str],
+) -> str:
stem = "-".join(
[
name,
@@ -260,24 +248,21 @@ def wheel_name(name, version, pythons, abis, platforms):
class WheelBuilder:
"""A wheel that can be saved or converted to several formats."""
- def __init__(self, name, files):
- # type: (str, List[File]) -> None
+ def __init__(self, name: str, files: Iterable[File]) -> None:
self._name = name
self._files = files
- def save_to_dir(self, path):
- # type: (Union[Path, str]) -> str
+ def save_to_dir(self, path: Union[Path, str]) -> str:
"""Generate wheel file with correct name and save into the provided
directory.
:returns the wheel file path
"""
- path = Path(path) / self._name
- path.write_bytes(self.as_bytes())
- return str(path)
+ p = Path(path) / self._name
+ p.write_bytes(self.as_bytes())
+ return str(p)
- def save_to(self, path):
- # type: (Union[Path, str]) -> str
+ def save_to(self, path: Union[Path, str]) -> str:
"""Generate wheel file, saving to the provided path. Any parent
directories must already exist.
@@ -287,36 +272,36 @@ class WheelBuilder:
path.write_bytes(self.as_bytes())
return str(path)
- def as_bytes(self):
- # type: () -> bytes
+ def as_bytes(self) -> bytes:
with BytesIO() as buf:
with ZipFile(buf, "w") as z:
for file in self._files:
z.writestr(file.name, file.contents)
return buf.getvalue()
- def as_zipfile(self):
- # type: () -> ZipFile
+ def as_zipfile(self) -> ZipFile:
return ZipFile(BytesIO(self.as_bytes()))
+ def as_distribution(self, name: str) -> BaseDistribution:
+ stream = BytesIO(self.as_bytes())
+ return get_wheel_distribution(MemoryWheel(self._name, stream), name)
+
def make_wheel(
- name, # type: str
- version, # type: str
- wheel_metadata=_default, # type: Defaulted[Optional[AnyStr]]
- wheel_metadata_updates=_default, # type: Defaulted[Dict[str, HeaderValue]]
- metadata=_default, # type: Defaulted[Optional[AnyStr]]
- metadata_body=_default, # type: Defaulted[AnyStr]
- metadata_updates=_default, # type: Defaulted[Dict[str, HeaderValue]]
- extra_files=_default, # type: Defaulted[Dict[str, AnyStr]]
- extra_metadata_files=_default, # type: Defaulted[Dict[str, AnyStr]]
- extra_data_files=_default, # type: Defaulted[Dict[str, AnyStr]]
- console_scripts=_default, # type: Defaulted[List[str]]
- entry_points=_default, # type: Defaulted[Dict[str, List[str]]]
- record=_default, # type: Defaulted[Optional[AnyStr]]
- record_callback=_default, # type: Defaulted[RecordCallback]
-):
- # type: (...) -> WheelBuilder
+ name: str,
+ version: str,
+ wheel_metadata: Defaulted[Optional[AnyStr]] = _default,
+ wheel_metadata_updates: Defaulted[Dict[str, HeaderValue]] = _default,
+ metadata: Defaulted[Optional[AnyStr]] = _default,
+ metadata_body: Defaulted[AnyStr] = _default,
+ metadata_updates: Defaulted[Dict[str, HeaderValue]] = _default,
+ extra_files: Defaulted[Dict[str, Union[bytes, str]]] = _default,
+ extra_metadata_files: Defaulted[Dict[str, AnyStr]] = _default,
+ extra_data_files: Defaulted[Dict[str, AnyStr]] = _default,
+ console_scripts: Defaulted[List[str]] = _default,
+ entry_points: Defaulted[Dict[str, List[str]]] = _default,
+ record: Defaulted[Optional[AnyStr]] = _default,
+) -> WheelBuilder:
"""
Helper function for generating test wheels which are compliant by default.
@@ -376,9 +361,6 @@ def make_wheel(
:param entry_points:
:param record: if provided and None, then no RECORD file is generated;
else if a string then sets the content of the RECORD file
- :param record_callback: callback function that receives and can edit the
- records before they are written to RECORD, ignored if record is
- provided
"""
pythons = ["py2", "py3"]
abis = ["none"]
@@ -405,7 +387,7 @@ def make_wheel(
actual_files = filter(None, possible_files)
files_and_record_file = record_file_maker_wrapper(
- name, version, actual_files, record, record_callback
+ name, version, actual_files, record
)
wheel_file_name = wheel_name(name, version, pythons, abis, platforms)
diff --git a/tests/requirements-common_wheels.txt b/tests/requirements-common_wheels.txt
index f0edf0b02..6403ed738 100644
--- a/tests/requirements-common_wheels.txt
+++ b/tests/requirements-common_wheels.txt
@@ -5,7 +5,7 @@
# 4. Replacing the `setuptools` entry below with a `file:///...` URL
# (Adjust artifact directory used based on preference and operating system)
-setuptools >= 40.8.0
+setuptools >= 40.8.0, != 60.6.0
wheel
# As required by pytest-cov.
coverage >= 4.4
diff --git a/tests/requirements.txt b/tests/requirements.txt
index ee453e073..9ce6d6207 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,6 +1,6 @@
cryptography
freezegun
-pretend
+installer
pytest
pytest-cov
pytest-rerunfailures
@@ -10,4 +10,4 @@ setuptools
virtualenv < 20.0
werkzeug
wheel
-toml
+tomli-w
diff --git a/tests/unit/metadata/test_metadata.py b/tests/unit/metadata/test_metadata.py
new file mode 100644
index 000000000..f77178fb9
--- /dev/null
+++ b/tests/unit/metadata/test_metadata.py
@@ -0,0 +1,131 @@
+import logging
+import os
+from pathlib import Path
+from typing import cast
+from unittest import mock
+
+import pytest
+from pip._vendor.packaging.utils import NormalizedName
+
+from pip._internal.metadata import (
+ BaseDistribution,
+ get_directory_distribution,
+ get_environment,
+ get_wheel_distribution,
+)
+from pip._internal.metadata.base import FilesystemWheel
+from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, ArchiveInfo
+from tests.lib.wheel import make_wheel
+
+
+@mock.patch.object(BaseDistribution, "read_text", side_effect=FileNotFoundError)
+def test_dist_get_direct_url_no_metadata(mock_read_text: mock.Mock) -> None:
+ class FakeDistribution(BaseDistribution):
+ pass
+
+ dist = FakeDistribution()
+ assert dist.direct_url is None
+ mock_read_text.assert_called_once_with(DIRECT_URL_METADATA_NAME)
+
+
+@mock.patch.object(BaseDistribution, "read_text", return_value="{}")
+def test_dist_get_direct_url_invalid_json(
+ mock_read_text: mock.Mock, caplog: pytest.LogCaptureFixture
+) -> None:
+ class FakeDistribution(BaseDistribution):
+ canonical_name = cast(NormalizedName, "whatever") # Needed for error logging.
+
+ dist = FakeDistribution()
+ with caplog.at_level(logging.WARNING):
+ assert dist.direct_url is None
+
+ mock_read_text.assert_called_once_with(DIRECT_URL_METADATA_NAME)
+ assert (
+ caplog.records[-1]
+ .getMessage()
+ .startswith(
+ "Error parsing direct_url.json for whatever:",
+ )
+ )
+
+
+def test_metadata_reads_egg_info_requires_txt(tmp_path: Path) -> None:
+ """Check Requires-Dist is obtained from requires.txt if absent in PKG-INFO."""
+ egg_info_path = tmp_path / "whatever.egg-info"
+ egg_info_path.mkdir()
+ dist = get_directory_distribution(str(egg_info_path))
+ assert dist.installed_with_setuptools_egg_info
+ pkg_info_path = egg_info_path / "PKG-INFO"
+ pkg_info_path.write_text("Name: whatever\n")
+ egg_info_path.joinpath("requires.txt").write_text("pkga\npkgb\n")
+ assert dist.metadata.get_all("Requires-Dist") == ["pkga", "pkgb"]
+
+
+def test_metadata_pkg_info_requires_priority(tmp_path: Path) -> None:
+ """Check Requires-Dist in PKG-INFO has priority over requires.txt."""
+ egg_info_path = tmp_path / "whatever.egg-info"
+ egg_info_path.mkdir()
+ dist = get_directory_distribution(str(egg_info_path))
+ assert dist.installed_with_setuptools_egg_info
+ pkg_info_path = egg_info_path / "PKG-INFO"
+ pkg_info_path.write_text(
+ "Name: whatever\nRequires-Dist: pkgc\nRequires-Dist: pkgd\n"
+ )
+ egg_info_path.joinpath("requires.txt").write_text("pkga\npkgb\n")
+ assert dist.metadata.get_all("Requires-Dist") == ["pkgc", "pkgd"]
+
+
+@mock.patch.object(
+ BaseDistribution,
+ "read_text",
+ return_value='{"url": "https://e.c/p.tgz", "archive_info": {}}',
+)
+def test_dist_get_direct_url_valid_metadata(mock_read_text: mock.Mock) -> None:
+ class FakeDistribution(BaseDistribution):
+ pass
+
+ dist = FakeDistribution()
+ direct_url = dist.direct_url
+ assert direct_url is not None
+ mock_read_text.assert_called_once_with(DIRECT_URL_METADATA_NAME)
+ assert direct_url.url == "https://e.c/p.tgz"
+ assert isinstance(direct_url.info, ArchiveInfo)
+
+
+def test_metadata_dict(tmp_path: Path) -> None:
+ """Basic test of BaseDistribution metadata_dict.
+
+ More tests are available in the original pkg_metadata project where this
+ function comes from, and which we may vendor in the future.
+ """
+ wheel_path = make_wheel(name="pkga", version="1.0.1").save_to_dir(tmp_path)
+ wheel = FilesystemWheel(wheel_path)
+ dist = get_wheel_distribution(wheel, "pkga")
+ metadata_dict = dist.metadata_dict
+ assert metadata_dict["name"] == "pkga"
+ assert metadata_dict["version"] == "1.0.1"
+
+
+def test_no_dist_found_in_wheel(tmp_path: Path) -> None:
+ location = os.fspath(tmp_path.joinpath("pkg-1-py3-none-any.whl"))
+ make_wheel(name="pkg", version="1").save_to(location)
+ assert get_environment([location]).get_distribution("pkg") is None
+
+
+def test_dist_found_in_directory_named_whl(tmp_path: Path) -> None:
+ dir_path = tmp_path.joinpath("pkg-1-py3-none-any.whl")
+ info_path = dir_path.joinpath("pkg-1.dist-info")
+ info_path.mkdir(parents=True)
+ info_path.joinpath("METADATA").write_text("Name: pkg")
+ location = os.fspath(dir_path)
+ dist = get_environment([location]).get_distribution("pkg")
+ assert dist is not None and dist.location is not None
+ assert Path(dist.location) == Path(location)
+
+
+def test_dist_found_in_zip(tmp_path: Path) -> None:
+ location = os.fspath(tmp_path.joinpath("pkg.zip"))
+ make_wheel(name="pkg", version="1").save_to(location)
+ dist = get_environment([location]).get_distribution("pkg")
+ assert dist is not None and dist.location is not None
+ assert Path(dist.location) == Path(location)
diff --git a/tests/unit/metadata/test_metadata_pkg_resources.py b/tests/unit/metadata/test_metadata_pkg_resources.py
new file mode 100644
index 000000000..ab1a56107
--- /dev/null
+++ b/tests/unit/metadata/test_metadata_pkg_resources.py
@@ -0,0 +1,123 @@
+import email.message
+import itertools
+from typing import List, cast
+from unittest import mock
+
+import pytest
+from pip._vendor.packaging.specifiers import SpecifierSet
+from pip._vendor.packaging.version import parse as parse_version
+
+from pip._internal.exceptions import UnsupportedWheel
+from pip._internal.metadata.pkg_resources import (
+ Distribution,
+ Environment,
+ InMemoryMetadata,
+)
+
+pkg_resources = pytest.importorskip("pip._vendor.pkg_resources")
+
+
+def _dist_is_local(dist: mock.Mock) -> bool:
+ return dist.kind != "global" and dist.kind != "user"
+
+
+def _dist_in_usersite(dist: mock.Mock) -> bool:
+ return dist.kind == "user"
+
+
+@pytest.fixture(autouse=True)
+def patch_distribution_lookups(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr(Distribution, "local", property(_dist_is_local))
+ monkeypatch.setattr(Distribution, "in_usersite", property(_dist_in_usersite))
+
+
+class _MockWorkingSet(List[mock.Mock]):
+ def require(self, name: str) -> None:
+ pass
+
+
+workingset = _MockWorkingSet(
+ (
+ mock.Mock(test_name="global", project_name="global"),
+ mock.Mock(test_name="editable", project_name="editable"),
+ mock.Mock(test_name="normal", project_name="normal"),
+ mock.Mock(test_name="user", project_name="user"),
+ )
+)
+
+workingset_stdlib = _MockWorkingSet(
+ (
+ mock.Mock(test_name="normal", project_name="argparse"),
+ mock.Mock(test_name="normal", project_name="wsgiref"),
+ )
+)
+
+
+@pytest.mark.parametrize(
+ "ws, req_name",
+ itertools.chain(
+ itertools.product(
+ [workingset],
+ (d.project_name for d in workingset),
+ ),
+ itertools.product(
+ [workingset_stdlib],
+ (d.project_name for d in workingset_stdlib),
+ ),
+ ),
+)
+def test_get_distribution(ws: _MockWorkingSet, req_name: str) -> None:
+ """Ensure get_distribution() finds all kinds of distributions."""
+ dist = Environment(ws).get_distribution(req_name)
+ assert dist is not None
+ assert cast(Distribution, dist)._dist.project_name == req_name
+
+
+def test_get_distribution_nonexist() -> None:
+ dist = Environment(workingset).get_distribution("non-exist")
+ assert dist is None
+
+
+def test_wheel_metadata_works() -> None:
+ name = "simple"
+ version = "0.1.0"
+ require_a = "a==1.0"
+ require_b = 'b==1.1; extra == "also_b"'
+ requires = [require_a, require_b, 'c==1.2; extra == "also_c"']
+ extras = ["also_b", "also_c"]
+ requires_python = ">=3"
+
+ metadata = email.message.Message()
+ metadata["Name"] = name
+ metadata["Version"] = version
+ for require in requires:
+ metadata["Requires-Dist"] = require
+ for extra in extras:
+ metadata["Provides-Extra"] = extra
+ metadata["Requires-Python"] = requires_python
+
+ dist = Distribution(
+ pkg_resources.DistInfoDistribution(
+ location="<in-memory>",
+ metadata=InMemoryMetadata({"METADATA": metadata.as_bytes()}, "<in-memory>"),
+ project_name=name,
+ ),
+ )
+
+ assert name == dist.canonical_name == dist.raw_name
+ assert parse_version(version) == dist.version
+ assert set(extras) == set(dist.iter_provided_extras())
+ assert [require_a] == [str(r) for r in dist.iter_dependencies()]
+ assert [require_a, require_b] == [
+ str(r) for r in dist.iter_dependencies(["also_b"])
+ ]
+ assert metadata.as_string() == dist.metadata.as_string()
+ assert SpecifierSet(requires_python) == dist.requires_python
+
+
+def test_wheel_metadata_throws_on_bad_unicode() -> None:
+ metadata = InMemoryMetadata({"METADATA": b"\xff"}, "<in-memory>")
+
+ with pytest.raises(UnsupportedWheel) as e:
+ metadata.get_metadata("METADATA")
+ assert "METADATA" in str(e.value)
diff --git a/tests/unit/resolution_resolvelib/__init__.py b/tests/unit/resolution_resolvelib/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/unit/resolution_resolvelib/__init__.py
diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py
index f77b98ee1..a4ee32444 100644
--- a/tests/unit/resolution_resolvelib/conftest.py
+++ b/tests/unit/resolution_resolvelib/conftest.py
@@ -1,3 +1,5 @@
+from typing import Iterator
+
import pytest
from pip._internal.cli.req_command import RequirementCommand
@@ -9,17 +11,19 @@ from pip._internal.index.package_finder import PackageFinder
from pip._internal.models.search_scope import SearchScope
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.network.session import PipSession
+from pip._internal.operations.build.build_tracker import get_build_tracker
+from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req.constructors import install_req_from_line
-from pip._internal.req.req_tracker import get_requirement_tracker
from pip._internal.resolution.resolvelib.factory import Factory
from pip._internal.resolution.resolvelib.provider import PipProvider
from pip._internal.utils.temp_dir import TempDirectory, global_tempdir_manager
+from tests.lib import TestData
@pytest.fixture
-def finder(data):
+def finder(data: TestData) -> Iterator[PackageFinder]:
session = PipSession()
- scope = SearchScope([str(data.packages)], [])
+ scope = SearchScope([str(data.packages)], [], False)
collector = LinkCollector(session, scope)
prefs = SelectionPreferences(allow_yanked=False)
finder = PackageFinder.create(collector, prefs)
@@ -27,28 +31,29 @@ def finder(data):
@pytest.fixture
-def preparer(finder):
+def preparer(finder: PackageFinder) -> Iterator[RequirementPreparer]:
session = PipSession()
rc = InstallCommand("x", "y")
o = rc.parse_args([])
with global_tempdir_manager():
with TempDirectory() as tmp:
- with get_requirement_tracker() as tracker:
+ with get_build_tracker() as tracker:
preparer = RequirementCommand.make_requirement_preparer(
tmp,
options=o[0],
- req_tracker=tracker,
+ build_tracker=tracker,
session=session,
finder=finder,
- use_user_site=False
+ use_user_site=False,
+ verbosity=0,
)
yield preparer
@pytest.fixture
-def factory(finder, preparer):
+def factory(finder: PackageFinder, preparer: RequirementPreparer) -> Iterator[Factory]:
yield Factory(
finder=finder,
preparer=preparer,
@@ -63,7 +68,7 @@ def factory(finder, preparer):
@pytest.fixture
-def provider(factory):
+def provider(factory: Factory) -> Iterator[PipProvider]:
yield PipProvider(
factory=factory,
constraints={},
diff --git a/tests/unit/resolution_resolvelib/test_provider.py b/tests/unit/resolution_resolvelib/test_provider.py
new file mode 100644
index 000000000..ab1dc74ca
--- /dev/null
+++ b/tests/unit/resolution_resolvelib/test_provider.py
@@ -0,0 +1,78 @@
+from typing import TYPE_CHECKING, List, Optional
+
+from pip._vendor.resolvelib.resolvers import RequirementInformation
+
+from pip._internal.models.candidate import InstallationCandidate
+from pip._internal.models.link import Link
+from pip._internal.req.constructors import install_req_from_req_string
+from pip._internal.resolution.resolvelib.factory import Factory
+from pip._internal.resolution.resolvelib.provider import PipProvider
+from pip._internal.resolution.resolvelib.requirements import SpecifierRequirement
+
+if TYPE_CHECKING:
+ from pip._internal.resolution.resolvelib.provider import PreferenceInformation
+
+
+def build_requirement_information(
+ name: str, parent: Optional[InstallationCandidate]
+) -> List["PreferenceInformation"]:
+ install_requirement = install_req_from_req_string(name)
+ # RequirementInformation is typed as a tuple, but it is a namedtupled.
+ # https://github.com/sarugaku/resolvelib/blob/7bc025aa2a4e979597c438ad7b17d2e8a08a364e/src/resolvelib/resolvers.pyi#L20-L22
+ requirement_information: "PreferenceInformation" = RequirementInformation(
+ requirement=SpecifierRequirement(install_requirement), # type: ignore[call-arg]
+ parent=parent,
+ )
+ return [requirement_information]
+
+
+def test_provider_known_depths(factory: Factory) -> None:
+ # Root requirement is specified by the user
+ # therefore has an inferred depth of 1
+ root_requirement_name = "my-package"
+ provider = PipProvider(
+ factory=factory,
+ constraints={},
+ ignore_dependencies=False,
+ upgrade_strategy="to-satisfy-only",
+ user_requested={root_requirement_name: 0},
+ )
+
+ root_requirement_information = build_requirement_information(
+ name=root_requirement_name, parent=None
+ )
+ provider.get_preference(
+ identifier=root_requirement_name,
+ resolutions={},
+ candidates={},
+ information={root_requirement_name: root_requirement_information},
+ backtrack_causes=[],
+ )
+ assert provider._known_depths == {root_requirement_name: 1.0}
+
+ # Transative requirement is a dependency of root requirement
+ # theforefore has an inferred depth of 2
+ root_package_candidate = InstallationCandidate(
+ root_requirement_name,
+ "1.0",
+ Link("https://{root_requirement_name}.com"),
+ )
+ transative_requirement_name = "my-transitive-package"
+
+ transative_package_information = build_requirement_information(
+ name=transative_requirement_name, parent=root_package_candidate
+ )
+ provider.get_preference(
+ identifier=transative_requirement_name,
+ resolutions={},
+ candidates={},
+ information={
+ root_requirement_name: root_requirement_information,
+ transative_requirement_name: transative_package_information,
+ },
+ backtrack_causes=[],
+ )
+ assert provider._known_depths == {
+ transative_requirement_name: 2.0,
+ root_requirement_name: 1.0,
+ }
diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py
index 1f7b0c53d..6864e70ea 100644
--- a/tests/unit/resolution_resolvelib/test_requirement.py
+++ b/tests/unit/resolution_resolvelib/test_requirement.py
@@ -1,8 +1,14 @@
+import os
+from pathlib import Path
+from typing import Iterator, List, Tuple
+
import pytest
from pip._vendor.resolvelib import BaseReporter, Resolver
-from pip._internal.resolution.resolvelib.base import Candidate, Constraint
-from pip._internal.utils.urls import path_to_url
+from pip._internal.resolution.resolvelib.base import Candidate, Constraint, Requirement
+from pip._internal.resolution.resolvelib.factory import Factory
+from pip._internal.resolution.resolvelib.provider import PipProvider
+from tests.lib import TestData
# NOTE: All tests are prefixed `test_rlr` (for "test resolvelib resolver").
# This helps select just these tests using pytest's `-k` option, and
@@ -18,12 +24,15 @@ from pip._internal.utils.urls import path_to_url
@pytest.fixture
-def test_cases(data):
- def data_file(name):
+def test_cases(data: TestData) -> Iterator[List[Tuple[str, str, int]]]:
+ def _data_file(name: str) -> Path:
return data.packages.joinpath(name)
- def data_url(name):
- return path_to_url(data_file(name))
+ def data_file(name: str) -> str:
+ return os.fspath(_data_file(name))
+
+ def data_url(name: str) -> str:
+ return _data_file(name).as_uri()
test_cases = [
# requirement, name, matches
@@ -47,17 +56,23 @@ def test_cases(data):
yield test_cases
-def test_new_resolver_requirement_has_name(test_cases, factory):
+def test_new_resolver_requirement_has_name(
+ test_cases: List[Tuple[str, str, int]], factory: Factory
+) -> None:
"""All requirements should have a name"""
for spec, name, _ in test_cases:
req = factory.make_requirement_from_spec(spec, comes_from=None)
+ assert req is not None
assert req.name == name
-def test_new_resolver_correct_number_of_matches(test_cases, factory):
+def test_new_resolver_correct_number_of_matches(
+ test_cases: List[Tuple[str, str, int]], factory: Factory
+) -> None:
"""Requirements should return the correct number of candidates"""
for spec, _, match_count in test_cases:
req = factory.make_requirement_from_spec(spec, comes_from=None)
+ assert req is not None
matches = factory.find_candidates(
req.name,
{req.name: [req]},
@@ -68,11 +83,13 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory):
assert sum(1 for _ in matches) == match_count
-def test_new_resolver_candidates_match_requirement(test_cases, factory):
- """Candidates returned from find_candidates should satisfy the requirement
- """
+def test_new_resolver_candidates_match_requirement(
+ test_cases: List[Tuple[str, str, int]], factory: Factory
+) -> None:
+ """Candidates returned from find_candidates should satisfy the requirement"""
for spec, _, _ in test_cases:
req = factory.make_requirement_from_spec(spec, comes_from=None)
+ assert req is not None
candidates = factory.find_candidates(
req.name,
{req.name: [req]},
@@ -85,9 +102,10 @@ def test_new_resolver_candidates_match_requirement(test_cases, factory):
assert req.is_satisfied_by(c)
-def test_new_resolver_full_resolve(factory, provider):
+def test_new_resolver_full_resolve(factory: Factory, provider: PipProvider) -> None:
"""A very basic full resolve"""
req = factory.make_requirement_from_spec("simplewheel", comes_from=None)
- r = Resolver(provider, BaseReporter())
+ assert req is not None
+ r: Resolver[Requirement, Candidate, str] = Resolver(provider, BaseReporter())
result = r.resolve([req])
- assert set(result.mapping.keys()) == {'simplewheel'}
+ assert set(result.mapping.keys()) == {"simplewheel"}
diff --git a/tests/unit/resolution_resolvelib/test_resolver.py b/tests/unit/resolution_resolvelib/test_resolver.py
index b9772fb17..87c2b5f35 100644
--- a/tests/unit/resolution_resolvelib/test_resolver.py
+++ b/tests/unit/resolution_resolvelib/test_resolver.py
@@ -1,3 +1,4 @@
+from typing import Dict, List, Optional, Set, Tuple, cast
from unittest import mock
import pytest
@@ -5,6 +6,8 @@ from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.resolvelib.resolvers import Result
from pip._vendor.resolvelib.structs import DirectedGraph
+from pip._internal.index.package_finder import PackageFinder
+from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req.constructors import install_req_from_line
from pip._internal.req.req_set import RequirementSet
from pip._internal.resolution.resolvelib.resolver import (
@@ -14,30 +17,31 @@ from pip._internal.resolution.resolvelib.resolver import (
@pytest.fixture()
-def resolver(preparer, finder):
+def resolver(preparer: RequirementPreparer, finder: PackageFinder) -> Resolver:
resolver = Resolver(
preparer=preparer,
finder=finder,
wheel_cache=None,
make_install_req=mock.Mock(),
- use_user_site="not-used",
- ignore_dependencies="not-used",
- ignore_installed="not-used",
- ignore_requires_python="not-used",
- force_reinstall="not-used",
+ use_user_site=False,
+ ignore_dependencies=False,
+ ignore_installed=False,
+ ignore_requires_python=False,
+ force_reinstall=False,
upgrade_strategy="to-satisfy-only",
)
return resolver
-def _make_graph(edges):
- """Build graph from edge declarations.
- """
+def _make_graph(
+ edges: List[Tuple[Optional[str], Optional[str]]]
+) -> "DirectedGraph[Optional[str]]":
+ """Build graph from edge declarations."""
- graph = DirectedGraph()
+ graph: "DirectedGraph[Optional[str]]" = DirectedGraph()
for parent, child in edges:
- parent = canonicalize_name(parent) if parent else None
- child = canonicalize_name(child) if child else None
+ parent = cast(str, canonicalize_name(parent)) if parent else None
+ child = cast(str, canonicalize_name(child)) if child else None
for v in (parent, child):
if v not in graph:
graph.add(v)
@@ -77,12 +81,16 @@ def _make_graph(edges):
),
],
)
-def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
+def test_new_resolver_get_installation_order(
+ resolver: Resolver,
+ edges: List[Tuple[Optional[str], Optional[str]]],
+ ordered_reqs: List[str],
+) -> None:
graph = _make_graph(edges)
# Mapping values and criteria are not used in test, so we stub them out.
mapping = {vertex: None for vertex in graph if vertex is not None}
- resolver._result = Result(mapping, graph, criteria=None)
+ resolver._result = Result(mapping, graph, criteria=None) # type: ignore
reqset = RequirementSet()
for r in ordered_reqs:
@@ -94,7 +102,7 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
@pytest.mark.parametrize(
- "name, edges, expected_weights",
+ "name, edges, requirement_keys, expected_weights",
[
(
# From https://github.com/pypa/pip/pull/8127#discussion_r414564664
@@ -107,7 +115,8 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
("three", "four"),
("four", "five"),
],
- {None: 0, "one": 1, "two": 1, "three": 2, "four": 3, "five": 4},
+ {"one", "two", "three", "four", "five"},
+ {"five": 5, "four": 4, "one": 4, "three": 2, "two": 1},
),
(
"linear",
@@ -118,7 +127,20 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
("three", "four"),
("four", "five"),
],
- {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
+ {"one", "two", "three", "four", "five"},
+ {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
+ ),
+ (
+ "linear AND restricted",
+ [
+ (None, "one"),
+ ("one", "two"),
+ ("two", "three"),
+ ("three", "four"),
+ ("four", "five"),
+ ],
+ {"one", "three", "five"},
+ {"one": 1, "three": 3, "five": 5},
),
(
"linear AND root -> two",
@@ -130,7 +152,8 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
("four", "five"),
(None, "two"),
],
- {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
+ {"one", "two", "three", "four", "five"},
+ {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
),
(
"linear AND root -> three",
@@ -142,7 +165,8 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
("four", "five"),
(None, "three"),
],
- {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
+ {"one", "two", "three", "four", "five"},
+ {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
),
(
"linear AND root -> four",
@@ -154,7 +178,8 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
("four", "five"),
(None, "four"),
],
- {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
+ {"one", "two", "three", "four", "five"},
+ {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
),
(
"linear AND root -> five",
@@ -166,7 +191,8 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
("four", "five"),
(None, "five"),
],
- {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
+ {"one", "two", "three", "four", "five"},
+ {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
),
(
"linear AND one -> four",
@@ -178,7 +204,8 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
("four", "five"),
("one", "four"),
],
- {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
+ {"one", "two", "three", "four", "five"},
+ {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
),
(
"linear AND two -> four",
@@ -190,7 +217,8 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
("four", "five"),
("two", "four"),
],
- {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
+ {"one", "two", "three", "four", "five"},
+ {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
),
(
"linear AND four -> one (cycle)",
@@ -202,7 +230,8 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
("four", "five"),
("four", "one"),
],
- {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
+ {"one", "two", "three", "four", "five"},
+ {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
),
(
"linear AND four -> two (cycle)",
@@ -214,7 +243,8 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
("four", "five"),
("four", "two"),
],
- {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
+ {"one", "two", "three", "four", "five"},
+ {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
),
(
"linear AND four -> three (cycle)",
@@ -226,12 +256,44 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs):
("four", "five"),
("four", "three"),
],
- {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
+ {"one", "two", "three", "four", "five"},
+ {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5},
+ ),
+ (
+ "linear AND four -> three (cycle) AND restricted 1-2-3",
+ [
+ (None, "one"),
+ ("one", "two"),
+ ("two", "three"),
+ ("three", "four"),
+ ("four", "five"),
+ ("four", "three"),
+ ],
+ {"one", "two", "three"},
+ {"one": 1, "two": 2, "three": 3},
+ ),
+ (
+ "linear AND four -> three (cycle) AND restricted 4-5",
+ [
+ (None, "one"),
+ ("one", "two"),
+ ("two", "three"),
+ ("three", "four"),
+ ("four", "five"),
+ ("four", "three"),
+ ],
+ {"four", "five"},
+ {"four": 4, "five": 5},
),
],
)
-def test_new_resolver_topological_weights(name, edges, expected_weights):
+def test_new_resolver_topological_weights(
+ name: str,
+ edges: List[Tuple[Optional[str], Optional[str]]],
+ requirement_keys: Set[str],
+ expected_weights: Dict[Optional[str], int],
+) -> None:
graph = _make_graph(edges)
- weights = get_topological_weights(graph, len(expected_weights))
+ weights = get_topological_weights(graph, requirement_keys)
assert weights == expected_weights
diff --git a/tests/unit/test_appdirs.py b/tests/unit/test_appdirs.py
index 623486b28..fd3ea143b 100644
--- a/tests/unit/test_appdirs.py
+++ b/tests/unit/test_appdirs.py
@@ -1,229 +1,220 @@
-import ntpath
+# mypy: no-warn-unused-ignores
+
import os
-import posixpath
import sys
+from unittest import mock
-import pretend
-from pip._vendor import appdirs as _appdirs
+import pytest
+from pip._vendor import platformdirs
from pip._internal.utils import appdirs
class TestUserCacheDir:
-
- def test_user_cache_dir_win(self, monkeypatch):
- @pretend.call_recorder
- def _get_win_folder(base):
- return "C:\\Users\\test\\AppData\\Local"
+ @pytest.mark.skipif(sys.platform != "win32", reason="Windows-only test")
+ def test_user_cache_dir_win(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ _get_win_folder = mock.Mock(return_value="C:\\Users\\test\\AppData\\Local")
monkeypatch.setattr(
- _appdirs,
- "_get_win_folder",
+ platformdirs.windows, # type: ignore
+ "get_win_folder",
_get_win_folder,
raising=False,
)
- monkeypatch.setattr(_appdirs, "system", "win32")
- monkeypatch.setattr(os, "path", ntpath)
- assert (appdirs.user_cache_dir("pip") ==
- "C:\\Users\\test\\AppData\\Local\\pip\\Cache")
- assert _get_win_folder.calls == [pretend.call("CSIDL_LOCAL_APPDATA")]
+ assert (
+ appdirs.user_cache_dir("pip")
+ == "C:\\Users\\test\\AppData\\Local\\pip\\Cache"
+ )
+ assert _get_win_folder.call_args_list == [mock.call("CSIDL_LOCAL_APPDATA")]
- def test_user_cache_dir_osx(self, monkeypatch):
- monkeypatch.setattr(_appdirs, "system", "darwin")
- monkeypatch.setattr(os, "path", posixpath)
+ @pytest.mark.skipif(sys.platform != "darwin", reason="MacOS-only test")
+ def test_user_cache_dir_osx(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("HOME", "/home/test")
- monkeypatch.setattr(sys, "platform", "darwin")
assert appdirs.user_cache_dir("pip") == "/home/test/Library/Caches/pip"
- def test_user_cache_dir_linux(self, monkeypatch):
- monkeypatch.setattr(_appdirs, "system", "linux2")
- monkeypatch.setattr(os, "path", posixpath)
+ @pytest.mark.skipif(sys.platform != "linux", reason="Linux-only test")
+ def test_user_cache_dir_linux(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("XDG_CACHE_HOME", raising=False)
monkeypatch.setenv("HOME", "/home/test")
- monkeypatch.setattr(sys, "platform", "linux2")
assert appdirs.user_cache_dir("pip") == "/home/test/.cache/pip"
- def test_user_cache_dir_linux_override(self, monkeypatch):
- monkeypatch.setattr(_appdirs, "system", "linux2")
- monkeypatch.setattr(os, "path", posixpath)
+ @pytest.mark.skipif(sys.platform != "linux", reason="Linux-only test")
+ def test_user_cache_dir_linux_override(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
monkeypatch.setenv("XDG_CACHE_HOME", "/home/test/.other-cache")
monkeypatch.setenv("HOME", "/home/test")
- monkeypatch.setattr(sys, "platform", "linux2")
assert appdirs.user_cache_dir("pip") == "/home/test/.other-cache/pip"
- def test_user_cache_dir_linux_home_slash(self, monkeypatch):
- monkeypatch.setattr(_appdirs, "system", "linux2")
- monkeypatch.setattr(os, "path", posixpath)
+ @pytest.mark.skipif(sys.platform != "linux", reason="Linux-only test")
+ def test_user_cache_dir_linux_home_slash(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
# Verify that we are not affected by https://bugs.python.org/issue14768
monkeypatch.delenv("XDG_CACHE_HOME", raising=False)
monkeypatch.setenv("HOME", "/")
- monkeypatch.setattr(sys, "platform", "linux2")
assert appdirs.user_cache_dir("pip") == "/.cache/pip"
- def test_user_cache_dir_unicode(self, monkeypatch):
- if sys.platform != 'win32':
+ def test_user_cache_dir_unicode(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ if sys.platform != "win32":
return
- def my_get_win_folder(csidl_name):
+ def my_get_win_folder(csidl_name: str) -> str:
return "\u00DF\u00E4\u03B1\u20AC"
- monkeypatch.setattr(_appdirs, "_get_win_folder", my_get_win_folder)
+ monkeypatch.setattr(
+ platformdirs.windows, # type: ignore
+ "get_win_folder",
+ my_get_win_folder,
+ )
# Do not use the isinstance expression directly in the
# assert statement, as the Unicode characters in the result
# cause pytest to fail with an internal error on Python 2.7
- result_is_str = isinstance(appdirs.user_cache_dir('test'), str)
+ result_is_str = isinstance(appdirs.user_cache_dir("test"), str)
assert result_is_str, "user_cache_dir did not return a str"
# Test against regression #3463
from pip._internal.cli.main_parser import create_main_parser
+
create_main_parser().print_help() # This should not crash
class TestSiteConfigDirs:
-
- def test_site_config_dirs_win(self, monkeypatch):
- @pretend.call_recorder
- def _get_win_folder(base):
- return "C:\\ProgramData"
+ @pytest.mark.skipif(sys.platform != "win32", reason="Windows-only test")
+ def test_site_config_dirs_win(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ _get_win_folder = mock.Mock(return_value="C:\\ProgramData")
monkeypatch.setattr(
- _appdirs,
- "_get_win_folder",
+ platformdirs.windows, # type: ignore
+ "get_win_folder",
_get_win_folder,
raising=False,
)
- monkeypatch.setattr(_appdirs, "system", "win32")
- monkeypatch.setattr(os, "path", ntpath)
assert appdirs.site_config_dirs("pip") == ["C:\\ProgramData\\pip"]
- assert _get_win_folder.calls == [pretend.call("CSIDL_COMMON_APPDATA")]
+ assert _get_win_folder.call_args_list == [mock.call("CSIDL_COMMON_APPDATA")]
- def test_site_config_dirs_osx(self, monkeypatch):
- monkeypatch.setattr(_appdirs, "system", "darwin")
- monkeypatch.setattr(os, "path", posixpath)
+ @pytest.mark.skipif(sys.platform != "darwin", reason="MacOS-only test")
+ def test_site_config_dirs_osx(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("HOME", "/home/test")
- monkeypatch.setattr(sys, "platform", "darwin")
- assert appdirs.site_config_dirs("pip") == \
- ["/Library/Application Support/pip"]
+ assert appdirs.site_config_dirs("pip") == [
+ "/Library/Application Support/pip",
+ ]
- def test_site_config_dirs_linux(self, monkeypatch):
- monkeypatch.setattr(_appdirs, "system", "linux2")
- monkeypatch.setattr(os, "path", posixpath)
+ @pytest.mark.skipif(sys.platform != "linux", reason="Linux-only test")
+ def test_site_config_dirs_linux(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("XDG_CONFIG_DIRS", raising=False)
- monkeypatch.setattr(sys, "platform", "linux2")
assert appdirs.site_config_dirs("pip") == [
- '/etc/xdg/pip',
- '/etc'
+ "/etc/xdg/pip",
+ "/etc",
]
- def test_site_config_dirs_linux_override(self, monkeypatch):
- monkeypatch.setattr(_appdirs, "system", "linux2")
- monkeypatch.setattr(os, "path", posixpath)
- monkeypatch.setattr(os, "pathsep", ':')
+ @pytest.mark.skipif(sys.platform != "linux", reason="Linux-only test")
+ def test_site_config_dirs_linux_override(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ monkeypatch.setattr(os, "pathsep", ":")
monkeypatch.setenv("XDG_CONFIG_DIRS", "/spam:/etc:/etc/xdg")
- monkeypatch.setattr(sys, "platform", "linux2")
assert appdirs.site_config_dirs("pip") == [
- '/spam/pip',
- '/etc/pip',
- '/etc/xdg/pip',
- '/etc'
+ "/spam/pip",
+ "/etc/pip",
+ "/etc/xdg/pip",
+ "/etc",
]
- def test_site_config_dirs_linux_empty(self, monkeypatch):
- monkeypatch.setattr(_appdirs, "system", "linux2")
- monkeypatch.setattr(os, "path", posixpath)
- monkeypatch.setattr(os, "pathsep", ':')
+ @pytest.mark.skipif(sys.platform != "linux", reason="Linux-only test")
+ def test_site_config_dirs_linux_empty(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ monkeypatch.setattr(os, "pathsep", ":")
monkeypatch.setenv("XDG_CONFIG_DIRS", "")
- monkeypatch.setattr(sys, "platform", "linux2")
- assert appdirs.site_config_dirs("pip") == ['/etc/xdg/pip', '/etc']
+ assert appdirs.site_config_dirs("pip") == [
+ "/etc/xdg/pip",
+ "/etc",
+ ]
class TestUserConfigDir:
-
- def test_user_config_dir_win_no_roaming(self, monkeypatch):
- @pretend.call_recorder
- def _get_win_folder(base):
- return "C:\\Users\\test\\AppData\\Local"
+ @pytest.mark.skipif(sys.platform != "win32", reason="Windows-only test")
+ def test_user_config_dir_win_no_roaming(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ _get_win_folder = mock.Mock(return_value="C:\\Users\\test\\AppData\\Local")
monkeypatch.setattr(
- _appdirs,
- "_get_win_folder",
+ platformdirs.windows, # type: ignore
+ "get_win_folder",
_get_win_folder,
raising=False,
)
- monkeypatch.setattr(_appdirs, "system", "win32")
- monkeypatch.setattr(os, "path", ntpath)
assert (
- appdirs.user_config_dir("pip", roaming=False) ==
- "C:\\Users\\test\\AppData\\Local\\pip"
+ appdirs.user_config_dir("pip", roaming=False)
+ == "C:\\Users\\test\\AppData\\Local\\pip"
)
- assert _get_win_folder.calls == [pretend.call("CSIDL_LOCAL_APPDATA")]
+ assert _get_win_folder.call_args_list == [mock.call("CSIDL_LOCAL_APPDATA")]
- def test_user_config_dir_win_yes_roaming(self, monkeypatch):
- @pretend.call_recorder
- def _get_win_folder(base):
- return "C:\\Users\\test\\AppData\\Roaming"
+ @pytest.mark.skipif(sys.platform != "win32", reason="Windows-only test")
+ def test_user_config_dir_win_yes_roaming(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ _get_win_folder = mock.Mock(return_value="C:\\Users\\test\\AppData\\Roaming")
monkeypatch.setattr(
- _appdirs,
- "_get_win_folder",
+ platformdirs.windows, # type: ignore
+ "get_win_folder",
_get_win_folder,
raising=False,
)
- monkeypatch.setattr(_appdirs, "system", "win32")
- monkeypatch.setattr(os, "path", ntpath)
- assert (appdirs.user_config_dir("pip") ==
- "C:\\Users\\test\\AppData\\Roaming\\pip")
- assert _get_win_folder.calls == [pretend.call("CSIDL_APPDATA")]
+ assert (
+ appdirs.user_config_dir("pip") == "C:\\Users\\test\\AppData\\Roaming\\pip"
+ )
+ assert _get_win_folder.call_args_list == [mock.call("CSIDL_APPDATA")]
- def test_user_config_dir_osx(self, monkeypatch):
- monkeypatch.setattr(_appdirs, "system", "darwin")
- monkeypatch.setattr(os, "path", posixpath)
+ @pytest.mark.skipif(sys.platform != "darwin", reason="MacOS-only test")
+ def test_user_config_dir_osx(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("HOME", "/home/test")
- monkeypatch.setattr(sys, "platform", "darwin")
- if os.path.isdir('/home/test/Library/Application Support/'):
- assert (appdirs.user_config_dir("pip") ==
- "/home/test/Library/Application Support/pip")
+ if os.path.isdir("/home/test/Library/Application Support/"):
+ assert (
+ appdirs.user_config_dir("pip")
+ == "/home/test/Library/Application Support/pip"
+ )
else:
- assert (appdirs.user_config_dir("pip") ==
- "/home/test/.config/pip")
+ assert appdirs.user_config_dir("pip") == "/home/test/.config/pip"
- def test_user_config_dir_linux(self, monkeypatch):
- monkeypatch.setattr(_appdirs, "system", "linux2")
- monkeypatch.setattr(os, "path", posixpath)
+ @pytest.mark.skipif(sys.platform != "linux", reason="Linux-only test")
+ def test_user_config_dir_linux(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
monkeypatch.setenv("HOME", "/home/test")
- monkeypatch.setattr(sys, "platform", "linux2")
assert appdirs.user_config_dir("pip") == "/home/test/.config/pip"
- def test_user_config_dir_linux_override(self, monkeypatch):
- monkeypatch.setattr(_appdirs, "system", "linux2")
- monkeypatch.setattr(os, "path", posixpath)
+ @pytest.mark.skipif(sys.platform != "linux", reason="Linux-only test")
+ def test_user_config_dir_linux_override(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
monkeypatch.setenv("XDG_CONFIG_HOME", "/home/test/.other-config")
monkeypatch.setenv("HOME", "/home/test")
- monkeypatch.setattr(sys, "platform", "linux2")
assert appdirs.user_config_dir("pip") == "/home/test/.other-config/pip"
- def test_user_config_dir_linux_home_slash(self, monkeypatch):
- monkeypatch.setattr(_appdirs, "system", "linux2")
- monkeypatch.setattr(os, "path", posixpath)
+ @pytest.mark.skipif(sys.platform != "linux", reason="Linux-only test")
+ def test_user_config_dir_linux_home_slash(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
# Verify that we are not affected by https://bugs.python.org/issue14768
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
monkeypatch.setenv("HOME", "/")
- monkeypatch.setattr(sys, "platform", "linux2")
assert appdirs.user_config_dir("pip") == "/.config/pip"
diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py
index fa16d2fd7..71a50fca6 100644
--- a/tests/unit/test_base_command.py
+++ b/tests/unit/test_base_command.py
@@ -1,5 +1,9 @@
import logging
import os
+import time
+from optparse import Values
+from pathlib import Path
+from typing import Callable, Iterator, List, NoReturn, Optional
from unittest.mock import Mock, patch
import pytest
@@ -12,28 +16,31 @@ from pip._internal.utils.temp_dir import TempDirectory
@pytest.fixture
-def fixed_time(utc):
- with patch('time.time', lambda: 1547704837.040001):
+def fixed_time() -> Iterator[None]:
+ with patch("time.time", lambda: 1547704837.040001 + time.timezone):
yield
class FakeCommand(Command):
- _name = 'fake'
+ _name = "fake"
- def __init__(self, run_func=None, error=False):
+ def __init__(
+ self, run_func: Optional[Callable[[], int]] = None, error: bool = False
+ ) -> None:
if error:
- def run_func():
+
+ def run_func() -> int:
raise SystemExit(1)
self.run_func = run_func
super().__init__(self._name, self._name)
- def main(self, args):
+ def main(self, args: List[str]) -> int:
args.append("--disable-pip-version-check")
return super().main(args)
- def run(self, options, args):
+ def run(self, options: Values, args: List[str]) -> int:
logging.getLogger("pip.tests").info("fake")
# Return SUCCESS from run if run_func is not provided
if self.run_func:
@@ -43,22 +50,21 @@ class FakeCommand(Command):
class FakeCommandWithUnicode(FakeCommand):
- _name = 'fake_unicode'
+ _name = "fake_unicode"
- def run(self, options, args):
+ def run(self, options: Values, args: List[str]) -> int:
logging.getLogger("pip.tests").info(b"bytes here \xE9")
- logging.getLogger("pip.tests").info(
- b"unicode here \xC3\xA9".decode("utf-8")
- )
+ logging.getLogger("pip.tests").info(b"unicode here \xC3\xA9".decode("utf-8"))
+ return SUCCESS
class TestCommand:
-
- def call_main(self, capsys, args):
+ def call_main(self, capsys: pytest.CaptureFixture[str], args: List[str]) -> str:
"""
Call command.main(), and return the command's stderr.
"""
- def raise_broken_stdout():
+
+ def raise_broken_stdout() -> NoReturn:
raise BrokenStdoutLoggingError()
cmd = FakeCommand(run_func=raise_broken_stdout)
@@ -68,26 +74,28 @@ class TestCommand:
return stderr
- def test_raise_broken_stdout(self, capsys):
+ def test_raise_broken_stdout(self, capsys: pytest.CaptureFixture[str]) -> None:
"""
Test raising BrokenStdoutLoggingError.
"""
stderr = self.call_main(capsys, [])
- assert stderr.rstrip() == 'ERROR: Pipe to stdout was broken'
+ assert stderr.rstrip() == "ERROR: Pipe to stdout was broken"
- def test_raise_broken_stdout__debug_logging(self, capsys):
+ def test_raise_broken_stdout__debug_logging(
+ self, capsys: pytest.CaptureFixture[str]
+ ) -> None:
"""
Test raising BrokenStdoutLoggingError with debug logging enabled.
"""
- stderr = self.call_main(capsys, ['-vv'])
+ stderr = self.call_main(capsys, ["-vv"])
- assert 'ERROR: Pipe to stdout was broken' in stderr
- assert 'Traceback (most recent call last):' in stderr
+ assert "ERROR: Pipe to stdout was broken" in stderr
+ assert "Traceback (most recent call last):" in stderr
-@patch('pip._internal.cli.req_command.Command.handle_pip_version_check')
-def test_handle_pip_version_check_called(mock_handle_version_check):
+@patch("pip._internal.cli.req_command.Command.handle_pip_version_check")
+def test_handle_pip_version_check_called(mock_handle_version_check: Mock) -> None:
"""
Check that Command.handle_pip_version_check() is called.
"""
@@ -96,54 +104,55 @@ def test_handle_pip_version_check_called(mock_handle_version_check):
mock_handle_version_check.assert_called_once()
-def test_log_command_success(fixed_time, tmpdir):
+def test_log_command_success(fixed_time: None, tmpdir: Path) -> None:
"""Test the --log option logs when command succeeds."""
cmd = FakeCommand()
- log_path = tmpdir.joinpath('log')
- cmd.main(['fake', '--log', log_path])
+ log_path = os.path.join(tmpdir, "log")
+ cmd.main(["fake", "--log", log_path])
with open(log_path) as f:
- assert f.read().rstrip() == '2019-01-17T06:00:37,040 fake'
+ assert f.read().rstrip() == "2019-01-17T06:00:37,040 fake"
-def test_log_command_error(fixed_time, tmpdir):
+def test_log_command_error(fixed_time: None, tmpdir: Path) -> None:
"""Test the --log option logs when command fails."""
cmd = FakeCommand(error=True)
- log_path = tmpdir.joinpath('log')
- cmd.main(['fake', '--log', log_path])
+ log_path = os.path.join(tmpdir, "log")
+ cmd.main(["fake", "--log", log_path])
with open(log_path) as f:
- assert f.read().startswith('2019-01-17T06:00:37,040 fake')
+ assert f.read().startswith("2019-01-17T06:00:37,040 fake")
-def test_log_file_command_error(fixed_time, tmpdir):
+def test_log_file_command_error(fixed_time: None, tmpdir: Path) -> None:
"""Test the --log-file option logs (when there's an error)."""
cmd = FakeCommand(error=True)
- log_file_path = tmpdir.joinpath('log_file')
- cmd.main(['fake', '--log-file', log_file_path])
+ log_file_path = os.path.join(tmpdir, "log_file")
+ cmd.main(["fake", "--log-file", log_file_path])
with open(log_file_path) as f:
- assert f.read().startswith('2019-01-17T06:00:37,040 fake')
+ assert f.read().startswith("2019-01-17T06:00:37,040 fake")
-def test_log_unicode_messages(fixed_time, tmpdir):
+def test_log_unicode_messages(fixed_time: None, tmpdir: Path) -> None:
"""Tests that logging bytestrings and unicode objects
don't break logging.
"""
cmd = FakeCommandWithUnicode()
- log_path = tmpdir.joinpath('log')
- cmd.main(['fake_unicode', '--log', log_path])
+ log_path = os.path.join(tmpdir, "log")
+ cmd.main(["fake_unicode", "--log", log_path])
@pytest.mark.no_auto_tempdir_manager
-def test_base_command_provides_tempdir_helpers():
+def test_base_command_provides_tempdir_helpers() -> None:
assert temp_dir._tempdir_manager is None
assert temp_dir._tempdir_registry is None
- def assert_helpers_set(options, args):
+ def assert_helpers_set(options: Values, args: List[str]) -> int:
assert temp_dir._tempdir_manager is not None
assert temp_dir._tempdir_registry is not None
return SUCCESS
c = Command("fake", "fake")
- c.run = Mock(side_effect=assert_helpers_set)
+ # https://github.com/python/mypy/issues/2427
+ c.run = Mock(side_effect=assert_helpers_set) # type: ignore[assignment]
assert c.main(["fake"]) == SUCCESS
c.run.assert_called_once()
@@ -151,38 +160,37 @@ def test_base_command_provides_tempdir_helpers():
not_deleted = "not_deleted"
-@pytest.mark.parametrize("kind,exists", [
- (not_deleted, True), ("deleted", False)
-])
+@pytest.mark.parametrize("kind,exists", [(not_deleted, True), ("deleted", False)])
@pytest.mark.no_auto_tempdir_manager
-def test_base_command_global_tempdir_cleanup(kind, exists):
+def test_base_command_global_tempdir_cleanup(kind: str, exists: bool) -> None:
assert temp_dir._tempdir_manager is None
assert temp_dir._tempdir_registry is None
class Holder:
- value = None
+ value: str
- def create_temp_dirs(options, args):
+ def create_temp_dirs(options: Values, args: List[str]) -> int:
+ assert c.tempdir_registry is not None
c.tempdir_registry.set_delete(not_deleted, False)
Holder.value = TempDirectory(kind=kind, globally_managed=True).path
return SUCCESS
c = Command("fake", "fake")
- c.run = Mock(side_effect=create_temp_dirs)
+ # https://github.com/python/mypy/issues/2427
+ c.run = Mock(side_effect=create_temp_dirs) # type: ignore[assignment]
assert c.main(["fake"]) == SUCCESS
c.run.assert_called_once()
assert os.path.exists(Holder.value) == exists
-@pytest.mark.parametrize("kind,exists", [
- (not_deleted, True), ("deleted", False)
-])
+@pytest.mark.parametrize("kind,exists", [(not_deleted, True), ("deleted", False)])
@pytest.mark.no_auto_tempdir_manager
-def test_base_command_local_tempdir_cleanup(kind, exists):
+def test_base_command_local_tempdir_cleanup(kind: str, exists: bool) -> None:
assert temp_dir._tempdir_manager is None
assert temp_dir._tempdir_registry is None
- def create_temp_dirs(options, args):
+ def create_temp_dirs(options: Values, args: List[str]) -> int:
+ assert c.tempdir_registry is not None
c.tempdir_registry.set_delete(not_deleted, False)
with TempDirectory(kind=kind) as d:
@@ -192,6 +200,7 @@ def test_base_command_local_tempdir_cleanup(kind, exists):
return SUCCESS
c = Command("fake", "fake")
- c.run = Mock(side_effect=create_temp_dirs)
+ # https://github.com/python/mypy/issues/2427
+ c.run = Mock(side_effect=create_temp_dirs) # type: ignore[assignment]
assert c.main(["fake"]) == SUCCESS
c.run.assert_called_once()
diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py
index bab62d4e3..f1f0141c7 100644
--- a/tests/unit/test_cache.py
+++ b/tests/unit/test_cache.py
@@ -1,4 +1,5 @@
import os
+from pathlib import Path
from pip._vendor.packaging.tags import Tag
@@ -8,27 +9,27 @@ from pip._internal.models.link import Link
from pip._internal.utils.misc import ensure_dir
-def test_falsey_path_none():
- wc = WheelCache(False, None)
+def test_falsey_path_none() -> None:
+ wc = WheelCache("", FormatControl())
assert wc.cache_dir is None
-def test_subdirectory_fragment():
+def test_subdirectory_fragment() -> None:
"""
Test the subdirectory URL fragment is part of the cache key.
"""
- wc = WheelCache("/tmp/.foo/", None)
+ wc = WheelCache("/tmp/.foo/", FormatControl())
link1 = Link("git+https://g.c/o/r#subdirectory=d1")
link2 = Link("git+https://g.c/o/r#subdirectory=d2")
assert wc.get_path_for_link(link1) != wc.get_path_for_link(link2)
-def test_wheel_name_filter(tmpdir):
+def test_wheel_name_filter(tmpdir: Path) -> None:
"""
Test the wheel cache filters on wheel name when several wheels
for different package are stored under the same cache directory.
"""
- wc = WheelCache(tmpdir, FormatControl())
+ wc = WheelCache(os.fspath(tmpdir), FormatControl())
link = Link("https://g.c/package.tar.gz")
cache_path = wc.get_path_for_link(link)
ensure_dir(cache_path)
@@ -42,7 +43,7 @@ def test_wheel_name_filter(tmpdir):
assert wc.get(link, "package2", [Tag("py3", "none", "any")]) is link
-def test_cache_hash():
+def test_cache_hash() -> None:
h = _hash_dict({"url": "https://g.c/o/r"})
assert h == "72aa79d3315c181d2cc23239d7109a782de663b6f89982624d8c1e86"
h = _hash_dict({"url": "https://g.c/o/r", "subdirectory": "sd"})
@@ -51,8 +52,8 @@ def test_cache_hash():
assert h == "f83b32dfa27a426dec08c21bf006065dd003d0aac78e7fc493d9014d"
-def test_get_cache_entry(tmpdir):
- wc = WheelCache(tmpdir, FormatControl())
+def test_get_cache_entry(tmpdir: Path) -> None:
+ wc = WheelCache(os.fspath(tmpdir), FormatControl())
persi_link = Link("https://g.c/o/r/persi")
persi_path = wc.get_path_for_link(persi_link)
ensure_dir(persi_path)
@@ -65,10 +66,12 @@ def test_get_cache_entry(tmpdir):
pass
other_link = Link("https://g.c/o/r/other")
supported_tags = [Tag("py3", "none", "any")]
- assert (
- wc.get_cache_entry(persi_link, "persi", supported_tags).persistent
- )
- assert (
- not wc.get_cache_entry(ephem_link, "ephem", supported_tags).persistent
- )
+ entry = wc.get_cache_entry(persi_link, "persi", supported_tags)
+ assert entry is not None
+ assert entry.persistent
+
+ entry = wc.get_cache_entry(ephem_link, "ephem", supported_tags)
+ assert entry is not None
+ assert not entry.persistent
+
assert wc.get_cache_entry(other_link, "other", supported_tags) is None
diff --git a/tests/unit/test_cmdoptions.py b/tests/unit/test_cmdoptions.py
index bac33ce77..8c33ca8c1 100644
--- a/tests/unit/test_cmdoptions.py
+++ b/tests/unit/test_cmdoptions.py
@@ -1,24 +1,52 @@
+import os
+from pathlib import Path
+from typing import Optional, Tuple
+from venv import EnvBuilder
+
import pytest
from pip._internal.cli.cmdoptions import _convert_python_version
+from pip._internal.cli.main_parser import identify_python_interpreter
-@pytest.mark.parametrize('value, expected', [
- ('', (None, None)),
- ('2', ((2,), None)),
- ('3', ((3,), None)),
- ('3.7', ((3, 7), None)),
- ('3.7.3', ((3, 7, 3), None)),
- # Test strings without dots of length bigger than 1.
- ('34', ((3, 4), None)),
- # Test a 2-digit minor version.
- ('310', ((3, 10), None)),
- # Test some values that fail to parse.
- ('ab', ((), 'each version part must be an integer')),
- ('3a', ((), 'each version part must be an integer')),
- ('3.7.a', ((), 'each version part must be an integer')),
- ('3.7.3.1', ((), 'at most three version parts are allowed')),
-])
-def test_convert_python_version(value, expected):
+@pytest.mark.parametrize(
+ "value, expected",
+ [
+ ("", (None, None)),
+ ("2", ((2,), None)),
+ ("3", ((3,), None)),
+ ("3.7", ((3, 7), None)),
+ ("3.7.3", ((3, 7, 3), None)),
+ # Test strings without dots of length bigger than 1.
+ ("34", ((3, 4), None)),
+ # Test a 2-digit minor version.
+ ("310", ((3, 10), None)),
+ # Test some values that fail to parse.
+ ("ab", ((), "each version part must be an integer")),
+ ("3a", ((), "each version part must be an integer")),
+ ("3.7.a", ((), "each version part must be an integer")),
+ ("3.7.3.1", ((), "at most three version parts are allowed")),
+ ],
+)
+def test_convert_python_version(
+ value: str, expected: Tuple[Optional[Tuple[int, ...]], Optional[str]]
+) -> None:
actual = _convert_python_version(value)
- assert actual == expected, f'actual: {actual!r}'
+ assert actual == expected, f"actual: {actual!r}"
+
+
+def test_identify_python_interpreter_venv(tmpdir: Path) -> None:
+ env_path = tmpdir / "venv"
+ env = EnvBuilder(with_pip=False)
+ env.create(env_path)
+
+ # Passing a virtual environment returns the Python executable
+ interp = identify_python_interpreter(os.fspath(env_path))
+ assert interp is not None
+ assert Path(interp).exists()
+
+ # Passing an executable returns it
+ assert identify_python_interpreter(interp) == interp
+
+ # Passing a non-existent file returns None
+ assert identify_python_interpreter(str(tmpdir / "nonexistent")) is None
diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py
index 969a92007..55676a4fc 100644
--- a/tests/unit/test_collector.py
+++ b/tests/unit/test_collector.py
@@ -1,36 +1,48 @@
import itertools
+import json
import logging
-import os.path
+import os
import re
-import urllib.request
import uuid
+from pathlib import Path
from textwrap import dedent
+from typing import List, Optional, Tuple
from unittest import mock
-from unittest.mock import Mock, patch
-import pretend
import pytest
-from pip._vendor import html5lib, requests
+from pip._vendor import requests
+from pip._vendor.packaging.requirements import Requirement
from pip._internal.exceptions import NetworkConnectionError
from pip._internal.index.collector import (
- HTMLPage,
+ IndexContent,
LinkCollector,
- _clean_link,
- _clean_url_path,
- _determine_base_url,
- _get_html_page,
- _get_html_response,
- _make_html_page,
- _NotHTML,
+ _get_index_content,
+ _get_simple_response,
+ _make_index_content,
+ _NotAPIContent,
_NotHTTP,
parse_links,
)
from pip._internal.index.sources import _FlatDirectorySource, _IndexDirectorySource
+from pip._internal.models.candidate import InstallationCandidate
from pip._internal.models.index import PyPI
-from pip._internal.models.link import Link
+from pip._internal.models.link import (
+ Link,
+ LinkHash,
+ _clean_url_path,
+ _ensure_quoted_url,
+)
from pip._internal.network.session import PipSession
-from tests.lib import make_test_link_collector
+from tests.lib import TestData, make_test_link_collector
+
+ACCEPT = ", ".join(
+ [
+ "application/vnd.pypi.simple.v1+json",
+ "application/vnd.pypi.simple.v1+html; q=0.1",
+ "text/html; q=0.01",
+ ]
+)
@pytest.mark.parametrize(
@@ -40,13 +52,13 @@ from tests.lib import make_test_link_collector
"file:///opt/data/pip-18.0.tar.gz",
],
)
-def test_get_html_response_archive_to_naive_scheme(url):
+def test_get_simple_response_archive_to_naive_scheme(url: str) -> None:
"""
- `_get_html_response()` should error on an archive-like URL if the scheme
+ `_get_simple_response()` should error on an archive-like URL if the scheme
does not allow "poking" without getting data.
"""
with pytest.raises(_NotHTTP):
- _get_html_response(url, session=mock.Mock(PipSession))
+ _get_simple_response(url, session=mock.Mock(PipSession))
@pytest.mark.parametrize(
@@ -57,24 +69,29 @@ def test_get_html_response_archive_to_naive_scheme(url):
],
)
@mock.patch("pip._internal.index.collector.raise_for_status")
-def test_get_html_response_archive_to_http_scheme(mock_raise_for_status, url,
- content_type):
+def test_get_simple_response_archive_to_http_scheme(
+ mock_raise_for_status: mock.Mock, url: str, content_type: str
+) -> None:
"""
- `_get_html_response()` should send a HEAD request on an archive-like URL
- if the scheme supports it, and raise `_NotHTML` if the response isn't HTML.
+ `_get_simple_response()` should send a HEAD request on an archive-like URL
+ if the scheme supports it, and raise `_NotAPIContent` if the response isn't HTML.
"""
session = mock.Mock(PipSession)
- session.head.return_value = mock.Mock(**{
- "request.method": "HEAD",
- "headers": {"Content-Type": content_type},
- })
+ session.head.return_value = mock.Mock(
+ **{
+ "request.method": "HEAD",
+ "headers": {"Content-Type": content_type},
+ }
+ )
- with pytest.raises(_NotHTML) as ctx:
- _get_html_response(url, session=session)
+ with pytest.raises(_NotAPIContent) as ctx:
+ _get_simple_response(url, session=session)
- session.assert_has_calls([
- mock.call.head(url, allow_redirects=True),
- ])
+ session.assert_has_calls(
+ [
+ mock.call.head(url, allow_redirects=True),
+ ]
+ )
mock_raise_for_status.assert_called_once_with(session.head.return_value)
assert ctx.value.args == (content_type, "HEAD")
@@ -86,8 +103,10 @@ def test_get_html_response_archive_to_http_scheme(mock_raise_for_status, url,
("file:///opt/data/pip-18.0.tar.gz"),
],
)
-def test_get_html_page_invalid_content_type_archive(caplog, url):
- """`_get_html_page()` should warn if an archive URL is not HTML
+def test_get_index_content_invalid_content_type_archive(
+ caplog: pytest.LogCaptureFixture, url: str
+) -> None:
+ """`_get_index_content()` should warn if an archive URL is not HTML
and therefore cannot be used for a HEAD request.
"""
caplog.set_level(logging.WARNING)
@@ -95,13 +114,13 @@ def test_get_html_page_invalid_content_type_archive(caplog, url):
session = mock.Mock(PipSession)
- assert _get_html_page(link, session=session) is None
- assert ('pip._internal.index.collector',
- logging.WARNING,
- 'Skipping page {} because it looks like an archive, and cannot '
- 'be checked by a HTTP HEAD request.'.format(
- url)) \
- in caplog.record_tuples
+ assert _get_index_content(link, session=session) is None
+ assert (
+ "pip._internal.index.collector",
+ logging.WARNING,
+ "Skipping page {} because it looks like an archive, and cannot "
+ "be checked by a HTTP HEAD request.".format(url),
+ ) in caplog.record_tuples
@pytest.mark.parametrize(
@@ -112,32 +131,38 @@ def test_get_html_page_invalid_content_type_archive(caplog, url):
],
)
@mock.patch("pip._internal.index.collector.raise_for_status")
-def test_get_html_response_archive_to_http_scheme_is_html(
- mock_raise_for_status, url
-):
+def test_get_simple_response_archive_to_http_scheme_is_html(
+ mock_raise_for_status: mock.Mock, url: str
+) -> None:
"""
- `_get_html_response()` should work with archive-like URLs if the HEAD
+ `_get_simple_response()` should work with archive-like URLs if the HEAD
request is responded with text/html.
"""
session = mock.Mock(PipSession)
- session.head.return_value = mock.Mock(**{
- "request.method": "HEAD",
- "headers": {"Content-Type": "text/html"},
- })
+ session.head.return_value = mock.Mock(
+ **{
+ "request.method": "HEAD",
+ "headers": {"Content-Type": "text/html"},
+ }
+ )
session.get.return_value = mock.Mock(headers={"Content-Type": "text/html"})
- resp = _get_html_response(url, session=session)
+ resp = _get_simple_response(url, session=session)
assert resp is not None
assert session.mock_calls == [
mock.call.head(url, allow_redirects=True),
- mock.call.get(url, headers={
- "Accept": "text/html", "Cache-Control": "max-age=0",
- }),
+ mock.call.get(
+ url,
+ headers={
+ "Accept": ACCEPT,
+ "Cache-Control": "max-age=0",
+ },
+ ),
]
assert mock_raise_for_status.mock_calls == [
mock.call(session.head.return_value),
- mock.call(resp)
+ mock.call(resp),
]
@@ -150,148 +175,142 @@ def test_get_html_response_archive_to_http_scheme_is_html(
],
)
@mock.patch("pip._internal.index.collector.raise_for_status")
-def test_get_html_response_no_head(mock_raise_for_status, url):
+def test_get_simple_response_no_head(
+ mock_raise_for_status: mock.Mock, url: str
+) -> None:
"""
- `_get_html_response()` shouldn't send a HEAD request if the URL does not
+ `_get_simple_response()` shouldn't send a HEAD request if the URL does not
look like an archive, only the GET request that retrieves data.
"""
session = mock.Mock(PipSession)
# Mock the headers dict to ensure it is accessed.
- session.get.return_value = mock.Mock(headers=mock.Mock(**{
- "get.return_value": "text/html",
- }))
+ session.get.return_value = mock.Mock(
+ headers=mock.Mock(
+ **{
+ "get.return_value": "text/html",
+ }
+ )
+ )
- resp = _get_html_response(url, session=session)
+ resp = _get_simple_response(url, session=session)
assert resp is not None
assert session.head.call_count == 0
assert session.get.mock_calls == [
- mock.call(url, headers={
- "Accept": "text/html", "Cache-Control": "max-age=0",
- }),
- mock.call().headers.get("Content-Type", ""),
+ mock.call(
+ url,
+ headers={
+ "Accept": ACCEPT,
+ "Cache-Control": "max-age=0",
+ },
+ ),
+ mock.call().headers.get("Content-Type", "Unknown"),
+ mock.call().headers.get("Content-Type", "Unknown"),
]
mock_raise_for_status.assert_called_once_with(resp)
@mock.patch("pip._internal.index.collector.raise_for_status")
-def test_get_html_response_dont_log_clear_text_password(mock_raise_for_status,
- caplog):
+def test_get_simple_response_dont_log_clear_text_password(
+ mock_raise_for_status: mock.Mock, caplog: pytest.LogCaptureFixture
+) -> None:
"""
- `_get_html_response()` should redact the password from the index URL
+ `_get_simple_response()` should redact the password from the index URL
in its DEBUG log message.
"""
session = mock.Mock(PipSession)
# Mock the headers dict to ensure it is accessed.
- session.get.return_value = mock.Mock(headers=mock.Mock(**{
- "get.return_value": "text/html",
- }))
+ session.get.return_value = mock.Mock(
+ headers=mock.Mock(
+ **{
+ "get.return_value": "text/html",
+ }
+ )
+ )
caplog.set_level(logging.DEBUG)
- resp = _get_html_response(
+ resp = _get_simple_response(
"https://user:my_password@example.com/simple/", session=session
)
assert resp is not None
mock_raise_for_status.assert_called_once_with(resp)
- assert len(caplog.records) == 1
+ assert len(caplog.records) == 2
record = caplog.records[0]
- assert record.levelname == 'DEBUG'
+ assert record.levelname == "DEBUG"
assert record.message.splitlines() == [
"Getting page https://user:****@example.com/simple/",
]
+ record = caplog.records[1]
+ assert record.levelname == "DEBUG"
+ assert record.message.splitlines() == [
+ "Fetched page https://user:****@example.com/simple/ as text/html",
+ ]
@pytest.mark.parametrize(
- ("html", "url", "expected"),
- [
- (b"<html></html>", "https://example.com/", "https://example.com/"),
- (
- b"<html><head>"
- b"<base href=\"https://foo.example.com/\">"
- b"</head></html>",
- "https://example.com/",
- "https://foo.example.com/",
- ),
- (
- b"<html><head>"
- b"<base><base href=\"https://foo.example.com/\">"
- b"</head></html>",
- "https://example.com/",
- "https://foo.example.com/",
- ),
- ],
-)
-def test_determine_base_url(html, url, expected):
- document = html5lib.parse(
- html, transport_encoding=None, namespaceHTMLElements=False,
- )
- assert _determine_base_url(document, url) == expected
-
-
-@pytest.mark.parametrize(
- ('path', 'expected'),
+ ("path", "expected"),
[
# Test a character that needs quoting.
- ('a b', 'a%20b'),
+ ("a b", "a%20b"),
# Test an unquoted "@".
- ('a @ b', 'a%20@%20b'),
+ ("a @ b", "a%20@%20b"),
# Test multiple unquoted "@".
- ('a @ @ b', 'a%20@%20@%20b'),
+ ("a @ @ b", "a%20@%20@%20b"),
# Test a quoted "@".
- ('a %40 b', 'a%20%40%20b'),
+ ("a %40 b", "a%20%40%20b"),
# Test a quoted "@" before an unquoted "@".
- ('a %40b@ c', 'a%20%40b@%20c'),
+ ("a %40b@ c", "a%20%40b@%20c"),
# Test a quoted "@" after an unquoted "@".
- ('a @b%40 c', 'a%20@b%40%20c'),
+ ("a @b%40 c", "a%20@b%40%20c"),
# Test alternating quoted and unquoted "@".
- ('a %40@b %40@c %40', 'a%20%40@b%20%40@c%20%40'),
+ ("a %40@b %40@c %40", "a%20%40@b%20%40@c%20%40"),
# Test an unquoted "/".
- ('a / b', 'a%20/%20b'),
+ ("a / b", "a%20/%20b"),
# Test multiple unquoted "/".
- ('a / / b', 'a%20/%20/%20b'),
+ ("a / / b", "a%20/%20/%20b"),
# Test a quoted "/".
- ('a %2F b', 'a%20%2F%20b'),
+ ("a %2F b", "a%20%2F%20b"),
# Test a quoted "/" before an unquoted "/".
- ('a %2Fb/ c', 'a%20%2Fb/%20c'),
+ ("a %2Fb/ c", "a%20%2Fb/%20c"),
# Test a quoted "/" after an unquoted "/".
- ('a /b%2F c', 'a%20/b%2F%20c'),
+ ("a /b%2F c", "a%20/b%2F%20c"),
# Test alternating quoted and unquoted "/".
- ('a %2F/b %2F/c %2F', 'a%20%2F/b%20%2F/c%20%2F'),
+ ("a %2F/b %2F/c %2F", "a%20%2F/b%20%2F/c%20%2F"),
# Test normalizing non-reserved quoted characters "[" and "]"
- ('a %5b %5d b', 'a%20%5B%20%5D%20b'),
+ ("a %5b %5d b", "a%20%5B%20%5D%20b"),
# Test normalizing a reserved quoted "/"
- ('a %2f b', 'a%20%2F%20b'),
- ]
+ ("a %2f b", "a%20%2F%20b"),
+ ],
)
-@pytest.mark.parametrize('is_local_path', [True, False])
-def test_clean_url_path(path, expected, is_local_path):
+@pytest.mark.parametrize("is_local_path", [True, False])
+def test_clean_url_path(path: str, expected: str, is_local_path: bool) -> None:
assert _clean_url_path(path, is_local_path=is_local_path) == expected
@pytest.mark.parametrize(
- ('path', 'expected'),
+ ("path", "expected"),
[
# Test a VCS path with a Windows drive letter and revision.
pytest.param(
- '/T:/with space/repo.git@1.0',
- '///T:/with%20space/repo.git@1.0',
+ "/T:/with space/repo.git@1.0",
+ "///T:/with%20space/repo.git@1.0",
marks=pytest.mark.skipif("sys.platform != 'win32'"),
),
# Test a VCS path with a Windows drive letter and revision,
# running on non-windows platform.
pytest.param(
- '/T:/with space/repo.git@1.0',
- '/T%3A/with%20space/repo.git@1.0',
+ "/T:/with space/repo.git@1.0",
+ "/T%3A/with%20space/repo.git@1.0",
marks=pytest.mark.skipif("sys.platform == 'win32'"),
),
- ]
+ ],
)
-def test_clean_url_path_with_local_path(path, expected):
+def test_clean_url_path_with_local_path(path: str, expected: str) -> None:
actual = _clean_url_path(path, is_local_path=True)
assert actual == expected
@@ -300,41 +319,63 @@ def test_clean_url_path_with_local_path(path, expected):
("url", "clean_url"),
[
# URL with hostname and port. Port separator should not be quoted.
- ("https://localhost.localdomain:8181/path/with space/",
- "https://localhost.localdomain:8181/path/with%20space/"),
+ (
+ "https://localhost.localdomain:8181/path/with space/",
+ "https://localhost.localdomain:8181/path/with%20space/",
+ ),
# URL that is already properly quoted. The quoting `%`
# characters should not be quoted again.
- ("https://localhost.localdomain:8181/path/with%20quoted%20space/",
- "https://localhost.localdomain:8181/path/with%20quoted%20space/"),
+ (
+ "https://localhost.localdomain:8181/path/with%20quoted%20space/",
+ "https://localhost.localdomain:8181/path/with%20quoted%20space/",
+ ),
# URL with IPv4 address and port.
- ("https://127.0.0.1:8181/path/with space/",
- "https://127.0.0.1:8181/path/with%20space/"),
+ (
+ "https://127.0.0.1:8181/path/with space/",
+ "https://127.0.0.1:8181/path/with%20space/",
+ ),
# URL with IPv6 address and port. The `[]` brackets around the
# IPv6 address should not be quoted.
- ("https://[fd00:0:0:236::100]:8181/path/with space/",
- "https://[fd00:0:0:236::100]:8181/path/with%20space/"),
+ (
+ "https://[fd00:0:0:236::100]:8181/path/with space/",
+ "https://[fd00:0:0:236::100]:8181/path/with%20space/",
+ ),
# URL with query. The leading `?` should not be quoted.
- ("https://localhost.localdomain:8181/path/with/query?request=test",
- "https://localhost.localdomain:8181/path/with/query?request=test"),
+ (
+ "https://localhost.localdomain:8181/path/with/query?request=test",
+ "https://localhost.localdomain:8181/path/with/query?request=test",
+ ),
# URL with colon in the path portion.
- ("https://localhost.localdomain:8181/path:/with:/colon",
- "https://localhost.localdomain:8181/path%3A/with%3A/colon"),
+ (
+ "https://localhost.localdomain:8181/path:/with:/colon",
+ "https://localhost.localdomain:8181/path%3A/with%3A/colon",
+ ),
# URL with something that looks like a drive letter, but is
# not. The `:` should be quoted.
- ("https://localhost.localdomain/T:/path/",
- "https://localhost.localdomain/T%3A/path/"),
+ (
+ "https://localhost.localdomain/T:/path/",
+ "https://localhost.localdomain/T%3A/path/",
+ ),
# URL with a quoted "/" in the path portion.
- ("https://example.com/access%2Ftoken/path/",
- "https://example.com/access%2Ftoken/path/"),
+ (
+ "https://example.com/access%2Ftoken/path/",
+ "https://example.com/access%2Ftoken/path/",
+ ),
# VCS URL containing revision string.
- ("git+ssh://example.com/path to/repo.git@1.0#egg=my-package-1.0",
- "git+ssh://example.com/path%20to/repo.git@1.0#egg=my-package-1.0"),
+ (
+ "git+ssh://example.com/path to/repo.git@1.0#egg=my-package-1.0",
+ "git+ssh://example.com/path%20to/repo.git@1.0#egg=my-package-1.0",
+ ),
# VCS URL with a quoted "#" in the revision string.
- ("git+https://example.com/repo.git@hash%23symbol#egg=my-package-1.0",
- "git+https://example.com/repo.git@hash%23symbol#egg=my-package-1.0"),
+ (
+ "git+https://example.com/repo.git@hash%23symbol#egg=my-package-1.0",
+ "git+https://example.com/repo.git@hash%23symbol#egg=my-package-1.0",
+ ),
# VCS URL with a quoted "@" in the revision string.
- ("git+https://example.com/repo.git@at%40 space#egg=my-package-1.0",
- "git+https://example.com/repo.git@at%40%20space#egg=my-package-1.0"),
+ (
+ "git+https://example.com/repo.git@at%40 space#egg=my-package-1.0",
+ "git+https://example.com/repo.git@at%40%20space#egg=my-package-1.0",
+ ),
# URL with Windows drive letter. The `:` after the drive
# letter should not be quoted. The trailing `/` should be
# removed.
@@ -363,75 +404,244 @@ def test_clean_url_path_with_local_path(path, expected):
"git+file:/T%3A/with%20space/repo.git@1.0#egg=my-package-1.0",
marks=pytest.mark.skipif("sys.platform == 'win32'"),
),
- ]
+ ],
)
-def test_clean_link(url, clean_url):
- assert _clean_link(url) == clean_url
-
-
-@pytest.mark.parametrize('anchor_html, expected', [
- # Test not present.
- ('<a href="/pkg1-1.0.tar.gz"></a>', None),
- # Test present with no value.
- ('<a href="/pkg2-1.0.tar.gz" data-yanked></a>', ''),
- # Test the empty string.
- ('<a href="/pkg3-1.0.tar.gz" data-yanked=""></a>', ''),
- # Test a non-empty string.
- ('<a href="/pkg4-1.0.tar.gz" data-yanked="error"></a>', 'error'),
- # Test a value with an escaped character.
- ('<a href="/pkg4-1.0.tar.gz" data-yanked="version &lt 1"></a>',
- 'version < 1'),
- # Test a yanked reason with a non-ascii character.
- ('<a href="/pkg-1.0.tar.gz" data-yanked="curlyquote \u2018"></a>',
- 'curlyquote \u2018'),
-])
-def test_parse_links__yanked_reason(anchor_html, expected):
+def test_ensure_quoted_url(url: str, clean_url: str) -> None:
+ assert _ensure_quoted_url(url) == clean_url
+
+
+def _test_parse_links_data_attribute(
+ anchor_html: str, attr: str, expected: Optional[str]
+) -> Link:
html = (
- # Mark this as a unicode string for Python 2 since anchor_html
- # can contain non-ascii.
+ "<!DOCTYPE html>"
'<html><head><meta charset="utf-8"><head>'
- '<body>{}</body></html>'
+ "<body>{}</body></html>"
).format(anchor_html)
- html_bytes = html.encode('utf-8')
- page = HTMLPage(
+ html_bytes = html.encode("utf-8")
+ page = IndexContent(
html_bytes,
+ "text/html",
encoding=None,
# parse_links() is cached by url, so we inject a random uuid to ensure
# the page content isn't cached.
- url=f'https://example.com/simple-{uuid.uuid4()}/',
+ url=f"https://example.com/simple-{uuid.uuid4()}/",
)
links = list(parse_links(page))
- link, = links
- actual = link.yanked_reason
+ (link,) = links
+ actual = getattr(link, attr)
assert actual == expected
+ return link
-def test_parse_links_caches_same_page_by_url():
+@pytest.mark.parametrize(
+ "anchor_html, expected",
+ [
+ # Test not present.
+ ('<a href="/pkg-1.0.tar.gz"></a>', None),
+ # Test present with no value.
+ ('<a href="/pkg-1.0.tar.gz" data-requires-python></a>', None),
+ # Test a value with an escaped character.
+ (
+ '<a href="/pkg-1.0.tar.gz" data-requires-python="&gt;=3.6"></a>',
+ ">=3.6",
+ ),
+ # Test requires python is unescaped once.
+ (
+ '<a href="/pkg-1.0.tar.gz" data-requires-python="&amp;gt;=3.6"></a>',
+ "&gt;=3.6",
+ ),
+ ],
+)
+def test_parse_links__requires_python(
+ anchor_html: str, expected: Optional[str]
+) -> None:
+ _test_parse_links_data_attribute(anchor_html, "requires_python", expected)
+
+
+# TODO: this test generates its own examples to validate the json client implementation
+# instead of sharing those examples with the html client testing. We expect this won't
+# hide any bugs because operations like resolving PEP 658 metadata should use the same
+# code for both types of indices, but it might be nice to explicitly have all our tests
+# in test_download.py execute over both html and json indices with
+# a pytest.mark.parameterize decorator to ensure nothing slips through the cracks.
+def test_parse_links_json() -> None:
+ json_bytes = json.dumps(
+ {
+ "meta": {"api-version": "1.0"},
+ "name": "holygrail",
+ "files": [
+ {
+ "filename": "holygrail-1.0.tar.gz",
+ "url": "https://example.com/files/holygrail-1.0.tar.gz",
+ "hashes": {"sha256": "sha256 hash", "blake2b": "blake2b hash"},
+ "requires-python": ">=3.7",
+ "yanked": "Had a vulnerability",
+ },
+ {
+ "filename": "holygrail-1.0-py3-none-any.whl",
+ "url": "/files/holygrail-1.0-py3-none-any.whl",
+ "hashes": {"sha256": "sha256 hash", "blake2b": "blake2b hash"},
+ "requires-python": ">=3.7",
+ "dist-info-metadata": False,
+ },
+ # Same as above, but parsing dist-info-metadata.
+ {
+ "filename": "holygrail-1.0-py3-none-any.whl",
+ "url": "/files/holygrail-1.0-py3-none-any.whl",
+ "hashes": {"sha256": "sha256 hash", "blake2b": "blake2b hash"},
+ "requires-python": ">=3.7",
+ "dist-info-metadata": "sha512=aabdd41",
+ },
+ ],
+ }
+ ).encode("utf8")
+ page = IndexContent(
+ json_bytes,
+ "application/vnd.pypi.simple.v1+json",
+ encoding=None,
+ # parse_links() is cached by url, so we inject a random uuid to ensure
+ # the page content isn't cached.
+ url=f"https://example.com/simple-{uuid.uuid4()}/",
+ )
+ links = list(parse_links(page))
+
+ assert links == [
+ Link(
+ "https://example.com/files/holygrail-1.0.tar.gz",
+ comes_from=page.url,
+ requires_python=">=3.7",
+ yanked_reason="Had a vulnerability",
+ hashes={"sha256": "sha256 hash", "blake2b": "blake2b hash"},
+ ),
+ Link(
+ "https://example.com/files/holygrail-1.0-py3-none-any.whl",
+ comes_from=page.url,
+ requires_python=">=3.7",
+ yanked_reason=None,
+ hashes={"sha256": "sha256 hash", "blake2b": "blake2b hash"},
+ ),
+ Link(
+ "https://example.com/files/holygrail-1.0-py3-none-any.whl",
+ comes_from=page.url,
+ requires_python=">=3.7",
+ yanked_reason=None,
+ hashes={"sha256": "sha256 hash", "blake2b": "blake2b hash"},
+ dist_info_metadata="sha512=aabdd41",
+ ),
+ ]
+
+ # Ensure the metadata info can be parsed into the correct link.
+ metadata_link = links[2].metadata_link()
+ assert metadata_link is not None
+ assert (
+ metadata_link.url
+ == "https://example.com/files/holygrail-1.0-py3-none-any.whl.metadata"
+ )
+ assert metadata_link.link_hash == LinkHash("sha512", "aabdd41")
+
+
+@pytest.mark.parametrize(
+ "anchor_html, expected",
+ [
+ # Test not present.
+ ('<a href="/pkg1-1.0.tar.gz"></a>', None),
+ # Test present with no value.
+ ('<a href="/pkg2-1.0.tar.gz" data-yanked></a>', None),
+ # Test the empty string.
+ ('<a href="/pkg3-1.0.tar.gz" data-yanked=""></a>', ""),
+ # Test a non-empty string.
+ ('<a href="/pkg4-1.0.tar.gz" data-yanked="error"></a>', "error"),
+ # Test a value with an escaped character.
+ ('<a href="/pkg4-1.0.tar.gz" data-yanked="version &lt 1"></a>', "version < 1"),
+ # Test a yanked reason with a non-ascii character.
+ (
+ '<a href="/pkg-1.0.tar.gz" data-yanked="curlyquote \u2018"></a>',
+ "curlyquote \u2018",
+ ),
+ # Test yanked reason is unescaped once.
+ (
+ '<a href="/pkg-1.0.tar.gz" data-yanked="version &amp;lt; 1"></a>',
+ "version &lt; 1",
+ ),
+ ],
+)
+def test_parse_links__yanked_reason(anchor_html: str, expected: Optional[str]) -> None:
+ _test_parse_links_data_attribute(anchor_html, "yanked_reason", expected)
+
+
+# Requirement objects do not == each other unless they point to the same instance!
+_pkg1_requirement = Requirement("pkg1==1.0")
+
+
+@pytest.mark.parametrize(
+ "anchor_html, expected, link_hash",
+ [
+ # Test not present.
+ (
+ '<a href="/pkg1-1.0.tar.gz"></a>',
+ None,
+ None,
+ ),
+ # Test with value "true".
+ (
+ '<a href="/pkg1-1.0.tar.gz" data-dist-info-metadata="true"></a>',
+ "true",
+ None,
+ ),
+ # Test with a provided hash value.
+ (
+ '<a href="/pkg1-1.0.tar.gz" data-dist-info-metadata="sha256=aa113592bbe"></a>', # noqa: E501
+ "sha256=aa113592bbe",
+ None,
+ ),
+ # Test with a provided hash value for both the requirement as well as metadata.
+ (
+ '<a href="/pkg1-1.0.tar.gz#sha512=abc132409cb" data-dist-info-metadata="sha256=aa113592bbe"></a>', # noqa: E501
+ "sha256=aa113592bbe",
+ LinkHash("sha512", "abc132409cb"),
+ ),
+ ],
+)
+def test_parse_links__dist_info_metadata(
+ anchor_html: str,
+ expected: Optional[str],
+ link_hash: Optional[LinkHash],
+) -> None:
+ link = _test_parse_links_data_attribute(anchor_html, "dist_info_metadata", expected)
+ assert link.link_hash == link_hash
+
+
+def test_parse_links_caches_same_page_by_url() -> None:
html = (
+ "<!DOCTYPE html>"
'<html><head><meta charset="utf-8"><head>'
'<body><a href="/pkg1-1.0.tar.gz"></a></body></html>'
)
- html_bytes = html.encode('utf-8')
+ html_bytes = html.encode("utf-8")
- url = 'https://example.com/simple/'
+ url = "https://example.com/simple/"
- page_1 = HTMLPage(
+ page_1 = IndexContent(
html_bytes,
+ "text/html",
encoding=None,
url=url,
)
# Make a second page with zero content, to ensure that it's not accessed,
# because the page was cached by url.
- page_2 = HTMLPage(
- b'',
+ page_2 = IndexContent(
+ b"",
+ "text/html",
encoding=None,
url=url,
)
# Make a third page which represents an index url, which should not be
# cached, even for the same url. We modify the page content slightly to
# verify that the result is not cached.
- page_3 = HTMLPage(
- re.sub(b'pkg1', b'pkg2', html_bytes),
+ page_3 = IndexContent(
+ re.sub(b"pkg1", b"pkg2", html_bytes),
+ "text/html",
encoding=None,
url=url,
cache_link_parsing=False,
@@ -439,7 +649,7 @@ def test_parse_links_caches_same_page_by_url():
parsed_links_1 = list(parse_links(page_1))
assert len(parsed_links_1) == 1
- assert 'pkg1' in parsed_links_1[0].url
+ assert "pkg1" in parsed_links_1[0].url
parsed_links_2 = list(parse_links(page_2))
assert parsed_links_2 == parsed_links_1
@@ -447,47 +657,43 @@ def test_parse_links_caches_same_page_by_url():
parsed_links_3 = list(parse_links(page_3))
assert len(parsed_links_3) == 1
assert parsed_links_3 != parsed_links_1
- assert 'pkg2' in parsed_links_3[0].url
+ assert "pkg2" in parsed_links_3[0].url
@mock.patch("pip._internal.index.collector.raise_for_status")
-def test_request_http_error(mock_raise_for_status, caplog):
+def test_request_http_error(
+ mock_raise_for_status: mock.Mock, caplog: pytest.LogCaptureFixture
+) -> None:
caplog.set_level(logging.DEBUG)
- link = Link('http://localhost')
- session = Mock(PipSession)
- session.get.return_value = Mock()
- mock_raise_for_status.side_effect = NetworkConnectionError('Http error')
- assert _get_html_page(link, session=session) is None
- assert (
- 'Could not fetch URL http://localhost: Http error - skipping'
- in caplog.text
- )
+ link = Link("http://localhost")
+ session = mock.Mock(PipSession)
+ session.get.return_value = mock.Mock()
+ mock_raise_for_status.side_effect = NetworkConnectionError("Http error")
+ assert _get_index_content(link, session=session) is None
+ assert "Could not fetch URL http://localhost: Http error - skipping" in caplog.text
-def test_request_retries(caplog):
+def test_request_retries(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.DEBUG)
- link = Link('http://localhost')
- session = Mock(PipSession)
- session.get.side_effect = requests.exceptions.RetryError('Retry error')
- assert _get_html_page(link, session=session) is None
- assert (
- 'Could not fetch URL http://localhost: Retry error - skipping'
- in caplog.text
- )
+ link = Link("http://localhost")
+ session = mock.Mock(PipSession)
+ session.get.side_effect = requests.exceptions.RetryError("Retry error")
+ assert _get_index_content(link, session=session) is None
+ assert "Could not fetch URL http://localhost: Retry error - skipping" in caplog.text
-def test_make_html_page():
- headers = {'Content-Type': 'text/html; charset=UTF-8'}
- response = pretend.stub(
- content=b'<content>',
- url='https://example.com/index.html',
+def test_make_index_content() -> None:
+ headers = {"Content-Type": "text/html; charset=UTF-8"}
+ response = mock.Mock(
+ content=b"<content>",
+ url="https://example.com/index.html",
headers=headers,
)
- actual = _make_html_page(response)
- assert actual.content == b'<content>'
- assert actual.encoding == 'UTF-8'
- assert actual.url == 'https://example.com/index.html'
+ actual = _make_index_content(response)
+ assert actual.content == b"<content>"
+ assert actual.encoding == "UTF-8"
+ assert actual.url == "https://example.com/index.html"
@pytest.mark.parametrize(
@@ -497,13 +703,15 @@ def test_make_html_page():
("git+https://github.com/pypa/pip.git", "git"),
],
)
-def test_get_html_page_invalid_scheme(caplog, url, vcs_scheme):
- """`_get_html_page()` should error if an invalid scheme is given.
+def test_get_index_content_invalid_scheme(
+ caplog: pytest.LogCaptureFixture, url: str, vcs_scheme: str
+) -> None:
+ """`_get_index_content()` should error if an invalid scheme is given.
Only file:, http:, https:, and ftp: are allowed.
"""
with caplog.at_level(logging.WARNING):
- page = _get_html_page(Link(url), session=mock.Mock(PipSession))
+ page = _get_index_content(Link(url), session=mock.Mock(PipSession))
assert page is None
assert caplog.record_tuples == [
@@ -524,76 +732,82 @@ def test_get_html_page_invalid_scheme(caplog, url, vcs_scheme):
],
)
@mock.patch("pip._internal.index.collector.raise_for_status")
-def test_get_html_page_invalid_content_type(mock_raise_for_status,
- caplog, content_type):
- """`_get_html_page()` should warn if an invalid content-type is given.
+def test_get_index_content_invalid_content_type(
+ mock_raise_for_status: mock.Mock,
+ caplog: pytest.LogCaptureFixture,
+ content_type: str,
+) -> None:
+ """`_get_index_content()` should warn if an invalid content-type is given.
Only text/html is allowed.
"""
caplog.set_level(logging.DEBUG)
- url = 'https://pypi.org/simple/pip'
+ url = "https://pypi.org/simple/pip"
link = Link(url)
session = mock.Mock(PipSession)
- session.get.return_value = mock.Mock(**{
- "request.method": "GET",
- "headers": {"Content-Type": content_type},
- })
- assert _get_html_page(link, session=session) is None
+ session.get.return_value = mock.Mock(
+ **{
+ "request.method": "GET",
+ "headers": {"Content-Type": content_type},
+ }
+ )
+ assert _get_index_content(link, session=session) is None
mock_raise_for_status.assert_called_once_with(session.get.return_value)
- assert ('pip._internal.index.collector',
- logging.WARNING,
- 'Skipping page {} because the GET request got Content-Type: {}.'
- 'The only supported Content-Type is text/html'.format(
- url, content_type)) \
- in caplog.record_tuples
+ assert (
+ "pip._internal.index.collector",
+ logging.WARNING,
+ "Skipping page {} because the GET request got Content-Type: {}. "
+ "The only supported Content-Types are application/vnd.pypi.simple.v1+json, "
+ "application/vnd.pypi.simple.v1+html, and text/html".format(url, content_type),
+ ) in caplog.record_tuples
-def make_fake_html_response(url):
+def make_fake_html_response(url: str) -> mock.Mock:
"""
Create a fake requests.Response object.
"""
- html = dedent("""\
+ html = dedent(
+ """\
<html><head><meta name="api-version" value="2" /></head>
<body>
<a href="/abc-1.0.tar.gz#md5=000000000">abc-1.0.tar.gz</a>
</body></html>
- """)
- content = html.encode('utf-8')
- return pretend.stub(content=content, url=url, headers={})
+ """
+ )
+ content = html.encode("utf-8")
+ return mock.Mock(content=content, url=url, headers={"Content-Type": "text/html"})
-def test_get_html_page_directory_append_index(tmpdir):
- """`_get_html_page()` should append "index.html" to a directory URL.
- """
+def test_get_index_content_directory_append_index(tmpdir: Path) -> None:
+ """`_get_index_content()` should append "index.html" to a directory URL."""
dirpath = tmpdir / "something"
dirpath.mkdir()
- dir_url = "file:///{}".format(
- urllib.request.pathname2url(dirpath).lstrip("/"),
- )
+ dir_url = dirpath.as_uri()
expected_url = "{}/index.html".format(dir_url.rstrip("/"))
session = mock.Mock(PipSession)
fake_response = make_fake_html_response(expected_url)
- mock_func = mock.patch("pip._internal.index.collector._get_html_response")
+ mock_func = mock.patch("pip._internal.index.collector._get_simple_response")
with mock_func as mock_func:
mock_func.return_value = fake_response
- actual = _get_html_page(Link(dir_url), session=session)
+ actual = _get_index_content(Link(dir_url), session=session)
assert mock_func.mock_calls == [
mock.call(expected_url, session=session),
- ], f'actual calls: {mock_func.mock_calls}'
+ ], f"actual calls: {mock_func.mock_calls}"
+ assert actual is not None
assert actual.content == fake_response.content
assert actual.encoding is None
assert actual.url == expected_url
-def test_collect_sources__file_expand_dir(data):
+def test_collect_sources__file_expand_dir(data: TestData) -> None:
"""
Test that a file:// dir from --find-links becomes _FlatDirectorySource
"""
collector = LinkCollector.create(
- session=pretend.stub(is_secure_origin=None), # Shouldn't be used.
- options=pretend.stub(
+ session=mock.Mock(is_secure_origin=None), # Shouldn't be used.
+ options=mock.Mock(
index_url="ignored-by-no-index",
extra_index_urls=[],
no_index=True,
@@ -601,8 +815,9 @@ def test_collect_sources__file_expand_dir(data):
),
)
sources = collector.collect_sources(
- project_name=None, # Shouldn't be used.
- candidates_from_page=None, # Shouldn't be used.
+ # Shouldn't be used.
+ project_name=None, # type: ignore[arg-type]
+ candidates_from_page=None, # type: ignore[arg-type]
)
assert (
not sources.index_urls
@@ -614,14 +829,14 @@ def test_collect_sources__file_expand_dir(data):
)
-def test_collect_sources__file_not_find_link(data):
+def test_collect_sources__file_not_find_link(data: TestData) -> None:
"""
Test that a file:// dir from --index-url doesn't become _FlatDirectorySource
run
"""
collector = LinkCollector.create(
- session=pretend.stub(is_secure_origin=None), # Shouldn't be used.
- options=pretend.stub(
+ session=mock.Mock(is_secure_origin=None), # Shouldn't be used.
+ options=mock.Mock(
index_url=data.index_url("empty_with_pkg"),
extra_index_urls=[],
no_index=False,
@@ -630,7 +845,8 @@ def test_collect_sources__file_not_find_link(data):
)
sources = collector.collect_sources(
project_name="",
- candidates_from_page=None, # Shouldn't be used.
+ # Shouldn't be used.
+ candidates_from_page=None, # type: ignore[arg-type]
)
assert (
not sources.find_links
@@ -639,13 +855,13 @@ def test_collect_sources__file_not_find_link(data):
), "Directory specified as index should be treated as a page"
-def test_collect_sources__non_existing_path():
+def test_collect_sources__non_existing_path() -> None:
"""
Test that a non-existing path is ignored.
"""
collector = LinkCollector.create(
- session=pretend.stub(is_secure_origin=None), # Shouldn't be used.
- options=pretend.stub(
+ session=mock.Mock(is_secure_origin=None), # Shouldn't be used.
+ options=mock.Mock(
index_url="ignored-by-no-index",
extra_index_urls=[],
no_index=True,
@@ -653,51 +869,54 @@ def test_collect_sources__non_existing_path():
),
)
sources = collector.collect_sources(
- project_name=None, # Shouldn't be used.
- candidates_from_page=None, # Shouldn't be used.
+ # Shouldn't be used.
+ project_name=None, # type: ignore[arg-type]
+ candidates_from_page=None, # type: ignore[arg-type]
)
- assert (
- not sources.index_urls
- and sources.find_links == [None]
- ), "Nothing should have been found"
+ assert not sources.index_urls and sources.find_links == [
+ None
+ ], "Nothing should have been found"
-def check_links_include(links, names):
+def check_links_include(links: List[Link], names: List[str]) -> None:
"""
Assert that the given list of Link objects includes, for each of the
given names, a link whose URL has a base name matching that name.
"""
for name in names:
- assert any(link.url.endswith(name) for link in links), (
- f'name {name!r} not among links: {links}'
- )
+ assert any(
+ link.url.endswith(name) for link in links
+ ), f"name {name!r} not among links: {links}"
class TestLinkCollector:
-
- @patch('pip._internal.index.collector._get_html_response')
- def test_fetch_page(self, mock_get_html_response):
- url = 'https://pypi.org/simple/twine/'
+ @mock.patch("pip._internal.index.collector._get_simple_response")
+ def test_fetch_response(self, mock_get_simple_response: mock.Mock) -> None:
+ url = "https://pypi.org/simple/twine/"
fake_response = make_fake_html_response(url)
- mock_get_html_response.return_value = fake_response
+ mock_get_simple_response.return_value = fake_response
location = Link(url, cache_link_parsing=False)
link_collector = make_test_link_collector()
- actual = link_collector.fetch_page(location)
+ actual = link_collector.fetch_response(location)
+ assert actual is not None
assert actual.content == fake_response.content
assert actual.encoding is None
assert actual.url == url
assert actual.cache_link_parsing == location.cache_link_parsing
# Also check that the right session object was passed to
- # _get_html_response().
- mock_get_html_response.assert_called_once_with(
- url, session=link_collector.session,
+ # _get_simple_response().
+ mock_get_simple_response.assert_called_once_with(
+ url,
+ session=link_collector.session,
)
- def test_collect_sources(self, caplog, data):
+ def test_collect_sources(
+ self, caplog: pytest.LogCaptureFixture, data: TestData
+ ) -> None:
caplog.set_level(logging.DEBUG)
link_collector = make_test_link_collector(
@@ -708,7 +927,9 @@ class TestLinkCollector:
)
collected_sources = link_collector.collect_sources(
"twine",
- candidates_from_page=lambda link: [link],
+ candidates_from_page=lambda link: [
+ InstallationCandidate("twine", "1.0", link)
+ ],
)
files_it = itertools.chain.from_iterable(
@@ -730,46 +951,53 @@ class TestLinkCollector:
assert len(files) > 20
check_links_include(files, names=["simple-1.0.tar.gz"])
- assert pages == [Link('https://pypi.org/simple/twine/')]
+ assert [page.link for page in pages] == [Link("https://pypi.org/simple/twine/")]
# Check that index URLs are marked as *un*cacheable.
- assert not pages[0].cache_link_parsing
+ assert not pages[0].link.cache_link_parsing
- expected_message = dedent("""\
+ expected_message = dedent(
+ """\
1 location(s) to search for versions of twine:
- * https://pypi.org/simple/twine/""")
+ * https://pypi.org/simple/twine/"""
+ )
assert caplog.record_tuples == [
- ('pip._internal.index.collector', logging.DEBUG, expected_message),
+ ("pip._internal.index.collector", logging.DEBUG, expected_message),
]
@pytest.mark.parametrize(
- 'find_links, no_index, suppress_no_index, expected', [
- (['link1'], False, False,
- (['link1'], ['default_url', 'url1', 'url2'])),
- (['link1'], False, True, (['link1'], ['default_url', 'url1', 'url2'])),
- (['link1'], True, False, (['link1'], [])),
+ "find_links, no_index, suppress_no_index, expected",
+ [
+ (["link1"], False, False, (["link1"], ["default_url", "url1", "url2"])),
+ (["link1"], False, True, (["link1"], ["default_url", "url1", "url2"])),
+ (["link1"], True, False, (["link1"], [])),
# Passing suppress_no_index=True suppresses no_index=True.
- (['link1'], True, True, (['link1'], ['default_url', 'url1', 'url2'])),
+ (["link1"], True, True, (["link1"], ["default_url", "url1", "url2"])),
# Test options.find_links=False.
- (False, False, False, ([], ['default_url', 'url1', 'url2'])),
+ (False, False, False, ([], ["default_url", "url1", "url2"])),
],
)
def test_link_collector_create(
- find_links, no_index, suppress_no_index, expected,
-):
+ find_links: List[str],
+ no_index: bool,
+ suppress_no_index: bool,
+ expected: Tuple[List[str], List[str]],
+) -> None:
"""
:param expected: the expected (find_links, index_urls) values.
"""
expected_find_links, expected_index_urls = expected
session = PipSession()
- options = pretend.stub(
+ options = mock.Mock(
find_links=find_links,
- index_url='default_url',
- extra_index_urls=['url1', 'url2'],
+ index_url="default_url",
+ extra_index_urls=["url1", "url2"],
no_index=no_index,
)
link_collector = LinkCollector.create(
- session, options=options, suppress_no_index=suppress_no_index,
+ session,
+ options=options,
+ suppress_no_index=suppress_no_index,
)
assert link_collector.session is session
@@ -779,31 +1007,31 @@ def test_link_collector_create(
assert search_scope.index_urls == expected_index_urls
-@patch('os.path.expanduser')
+@mock.patch("os.path.expanduser")
def test_link_collector_create_find_links_expansion(
- mock_expanduser, tmpdir,
-):
+ mock_expanduser: mock.Mock, tmpdir: Path
+) -> None:
"""
Test "~" expansion in --find-links paths.
"""
# This is a mock version of expanduser() that expands "~" to the tmpdir.
- def expand_path(path):
- if path.startswith('~/'):
+ def expand_path(path: str) -> str:
+ if path.startswith("~/"):
path = os.path.join(tmpdir, path[2:])
return path
mock_expanduser.side_effect = expand_path
session = PipSession()
- options = pretend.stub(
- find_links=['~/temp1', '~/temp2'],
- index_url='default_url',
+ options = mock.Mock(
+ find_links=["~/temp1", "~/temp2"],
+ index_url="default_url",
extra_index_urls=[],
no_index=False,
)
# Only create temp2 and not temp1 to test that "~" expansion only occurs
# when the directory exists.
- temp2_dir = os.path.join(tmpdir, 'temp2')
+ temp2_dir = os.path.join(tmpdir, "temp2")
os.mkdir(temp2_dir)
link_collector = LinkCollector.create(session, options=options)
@@ -811,5 +1039,25 @@ def test_link_collector_create_find_links_expansion(
search_scope = link_collector.search_scope
# Only ~/temp2 gets expanded. Also, the path is normalized when expanded.
expected_temp2_dir = os.path.normcase(temp2_dir)
- assert search_scope.find_links == ['~/temp1', expected_temp2_dir]
- assert search_scope.index_urls == ['default_url']
+ assert search_scope.find_links == ["~/temp1", expected_temp2_dir]
+ assert search_scope.index_urls == ["default_url"]
+
+
+@pytest.mark.parametrize(
+ "url, result",
+ [
+ (
+ "https://pypi.org/pip-18.0.tar.gz#sha256=aa113592bbe",
+ LinkHash("sha256", "aa113592bbe"),
+ ),
+ (
+ "https://pypi.org/pip-18.0.tar.gz#md5=aa113592bbe",
+ LinkHash("md5", "aa113592bbe"),
+ ),
+ ("https://pypi.org/pip-18.0.tar.gz", None),
+ # We don't recognize the "sha500" algorithm, so we discard it.
+ ("https://pypi.org/pip-18.0.tar.gz#sha500=aa113592bbe", None),
+ ],
+)
+def test_link_hash_parsing(url: str, result: Optional[LinkHash]) -> None:
+ assert LinkHash.split_hash_name_and_value(url) == result
diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py
index dc8365efc..69792dd98 100644
--- a/tests/unit/test_command_install.py
+++ b/tests/unit/test_command_install.py
@@ -1,5 +1,5 @@
import errno
-from unittest.mock import patch
+from unittest import mock
import pytest
from pip._vendor.packaging.requirements import Requirement
@@ -15,38 +15,40 @@ from pip._internal.req.req_install import InstallRequirement
class TestDecideUserInstall:
- @patch('site.ENABLE_USER_SITE', True)
- @patch('pip._internal.commands.install.site_packages_writable')
- def test_prefix_and_target(self, sp_writable):
+ @mock.patch("site.ENABLE_USER_SITE", True)
+ @mock.patch("pip._internal.commands.install.site_packages_writable")
+ def test_prefix_and_target(self, sp_writable: mock.Mock) -> None:
sp_writable.return_value = False
- assert decide_user_install(
- use_user_site=None, prefix_path='foo'
- ) is False
+ assert decide_user_install(use_user_site=None, prefix_path="foo") is False
- assert decide_user_install(
- use_user_site=None, target_dir='bar'
- ) is False
+ assert decide_user_install(use_user_site=None, target_dir="bar") is False
@pytest.mark.parametrize(
- "enable_user_site,site_packages_writable,result", [
+ "enable_user_site,site_packages_writable,result",
+ [
(True, True, False),
(True, False, True),
(False, True, False),
(False, False, False),
- ])
+ ],
+ )
def test_most_cases(
- self, enable_user_site, site_packages_writable, result, monkeypatch,
- ):
- monkeypatch.setattr('site.ENABLE_USER_SITE', enable_user_site)
+ self,
+ enable_user_site: bool,
+ site_packages_writable: bool,
+ result: bool,
+ monkeypatch: pytest.MonkeyPatch,
+ ) -> None:
+ monkeypatch.setattr("site.ENABLE_USER_SITE", enable_user_site)
monkeypatch.setattr(
- 'pip._internal.commands.install.site_packages_writable',
- lambda **kw: site_packages_writable
+ "pip._internal.commands.install.site_packages_writable",
+ lambda **kw: site_packages_writable,
)
assert decide_user_install(use_user_site=None) is result
-def test_rejection_for_pip_install_options():
+def test_rejection_for_pip_install_options() -> None:
install_options = ["--prefix=/hello"]
with pytest.raises(CommandError) as e:
reject_location_related_install_options([], install_options)
@@ -54,13 +56,10 @@ def test_rejection_for_pip_install_options():
assert "['--prefix'] from command line" in str(e.value)
-def test_rejection_for_location_requirement_options():
- install_options = []
-
+def test_rejection_for_location_requirement_options() -> None:
bad_named_req_options = ["--home=/wow"]
bad_named_req = InstallRequirement(
- Requirement("hello"), "requirements.txt",
- install_options=bad_named_req_options
+ Requirement("hello"), "requirements.txt", install_options=bad_named_req_options
)
bad_unnamed_req_options = ["--install-lib=/lib"]
@@ -70,7 +69,7 @@ def test_rejection_for_location_requirement_options():
with pytest.raises(CommandError) as e:
reject_location_related_install_options(
- [bad_named_req, bad_unnamed_req], install_options
+ [bad_named_req, bad_unnamed_req], options=[]
)
assert (
@@ -80,38 +79,82 @@ def test_rejection_for_location_requirement_options():
assert "['--home'] from hello (from requirements.txt)" in str(e.value)
-@pytest.mark.parametrize('error, show_traceback, using_user_site, expected', [
- # show_traceback = True, using_user_site = True
- (OSError("Illegal byte sequence"), True, True, 'Could not install'
- ' packages due to an OSError.\n'),
- (OSError(errno.EACCES, "No file permission"), True, True, 'Could'
- ' not install packages due to an OSError.\nCheck the'
- ' permissions.\n'),
- # show_traceback = True, using_user_site = False
- (OSError("Illegal byte sequence"), True, False, 'Could not'
- ' install packages due to an OSError.\n'),
- (OSError(errno.EACCES, "No file permission"), True, False, 'Could'
- ' not install packages due to an OSError.\nConsider using the'
- ' `--user` option or check the permissions.\n'),
- # show_traceback = False, using_user_site = True
- (OSError("Illegal byte sequence"), False, True, 'Could not'
- ' install packages due to an OSError: Illegal byte'
- ' sequence\n'),
- (OSError(errno.EACCES, "No file permission"), False, True, 'Could'
- ' not install packages due to an OSError: [Errno 13] No file'
- ' permission\nCheck the permissions.\n'),
- # show_traceback = False, using_user_site = False
- (OSError("Illegal byte sequence"), False, False, 'Could not'
- ' install packages due to an OSError: Illegal byte sequence'
- '\n'),
- (OSError(errno.EACCES, "No file permission"), False, False,
- 'Could not install packages due to an OSError: [Errno 13] No'
- ' file permission\nConsider using the `--user` option or check the'
- ' permissions.\n'),
-])
+@pytest.mark.parametrize(
+ "error, show_traceback, using_user_site, expected",
+ [
+ # show_traceback = True, using_user_site = True
+ (
+ OSError("Illegal byte sequence"),
+ True,
+ True,
+ "Could not install packages due to an OSError.\n",
+ ),
+ (
+ OSError(errno.EACCES, "No file permission"),
+ True,
+ True,
+ "Could"
+ " not install packages due to an OSError.\nCheck the"
+ " permissions.\n",
+ ),
+ # show_traceback = True, using_user_site = False
+ (
+ OSError("Illegal byte sequence"),
+ True,
+ False,
+ "Could not install packages due to an OSError.\n",
+ ),
+ (
+ OSError(errno.EACCES, "No file permission"),
+ True,
+ False,
+ "Could"
+ " not install packages due to an OSError.\nConsider using the"
+ " `--user` option or check the permissions.\n",
+ ),
+ # show_traceback = False, using_user_site = True
+ (
+ OSError("Illegal byte sequence"),
+ False,
+ True,
+ "Could not"
+ " install packages due to an OSError: Illegal byte"
+ " sequence\n",
+ ),
+ (
+ OSError(errno.EACCES, "No file permission"),
+ False,
+ True,
+ "Could"
+ " not install packages due to an OSError: [Errno 13] No file"
+ " permission\nCheck the permissions.\n",
+ ),
+ # show_traceback = False, using_user_site = False
+ (
+ OSError("Illegal byte sequence"),
+ False,
+ False,
+ "Could not"
+ " install packages due to an OSError: Illegal byte sequence"
+ "\n",
+ ),
+ (
+ OSError(errno.EACCES, "No file permission"),
+ False,
+ False,
+ "Could not install packages due to an OSError: [Errno 13] No"
+ " file permission\nConsider using the `--user` option or check the"
+ " permissions.\n",
+ ),
+ ],
+)
def test_create_os_error_message(
- monkeypatch, error, show_traceback, using_user_site, expected
-):
+ monkeypatch: pytest.MonkeyPatch,
+ error: OSError,
+ show_traceback: bool,
+ using_user_site: bool,
+ expected: str,
+) -> None:
monkeypatch.setattr(install, "running_under_virtualenv", lambda: False)
msg = create_os_error_message(error, show_traceback, using_user_site)
assert msg == expected
diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py
index cb144c5f6..7a5c4e831 100644
--- a/tests/unit/test_commands.py
+++ b/tests/unit/test_commands.py
@@ -1,7 +1,9 @@
-from unittest.mock import patch
+from typing import Callable, List
+from unittest import mock
import pytest
+from pip._internal.cli.base_command import Command
from pip._internal.cli.req_command import (
IndexGroupCommand,
RequirementCommand,
@@ -11,71 +13,70 @@ from pip._internal.commands import commands_dict, create_command
# These are the expected names of the commands whose classes inherit from
# IndexGroupCommand.
-EXPECTED_INDEX_GROUP_COMMANDS = [
- 'download', 'index', 'install', 'list', 'wheel']
+EXPECTED_INDEX_GROUP_COMMANDS = ["download", "index", "install", "list", "wheel"]
-def check_commands(pred, expected):
+def check_commands(pred: Callable[[Command], bool], expected: List[str]) -> None:
"""
Check the commands satisfying a predicate.
"""
commands = [create_command(name) for name in sorted(commands_dict)]
actual = [command.name for command in commands if pred(command)]
- assert actual == expected, f'actual: {actual}'
+ assert actual == expected, f"actual: {actual}"
-def test_commands_dict__order():
+def test_commands_dict__order() -> None:
"""
Check the ordering of commands_dict.
"""
names = list(commands_dict)
# A spot-check is sufficient to check that commands_dict encodes an
# ordering.
- assert names[0] == 'install'
- assert names[-1] == 'help'
+ assert names[0] == "install"
+ assert names[-1] == "help"
-@pytest.mark.parametrize('name', list(commands_dict))
-def test_create_command(name):
+@pytest.mark.parametrize("name", list(commands_dict))
+def test_create_command(name: str) -> None:
"""Test creating an instance of each available command."""
command = create_command(name)
assert command.name == name
assert command.summary == commands_dict[name].summary
-def test_session_commands():
+def test_session_commands() -> None:
"""
Test which commands inherit from SessionCommandMixin.
"""
- def is_session_command(command):
+
+ def is_session_command(command: Command) -> bool:
return isinstance(command, SessionCommandMixin)
- expected = [
- 'download', 'index', 'install', 'list', 'search', 'uninstall', 'wheel'
- ]
+ expected = ["download", "index", "install", "list", "search", "uninstall", "wheel"]
check_commands(is_session_command, expected)
-def test_index_group_commands():
+def test_index_group_commands() -> None:
"""
Test the commands inheriting from IndexGroupCommand.
"""
- def is_index_group_command(command):
+
+ def is_index_group_command(command: Command) -> bool:
return isinstance(command, IndexGroupCommand)
check_commands(is_index_group_command, EXPECTED_INDEX_GROUP_COMMANDS)
# Also check that the commands inheriting from IndexGroupCommand are
# exactly the commands with the --no-index option.
- def has_option_no_index(command):
- return command.parser.has_option('--no-index')
+ def has_option_no_index(command: Command) -> bool:
+ return command.parser.has_option("--no-index")
check_commands(has_option_no_index, EXPECTED_INDEX_GROUP_COMMANDS)
-@pytest.mark.parametrize('command_name', EXPECTED_INDEX_GROUP_COMMANDS)
+@pytest.mark.parametrize("command_name", EXPECTED_INDEX_GROUP_COMMANDS)
@pytest.mark.parametrize(
- 'disable_pip_version_check, no_index, expected_called',
+ "disable_pip_version_check, no_index, expected_called",
[
# pip_self_version_check() is only called when both
# disable_pip_version_check and no_index are False.
@@ -85,11 +86,14 @@ def test_index_group_commands():
(True, True, False),
],
)
-@patch('pip._internal.cli.req_command.pip_self_version_check')
+@mock.patch("pip._internal.cli.req_command.pip_self_version_check")
def test_index_group_handle_pip_version_check(
- mock_version_check, command_name, disable_pip_version_check, no_index,
- expected_called,
-):
+ mock_version_check: mock.Mock,
+ command_name: str,
+ disable_pip_version_check: bool,
+ no_index: bool,
+ expected_called: bool,
+) -> None:
"""
Test whether pip_self_version_check() is called when
handle_pip_version_check() is called, for each of the
@@ -107,11 +111,12 @@ def test_index_group_handle_pip_version_check(
mock_version_check.assert_not_called()
-def test_requirement_commands():
+def test_requirement_commands() -> None:
"""
Test which commands inherit from RequirementCommand.
"""
- def is_requirement_command(command):
+
+ def is_requirement_command(command: Command) -> bool:
return isinstance(command, RequirementCommand)
- check_commands(is_requirement_command, ['download', 'install', 'wheel'])
+ check_commands(is_requirement_command, ["download", "install", "wheel"])
diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py
index 2d7cbf5c3..b7f46ca90 100644
--- a/tests/unit/test_compat.py
+++ b/tests/unit/test_compat.py
@@ -1,17 +1,18 @@
import os
+from pathlib import Path
import pytest
from pip._internal.utils.compat import get_path_uid
-def test_get_path_uid():
+def test_get_path_uid() -> None:
path = os.getcwd()
assert get_path_uid(path) == os.stat(path).st_uid
@pytest.mark.skipif("not hasattr(os, 'O_NOFOLLOW')")
-def test_get_path_uid_without_NOFOLLOW(monkeypatch):
+def test_get_path_uid_without_NOFOLLOW(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delattr("os.O_NOFOLLOW")
path = os.getcwd()
assert get_path_uid(path) == os.stat(path).st_uid
@@ -20,11 +21,11 @@ def test_get_path_uid_without_NOFOLLOW(monkeypatch):
# Skip unconditionally on Windows, as symlinks need admin privs there
@pytest.mark.skipif("sys.platform == 'win32'")
@pytest.mark.skipif("not hasattr(os, 'symlink')")
-def test_get_path_uid_symlink(tmpdir):
+def test_get_path_uid_symlink(tmpdir: Path) -> None:
f = tmpdir / "symlink" / "somefile"
f.parent.mkdir()
f.write_text("content")
- fs = f + '_link'
+ fs = f"{f}_link"
os.symlink(f, fs)
with pytest.raises(OSError):
get_path_uid(fs)
@@ -32,12 +33,14 @@ def test_get_path_uid_symlink(tmpdir):
@pytest.mark.skipif("not hasattr(os, 'O_NOFOLLOW')")
@pytest.mark.skipif("not hasattr(os, 'symlink')")
-def test_get_path_uid_symlink_without_NOFOLLOW(tmpdir, monkeypatch):
+def test_get_path_uid_symlink_without_NOFOLLOW(
+ tmpdir: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
monkeypatch.delattr("os.O_NOFOLLOW")
f = tmpdir / "symlink" / "somefile"
f.parent.mkdir()
f.write_text("content")
- fs = f + '_link'
+ fs = f"{f}_link"
os.symlink(f, fs)
with pytest.raises(OSError):
get_path_uid(fs)
diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py
index 9af6d0515..c6b44d45a 100644
--- a/tests/unit/test_configuration.py
+++ b/tests/unit/test_configuration.py
@@ -1,6 +1,7 @@
"""Tests for all things related to the configuration
"""
+import re
from unittest.mock import MagicMock
import pytest
@@ -11,26 +12,25 @@ from tests.lib.configuration_helpers import ConfigurationMixin
class TestConfigurationLoading(ConfigurationMixin):
-
- def test_global_loading(self):
+ def test_global_loading(self) -> None:
self.patch_configuration(kinds.GLOBAL, {"test.hello": "1"})
self.configuration.load()
assert self.configuration.get_value("test.hello") == "1"
- def test_user_loading(self):
+ def test_user_loading(self) -> None:
self.patch_configuration(kinds.USER, {"test.hello": "2"})
self.configuration.load()
assert self.configuration.get_value("test.hello") == "2"
- def test_site_loading(self):
+ def test_site_loading(self) -> None:
self.patch_configuration(kinds.SITE, {"test.hello": "3"})
self.configuration.load()
assert self.configuration.get_value("test.hello") == "3"
- def test_environment_config_loading(self, monkeypatch):
+ def test_environment_config_loading(self, monkeypatch: pytest.MonkeyPatch) -> None:
contents = """
[test]
hello = 4
@@ -40,24 +40,29 @@ class TestConfigurationLoading(ConfigurationMixin):
monkeypatch.setenv("PIP_CONFIG_FILE", config_file)
self.configuration.load()
- assert self.configuration.get_value("test.hello") == "4", \
- self.configuration._config
+ assert (
+ self.configuration.get_value("test.hello") == "4"
+ ), self.configuration._config
- def test_environment_var_loading(self, monkeypatch):
+ def test_environment_var_loading(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("PIP_HELLO", "5")
self.configuration.load()
assert self.configuration.get_value(":env:.hello") == "5"
@pytest.mark.skipif("sys.platform == 'win32'")
- def test_environment_var_does_not_load_lowercase(self, monkeypatch):
+ def test_environment_var_does_not_load_lowercase(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
monkeypatch.setenv("pip_hello", "5")
self.configuration.load()
with pytest.raises(ConfigurationError):
self.configuration.get_value(":env:.hello")
- def test_environment_var_does_not_load_version(self, monkeypatch):
+ def test_environment_var_does_not_load_version(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
monkeypatch.setenv("PIP_VERSION", "True")
self.configuration.load()
@@ -65,7 +70,9 @@ class TestConfigurationLoading(ConfigurationMixin):
with pytest.raises(ConfigurationError):
self.configuration.get_value(":env:.version")
- def test_environment_config_errors_if_malformed(self, monkeypatch):
+ def test_environment_config_errors_if_malformed(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
contents = """
test]
hello = 4
@@ -77,59 +84,79 @@ class TestConfigurationLoading(ConfigurationMixin):
assert "section header" in str(err.value) # error kind
assert "1" in str(err.value) # line number
- assert ( # file name
- config_file in str(err.value) or
- repr(config_file) in str(err.value)
+ assert config_file in str(err.value) or repr(config_file) in str( # file name
+ err.value
+ )
+
+ def test_no_such_key_error_message_no_command(self) -> None:
+ self.configuration.load_only = kinds.GLOBAL
+ self.configuration.load()
+ expected_msg = (
+ "Key does not contain dot separated section and key. "
+ "Perhaps you wanted to use 'global.index-url' instead?"
)
+ pat = f"^{re.escape(expected_msg)}$"
+ with pytest.raises(ConfigurationError, match=pat):
+ self.configuration.get_value("index-url")
+
+ def test_no_such_key_error_message_missing_option(self) -> None:
+ self.configuration.load_only = kinds.GLOBAL
+ self.configuration.load()
+ expected_msg = "No such key - global.index-url"
+ pat = f"^{re.escape(expected_msg)}$"
+ with pytest.raises(ConfigurationError, match=pat):
+ self.configuration.get_value("global.index-url")
class TestConfigurationPrecedence(ConfigurationMixin):
# Tests for methods to that determine the order of precedence of
# configuration options
- def test_env_overides_site(self):
+ def test_env_overides_site(self) -> None:
self.patch_configuration(kinds.SITE, {"test.hello": "1"})
self.patch_configuration(kinds.ENV, {"test.hello": "0"})
self.configuration.load()
assert self.configuration.get_value("test.hello") == "0"
- def test_env_overides_user(self):
+ def test_env_overides_user(self) -> None:
self.patch_configuration(kinds.USER, {"test.hello": "2"})
self.patch_configuration(kinds.ENV, {"test.hello": "0"})
self.configuration.load()
assert self.configuration.get_value("test.hello") == "0"
- def test_env_overides_global(self):
+ def test_env_overides_global(self) -> None:
self.patch_configuration(kinds.GLOBAL, {"test.hello": "3"})
self.patch_configuration(kinds.ENV, {"test.hello": "0"})
self.configuration.load()
assert self.configuration.get_value("test.hello") == "0"
- def test_site_overides_user(self):
+ def test_site_overides_user(self) -> None:
self.patch_configuration(kinds.USER, {"test.hello": "2"})
self.patch_configuration(kinds.SITE, {"test.hello": "1"})
self.configuration.load()
assert self.configuration.get_value("test.hello") == "1"
- def test_site_overides_global(self):
+ def test_site_overides_global(self) -> None:
self.patch_configuration(kinds.GLOBAL, {"test.hello": "3"})
self.patch_configuration(kinds.SITE, {"test.hello": "1"})
self.configuration.load()
assert self.configuration.get_value("test.hello") == "1"
- def test_user_overides_global(self):
+ def test_user_overides_global(self) -> None:
self.patch_configuration(kinds.GLOBAL, {"test.hello": "3"})
self.patch_configuration(kinds.USER, {"test.hello": "2"})
self.configuration.load()
assert self.configuration.get_value("test.hello") == "2"
- def test_env_not_overriden_by_environment_var(self, monkeypatch):
+ def test_env_not_overriden_by_environment_var(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
self.patch_configuration(kinds.ENV, {"test.hello": "1"})
monkeypatch.setenv("PIP_HELLO", "5")
@@ -138,7 +165,9 @@ class TestConfigurationPrecedence(ConfigurationMixin):
assert self.configuration.get_value("test.hello") == "1"
assert self.configuration.get_value(":env:.hello") == "5"
- def test_site_not_overriden_by_environment_var(self, monkeypatch):
+ def test_site_not_overriden_by_environment_var(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
self.patch_configuration(kinds.SITE, {"test.hello": "2"})
monkeypatch.setenv("PIP_HELLO", "5")
@@ -147,7 +176,9 @@ class TestConfigurationPrecedence(ConfigurationMixin):
assert self.configuration.get_value("test.hello") == "2"
assert self.configuration.get_value(":env:.hello") == "5"
- def test_user_not_overriden_by_environment_var(self, monkeypatch):
+ def test_user_not_overriden_by_environment_var(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
self.patch_configuration(kinds.USER, {"test.hello": "3"})
monkeypatch.setenv("PIP_HELLO", "5")
@@ -156,7 +187,9 @@ class TestConfigurationPrecedence(ConfigurationMixin):
assert self.configuration.get_value("test.hello") == "3"
assert self.configuration.get_value(":env:.hello") == "5"
- def test_global_not_overriden_by_environment_var(self, monkeypatch):
+ def test_global_not_overriden_by_environment_var(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
self.patch_configuration(kinds.GLOBAL, {"test.hello": "4"})
monkeypatch.setenv("PIP_HELLO", "5")
@@ -169,40 +202,36 @@ class TestConfigurationPrecedence(ConfigurationMixin):
class TestConfigurationModification(ConfigurationMixin):
# Tests for methods to that modify the state of a Configuration
- def test_no_specific_given_modification(self):
+ def test_no_specific_given_modification(self) -> None:
self.configuration.load()
- try:
+ with pytest.raises(ConfigurationError):
self.configuration.set_value("test.hello", "10")
- except ConfigurationError:
- pass
- else:
- assert False, "Should have raised an error."
- def test_site_modification(self):
+ def test_site_modification(self) -> None:
self.configuration.load_only = kinds.SITE
self.configuration.load()
# Mock out the method
mymock = MagicMock(spec=self.configuration._mark_as_modified)
- self.configuration._mark_as_modified = mymock
+ # https://github.com/python/mypy/issues/2427
+ self.configuration._mark_as_modified = mymock # type: ignore[assignment]
self.configuration.set_value("test.hello", "10")
# get the path to site config file
assert mymock.call_count == 1
- assert mymock.call_args[0][0] == (
- get_configuration_files()[kinds.SITE][0]
- )
+ assert mymock.call_args[0][0] == (get_configuration_files()[kinds.SITE][0])
- def test_user_modification(self):
+ def test_user_modification(self) -> None:
# get the path to local config file
self.configuration.load_only = kinds.USER
self.configuration.load()
# Mock out the method
mymock = MagicMock(spec=self.configuration._mark_as_modified)
- self.configuration._mark_as_modified = mymock
+ # https://github.com/python/mypy/issues/2427
+ self.configuration._mark_as_modified = mymock # type: ignore[assignment]
self.configuration.set_value("test.hello", "10")
@@ -213,19 +242,31 @@ class TestConfigurationModification(ConfigurationMixin):
get_configuration_files()[kinds.USER][1]
)
- def test_global_modification(self):
+ def test_global_modification(self) -> None:
# get the path to local config file
self.configuration.load_only = kinds.GLOBAL
self.configuration.load()
# Mock out the method
mymock = MagicMock(spec=self.configuration._mark_as_modified)
- self.configuration._mark_as_modified = mymock
+ # https://github.com/python/mypy/issues/2427
+ self.configuration._mark_as_modified = mymock # type: ignore[assignment]
self.configuration.set_value("test.hello", "10")
# get the path to user config file
assert mymock.call_count == 1
- assert mymock.call_args[0][0] == (
- get_configuration_files()[kinds.GLOBAL][-1]
- )
+ assert mymock.call_args[0][0] == (get_configuration_files()[kinds.GLOBAL][-1])
+
+ def test_normalization(self) -> None:
+ # underscores and dashes can be used interchangeably.
+ # internally, underscores get converted into dashes before reading/writing file
+ self.configuration.load_only = kinds.GLOBAL
+ self.configuration.load()
+ self.configuration.set_value("global.index_url", "example.org")
+ assert self.configuration.get_value("global.index_url") == "example.org"
+ assert self.configuration.get_value("global.index-url") == "example.org"
+ self.configuration.unset_value("global.index-url")
+ pat = r"^No such key - global\.index-url$"
+ with pytest.raises(ConfigurationError, match=pat):
+ self.configuration.get_value("global.index-url")
diff --git a/tests/unit/test_direct_url.py b/tests/unit/test_direct_url.py
index ee6b7fbf4..c81e51292 100644
--- a/tests/unit/test_direct_url.py
+++ b/tests/unit/test_direct_url.py
@@ -9,14 +9,15 @@ from pip._internal.models.direct_url import (
)
-def test_from_json():
+def test_from_json() -> None:
json = '{"url": "file:///home/user/project", "dir_info": {}}'
direct_url = DirectUrl.from_json(json)
assert direct_url.url == "file:///home/user/project"
+ assert isinstance(direct_url.info, DirInfo)
assert direct_url.info.editable is False
-def test_to_json():
+def test_to_json() -> None:
direct_url = DirectUrl(
url="file:///home/user/archive.tgz",
info=ArchiveInfo(),
@@ -27,21 +28,21 @@ def test_to_json():
)
-def test_archive_info():
+def test_archive_info() -> None:
direct_url_dict = {
"url": "file:///home/user/archive.tgz",
- "archive_info": {
- "hash": "sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220"
- },
+ "archive_info": {"hash": "sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220"},
}
direct_url = DirectUrl.from_dict(direct_url_dict)
assert isinstance(direct_url.info, ArchiveInfo)
assert direct_url.url == direct_url_dict["url"]
- assert direct_url.info.hash == direct_url_dict["archive_info"]["hash"]
+ assert (
+ direct_url.info.hash == direct_url_dict["archive_info"]["hash"] # type: ignore
+ )
assert direct_url.to_dict() == direct_url_dict
-def test_dir_info():
+def test_dir_info() -> None:
direct_url_dict = {
"url": "file:///home/user/project",
"dir_info": {"editable": True},
@@ -54,10 +55,11 @@ def test_dir_info():
# test editable default to False
direct_url_dict = {"url": "file:///home/user/project", "dir_info": {}}
direct_url = DirectUrl.from_dict(direct_url_dict)
+ assert isinstance(direct_url.info, DirInfo)
assert direct_url.info.editable is False
-def test_vcs_info():
+def test_vcs_info() -> None:
direct_url_dict = {
"url": "https:///g.c/u/p.git",
"vcs_info": {
@@ -71,58 +73,42 @@ def test_vcs_info():
assert direct_url.url == direct_url_dict["url"]
assert direct_url.info.vcs == "git"
assert direct_url.info.requested_revision == "master"
- assert (
- direct_url.info.commit_id == "1b8c5bc61a86f377fea47b4276c8c8a5842d2220"
- )
+ assert direct_url.info.commit_id == "1b8c5bc61a86f377fea47b4276c8c8a5842d2220"
assert direct_url.to_dict() == direct_url_dict
-def test_parsing_validation():
- with pytest.raises(
- DirectUrlValidationError, match="url must have a value"
- ):
+def test_parsing_validation() -> None:
+ with pytest.raises(DirectUrlValidationError, match="url must have a value"):
DirectUrl.from_dict({"dir_info": {}})
with pytest.raises(
DirectUrlValidationError,
match="missing one of archive_info, dir_info, vcs_info",
):
DirectUrl.from_dict({"url": "http://..."})
- with pytest.raises(
- DirectUrlValidationError, match="unexpected type for editable"
- ):
- DirectUrl.from_dict(
- {"url": "http://...", "dir_info": {"editable": "false"}}
- )
- with pytest.raises(
- DirectUrlValidationError, match="unexpected type for hash"
- ):
+ with pytest.raises(DirectUrlValidationError, match="unexpected type for editable"):
+ DirectUrl.from_dict({"url": "http://...", "dir_info": {"editable": "false"}})
+ with pytest.raises(DirectUrlValidationError, match="unexpected type for hash"):
DirectUrl.from_dict({"url": "http://...", "archive_info": {"hash": 1}})
- with pytest.raises(
- DirectUrlValidationError, match="unexpected type for vcs"
- ):
+ with pytest.raises(DirectUrlValidationError, match="unexpected type for vcs"):
DirectUrl.from_dict({"url": "http://...", "vcs_info": {"vcs": None}})
- with pytest.raises(
- DirectUrlValidationError, match="commit_id must have a value"
- ):
+ with pytest.raises(DirectUrlValidationError, match="commit_id must have a value"):
DirectUrl.from_dict({"url": "http://...", "vcs_info": {"vcs": "git"}})
with pytest.raises(
DirectUrlValidationError,
match="more than one of archive_info, dir_info, vcs_info",
):
- DirectUrl.from_dict(
- {"url": "http://...", "dir_info": {}, "archive_info": {}}
- )
+ DirectUrl.from_dict({"url": "http://...", "dir_info": {}, "archive_info": {}})
-def test_redact_url():
- def _redact_git(url):
+def test_redact_url() -> None:
+ def _redact_git(url: str) -> str:
direct_url = DirectUrl(
url=url,
info=VcsInfo(vcs="git", commit_id="1"),
)
return direct_url.redacted_url
- def _redact_archive(url):
+ def _redact_archive(url: str) -> str:
direct_url = DirectUrl(
url=url,
info=ArchiveInfo(),
@@ -130,22 +116,16 @@ def test_redact_url():
return direct_url.redacted_url
assert (
- _redact_git("https://user:password@g.c/u/p.git@branch#egg=pkg") ==
- "https://g.c/u/p.git@branch#egg=pkg"
- )
- assert (
- _redact_git("https://${USER}:password@g.c/u/p.git") ==
- "https://g.c/u/p.git"
- )
- assert (
- _redact_archive("file://${U}:${PIP_PASSWORD}@g.c/u/p.tgz") ==
- "file://${U}:${PIP_PASSWORD}@g.c/u/p.tgz"
+ _redact_git("https://user:password@g.c/u/p.git@branch#egg=pkg")
+ == "https://g.c/u/p.git@branch#egg=pkg"
)
+ assert _redact_git("https://${USER}:password@g.c/u/p.git") == "https://g.c/u/p.git"
assert (
- _redact_git("https://${PIP_TOKEN}@g.c/u/p.git") ==
- "https://${PIP_TOKEN}@g.c/u/p.git"
+ _redact_archive("file://${U}:${PIP_PASSWORD}@g.c/u/p.tgz")
+ == "file://${U}:${PIP_PASSWORD}@g.c/u/p.tgz"
)
assert (
- _redact_git("ssh://git@g.c/u/p.git") ==
- "ssh://git@g.c/u/p.git"
+ _redact_git("https://${PIP_TOKEN}@g.c/u/p.git")
+ == "https://${PIP_TOKEN}@g.c/u/p.git"
)
+ assert _redact_git("ssh://git@g.c/u/p.git") == "ssh://git@g.c/u/p.git"
diff --git a/tests/unit/test_direct_url_helpers.py b/tests/unit/test_direct_url_helpers.py
index ffa7e9b04..3ab253462 100644
--- a/tests/unit/test_direct_url_helpers.py
+++ b/tests/unit/test_direct_url_helpers.py
@@ -1,5 +1,7 @@
+import os
from functools import partial
-from unittest.mock import patch
+from pathlib import Path
+from unittest import mock
from pip._internal.models.direct_url import ArchiveInfo, DirectUrl, DirInfo, VcsInfo
from pip._internal.models.link import Link
@@ -7,47 +9,48 @@ from pip._internal.utils.direct_url_helpers import (
direct_url_as_pep440_direct_reference,
direct_url_from_link,
)
-from pip._internal.utils.urls import path_to_url
+from pip._internal.vcs.git import Git
-def test_as_pep440_requirement_archive():
+def test_as_pep440_requirement_archive() -> None:
direct_url = DirectUrl(
url="file:///home/user/archive.tgz",
info=ArchiveInfo(),
)
direct_url.validate()
assert (
- direct_url_as_pep440_direct_reference(direct_url, "pkg") ==
- "pkg @ file:///home/user/archive.tgz"
+ direct_url_as_pep440_direct_reference(direct_url, "pkg")
+ == "pkg @ file:///home/user/archive.tgz"
)
direct_url.subdirectory = "subdir"
direct_url.validate()
assert (
- direct_url_as_pep440_direct_reference(direct_url, "pkg") ==
- "pkg @ file:///home/user/archive.tgz#subdirectory=subdir"
+ direct_url_as_pep440_direct_reference(direct_url, "pkg")
+ == "pkg @ file:///home/user/archive.tgz#subdirectory=subdir"
)
+ assert isinstance(direct_url.info, ArchiveInfo)
direct_url.info.hash = "sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220"
direct_url.validate()
assert (
- direct_url_as_pep440_direct_reference(direct_url, "pkg") ==
- "pkg @ file:///home/user/archive.tgz"
+ direct_url_as_pep440_direct_reference(direct_url, "pkg")
+ == "pkg @ file:///home/user/archive.tgz"
"#sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220&subdirectory=subdir"
)
-def test_as_pep440_requirement_dir():
+def test_as_pep440_requirement_dir() -> None:
direct_url = DirectUrl(
url="file:///home/user/project",
info=DirInfo(editable=False),
)
direct_url.validate()
assert (
- direct_url_as_pep440_direct_reference(direct_url, "pkg") ==
- "pkg @ file:///home/user/project"
+ direct_url_as_pep440_direct_reference(direct_url, "pkg")
+ == "pkg @ file:///home/user/project"
)
-def test_as_pep440_requirement_editable_dir():
+def test_as_pep440_requirement_editable_dir() -> None:
# direct_url_as_pep440_direct_reference behaves the same
# irrespective of the editable flag. It's the responsibility of
# callers to render it as editable
@@ -57,35 +60,33 @@ def test_as_pep440_requirement_editable_dir():
)
direct_url.validate()
assert (
- direct_url_as_pep440_direct_reference(direct_url, "pkg") ==
- "pkg @ file:///home/user/project"
+ direct_url_as_pep440_direct_reference(direct_url, "pkg")
+ == "pkg @ file:///home/user/project"
)
-def test_as_pep440_requirement_vcs():
+def test_as_pep440_requirement_vcs() -> None:
direct_url = DirectUrl(
url="https:///g.c/u/p.git",
- info=VcsInfo(
- vcs="git", commit_id="1b8c5bc61a86f377fea47b4276c8c8a5842d2220"
- )
+ info=VcsInfo(vcs="git", commit_id="1b8c5bc61a86f377fea47b4276c8c8a5842d2220"),
)
direct_url.validate()
assert (
- direct_url_as_pep440_direct_reference(direct_url, "pkg") ==
- "pkg @ git+https:///g.c/u/p.git"
+ direct_url_as_pep440_direct_reference(direct_url, "pkg")
+ == "pkg @ git+https:///g.c/u/p.git"
"@1b8c5bc61a86f377fea47b4276c8c8a5842d2220"
)
direct_url.subdirectory = "subdir"
direct_url.validate()
assert (
- direct_url_as_pep440_direct_reference(direct_url, "pkg") ==
- "pkg @ git+https:///g.c/u/p.git"
+ direct_url_as_pep440_direct_reference(direct_url, "pkg")
+ == "pkg @ git+https:///g.c/u/p.git"
"@1b8c5bc61a86f377fea47b4276c8c8a5842d2220#subdirectory=subdir"
)
-@patch("pip._internal.vcs.git.Git.get_revision")
-def test_from_link_vcs(mock_get_backend_for_scheme):
+@mock.patch("pip._internal.vcs.git.Git.get_revision")
+def test_from_link_vcs(mock_get_backend_for_scheme: mock.Mock) -> None:
_direct_url_from_link = partial(direct_url_from_link, source_dir="...")
direct_url = _direct_url_from_link(Link("git+https://g.c/u/p.git"))
assert direct_url.url == "https://g.c/u/p.git"
@@ -100,68 +101,61 @@ def test_from_link_vcs(mock_get_backend_for_scheme):
assert direct_url.subdirectory == "subdir"
direct_url = _direct_url_from_link(Link("git+https://g.c/u/p.git@branch"))
assert direct_url.url == "https://g.c/u/p.git"
+ assert isinstance(direct_url.info, VcsInfo)
assert direct_url.info.requested_revision == "branch"
- direct_url = _direct_url_from_link(
- Link("git+https://g.c/u/p.git@branch#egg=pkg")
- )
+ direct_url = _direct_url_from_link(Link("git+https://g.c/u/p.git@branch#egg=pkg"))
assert direct_url.url == "https://g.c/u/p.git"
+ assert isinstance(direct_url.info, VcsInfo)
assert direct_url.info.requested_revision == "branch"
- direct_url = _direct_url_from_link(
- Link("git+https://token@g.c/u/p.git")
- )
+ direct_url = _direct_url_from_link(Link("git+https://token@g.c/u/p.git"))
assert direct_url.to_dict()["url"] == "https://g.c/u/p.git"
-def test_from_link_vcs_with_source_dir_obtains_commit_id(script, tmpdir):
- repo_path = tmpdir / 'test-repo'
+def test_from_link_vcs_with_source_dir_obtains_commit_id(tmpdir: Path) -> None:
+ repo_path = tmpdir / "test-repo"
repo_path.mkdir()
- repo_dir = str(repo_path)
- script.run('git', 'init', cwd=repo_dir)
+ repo_dir = os.fspath(repo_path)
+ Git.run_command(["init"], cwd=repo_dir)
(repo_path / "somefile").touch()
- script.run('git', 'add', '.', cwd=repo_dir)
- script.run('git', 'commit', '-m', 'commit msg', cwd=repo_dir)
- commit_id = script.run(
- 'git', 'rev-parse', 'HEAD', cwd=repo_dir
- ).stdout.strip()
+ Git.run_command(["add", "."], cwd=repo_dir)
+ Git.run_command(["commit", "-m", "commit msg"], cwd=repo_dir)
+ commit_id = Git.get_revision(repo_dir)
direct_url = direct_url_from_link(
Link("git+https://g.c/u/p.git"), source_dir=repo_dir
)
assert direct_url.url == "https://g.c/u/p.git"
+ assert isinstance(direct_url.info, VcsInfo)
assert direct_url.info.commit_id == commit_id
-def test_from_link_vcs_without_source_dir(script, tmpdir):
+def test_from_link_vcs_without_source_dir() -> None:
direct_url = direct_url_from_link(
Link("git+https://g.c/u/p.git@1"), link_is_in_wheel_cache=True
)
assert direct_url.url == "https://g.c/u/p.git"
+ assert isinstance(direct_url.info, VcsInfo)
assert direct_url.info.commit_id == "1"
-def test_from_link_archive():
+def test_from_link_archive() -> None:
direct_url = direct_url_from_link(Link("https://g.c/archive.tgz"))
assert direct_url.url == "https://g.c/archive.tgz"
assert isinstance(direct_url.info, ArchiveInfo)
direct_url = direct_url_from_link(
- Link(
- "https://g.c/archive.tgz"
- "#sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220"
- )
+ Link("https://g.c/archive.tgz#sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220")
)
assert isinstance(direct_url.info, ArchiveInfo)
- assert (
- direct_url.info.hash == "sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220"
- )
+ assert direct_url.info.hash == "sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220"
-def test_from_link_dir(tmpdir):
- dir_url = path_to_url(tmpdir)
+def test_from_link_dir(tmpdir: Path) -> None:
+ dir_url = tmpdir.as_uri()
direct_url = direct_url_from_link(Link(dir_url))
assert direct_url.url == dir_url
assert isinstance(direct_url.info, DirInfo)
-def test_from_link_hide_user_password():
+def test_from_link_hide_user_password() -> None:
# Basic test only here, other variants are covered by
# direct_url.redact_url tests.
direct_url = direct_url_from_link(
diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py
new file mode 100644
index 000000000..8f8224dc8
--- /dev/null
+++ b/tests/unit/test_exceptions.py
@@ -0,0 +1,474 @@
+"""Tests the presentation style of exceptions."""
+
+import io
+import textwrap
+
+import pytest
+from pip._vendor import rich
+
+from pip._internal.exceptions import DiagnosticPipError
+
+
+class TestDiagnosticPipErrorCreation:
+ def test_fails_without_reference(self) -> None:
+ class DerivedError(DiagnosticPipError):
+ pass
+
+ with pytest.raises(AssertionError) as exc_info:
+ DerivedError(message="", context=None, hint_stmt=None)
+
+ assert str(exc_info.value) == "error reference not provided!"
+
+ def test_can_fetch_reference_from_subclass(self) -> None:
+ class DerivedError(DiagnosticPipError):
+ reference = "subclass-reference"
+
+ obj = DerivedError(message="", context=None, hint_stmt=None)
+ assert obj.reference == "subclass-reference"
+
+ def test_can_fetch_reference_from_arguments(self) -> None:
+ class DerivedError(DiagnosticPipError):
+ pass
+
+ obj = DerivedError(
+ message="", context=None, hint_stmt=None, reference="subclass-reference"
+ )
+ assert obj.reference == "subclass-reference"
+
+ @pytest.mark.parametrize(
+ "name",
+ [
+ "BADNAME",
+ "BadName",
+ "bad_name",
+ "BAD_NAME",
+ "_bad",
+ "bad-name-",
+ "bad--name",
+ "-bad-name",
+ "bad-name-due-to-1-number",
+ ],
+ )
+ def test_rejects_non_kebab_case_names(self, name: str) -> None:
+ class DerivedError(DiagnosticPipError):
+ reference = name
+
+ with pytest.raises(AssertionError) as exc_info:
+ DerivedError(message="", context=None, hint_stmt=None)
+
+ assert str(exc_info.value) == "error reference must be kebab-case!"
+
+
+def rendered_in_ascii(error: DiagnosticPipError, *, color: bool = False) -> str:
+ with io.BytesIO() as stream:
+ console = rich.console.Console(
+ force_terminal=False,
+ file=io.TextIOWrapper(stream, encoding="ascii", newline=""),
+ color_system="truecolor" if color else None,
+ )
+ console.print(error)
+ return stream.getvalue().decode("ascii")
+
+
+class TestDiagnosticPipErrorPresentation_ASCII:
+ def test_complete(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context="Something went wrong\nvery wrong.",
+ note_stmt="You did something wrong, which is what caused this error.",
+ hint_stmt="Do it better next time, by trying harder.",
+ )
+
+ assert rendered_in_ascii(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ Oh no!
+ It broke. :(
+
+ Something went wrong
+ very wrong.
+
+ note: You did something wrong, which is what caused this error.
+ hint: Do it better next time, by trying harder.
+ """
+ )
+
+ def test_complete_color(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke.",
+ context="Something went wrong\nvery wrong.",
+ note_stmt="You did something wrong.",
+ hint_stmt="Do it better next time, by trying harder.",
+ )
+
+ def esc(code: str = "0") -> str:
+ return f"\x1b[{code}m"
+
+ assert rendered_in_ascii(err, color=True) == textwrap.dedent(
+ f"""\
+ {esc("1;31")}error{esc("0")}: {esc("1")}test-diagnostic{esc("0")}
+
+ Oh no!
+ It broke.
+
+ Something went wrong
+ very wrong.
+
+ {esc("1;35")}note{esc("0")}: You did something wrong.
+ {esc("1;36")}hint{esc("0")}: Do it better next time, by trying harder.
+ """
+ )
+
+ def test_no_context(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context=None,
+ note_stmt="You did something wrong, which is what caused this error.",
+ hint_stmt="Do it better next time, by trying harder.",
+ )
+
+ assert rendered_in_ascii(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ Oh no!
+ It broke. :(
+
+ note: You did something wrong, which is what caused this error.
+ hint: Do it better next time, by trying harder.
+ """
+ )
+
+ def test_no_note(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context="Something went wrong\nvery wrong.",
+ note_stmt=None,
+ hint_stmt="Do it better next time, by trying harder.",
+ )
+
+ assert rendered_in_ascii(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ Oh no!
+ It broke. :(
+
+ Something went wrong
+ very wrong.
+
+ hint: Do it better next time, by trying harder.
+ """
+ )
+
+ def test_no_hint(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context="Something went wrong\nvery wrong.",
+ note_stmt="You did something wrong, which is what caused this error.",
+ hint_stmt=None,
+ )
+
+ assert rendered_in_ascii(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ Oh no!
+ It broke. :(
+
+ Something went wrong
+ very wrong.
+
+ note: You did something wrong, which is what caused this error.
+ """
+ )
+
+ def test_no_context_no_hint(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context=None,
+ note_stmt="You did something wrong, which is what caused this error.",
+ hint_stmt=None,
+ )
+
+ assert rendered_in_ascii(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ Oh no!
+ It broke. :(
+
+ note: You did something wrong, which is what caused this error.
+ """
+ )
+
+ def test_no_context_no_note(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context=None,
+ note_stmt=None,
+ hint_stmt="Do it better next time, by trying harder.",
+ )
+
+ assert rendered_in_ascii(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ Oh no!
+ It broke. :(
+
+ hint: Do it better next time, by trying harder.
+ """
+ )
+
+ def test_no_hint_no_note(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context="Something went wrong\nvery wrong.",
+ note_stmt=None,
+ hint_stmt=None,
+ )
+
+ assert rendered_in_ascii(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ Oh no!
+ It broke. :(
+
+ Something went wrong
+ very wrong.
+ """
+ )
+
+ def test_no_hint_no_note_no_context(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context=None,
+ hint_stmt=None,
+ note_stmt=None,
+ )
+
+ assert rendered_in_ascii(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ Oh no!
+ It broke. :(
+ """
+ )
+
+
+def rendered(error: DiagnosticPipError, *, color: bool = False) -> str:
+ with io.StringIO() as stream:
+ console = rich.console.Console(
+ force_terminal=False,
+ file=stream,
+ color_system="truecolor" if color else None,
+ )
+ console.print(error)
+ return stream.getvalue()
+
+
+class TestDiagnosticPipErrorPresentation_Unicode:
+ def test_complete(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context="Something went wrong\nvery wrong.",
+ note_stmt="You did something wrong, which is what caused this error.",
+ hint_stmt="Do it better next time, by trying harder.",
+ )
+
+ assert rendered(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ × Oh no!
+ │ It broke. :(
+ ╰─> Something went wrong
+ very wrong.
+
+ note: You did something wrong, which is what caused this error.
+ hint: Do it better next time, by trying harder.
+ """
+ )
+
+ def test_complete_color(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke.",
+ context="Something went wrong\nvery wrong.",
+ note_stmt="You did something wrong.",
+ hint_stmt="Do it better next time, by trying harder.",
+ )
+
+ def esc(code: str = "0") -> str:
+ return f"\x1b[{code}m"
+
+ assert rendered(err, color=True) == textwrap.dedent(
+ f"""\
+ {esc("1;31")}error{esc("0")}: {esc("1")}test-diagnostic{esc("0")}
+
+ {esc("31")}×{esc("0")} Oh no!
+ {esc("31")}│{esc("0")} It broke.
+ {esc("31")}╰─>{esc("0")} Something went wrong
+ {esc("31")} {esc("0")} very wrong.
+
+ {esc("1;35")}note{esc("0")}: You did something wrong.
+ {esc("1;36")}hint{esc("0")}: Do it better next time, by trying harder.
+ """
+ )
+
+ def test_no_context(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context=None,
+ note_stmt="You did something wrong, which is what caused this error.",
+ hint_stmt="Do it better next time, by trying harder.",
+ )
+
+ assert rendered(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ × Oh no!
+ It broke. :(
+
+ note: You did something wrong, which is what caused this error.
+ hint: Do it better next time, by trying harder.
+ """
+ )
+
+ def test_no_note(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context="Something went wrong\nvery wrong.",
+ note_stmt=None,
+ hint_stmt="Do it better next time, by trying harder.",
+ )
+
+ assert rendered(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ × Oh no!
+ │ It broke. :(
+ ╰─> Something went wrong
+ very wrong.
+
+ hint: Do it better next time, by trying harder.
+ """
+ )
+
+ def test_no_hint(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context="Something went wrong\nvery wrong.",
+ note_stmt="You did something wrong, which is what caused this error.",
+ hint_stmt=None,
+ )
+
+ assert rendered(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ × Oh no!
+ │ It broke. :(
+ ╰─> Something went wrong
+ very wrong.
+
+ note: You did something wrong, which is what caused this error.
+ """
+ )
+
+ def test_no_context_no_hint(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context=None,
+ note_stmt="You did something wrong, which is what caused this error.",
+ hint_stmt=None,
+ )
+
+ assert rendered(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ × Oh no!
+ It broke. :(
+
+ note: You did something wrong, which is what caused this error.
+ """
+ )
+
+ def test_no_context_no_note(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context=None,
+ note_stmt=None,
+ hint_stmt="Do it better next time, by trying harder.",
+ )
+
+ assert rendered(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ × Oh no!
+ It broke. :(
+
+ hint: Do it better next time, by trying harder.
+ """
+ )
+
+ def test_no_hint_no_note(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context="Something went wrong\nvery wrong.",
+ note_stmt=None,
+ hint_stmt=None,
+ )
+
+ assert rendered(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ × Oh no!
+ │ It broke. :(
+ ╰─> Something went wrong
+ very wrong.
+ """
+ )
+
+ def test_no_hint_no_note_no_context(self) -> None:
+ err = DiagnosticPipError(
+ reference="test-diagnostic",
+ message="Oh no!\nIt broke. :(",
+ context=None,
+ hint_stmt=None,
+ note_stmt=None,
+ )
+
+ assert rendered(err) == textwrap.dedent(
+ """\
+ error: test-diagnostic
+
+ × Oh no!
+ It broke. :(
+ """
+ )
diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py
index 9638199fb..366b7eeb4 100644
--- a/tests/unit/test_finder.py
+++ b/tests/unit/test_finder.py
@@ -1,11 +1,11 @@
import logging
-import sys
+from typing import Iterable
from unittest.mock import Mock, patch
import pytest
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.tags import Tag
-from pkg_resources import parse_version
+from pip._vendor.packaging.version import parse as parse_version
import pip._internal.utils.compatibility_tags
from pip._internal.exceptions import BestVersionAlreadyInstalled, DistributionNotFound
@@ -14,97 +14,80 @@ from pip._internal.index.package_finder import (
InstallationCandidate,
Link,
LinkEvaluator,
+ LinkType,
)
from pip._internal.models.target_python import TargetPython
from pip._internal.req.constructors import install_req_from_line
-from tests.lib import make_test_finder
+from tests.lib import TestData, make_test_finder
-def make_no_network_finder(
- find_links,
- allow_all_prereleases=False, # type: bool
-):
- """
- Create and return a PackageFinder instance for test purposes that
- doesn't make any network requests when _get_pages() is called.
- """
- finder = make_test_finder(
- find_links=find_links,
- allow_all_prereleases=allow_all_prereleases,
- )
- # Replace the PackageFinder._link_collector's _get_pages() with a no-op.
- link_collector = finder._link_collector
- link_collector._get_pages = lambda locations: []
-
- return finder
-
-
-def test_no_mpkg(data):
+def test_no_mpkg(data: TestData) -> None:
"""Finder skips zipfiles with "macosx10" in the name."""
finder = make_test_finder(find_links=[data.find_links])
req = install_req_from_line("pkgwithmpkg")
found = finder.find_requirement(req, False)
-
+ assert found is not None
assert found.link.url.endswith("pkgwithmpkg-1.0.tar.gz"), found
-def test_no_partial_name_match(data):
+def test_no_partial_name_match(data: TestData) -> None:
"""Finder requires the full project name to match, not just beginning."""
finder = make_test_finder(find_links=[data.find_links])
req = install_req_from_line("gmpy")
found = finder.find_requirement(req, False)
-
+ assert found is not None
assert found.link.url.endswith("gmpy-1.15.tar.gz"), found
-def test_tilde():
+def test_tilde() -> None:
"""Finder can accept a path with ~ in it and will normalize it."""
patched_exists = patch(
- 'pip._internal.index.collector.os.path.exists', return_value=True
+ "pip._internal.index.collector.os.path.exists", return_value=True
)
with patched_exists:
- finder = make_test_finder(find_links=['~/python-pkgs'])
+ finder = make_test_finder(find_links=["~/python-pkgs"])
req = install_req_from_line("gmpy")
with pytest.raises(DistributionNotFound):
finder.find_requirement(req, False)
-def test_duplicates_sort_ok(data):
+def test_duplicates_sort_ok(data: TestData) -> None:
"""Finder successfully finds one of a set of duplicates in different
locations"""
finder = make_test_finder(find_links=[data.find_links, data.find_links2])
req = install_req_from_line("duplicate")
found = finder.find_requirement(req, False)
-
+ assert found is not None
assert found.link.url.endswith("duplicate-1.0.tar.gz"), found
-def test_finder_detects_latest_find_links(data):
+def test_finder_detects_latest_find_links(data: TestData) -> None:
"""Test PackageFinder detects latest using find-links"""
- req = install_req_from_line('simple', None)
+ req = install_req_from_line("simple", None)
finder = make_test_finder(find_links=[data.find_links])
found = finder.find_requirement(req, False)
+ assert found is not None
assert found.link.url.endswith("simple-3.0.tar.gz")
-def test_incorrect_case_file_index(data):
+def test_incorrect_case_file_index(data: TestData) -> None:
"""Test PackageFinder detects latest using wrong case"""
- req = install_req_from_line('dinner', None)
+ req = install_req_from_line("dinner", None)
finder = make_test_finder(index_urls=[data.find_links3])
found = finder.find_requirement(req, False)
+ assert found is not None
assert found.link.url.endswith("Dinner-2.0.tar.gz")
@pytest.mark.network
-def test_finder_detects_latest_already_satisfied_find_links(data):
+def test_finder_detects_latest_already_satisfied_find_links(data: TestData) -> None:
"""Test PackageFinder detects latest already satisfied using find-links"""
- req = install_req_from_line('simple', None)
+ req = install_req_from_line("simple", None)
# the latest simple in local pkgs is 3.0
latest_version = "3.0"
satisfied_by = Mock(
location="/path",
- parsed_version=parse_version(latest_version),
- version=latest_version
+ version=parse_version(latest_version),
)
req.satisfied_by = satisfied_by
finder = make_test_finder(find_links=[data.find_links])
@@ -114,15 +97,14 @@ def test_finder_detects_latest_already_satisfied_find_links(data):
@pytest.mark.network
-def test_finder_detects_latest_already_satisfied_pypi_links():
+def test_finder_detects_latest_already_satisfied_pypi_links() -> None:
"""Test PackageFinder detects latest already satisfied using pypi links"""
- req = install_req_from_line('initools', None)
+ req = install_req_from_line("initools", None)
# the latest initools on PyPI is 0.3.1
latest_version = "0.3.1"
satisfied_by = Mock(
location="/path",
- parsed_version=parse_version(latest_version),
- version=latest_version,
+ version=parse_version(latest_version),
)
req.satisfied_by = satisfied_by
finder = make_test_finder(index_urls=["http://pypi.org/simple/"])
@@ -132,8 +114,9 @@ def test_finder_detects_latest_already_satisfied_pypi_links():
class TestWheel:
-
- def test_skip_invalid_wheel_link(self, caplog, data):
+ def test_skip_invalid_wheel_link(
+ self, caplog: pytest.LogCaptureFixture, data: TestData
+ ) -> None:
"""
Test if PackageFinder skips invalid wheel filenames
"""
@@ -145,9 +128,9 @@ class TestWheel:
with pytest.raises(DistributionNotFound):
finder.find_requirement(req, True)
- assert 'Skipping link: invalid wheel filename:' in caplog.text
+ assert "Skipping link: invalid wheel filename:" in caplog.text
- def test_not_find_wheel_not_supported(self, data, monkeypatch):
+ def test_not_find_wheel_not_supported(self, data: TestData) -> None:
"""
Test not finding an unsupported wheel.
"""
@@ -163,24 +146,25 @@ class TestWheel:
with pytest.raises(DistributionNotFound):
finder.find_requirement(req, True)
- def test_find_wheel_supported(self, data, monkeypatch):
+ def test_find_wheel_supported(
+ self, data: TestData, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
"""
Test finding supported wheel.
"""
monkeypatch.setattr(
pip._internal.utils.compatibility_tags,
"get_supported",
- lambda **kw: [('py2', 'none', 'any')],
+ lambda **kw: [("py2", "none", "any")],
)
req = install_req_from_line("simple.dist")
finder = make_test_finder(find_links=[data.find_links])
found = finder.find_requirement(req, True)
- assert (
- found.link.url.endswith("simple.dist-0.1-py2.py3-none-any.whl")
- ), found
+ assert found is not None
+ assert found.link.url.endswith("simple.dist-0.1-py2.py3-none-any.whl"), found
- def test_wheel_over_sdist_priority(self, data):
+ def test_wheel_over_sdist_priority(self, data: TestData) -> None:
"""
Test wheels have priority over sdists.
`test_link_sorting` also covers this at lower level
@@ -188,20 +172,19 @@ class TestWheel:
req = install_req_from_line("priority")
finder = make_test_finder(find_links=[data.find_links])
found = finder.find_requirement(req, True)
- assert found.link.url.endswith("priority-1.0-py2.py3-none-any.whl"), \
- found
+ assert found is not None
+ assert found.link.url.endswith("priority-1.0-py2.py3-none-any.whl"), found
- def test_existing_over_wheel_priority(self, data):
+ def test_existing_over_wheel_priority(self, data: TestData) -> None:
"""
Test existing install has priority over wheels.
`test_link_sorting` also covers this at a lower level
"""
- req = install_req_from_line('priority', None)
+ req = install_req_from_line("priority", None)
latest_version = "1.0"
satisfied_by = Mock(
location="/path",
- parsed_version=parse_version(latest_version),
- version=latest_version,
+ version=parse_version(latest_version),
)
req.satisfied_by = satisfied_by
finder = make_test_finder(find_links=[data.find_links])
@@ -211,41 +194,43 @@ class TestWheel:
class TestCandidateEvaluator:
- def test_link_sorting(self):
+ def test_link_sorting(self) -> None:
"""
Test link sorting
"""
links = [
- InstallationCandidate("simple", "2.0", Link('simple-2.0.tar.gz')),
+ InstallationCandidate("simple", "2.0", Link("simple-2.0.tar.gz")),
InstallationCandidate(
"simple",
"1.0",
- Link('simple-1.0-pyT-none-TEST.whl'),
+ Link("simple-1.0-pyT-none-TEST.whl"),
),
InstallationCandidate(
"simple",
- '1.0',
- Link('simple-1.0-pyT-TEST-any.whl'),
+ "1.0",
+ Link("simple-1.0-pyT-TEST-any.whl"),
),
InstallationCandidate(
"simple",
- '1.0',
- Link('simple-1.0-pyT-none-any.whl'),
+ "1.0",
+ Link("simple-1.0-pyT-none-any.whl"),
),
InstallationCandidate(
"simple",
- '1.0',
- Link('simple-1.0.tar.gz'),
+ "1.0",
+ Link("simple-1.0.tar.gz"),
),
]
valid_tags = [
- Tag('pyT', 'none', 'TEST'),
- Tag('pyT', 'TEST', 'any'),
- Tag('pyT', 'none', 'any'),
+ Tag("pyT", "none", "TEST"),
+ Tag("pyT", "TEST", "any"),
+ Tag("pyT", "none", "any"),
]
specifier = SpecifierSet()
evaluator = CandidateEvaluator(
- 'my-project', supported_tags=valid_tags, specifier=specifier,
+ "my-project",
+ supported_tags=valid_tags,
+ specifier=specifier,
)
sort_key = evaluator._sort_key
results = sorted(links, key=sort_key, reverse=True)
@@ -254,7 +239,7 @@ class TestCandidateEvaluator:
assert links == results, results
assert links == results2, results2
- def test_link_sorting_wheels_with_build_tags(self):
+ def test_link_sorting_wheels_with_build_tags(self) -> None:
"""Verify build tags affect sorting."""
links = [
InstallationCandidate(
@@ -273,7 +258,7 @@ class TestCandidateEvaluator:
Link("simplewheel-1.0-py2.py3-none-any.whl"),
),
]
- candidate_evaluator = CandidateEvaluator.create('my-project')
+ candidate_evaluator = CandidateEvaluator.create("my-project")
sort_key = candidate_evaluator._sort_key
results = sorted(links, key=sort_key, reverse=True)
results2 = sorted(reversed(links), key=sort_key, reverse=True)
@@ -281,36 +266,38 @@ class TestCandidateEvaluator:
assert links == results, results
assert links == results2, results2
- def test_build_tag_is_less_important_than_other_tags(self):
+ def test_build_tag_is_less_important_than_other_tags(self) -> None:
links = [
InstallationCandidate(
"simple",
"1.0",
- Link('simple-1.0-1-py3-abi3-linux_x86_64.whl'),
+ Link("simple-1.0-1-py3-abi3-linux_x86_64.whl"),
),
InstallationCandidate(
"simple",
- '1.0',
- Link('simple-1.0-2-py3-abi3-linux_i386.whl'),
+ "1.0",
+ Link("simple-1.0-2-py3-abi3-linux_i386.whl"),
),
InstallationCandidate(
"simple",
- '1.0',
- Link('simple-1.0-2-py3-any-none.whl'),
+ "1.0",
+ Link("simple-1.0-2-py3-any-none.whl"),
),
InstallationCandidate(
"simple",
- '1.0',
- Link('simple-1.0.tar.gz'),
+ "1.0",
+ Link("simple-1.0.tar.gz"),
),
]
valid_tags = [
- Tag('py3', 'abi3', 'linux_x86_64'),
- Tag('py3', 'abi3', 'linux_i386'),
- Tag('py3', 'any', 'none'),
+ Tag("py3", "abi3", "linux_x86_64"),
+ Tag("py3", "abi3", "linux_i386"),
+ Tag("py3", "any", "none"),
]
evaluator = CandidateEvaluator(
- 'my-project', supported_tags=valid_tags, specifier=SpecifierSet(),
+ "my-project",
+ supported_tags=valid_tags,
+ specifier=SpecifierSet(),
)
sort_key = evaluator._sort_key
results = sorted(links, key=sort_key, reverse=True)
@@ -320,49 +307,53 @@ class TestCandidateEvaluator:
assert links == results2, results2
-def test_finder_priority_file_over_page(data):
+def test_finder_priority_file_over_page(data: TestData) -> None:
"""Test PackageFinder prefers file links over equivalent page links"""
- req = install_req_from_line('gmpy==1.15', None)
+ req = install_req_from_line("gmpy==1.15", None)
finder = make_test_finder(
find_links=[data.find_links],
index_urls=["http://pypi.org/simple/"],
)
all_versions = finder.find_all_candidates(req.name)
# 1 file InstallationCandidate followed by all https ones
- assert all_versions[0].link.scheme == 'file'
- assert all(version.link.scheme == 'https'
- for version in all_versions[1:]), all_versions
+ assert all_versions[0].link.scheme == "file"
+ assert all(
+ version.link.scheme == "https" for version in all_versions[1:]
+ ), all_versions
found = finder.find_requirement(req, False)
+ assert found is not None
assert found.link.url.startswith("file://")
-def test_finder_priority_nonegg_over_eggfragments():
+def test_finder_priority_nonegg_over_eggfragments() -> None:
"""Test PackageFinder prefers non-egg links over "#egg=" links"""
- req = install_req_from_line('bar==1.0', None)
- links = ['http://foo/bar.py#egg=bar-1.0', 'http://foo/bar-1.0.tar.gz']
+ req = install_req_from_line("bar==1.0", None)
+ links = ["http://foo/bar.py#egg=bar-1.0", "http://foo/bar-1.0.tar.gz"]
- finder = make_no_network_finder(links)
+ finder = make_test_finder(links)
all_versions = finder.find_all_candidates(req.name)
- assert all_versions[0].link.url.endswith('tar.gz')
- assert all_versions[1].link.url.endswith('#egg=bar-1.0')
+ assert all_versions[0].link.url.endswith("tar.gz")
+ assert all_versions[1].link.url.endswith("#egg=bar-1.0")
found = finder.find_requirement(req, False)
- assert found.link.url.endswith('tar.gz')
+ assert found is not None
+ assert found.link.url.endswith("tar.gz")
links.reverse()
- finder = make_no_network_finder(links)
+ finder = make_test_finder(links)
all_versions = finder.find_all_candidates(req.name)
- assert all_versions[0].link.url.endswith('tar.gz')
- assert all_versions[1].link.url.endswith('#egg=bar-1.0')
+ assert all_versions[0].link.url.endswith("tar.gz")
+ assert all_versions[1].link.url.endswith("#egg=bar-1.0")
found = finder.find_requirement(req, False)
- assert found.link.url.endswith('tar.gz')
+ assert found is not None
+ assert found.link.url.endswith("tar.gz")
-def test_finder_only_installs_stable_releases(data):
+def test_finder_only_installs_stable_releases(data: TestData) -> None:
"""
Test PackageFinder only accepts stable versioned releases by default.
"""
@@ -372,23 +363,26 @@ def test_finder_only_installs_stable_releases(data):
# using a local index (that has pre & dev releases)
finder = make_test_finder(index_urls=[data.index_url("pre")])
found = finder.find_requirement(req, False)
+ assert found is not None
assert found.link.url.endswith("bar-1.0.tar.gz"), found.link.url
# using find-links
links = ["https://foo/bar-1.0.tar.gz", "https://foo/bar-2.0b1.tar.gz"]
- finder = make_no_network_finder(links)
+ finder = make_test_finder(links)
found = finder.find_requirement(req, False)
+ assert found is not None
assert found.link.url == "https://foo/bar-1.0.tar.gz"
links.reverse()
- finder = make_no_network_finder(links)
+ finder = make_test_finder(links)
found = finder.find_requirement(req, False)
+ assert found is not None
assert found.link.url == "https://foo/bar-1.0.tar.gz"
-def test_finder_only_installs_data_require(data):
+def test_finder_only_installs_data_require(data: TestData) -> None:
"""
Test whether the PackageFinder understand data-python-requires
@@ -402,17 +396,10 @@ def test_finder_only_installs_data_require(data):
# using a local index (that has pre & dev releases)
finder = make_test_finder(index_urls=[data.index_url("datarequire")])
links = finder.find_all_candidates("fakepackage")
+ assert {str(v.version) for v in links} == {"1.0.0", "3.3.0", "9.9.9"}
- expected = ['1.0.0', '9.9.9']
- if (2, 7) < sys.version_info < (3,):
- expected.append('2.7.0')
- elif sys.version_info > (3, 3):
- expected.append('3.3.0')
-
- assert {str(v.version) for v in links} == set(expected)
-
-def test_finder_installs_pre_releases(data):
+def test_finder_installs_pre_releases(data: TestData) -> None:
"""
Test PackageFinder finds pre-releases if asked to.
"""
@@ -425,23 +412,26 @@ def test_finder_installs_pre_releases(data):
allow_all_prereleases=True,
)
found = finder.find_requirement(req, False)
+ assert found is not None
assert found.link.url.endswith("bar-2.0b1.tar.gz"), found.link.url
# using find-links
links = ["https://foo/bar-1.0.tar.gz", "https://foo/bar-2.0b1.tar.gz"]
- finder = make_no_network_finder(links, allow_all_prereleases=True)
+ finder = make_test_finder(links, allow_all_prereleases=True)
found = finder.find_requirement(req, False)
+ assert found is not None
assert found.link.url == "https://foo/bar-2.0b1.tar.gz"
links.reverse()
- finder = make_no_network_finder(links, allow_all_prereleases=True)
+ finder = make_test_finder(links, allow_all_prereleases=True)
found = finder.find_requirement(req, False)
+ assert found is not None
assert found.link.url == "https://foo/bar-2.0b1.tar.gz"
-def test_finder_installs_dev_releases(data):
+def test_finder_installs_dev_releases(data: TestData) -> None:
"""
Test PackageFinder finds dev releases if asked to.
"""
@@ -454,105 +444,125 @@ def test_finder_installs_dev_releases(data):
allow_all_prereleases=True,
)
found = finder.find_requirement(req, False)
+ assert found is not None
assert found.link.url.endswith("bar-2.0.dev1.tar.gz"), found.link.url
-def test_finder_installs_pre_releases_with_version_spec():
+def test_finder_installs_pre_releases_with_version_spec() -> None:
"""
Test PackageFinder only accepts stable versioned releases by default.
"""
req = install_req_from_line("bar>=0.0.dev0", None)
links = ["https://foo/bar-1.0.tar.gz", "https://foo/bar-2.0b1.tar.gz"]
- finder = make_no_network_finder(links)
+ finder = make_test_finder(links)
found = finder.find_requirement(req, False)
+ assert found is not None
assert found.link.url == "https://foo/bar-2.0b1.tar.gz"
links.reverse()
- finder = make_no_network_finder(links)
+ finder = make_test_finder(links)
found = finder.find_requirement(req, False)
+ assert found is not None
assert found.link.url == "https://foo/bar-2.0b1.tar.gz"
class TestLinkEvaluator:
-
- def make_test_link_evaluator(self, formats):
+ def make_test_link_evaluator(self, formats: Iterable[str]) -> LinkEvaluator:
target_python = TargetPython()
return LinkEvaluator(
- project_name='pytest',
- canonical_name='pytest',
- formats=formats,
+ project_name="pytest",
+ canonical_name="pytest",
+ formats=frozenset(formats),
target_python=target_python,
allow_yanked=True,
)
- @pytest.mark.parametrize('url, expected_version', [
- ('http:/yo/pytest-1.0.tar.gz', '1.0'),
- ('http:/yo/pytest-1.0-py2.py3-none-any.whl', '1.0'),
- ])
- def test_evaluate_link__match(self, url, expected_version):
+ @pytest.mark.parametrize(
+ "url, expected_version",
+ [
+ ("http:/yo/pytest-1.0.tar.gz", "1.0"),
+ ("http:/yo/pytest-1.0-py2.py3-none-any.whl", "1.0"),
+ ],
+ )
+ def test_evaluate_link__match(self, url: str, expected_version: str) -> None:
"""Test that 'pytest' archives match for 'pytest'"""
link = Link(url)
- evaluator = self.make_test_link_evaluator(formats=['source', 'binary'])
+ evaluator = self.make_test_link_evaluator(formats=["source", "binary"])
actual = evaluator.evaluate_link(link)
- assert actual == (True, expected_version)
-
- @pytest.mark.parametrize('url, expected_msg', [
- # TODO: Uncomment this test case when #1217 is fixed.
- # 'http:/yo/pytest-xdist-1.0.tar.gz',
- ('http:/yo/pytest2-1.0.tar.gz',
- 'Missing project version for pytest'),
- ('http:/yo/pytest_xdist-1.0-py2.py3-none-any.whl',
- 'wrong project name (not pytest)'),
- ])
- def test_evaluate_link__substring_fails(self, url, expected_msg):
+ assert actual == (LinkType.candidate, expected_version)
+
+ @pytest.mark.parametrize(
+ "url, link_type, fail_reason",
+ [
+ # TODO: Uncomment this test case when #1217 is fixed.
+ # 'http:/yo/pytest-xdist-1.0.tar.gz',
+ (
+ "http:/yo/pytest2-1.0.tar.gz",
+ LinkType.format_invalid,
+ "Missing project version for pytest",
+ ),
+ (
+ "http:/yo/pytest_xdist-1.0-py2.py3-none-any.whl",
+ LinkType.different_project,
+ "wrong project name (not pytest)",
+ ),
+ ],
+ )
+ def test_evaluate_link__substring_fails(
+ self,
+ url: str,
+ link_type: LinkType,
+ fail_reason: str,
+ ) -> None:
"""Test that 'pytest<something> archives won't match for 'pytest'."""
link = Link(url)
- evaluator = self.make_test_link_evaluator(formats=['source', 'binary'])
+ evaluator = self.make_test_link_evaluator(formats=["source", "binary"])
actual = evaluator.evaluate_link(link)
- assert actual == (False, expected_msg)
+ assert actual == (link_type, fail_reason)
-def test_process_project_url(data):
- project_name = 'simple'
- index_url = data.index_url('simple')
- project_url = Link(f'{index_url}/{project_name}')
+def test_process_project_url(data: TestData) -> None:
+ project_name = "simple"
+ index_url = data.index_url("simple")
+ project_url = Link(f"{index_url}/{project_name}")
finder = make_test_finder(index_urls=[index_url])
link_evaluator = finder.make_link_evaluator(project_name)
actual = finder.process_project_url(
- project_url, link_evaluator=link_evaluator,
+ project_url,
+ link_evaluator=link_evaluator,
)
assert len(actual) == 1
package_link = actual[0]
- assert package_link.name == 'simple'
- assert str(package_link.version) == '1.0'
+ assert package_link.name == "simple"
+ assert str(package_link.version) == "1.0"
-def test_find_all_candidates_nothing():
+def test_find_all_candidates_nothing() -> None:
"""Find nothing without anything"""
finder = make_test_finder()
- assert not finder.find_all_candidates('pip')
+ assert not finder.find_all_candidates("pip")
-def test_find_all_candidates_find_links(data):
+def test_find_all_candidates_find_links(data: TestData) -> None:
finder = make_test_finder(find_links=[data.find_links])
- versions = finder.find_all_candidates('simple')
- assert [str(v.version) for v in versions] == ['3.0', '2.0', '1.0']
+ versions = finder.find_all_candidates("simple")
+ assert [str(v.version) for v in versions] == ["3.0", "2.0", "1.0"]
-def test_find_all_candidates_index(data):
- finder = make_test_finder(index_urls=[data.index_url('simple')])
- versions = finder.find_all_candidates('simple')
- assert [str(v.version) for v in versions] == ['1.0']
+def test_find_all_candidates_index(data: TestData) -> None:
+ finder = make_test_finder(index_urls=[data.index_url("simple")])
+ versions = finder.find_all_candidates("simple")
+ assert [str(v.version) for v in versions] == ["1.0"]
-def test_find_all_candidates_find_links_and_index(data):
+def test_find_all_candidates_find_links_and_index(data: TestData) -> None:
finder = make_test_finder(
find_links=[data.find_links],
- index_urls=[data.index_url('simple')],
+ index_urls=[data.index_url("simple")],
)
- versions = finder.find_all_candidates('simple')
+ versions = finder.find_all_candidates("simple")
# first the find-links versions then the page versions
- assert [str(v.version) for v in versions] == ['3.0', '2.0', '1.0', '1.0']
+ assert [str(v.version) for v in versions] == ["3.0", "2.0", "1.0", "1.0"]
diff --git a/tests/unit/test_format_control.py b/tests/unit/test_format_control.py
index f8498e8e5..33a03729d 100644
--- a/tests/unit/test_format_control.py
+++ b/tests/unit/test_format_control.py
@@ -1,57 +1,59 @@
+from optparse import Values
+from typing import FrozenSet, List, Set
+
import pytest
from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import Command
+from pip._internal.cli.status_codes import SUCCESS
from pip._internal.models.format_control import FormatControl
class SimpleCommand(Command):
+ def __init__(self) -> None:
+ super().__init__("fake", "fake summary")
- def __init__(self):
- super().__init__('fake', 'fake summary')
-
- def add_options(self):
+ def add_options(self) -> None:
self.cmd_opts.add_option(cmdoptions.no_binary())
self.cmd_opts.add_option(cmdoptions.only_binary())
- def run(self, options, args):
+ def run(self, options: Values, args: List[str]) -> int:
self.options = options
+ return SUCCESS
-def test_no_binary_overrides():
+def test_no_binary_overrides() -> None:
cmd = SimpleCommand()
- cmd.main(['fake', '--only-binary=:all:', '--no-binary=fred'])
- format_control = FormatControl({'fred'}, {':all:'})
+ cmd.main(["fake", "--only-binary=:all:", "--no-binary=fred"])
+ format_control = FormatControl({"fred"}, {":all:"})
assert cmd.options.format_control == format_control
-def test_only_binary_overrides():
+def test_only_binary_overrides() -> None:
cmd = SimpleCommand()
- cmd.main(['fake', '--no-binary=:all:', '--only-binary=fred'])
- format_control = FormatControl({':all:'}, {'fred'})
+ cmd.main(["fake", "--no-binary=:all:", "--only-binary=fred"])
+ format_control = FormatControl({":all:"}, {"fred"})
assert cmd.options.format_control == format_control
-def test_none_resets():
+def test_none_resets() -> None:
cmd = SimpleCommand()
- cmd.main(['fake', '--no-binary=:all:', '--no-binary=:none:'])
+ cmd.main(["fake", "--no-binary=:all:", "--no-binary=:none:"])
format_control = FormatControl(set(), set())
assert cmd.options.format_control == format_control
-def test_none_preserves_other_side():
+def test_none_preserves_other_side() -> None:
cmd = SimpleCommand()
- cmd.main(
- ['fake', '--no-binary=:all:', '--only-binary=fred',
- '--no-binary=:none:'])
- format_control = FormatControl(set(), {'fred'})
+ cmd.main(["fake", "--no-binary=:all:", "--only-binary=fred", "--no-binary=:none:"])
+ format_control = FormatControl(set(), {"fred"})
assert cmd.options.format_control == format_control
-def test_comma_separated_values():
+def test_comma_separated_values() -> None:
cmd = SimpleCommand()
- cmd.main(['fake', '--no-binary=1,2,3'])
- format_control = FormatControl({'1', '2', '3'}, set())
+ cmd.main(["fake", "--no-binary=1,2,3"])
+ format_control = FormatControl({"1", "2", "3"}, set())
assert cmd.options.format_control == format_control
@@ -61,9 +63,11 @@ def test_comma_separated_values():
({"fred"}, set(), "fred", frozenset(["source"])),
({"fred"}, {":all:"}, "fred", frozenset(["source"])),
(set(), {"fred"}, "fred", frozenset(["binary"])),
- ({":all:"}, {"fred"}, "fred", frozenset(["binary"]))
- ]
+ ({":all:"}, {"fred"}, "fred", frozenset(["binary"])),
+ ],
)
-def test_fmt_ctl_matches(no_binary, only_binary, argument, expected):
+def test_fmt_ctl_matches(
+ no_binary: Set[str], only_binary: Set[str], argument: str, expected: FrozenSet[str]
+) -> None:
fmt = FormatControl(no_binary, only_binary)
assert fmt.get_allowed_formats(argument) == expected
diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py
index 7859b4ac8..78837b94e 100644
--- a/tests/unit/test_index.py
+++ b/tests/unit/test_index.py
@@ -1,7 +1,9 @@
import logging
+from typing import FrozenSet, List, Optional, Set, Tuple
import pytest
from pip._vendor.packaging.specifiers import SpecifierSet
+from pip._vendor.packaging.tags import Tag
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import (
@@ -9,6 +11,7 @@ from pip._internal.index.package_finder import (
CandidatePreferences,
FormatControl,
LinkEvaluator,
+ LinkType,
PackageFinder,
_check_link_requires_python,
_extract_version_from_fragment,
@@ -26,49 +29,68 @@ from tests.lib import CURRENT_PY_VERSION_INFO
from tests.lib.index import make_mock_candidate
-@pytest.mark.parametrize('requires_python, expected', [
- ('== 3.6.4', False),
- ('== 3.6.5', True),
- # Test an invalid Requires-Python value.
- ('invalid', True),
-])
-def test_check_link_requires_python(requires_python, expected):
+@pytest.mark.parametrize(
+ "requires_python, expected",
+ [
+ ("== 3.6.4", False),
+ ("== 3.6.5", True),
+ # Test an invalid Requires-Python value.
+ ("invalid", True),
+ ],
+)
+def test_check_link_requires_python(requires_python: str, expected: bool) -> None:
version_info = (3, 6, 5)
- link = Link('https://example.com', requires_python=requires_python)
+ link = Link("https://example.com", requires_python=requires_python)
actual = _check_link_requires_python(link, version_info)
assert actual == expected
-def check_caplog(caplog, expected_level, expected_message):
+def check_caplog(
+ caplog: pytest.LogCaptureFixture, expected_level: str, expected_message: str
+) -> None:
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.levelname == expected_level
assert record.message == expected_message
-@pytest.mark.parametrize('ignore_requires_python, expected', [
- (None, (
- False, 'VERBOSE',
- "Link requires a different Python (3.6.5 not in: '== 3.6.4'): "
- "https://example.com"
- )),
- (True, (
- True, 'DEBUG',
- "Ignoring failed Requires-Python check (3.6.5 not in: '== 3.6.4') "
- "for link: https://example.com"
- )),
-])
+@pytest.mark.parametrize(
+ "ignore_requires_python, expected",
+ [
+ (
+ False,
+ (
+ False,
+ "VERBOSE",
+ "Link requires a different Python (3.6.5 not in: '== 3.6.4'): "
+ "https://example.com",
+ ),
+ ),
+ (
+ True,
+ (
+ True,
+ "DEBUG",
+ "Ignoring failed Requires-Python check (3.6.5 not in: '== 3.6.4') "
+ "for link: https://example.com",
+ ),
+ ),
+ ],
+)
def test_check_link_requires_python__incompatible_python(
- caplog, ignore_requires_python, expected,
-):
+ caplog: pytest.LogCaptureFixture,
+ ignore_requires_python: bool,
+ expected: Tuple[bool, str, str],
+) -> None:
"""
Test an incompatible Python.
"""
expected_return, expected_level, expected_message = expected
- link = Link('https://example.com', requires_python='== 3.6.4')
+ link = Link("https://example.com", requires_python="== 3.6.4")
caplog.set_level(logging.DEBUG)
actual = _check_link_requires_python(
- link, version_info=(3, 6, 5),
+ link,
+ version_info=(3, 6, 5),
ignore_requires_python=ignore_requires_python,
)
assert actual == expected_return
@@ -76,84 +98,123 @@ def test_check_link_requires_python__incompatible_python(
check_caplog(caplog, expected_level, expected_message)
-def test_check_link_requires_python__invalid_requires(caplog):
+def test_check_link_requires_python__invalid_requires(
+ caplog: pytest.LogCaptureFixture,
+) -> None:
"""
Test the log message for an invalid Requires-Python.
"""
- link = Link('https://example.com', requires_python='invalid')
+ link = Link("https://example.com", requires_python="invalid")
caplog.set_level(logging.DEBUG)
actual = _check_link_requires_python(link, version_info=(3, 6, 5))
assert actual
expected_message = (
- "Ignoring invalid Requires-Python ('invalid') for link: "
- "https://example.com"
+ "Ignoring invalid Requires-Python ('invalid') for link: https://example.com"
)
- check_caplog(caplog, 'DEBUG', expected_message)
+ check_caplog(caplog, "DEBUG", expected_message)
class TestLinkEvaluator:
-
@pytest.mark.parametrize(
- 'py_version_info,ignore_requires_python,expected', [
- ((3, 6, 5), None, (True, '1.12')),
- # Test an incompatible Python.
- ((3, 6, 4), None, (False, None)),
- # Test an incompatible Python with ignore_requires_python=True.
- ((3, 6, 4), True, (True, '1.12')),
+ "py_version_info, ignore_requires_python, expected",
+ [
+ pytest.param(
+ (3, 6, 5),
+ False,
+ (LinkType.candidate, "1.12"),
+ id="compatible",
+ ),
+ pytest.param(
+ (3, 6, 4),
+ False,
+ (
+ LinkType.requires_python_mismatch,
+ "1.12 Requires-Python == 3.6.5",
+ ),
+ id="requires-python-mismatch",
+ ),
+ pytest.param(
+ (3, 6, 4),
+ True,
+ (LinkType.candidate, "1.12"),
+ id="requires-python-mismatch-ignored",
+ ),
],
)
def test_evaluate_link(
- self, py_version_info, ignore_requires_python, expected,
- ):
+ self,
+ py_version_info: Tuple[int, int, int],
+ ignore_requires_python: bool,
+ expected: Tuple[LinkType, str],
+ ) -> None:
target_python = TargetPython(py_version_info=py_version_info)
evaluator = LinkEvaluator(
- project_name='twine',
- canonical_name='twine',
- formats={'source'},
+ project_name="twine",
+ canonical_name="twine",
+ formats=frozenset(["source"]),
target_python=target_python,
allow_yanked=True,
ignore_requires_python=ignore_requires_python,
)
link = Link(
- 'https://example.com/#egg=twine-1.12',
- requires_python='== 3.6.5',
+ "https://example.com/#egg=twine-1.12",
+ requires_python="== 3.6.5",
)
actual = evaluator.evaluate_link(link)
assert actual == expected
- @pytest.mark.parametrize('yanked_reason, allow_yanked, expected', [
- (None, True, (True, '1.12')),
- (None, False, (True, '1.12')),
- ('', True, (True, '1.12')),
- ('', False, (False, 'yanked for reason: <none given>')),
- ('bad metadata', True, (True, '1.12')),
- ('bad metadata', False,
- (False, 'yanked for reason: bad metadata')),
- # Test a unicode string with a non-ascii character.
- ('curly quote: \u2018', True, (True, '1.12')),
- ('curly quote: \u2018', False,
- (False, 'yanked for reason: curly quote: \u2018')),
- ])
+ @pytest.mark.parametrize(
+ "yanked_reason, allow_yanked, expected",
+ [
+ (None, True, (LinkType.candidate, "1.12")),
+ (None, False, (LinkType.candidate, "1.12")),
+ ("", True, (LinkType.candidate, "1.12")),
+ (
+ "",
+ False,
+ (LinkType.yanked, "yanked for reason: <none given>"),
+ ),
+ ("bad metadata", True, (LinkType.candidate, "1.12")),
+ (
+ "bad metadata",
+ False,
+ (LinkType.yanked, "yanked for reason: bad metadata"),
+ ),
+ # Test a unicode string with a non-ascii character.
+ ("curly quote: \u2018", True, (LinkType.candidate, "1.12")),
+ (
+ "curly quote: \u2018",
+ False,
+ (
+ LinkType.yanked,
+ "yanked for reason: curly quote: \u2018",
+ ),
+ ),
+ ],
+ )
def test_evaluate_link__allow_yanked(
- self, yanked_reason, allow_yanked, expected,
- ):
+ self,
+ yanked_reason: str,
+ allow_yanked: bool,
+ expected: Tuple[LinkType, str],
+ ) -> None:
target_python = TargetPython(py_version_info=(3, 6, 4))
evaluator = LinkEvaluator(
- project_name='twine',
- canonical_name='twine',
- formats={'source'},
+ project_name="twine",
+ canonical_name="twine",
+ formats=frozenset(["source"]),
target_python=target_python,
allow_yanked=allow_yanked,
)
link = Link(
- 'https://example.com/#egg=twine-1.12',
+ "https://example.com/#egg=twine-1.12",
yanked_reason=yanked_reason,
)
actual = evaluator.evaluate_link(link)
assert actual == expected
- def test_evaluate_link__incompatible_wheel(self):
+ def test_evaluate_link__incompatible_wheel(self) -> None:
"""
Test an incompatible wheel.
"""
@@ -161,40 +222,44 @@ class TestLinkEvaluator:
# Set the valid tags to an empty list to make sure nothing matches.
target_python._valid_tags = []
evaluator = LinkEvaluator(
- project_name='sample',
- canonical_name='sample',
- formats={'binary'},
+ project_name="sample",
+ canonical_name="sample",
+ formats=frozenset(["binary"]),
target_python=target_python,
allow_yanked=True,
)
- link = Link('https://example.com/sample-1.0-py2.py3-none-any.whl')
+ link = Link("https://example.com/sample-1.0-py2.py3-none-any.whl")
actual = evaluator.evaluate_link(link)
expected = (
- False,
+ LinkType.platform_mismatch,
"none of the wheel's tags (py2-none-any, py3-none-any) are compatible "
- "(run pip debug --verbose to show compatible tags)"
+ "(run pip debug --verbose to show compatible tags)",
)
assert actual == expected
-@pytest.mark.parametrize('hex_digest, expected_versions', [
- (None, ['1.0', '1.1', '1.2']),
- (64 * 'a', ['1.0', '1.1']),
- (64 * 'b', ['1.0', '1.2']),
- (64 * 'c', ['1.0', '1.1', '1.2']),
-])
-def test_filter_unallowed_hashes(hex_digest, expected_versions):
+@pytest.mark.parametrize(
+ "hex_digest, expected_versions",
+ [
+ (64 * "a", ["1.0", "1.1"]),
+ (64 * "b", ["1.0", "1.2"]),
+ (64 * "c", ["1.0", "1.1", "1.2"]),
+ ],
+)
+def test_filter_unallowed_hashes(hex_digest: str, expected_versions: List[str]) -> None:
candidates = [
- make_mock_candidate('1.0'),
- make_mock_candidate('1.1', hex_digest=(64 * 'a')),
- make_mock_candidate('1.2', hex_digest=(64 * 'b')),
+ make_mock_candidate("1.0"),
+ make_mock_candidate("1.1", hex_digest=(64 * "a")),
+ make_mock_candidate("1.2", hex_digest=(64 * "b")),
]
hashes_data = {
- 'sha256': [hex_digest],
+ "sha256": [hex_digest],
}
hashes = Hashes(hashes_data)
actual = filter_unallowed_hashes(
- candidates, hashes=hashes, project_name='my-project',
+ candidates,
+ hashes=hashes,
+ project_name="my-project",
)
actual_versions = [str(candidate.version) for candidate in actual]
@@ -203,15 +268,17 @@ def test_filter_unallowed_hashes(hex_digest, expected_versions):
assert actual is not candidates
-def test_filter_unallowed_hashes__no_hashes(caplog):
+def test_filter_unallowed_hashes__no_hashes(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.DEBUG)
candidates = [
- make_mock_candidate('1.0'),
- make_mock_candidate('1.1'),
+ make_mock_candidate("1.0"),
+ make_mock_candidate("1.1"),
]
actual = filter_unallowed_hashes(
- candidates, hashes=Hashes(), project_name='my-project',
+ candidates,
+ hashes=Hashes(),
+ project_name="my-project",
)
# Check that the return value is a copy.
@@ -222,28 +289,36 @@ def test_filter_unallowed_hashes__no_hashes(caplog):
"Given no hashes to check 2 links for project 'my-project': "
"discarding no candidates"
)
- check_caplog(caplog, 'DEBUG', expected_message)
+ check_caplog(caplog, "DEBUG", expected_message)
-def test_filter_unallowed_hashes__log_message_with_match(caplog):
+def test_filter_unallowed_hashes__log_message_with_match(
+ caplog: pytest.LogCaptureFixture,
+) -> None:
caplog.set_level(logging.DEBUG)
# Test 1 match, 2 non-matches, 3 no hashes so all 3 values will be
# different.
candidates = [
- make_mock_candidate('1.0'),
- make_mock_candidate('1.1',),
- make_mock_candidate('1.2',),
- make_mock_candidate('1.3', hex_digest=(64 * 'a')),
- make_mock_candidate('1.4', hex_digest=(64 * 'b')),
- make_mock_candidate('1.5', hex_digest=(64 * 'c')),
+ make_mock_candidate("1.0"),
+ make_mock_candidate(
+ "1.1",
+ ),
+ make_mock_candidate(
+ "1.2",
+ ),
+ make_mock_candidate("1.3", hex_digest=(64 * "a")),
+ make_mock_candidate("1.4", hex_digest=(64 * "b")),
+ make_mock_candidate("1.5", hex_digest=(64 * "c")),
]
hashes_data = {
- 'sha256': [64 * 'a', 64 * 'd'],
+ "sha256": [64 * "a", 64 * "d"],
}
hashes = Hashes(hashes_data)
actual = filter_unallowed_hashes(
- candidates, hashes=hashes, project_name='my-project',
+ candidates,
+ hashes=hashes,
+ project_name="my-project",
)
assert len(actual) == 4
@@ -255,23 +330,27 @@ def test_filter_unallowed_hashes__log_message_with_match(caplog):
" https://example.com/pkg-1.5.tar.gz#sha256="
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
)
- check_caplog(caplog, 'DEBUG', expected_message)
+ check_caplog(caplog, "DEBUG", expected_message)
-def test_filter_unallowed_hashes__log_message_with_no_match(caplog):
+def test_filter_unallowed_hashes__log_message_with_no_match(
+ caplog: pytest.LogCaptureFixture,
+) -> None:
caplog.set_level(logging.DEBUG)
candidates = [
- make_mock_candidate('1.0'),
- make_mock_candidate('1.1', hex_digest=(64 * 'b')),
- make_mock_candidate('1.2', hex_digest=(64 * 'c')),
+ make_mock_candidate("1.0"),
+ make_mock_candidate("1.1", hex_digest=(64 * "b")),
+ make_mock_candidate("1.2", hex_digest=(64 * "c")),
]
hashes_data = {
- 'sha256': [64 * 'a', 64 * 'd'],
+ "sha256": [64 * "a", 64 * "d"],
}
hashes = Hashes(hashes_data)
actual = filter_unallowed_hashes(
- candidates, hashes=hashes, project_name='my-project',
+ candidates,
+ hashes=hashes,
+ project_name="my-project",
)
assert len(actual) == 3
@@ -279,23 +358,25 @@ def test_filter_unallowed_hashes__log_message_with_no_match(caplog):
"Checked 3 links for project 'my-project' against 2 hashes "
"(0 matches, 1 no digest): discarding no candidates"
)
- check_caplog(caplog, 'DEBUG', expected_message)
+ check_caplog(caplog, "DEBUG", expected_message)
class TestCandidateEvaluator:
-
- @pytest.mark.parametrize('allow_all_prereleases, prefer_binary', [
- (False, False),
- (False, True),
- (True, False),
- (True, True),
- ])
- def test_create(self, allow_all_prereleases, prefer_binary):
+ @pytest.mark.parametrize(
+ "allow_all_prereleases, prefer_binary",
+ [
+ (False, False),
+ (False, True),
+ (True, False),
+ (True, True),
+ ],
+ )
+ def test_create(self, allow_all_prereleases: bool, prefer_binary: bool) -> None:
target_python = TargetPython()
- target_python._valid_tags = [('py36', 'none', 'any')]
+ target_python._valid_tags = [Tag("py36", "none", "any")]
specifier = SpecifierSet()
evaluator = CandidateEvaluator.create(
- project_name='my-project',
+ project_name="my-project",
target_python=target_python,
allow_all_prereleases=allow_all_prereleases,
prefer_binary=prefer_binary,
@@ -304,66 +385,69 @@ class TestCandidateEvaluator:
assert evaluator._allow_all_prereleases == allow_all_prereleases
assert evaluator._prefer_binary == prefer_binary
assert evaluator._specifier is specifier
- assert evaluator._supported_tags == [('py36', 'none', 'any')]
+ assert evaluator._supported_tags == [Tag("py36", "none", "any")]
- def test_create__target_python_none(self):
+ def test_create__target_python_none(self) -> None:
"""
Test passing target_python=None.
"""
- evaluator = CandidateEvaluator.create('my-project')
+ evaluator = CandidateEvaluator.create("my-project")
expected_tags = get_supported()
assert evaluator._supported_tags == expected_tags
- def test_create__specifier_none(self):
+ def test_create__specifier_none(self) -> None:
"""
Test passing specifier=None.
"""
- evaluator = CandidateEvaluator.create('my-project')
+ evaluator = CandidateEvaluator.create("my-project")
expected_specifier = SpecifierSet()
assert evaluator._specifier == expected_specifier
- def test_get_applicable_candidates(self):
- specifier = SpecifierSet('<= 1.11')
- versions = ['1.10', '1.11', '1.12']
- candidates = [
- make_mock_candidate(version) for version in versions
- ]
+ def test_get_applicable_candidates(self) -> None:
+ specifier = SpecifierSet("<= 1.11")
+ versions = ["1.10", "1.11", "1.12"]
+ candidates = [make_mock_candidate(version) for version in versions]
evaluator = CandidateEvaluator.create(
- 'my-project',
+ "my-project",
specifier=specifier,
)
actual = evaluator.get_applicable_candidates(candidates)
expected_applicable = candidates[:2]
assert [str(c.version) for c in expected_applicable] == [
- '1.10',
- '1.11',
+ "1.10",
+ "1.11",
]
assert actual == expected_applicable
- @pytest.mark.parametrize('specifier, expected_versions', [
- # Test no version constraint.
- (SpecifierSet(), ['1.0', '1.2']),
- # Test a version constraint that excludes the candidate whose
- # hash matches. Then the non-allowed hash is a candidate.
- (SpecifierSet('<= 1.1'), ['1.0', '1.1']),
- ])
+ @pytest.mark.parametrize(
+ "specifier, expected_versions",
+ [
+ # Test no version constraint.
+ (SpecifierSet(), ["1.0", "1.2"]),
+ # Test a version constraint that excludes the candidate whose
+ # hash matches. Then the non-allowed hash is a candidate.
+ (SpecifierSet("<= 1.1"), ["1.0", "1.1"]),
+ ],
+ )
def test_get_applicable_candidates__hashes(
- self, specifier, expected_versions,
- ):
+ self,
+ specifier: SpecifierSet,
+ expected_versions: List[str],
+ ) -> None:
"""
Test a non-None hashes value.
"""
candidates = [
- make_mock_candidate('1.0'),
- make_mock_candidate('1.1', hex_digest=(64 * 'a')),
- make_mock_candidate('1.2', hex_digest=(64 * 'b')),
+ make_mock_candidate("1.0"),
+ make_mock_candidate("1.1", hex_digest=(64 * "a")),
+ make_mock_candidate("1.2", hex_digest=(64 * "b")),
]
hashes_data = {
- 'sha256': [64 * 'b'],
+ "sha256": [64 * "b"],
}
hashes = Hashes(hashes_data)
evaluator = CandidateEvaluator.create(
- 'my-project',
+ "my-project",
specifier=specifier,
hashes=hashes,
)
@@ -371,14 +455,12 @@ class TestCandidateEvaluator:
actual_versions = [str(c.version) for c in actual]
assert actual_versions == expected_versions
- def test_compute_best_candidate(self):
- specifier = SpecifierSet('<= 1.11')
- versions = ['1.10', '1.11', '1.12']
- candidates = [
- make_mock_candidate(version) for version in versions
- ]
+ def test_compute_best_candidate(self) -> None:
+ specifier = SpecifierSet("<= 1.11")
+ versions = ["1.10", "1.11", "1.12"]
+ candidates = [make_mock_candidate(version) for version in versions]
evaluator = CandidateEvaluator.create(
- 'my-project',
+ "my-project",
specifier=specifier,
)
result = evaluator.compute_best_candidate(candidates)
@@ -386,24 +468,22 @@ class TestCandidateEvaluator:
assert result._candidates == candidates
expected_applicable = candidates[:2]
assert [str(c.version) for c in expected_applicable] == [
- '1.10',
- '1.11',
+ "1.10",
+ "1.11",
]
assert result._applicable_candidates == expected_applicable
assert result.best_candidate is expected_applicable[1]
- def test_compute_best_candidate__none_best(self):
+ def test_compute_best_candidate__none_best(self) -> None:
"""
Test returning a None best candidate.
"""
- specifier = SpecifierSet('<= 1.10')
- versions = ['1.11', '1.12']
- candidates = [
- make_mock_candidate(version) for version in versions
- ]
+ specifier = SpecifierSet("<= 1.10")
+ versions = ["1.11", "1.12"]
+ candidates = [make_mock_candidate(version) for version in versions]
evaluator = CandidateEvaluator.create(
- 'my-project',
+ "my-project",
specifier=specifier,
)
result = evaluator.compute_best_candidate(candidates)
@@ -412,95 +492,108 @@ class TestCandidateEvaluator:
assert result._applicable_candidates == []
assert result.best_candidate is None
- @pytest.mark.parametrize('hex_digest, expected', [
- # Test a link with no hash.
- (None, 0),
- # Test a link with an allowed hash.
- (64 * 'a', 1),
- # Test a link with a hash that isn't allowed.
- (64 * 'b', 0),
- ])
- def test_sort_key__hash(self, hex_digest, expected):
+ @pytest.mark.parametrize(
+ "hex_digest, expected",
+ [
+ # Test a link with no hash.
+ (None, 0),
+ # Test a link with an allowed hash.
+ (64 * "a", 1),
+ # Test a link with a hash that isn't allowed.
+ (64 * "b", 0),
+ ],
+ )
+ def test_sort_key__hash(self, hex_digest: Optional[str], expected: int) -> None:
"""
Test the effect of the link's hash on _sort_key()'s return value.
"""
- candidate = make_mock_candidate('1.0', hex_digest=hex_digest)
+ candidate = make_mock_candidate("1.0", hex_digest=hex_digest)
hashes_data = {
- 'sha256': [64 * 'a'],
+ "sha256": [64 * "a"],
}
hashes = Hashes(hashes_data)
- evaluator = CandidateEvaluator.create('my-project', hashes=hashes)
+ evaluator = CandidateEvaluator.create("my-project", hashes=hashes)
sort_value = evaluator._sort_key(candidate)
# The hash is reflected in the first element of the tuple.
actual = sort_value[0]
assert actual == expected
- @pytest.mark.parametrize('yanked_reason, expected', [
- # Test a non-yanked file.
- (None, 0),
- # Test a yanked file (has a lower value than non-yanked).
- ('bad metadata', -1),
- ])
- def test_sort_key__is_yanked(self, yanked_reason, expected):
+ @pytest.mark.parametrize(
+ "yanked_reason, expected",
+ [
+ # Test a non-yanked file.
+ (None, 0),
+ # Test a yanked file (has a lower value than non-yanked).
+ ("bad metadata", -1),
+ ],
+ )
+ def test_sort_key__is_yanked(
+ self, yanked_reason: Optional[str], expected: int
+ ) -> None:
"""
Test the effect of is_yanked on _sort_key()'s return value.
"""
- candidate = make_mock_candidate('1.0', yanked_reason=yanked_reason)
- evaluator = CandidateEvaluator.create('my-project')
+ candidate = make_mock_candidate("1.0", yanked_reason=yanked_reason)
+ evaluator = CandidateEvaluator.create("my-project")
sort_value = evaluator._sort_key(candidate)
# Yanked / non-yanked is reflected in the second element of the tuple.
actual = sort_value[1]
assert actual == expected
- def test_sort_best_candidate__no_candidates(self):
+ def test_sort_best_candidate__no_candidates(self) -> None:
"""
Test passing an empty list.
"""
- evaluator = CandidateEvaluator.create('my-project')
+ evaluator = CandidateEvaluator.create("my-project")
actual = evaluator.sort_best_candidate([])
assert actual is None
def test_sort_best_candidate__best_yanked_but_not_all(
- self, caplog,
- ):
+ self,
+ caplog: pytest.LogCaptureFixture,
+ ) -> None:
"""
Test the best candidates being yanked, but not all.
"""
caplog.set_level(logging.INFO)
candidates = [
- make_mock_candidate('4.0', yanked_reason='bad metadata #4'),
+ make_mock_candidate("4.0", yanked_reason="bad metadata #4"),
# Put the best candidate in the middle, to test sorting.
- make_mock_candidate('2.0'),
- make_mock_candidate('3.0', yanked_reason='bad metadata #3'),
- make_mock_candidate('1.0'),
+ make_mock_candidate("2.0"),
+ make_mock_candidate("3.0", yanked_reason="bad metadata #3"),
+ make_mock_candidate("1.0"),
]
expected_best = candidates[1]
- evaluator = CandidateEvaluator.create('my-project')
+ evaluator = CandidateEvaluator.create("my-project")
actual = evaluator.sort_best_candidate(candidates)
assert actual is expected_best
- assert str(actual.version) == '2.0'
+ assert str(actual.version) == "2.0"
# Check the log messages.
assert len(caplog.records) == 0
class TestPackageFinder:
-
- @pytest.mark.parametrize('allow_all_prereleases, prefer_binary', [
- (False, False),
- (False, True),
- (True, False),
- (True, True),
- ])
+ @pytest.mark.parametrize(
+ "allow_all_prereleases, prefer_binary",
+ [
+ (False, False),
+ (False, True),
+ (True, False),
+ (True, True),
+ ],
+ )
def test_create__candidate_prefs(
- self, allow_all_prereleases, prefer_binary,
- ):
+ self,
+ allow_all_prereleases: bool,
+ prefer_binary: bool,
+ ) -> None:
"""
Test that the _candidate_prefs attribute is set correctly.
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
selection_prefs = SelectionPreferences(
allow_yanked=True,
@@ -515,13 +608,13 @@ class TestPackageFinder:
assert candidate_prefs.allow_all_prereleases == allow_all_prereleases
assert candidate_prefs.prefer_binary == prefer_binary
- def test_create__link_collector(self):
+ def test_create__link_collector(self) -> None:
"""
Test that the _link_collector attribute is set correctly.
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
finder = PackageFinder.create(
link_collector=link_collector,
@@ -530,13 +623,13 @@ class TestPackageFinder:
assert finder._link_collector is link_collector
- def test_create__target_python(self):
+ def test_create__target_python(self) -> None:
"""
Test that the _target_python attribute is set correctly.
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
target_python = TargetPython(py_version_info=(3, 7, 3))
finder = PackageFinder.create(
@@ -550,13 +643,13 @@ class TestPackageFinder:
# Check that the attributes weren't reset.
assert actual_target_python.py_version_info == (3, 7, 3)
- def test_create__target_python_none(self):
+ def test_create__target_python_none(self) -> None:
"""
Test passing target_python=None.
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
finder = PackageFinder.create(
link_collector=link_collector,
@@ -568,14 +661,14 @@ class TestPackageFinder:
assert actual_target_python._given_py_version_info is None
assert actual_target_python.py_version_info == CURRENT_PY_VERSION_INFO
- @pytest.mark.parametrize('allow_yanked', [False, True])
- def test_create__allow_yanked(self, allow_yanked):
+ @pytest.mark.parametrize("allow_yanked", [False, True])
+ def test_create__allow_yanked(self, allow_yanked: bool) -> None:
"""
Test that the _allow_yanked attribute is set correctly.
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
selection_prefs = SelectionPreferences(allow_yanked=allow_yanked)
finder = PackageFinder.create(
@@ -584,14 +677,14 @@ class TestPackageFinder:
)
assert finder._allow_yanked == allow_yanked
- @pytest.mark.parametrize('ignore_requires_python', [False, True])
- def test_create__ignore_requires_python(self, ignore_requires_python):
+ @pytest.mark.parametrize("ignore_requires_python", [False, True])
+ def test_create__ignore_requires_python(self, ignore_requires_python: bool) -> None:
"""
Test that the _ignore_requires_python attribute is set correctly.
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
selection_prefs = SelectionPreferences(
allow_yanked=True,
@@ -603,15 +696,15 @@ class TestPackageFinder:
)
assert finder._ignore_requires_python == ignore_requires_python
- def test_create__format_control(self):
+ def test_create__format_control(self) -> None:
"""
Test that the format_control attribute is set correctly.
"""
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
- format_control = FormatControl(set(), {':all:'})
+ format_control = FormatControl(set(), {":all:"})
selection_prefs = SelectionPreferences(
allow_yanked=True,
format_control=format_control,
@@ -623,31 +716,34 @@ class TestPackageFinder:
actual_format_control = finder.format_control
assert actual_format_control is format_control
# Check that the attributes weren't reset.
- assert actual_format_control.only_binary == {':all:'}
+ assert actual_format_control.only_binary == {":all:"}
@pytest.mark.parametrize(
- 'allow_yanked, ignore_requires_python, only_binary, expected_formats',
+ "allow_yanked, ignore_requires_python, only_binary, expected_formats",
[
- (False, False, {}, frozenset({'binary', 'source'})),
+ (False, False, {}, frozenset({"binary", "source"})),
# Test allow_yanked=True.
- (True, False, {}, frozenset({'binary', 'source'})),
+ (True, False, {}, frozenset({"binary", "source"})),
# Test ignore_requires_python=True.
- (False, True, {}, frozenset({'binary', 'source'})),
+ (False, True, {}, frozenset({"binary", "source"})),
# Test a non-trivial only_binary.
- (False, False, {'twine'}, frozenset({'binary'})),
- ]
+ (False, False, {"twine"}, frozenset({"binary"})),
+ ],
)
def test_make_link_evaluator(
- self, allow_yanked, ignore_requires_python, only_binary,
- expected_formats,
- ):
+ self,
+ allow_yanked: bool,
+ ignore_requires_python: bool,
+ only_binary: Set[str],
+ expected_formats: FrozenSet[str],
+ ) -> None:
# Create a test TargetPython that we can check for.
target_python = TargetPython(py_version_info=(3, 7))
format_control = FormatControl(set(), only_binary)
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
finder = PackageFinder(
@@ -659,10 +755,10 @@ class TestPackageFinder:
)
# Pass a project_name that will be different from canonical_name.
- link_evaluator = finder.make_link_evaluator('Twine')
+ link_evaluator = finder.make_link_evaluator("Twine")
- assert link_evaluator.project_name == 'Twine'
- assert link_evaluator._canonical_name == 'twine'
+ assert link_evaluator.project_name == "Twine"
+ assert link_evaluator._canonical_name == "twine"
assert link_evaluator._allow_yanked == allow_yanked
assert link_evaluator._ignore_requires_python == ignore_requires_python
assert link_evaluator._formats == expected_formats
@@ -675,24 +771,29 @@ class TestPackageFinder:
assert actual_target_python._given_py_version_info == (3, 7)
assert actual_target_python.py_version_info == (3, 7, 0)
- @pytest.mark.parametrize('allow_all_prereleases, prefer_binary', [
- (False, False),
- (False, True),
- (True, False),
- (True, True),
- ])
+ @pytest.mark.parametrize(
+ "allow_all_prereleases, prefer_binary",
+ [
+ (False, False),
+ (False, True),
+ (True, False),
+ (True, True),
+ ],
+ )
def test_make_candidate_evaluator(
- self, allow_all_prereleases, prefer_binary,
- ):
+ self,
+ allow_all_prereleases: bool,
+ prefer_binary: bool,
+ ) -> None:
target_python = TargetPython()
- target_python._valid_tags = [('py36', 'none', 'any')]
+ target_python._valid_tags = [Tag("py36", "none", "any")]
candidate_prefs = CandidatePreferences(
prefer_binary=prefer_binary,
allow_all_prereleases=allow_all_prereleases,
)
link_collector = LinkCollector(
session=PipSession(),
- search_scope=SearchScope([], []),
+ search_scope=SearchScope([], [], False),
)
finder = PackageFinder(
link_collector=link_collector,
@@ -703,18 +804,18 @@ class TestPackageFinder:
specifier = SpecifierSet()
# Pass hashes to check that _hashes is set.
- hashes = Hashes({'sha256': [64 * 'a']})
+ hashes = Hashes({"sha256": [64 * "a"]})
evaluator = finder.make_candidate_evaluator(
- 'my-project',
+ "my-project",
specifier=specifier,
hashes=hashes,
)
assert evaluator._allow_all_prereleases == allow_all_prereleases
assert evaluator._hashes == hashes
assert evaluator._prefer_binary == prefer_binary
- assert evaluator._project_name == 'my-project'
+ assert evaluator._project_name == "my-project"
assert evaluator._specifier is specifier
- assert evaluator._supported_tags == [('py36', 'none', 'any')]
+ assert evaluator._supported_tags == [Tag("py36", "none", "any")]
@pytest.mark.parametrize(
@@ -723,31 +824,28 @@ class TestPackageFinder:
# Trivial.
("pip-18.0", "pip", 3),
("zope-interface-4.5.0", "zope-interface", 14),
-
# Canonicalized name match non-canonicalized egg info. (pypa/pip#5870)
("Jinja2-2.10", "jinja2", 6),
("zope.interface-4.5.0", "zope-interface", 14),
("zope_interface-4.5.0", "zope-interface", 14),
-
# Should be smart enough to parse ambiguous names from the provided
# package name.
("foo-2-2", "foo", 3),
("foo-2-2", "foo-2", 5),
-
# Should be able to detect collapsed characters in the egg info.
("foo--bar-1.0", "foo-bar", 8),
("foo-_bar-1.0", "foo-bar", 8),
-
# The package name must not ends with a dash (PEP 508), so the first
# dash would be the separator, not the second.
("zope.interface--4.5.0", "zope-interface", 14),
("zope.interface--", "zope-interface", 14),
-
# The version part is missing, but the split function does not care.
("zope.interface-", "zope-interface", 14),
],
)
-def test_find_name_version_sep(fragment, canonical_name, expected):
+def test_find_name_version_sep(
+ fragment: str, canonical_name: str, expected: int
+) -> None:
index = _find_name_version_sep(fragment, canonical_name)
assert index == expected
@@ -762,7 +860,7 @@ def test_find_name_version_sep(fragment, canonical_name, expected):
("zope.interface", "zope-interface"),
],
)
-def test_find_name_version_sep_failure(fragment, canonical_name):
+def test_find_name_version_sep_failure(fragment: str, canonical_name: str) -> None:
with pytest.raises(ValueError) as ctx:
_find_name_version_sep(fragment, canonical_name)
message = f"{fragment} does not match {canonical_name}"
@@ -775,23 +873,19 @@ def test_find_name_version_sep_failure(fragment, canonical_name):
# Trivial.
("pip-18.0", "pip", "18.0"),
("zope-interface-4.5.0", "zope-interface", "4.5.0"),
-
# Canonicalized name match non-canonicalized egg info. (pypa/pip#5870)
("Jinja2-2.10", "jinja2", "2.10"),
("zope.interface-4.5.0", "zope-interface", "4.5.0"),
("zope_interface-4.5.0", "zope-interface", "4.5.0"),
-
# Should be smart enough to parse ambiguous names from the provided
# package name.
("foo-2-2", "foo", "2-2"),
("foo-2-2", "foo-2", "2"),
("zope.interface--4.5.0", "zope-interface", "-4.5.0"),
("zope.interface--", "zope-interface", "-"),
-
# Should be able to detect collapsed characters in the egg info.
("foo--bar-1.0", "foo-bar", "1.0"),
("foo-_bar-1.0", "foo-bar", "1.0"),
-
# Invalid.
("the-package-name-8.19", "does-not-match", None),
("zope.interface.-4.5.0", "zope.interface", None),
@@ -802,6 +896,8 @@ def test_find_name_version_sep_failure(fragment, canonical_name):
("zope.interface", "zope-interface", None),
],
)
-def test_extract_version_from_fragment(fragment, canonical_name, expected):
+def test_extract_version_from_fragment(
+ fragment: str, canonical_name: str, expected: Optional[str]
+) -> None:
version = _extract_version_from_fragment(fragment, canonical_name)
assert version == expected
diff --git a/tests/unit/test_link.py b/tests/unit/test_link.py
index 77f569333..99ed0aba7 100644
--- a/tests/unit/test_link.py
+++ b/tests/unit/test_link.py
@@ -1,3 +1,5 @@
+from typing import Optional
+
import pytest
from pip._internal.models.link import Link, links_equivalent
@@ -5,137 +7,156 @@ from pip._internal.utils.hashes import Hashes
class TestLink:
-
- @pytest.mark.parametrize('url, expected', [
- (
- 'https://user:password@example.com/path/page.html',
- '<Link https://user:****@example.com/path/page.html>',
- ),
- ])
- def test_repr(self, url, expected):
+ @pytest.mark.parametrize(
+ "url, expected",
+ [
+ (
+ "https://user:password@example.com/path/page.html",
+ "<Link https://user:****@example.com/path/page.html>",
+ ),
+ ],
+ )
+ def test_repr(self, url: str, expected: str) -> None:
link = Link(url)
assert repr(link) == expected
- @pytest.mark.parametrize('url, expected', [
- ('http://yo/wheel.whl', 'wheel.whl'),
- ('http://yo/wheel', 'wheel'),
- ('https://example.com/path/page.html', 'page.html'),
- # Test a quoted character.
- ('https://example.com/path/page%231.html', 'page#1.html'),
- (
- 'http://yo/myproject-1.0%2Bfoobar.0-py2.py3-none-any.whl',
- 'myproject-1.0+foobar.0-py2.py3-none-any.whl',
- ),
- # Test a path that ends in a slash.
- ('https://example.com/path/', 'path'),
- ('https://example.com/path//', 'path'),
- # Test a url with no filename.
- ('https://example.com/', 'example.com'),
- # Test a url with no filename and with auth information.
- (
- 'https://user:password@example.com/',
- 'example.com',
- ),
- ])
- def test_filename(self, url, expected):
+ @pytest.mark.parametrize(
+ "url, expected",
+ [
+ ("http://yo/wheel.whl", "wheel.whl"),
+ ("http://yo/wheel", "wheel"),
+ ("https://example.com/path/page.html", "page.html"),
+ # Test a quoted character.
+ ("https://example.com/path/page%231.html", "page#1.html"),
+ (
+ "http://yo/myproject-1.0%2Bfoobar.0-py2.py3-none-any.whl",
+ "myproject-1.0+foobar.0-py2.py3-none-any.whl",
+ ),
+ # Test a path that ends in a slash.
+ ("https://example.com/path/", "path"),
+ ("https://example.com/path//", "path"),
+ # Test a url with no filename.
+ ("https://example.com/", "example.com"),
+ # Test a url with no filename and with auth information.
+ (
+ "https://user:password@example.com/",
+ "example.com",
+ ),
+ ],
+ )
+ def test_filename(self, url: str, expected: str) -> None:
link = Link(url)
assert link.filename == expected
- def test_splitext(self):
- assert ('wheel', '.whl') == Link('http://yo/wheel.whl').splitext()
+ def test_splitext(self) -> None:
+ assert ("wheel", ".whl") == Link("http://yo/wheel.whl").splitext()
- def test_no_ext(self):
- assert '' == Link('http://yo/wheel').ext
+ def test_no_ext(self) -> None:
+ assert "" == Link("http://yo/wheel").ext
- def test_ext(self):
- assert '.whl' == Link('http://yo/wheel.whl').ext
+ def test_ext(self) -> None:
+ assert ".whl" == Link("http://yo/wheel.whl").ext
- def test_ext_fragment(self):
- assert '.whl' == Link('http://yo/wheel.whl#frag').ext
+ def test_ext_fragment(self) -> None:
+ assert ".whl" == Link("http://yo/wheel.whl#frag").ext
- def test_ext_query(self):
- assert '.whl' == Link('http://yo/wheel.whl?a=b').ext
+ def test_ext_query(self) -> None:
+ assert ".whl" == Link("http://yo/wheel.whl?a=b").ext
- def test_is_wheel(self):
- assert Link('http://yo/wheel.whl').is_wheel
+ def test_is_wheel(self) -> None:
+ assert Link("http://yo/wheel.whl").is_wheel
- def test_is_wheel_false(self):
- assert not Link('http://yo/not_a_wheel').is_wheel
+ def test_is_wheel_false(self) -> None:
+ assert not Link("http://yo/not_a_wheel").is_wheel
- def test_fragments(self):
- url = 'git+https://example.com/package#egg=eggname'
- assert 'eggname' == Link(url).egg_fragment
+ def test_fragments(self) -> None:
+ url = "git+https://example.com/package#egg=eggname"
+ assert "eggname" == Link(url).egg_fragment
assert None is Link(url).subdirectory_fragment
- url = 'git+https://example.com/package#egg=eggname&subdirectory=subdir'
- assert 'eggname' == Link(url).egg_fragment
- assert 'subdir' == Link(url).subdirectory_fragment
- url = 'git+https://example.com/package#subdirectory=subdir&egg=eggname'
- assert 'eggname' == Link(url).egg_fragment
- assert 'subdir' == Link(url).subdirectory_fragment
-
- @pytest.mark.parametrize('yanked_reason, expected', [
- (None, False),
- ('', True),
- ('there was a mistake', True),
- ])
- def test_is_yanked(self, yanked_reason, expected):
+ url = "git+https://example.com/package#egg=eggname&subdirectory=subdir"
+ assert "eggname" == Link(url).egg_fragment
+ assert "subdir" == Link(url).subdirectory_fragment
+ url = "git+https://example.com/package#subdirectory=subdir&egg=eggname"
+ assert "eggname" == Link(url).egg_fragment
+ assert "subdir" == Link(url).subdirectory_fragment
+
+ @pytest.mark.parametrize(
+ "yanked_reason, expected",
+ [
+ (None, False),
+ ("", True),
+ ("there was a mistake", True),
+ ],
+ )
+ def test_is_yanked(self, yanked_reason: Optional[str], expected: bool) -> None:
link = Link(
- 'https://example.com/wheel.whl',
+ "https://example.com/wheel.whl",
yanked_reason=yanked_reason,
)
assert link.is_yanked == expected
- @pytest.mark.parametrize('hash_name, hex_digest, expected', [
- # Test a value that matches but with the wrong hash_name.
- ('sha384', 128 * 'a', False),
- # Test matching values, including values other than the first.
- ('sha512', 128 * 'a', True),
- ('sha512', 128 * 'b', True),
- # Test a matching hash_name with a value that doesn't match.
- ('sha512', 128 * 'c', False),
- # Test a link without a hash value.
- ('sha512', '', False),
- ])
- def test_is_hash_allowed(self, hash_name, hex_digest, expected):
- url = (
- 'https://example.com/wheel.whl#{hash_name}={hex_digest}'.format(
- hash_name=hash_name,
- hex_digest=hex_digest,
- )
+ @pytest.mark.parametrize(
+ "hash_name, hex_digest, expected",
+ [
+ # Test a value that matches but with the wrong hash_name.
+ ("sha384", 128 * "a", False),
+ # Test matching values, including values other than the first.
+ ("sha512", 128 * "a", True),
+ ("sha512", 128 * "b", True),
+ # Test a matching hash_name with a value that doesn't match.
+ ("sha512", 128 * "c", False),
+ # Test a link without a hash value.
+ ("sha512", "", False),
+ ],
+ )
+ def test_is_hash_allowed(
+ self, hash_name: str, hex_digest: str, expected: bool
+ ) -> None:
+ url = "https://example.com/wheel.whl#{hash_name}={hex_digest}".format(
+ hash_name=hash_name,
+ hex_digest=hex_digest,
)
link = Link(url)
hashes_data = {
- 'sha512': [128 * 'a', 128 * 'b'],
+ "sha512": [128 * "a", 128 * "b"],
}
hashes = Hashes(hashes_data)
assert link.is_hash_allowed(hashes) == expected
- def test_is_hash_allowed__no_hash(self):
- link = Link('https://example.com/wheel.whl')
+ def test_is_hash_allowed__no_hash(self) -> None:
+ link = Link("https://example.com/wheel.whl")
hashes_data = {
- 'sha512': [128 * 'a'],
+ "sha512": [128 * "a"],
}
hashes = Hashes(hashes_data)
assert not link.is_hash_allowed(hashes)
- @pytest.mark.parametrize('hashes, expected', [
- (None, False),
- # Also test a success case to show the test is correct.
- (Hashes({'sha512': [128 * 'a']}), True),
- ])
- def test_is_hash_allowed__none_hashes(self, hashes, expected):
- url = 'https://example.com/wheel.whl#sha512={}'.format(128 * 'a')
+ @pytest.mark.parametrize(
+ "hashes, expected",
+ [
+ (None, False),
+ # Also test a success case to show the test is correct.
+ (Hashes({"sha512": [128 * "a"]}), True),
+ ],
+ )
+ def test_is_hash_allowed__none_hashes(
+ self, hashes: Optional[Hashes], expected: bool
+ ) -> None:
+ url = "https://example.com/wheel.whl#sha512={}".format(128 * "a")
link = Link(url)
assert link.is_hash_allowed(hashes) == expected
- @pytest.mark.parametrize('url, expected', [
- ('git+https://github.com/org/repo', True),
- ('bzr+http://bzr.myproject.org/MyProject/trunk/#egg=MyProject', True),
- ('hg+file://hg.company.com/repo', True),
- ('https://example.com/some.whl', False),
- ('file://home/foo/some.whl', False),
- ])
- def test_is_vcs(self, url, expected):
+ @pytest.mark.parametrize(
+ "url, expected",
+ [
+ ("git+https://github.com/org/repo", True),
+ ("bzr+http://bzr.myproject.org/MyProject/trunk/#egg=MyProject", True),
+ ("hg+file://hg.company.com/repo", True),
+ ("https://example.com/some.whl", False),
+ ("file://home/foo/some.whl", False),
+ ],
+ )
+ def test_is_vcs(self, url: str, expected: bool) -> None:
link = Link(url)
assert link.is_vcs is expected
@@ -165,7 +186,7 @@ class TestLink:
),
],
)
-def test_links_equivalent(url1, url2):
+def test_links_equivalent(url1: str, url2: str) -> None:
assert links_equivalent(Link(url1), Link(url2))
@@ -189,5 +210,5 @@ def test_links_equivalent(url1, url2):
),
],
)
-def test_links_equivalent_false(url1, url2):
+def test_links_equivalent_false(url1: str, url2: str) -> None:
assert not links_equivalent(Link(url1), Link(url2))
diff --git a/tests/unit/test_locations.py b/tests/unit/test_locations.py
index 067f4e844..775676653 100644
--- a/tests/unit/test_locations.py
+++ b/tests/unit/test_locations.py
@@ -6,40 +6,43 @@ import getpass
import os
import shutil
import sys
+import sysconfig
import tempfile
+from pathlib import Path
+from typing import Any, Dict
from unittest.mock import Mock
import pytest
-from pip._internal.locations import SCHEME_KEYS, get_scheme
+from pip._internal.locations import SCHEME_KEYS, _should_use_sysconfig, get_scheme
-if sys.platform == 'win32':
+if sys.platform == "win32":
pwd = Mock()
else:
import pwd
-def _get_scheme_dict(*args, **kwargs):
+def _get_scheme_dict(*args: Any, **kwargs: Any) -> Dict[str, str]:
scheme = get_scheme(*args, **kwargs)
return {k: getattr(scheme, k) for k in SCHEME_KEYS}
class TestLocations:
- def setup(self):
+ def setup(self) -> None:
self.tempdir = tempfile.mkdtemp()
self.st_uid = 9999
self.username = "example"
self.patch()
- def teardown(self):
+ def teardown(self) -> None:
self.revert_patch()
shutil.rmtree(self.tempdir, ignore_errors=True)
- def patch(self):
- """ first store and then patch python methods pythons """
+ def patch(self) -> None:
+ """first store and then patch python methods pythons"""
self.tempfile_gettempdir = tempfile.gettempdir
self.old_os_fstat = os.fstat
- if sys.platform != 'win32':
+ if sys.platform != "win32":
# os.geteuid and pwd.getpwuid are not implemented on windows
self.old_os_geteuid = os.geteuid
self.old_pwd_getpwuid = pwd.getpwuid
@@ -48,46 +51,63 @@ class TestLocations:
# now patch
tempfile.gettempdir = lambda: self.tempdir
getpass.getuser = lambda: self.username
- os.geteuid = lambda: self.st_uid
os.fstat = lambda fd: self.get_mock_fstat(fd)
+ if sys.platform != "win32":
+ os.geteuid = lambda: self.st_uid
+ pwd.getpwuid = self.get_mock_getpwuid
- if sys.platform != 'win32':
- pwd.getpwuid = lambda uid: self.get_mock_getpwuid(uid)
-
- def revert_patch(self):
- """ revert the patches to python methods """
+ def revert_patch(self) -> None:
+ """revert the patches to python methods"""
tempfile.gettempdir = self.tempfile_gettempdir
getpass.getuser = self.old_getpass_getuser
- if sys.platform != 'win32':
+ if sys.platform != "win32":
# os.geteuid and pwd.getpwuid are not implemented on windows
os.geteuid = self.old_os_geteuid
pwd.getpwuid = self.old_pwd_getpwuid
os.fstat = self.old_os_fstat
- def get_mock_fstat(self, fd):
- """ returns a basic mock fstat call result.
- Currently only the st_uid attribute has been set.
+ def get_mock_fstat(self, fd: int) -> os.stat_result:
+ """returns a basic mock fstat call result.
+ Currently only the st_uid attribute has been set.
"""
result = Mock()
result.st_uid = self.st_uid
return result
- def get_mock_getpwuid(self, uid):
- """ returns a basic mock pwd.getpwuid call result.
- Currently only the pw_name attribute has been set.
+ def get_mock_getpwuid(self, uid: int) -> Any:
+ """returns a basic mock pwd.getpwuid call result.
+ Currently only the pw_name attribute has been set.
"""
result = Mock()
result.pw_name = self.username
return result
+ def test_default_should_use_sysconfig(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ monkeypatch.delattr(sysconfig, "_PIP_USE_SYSCONFIG", raising=False)
+ if sys.version_info[:2] >= (3, 10):
+ assert _should_use_sysconfig() is True
+ else:
+ assert _should_use_sysconfig() is False
+
+ @pytest.mark.parametrize("vendor_value", [True, False, None, "", 0, 1])
+ def test_vendor_overriden_should_use_sysconfig(
+ self, monkeypatch: pytest.MonkeyPatch, vendor_value: Any
+ ) -> None:
+ monkeypatch.setattr(
+ sysconfig, "_PIP_USE_SYSCONFIG", vendor_value, raising=False
+ )
+ assert _should_use_sysconfig() is bool(vendor_value)
-class TestDistutilsScheme:
- def test_root_modifies_appropriately(self, monkeypatch):
+class TestDistutilsScheme:
+ def test_root_modifies_appropriately(self) -> None:
# This deals with nt/posix path differences
# root is c:\somewhere\else or /somewhere/else
- root = os.path.normcase(os.path.abspath(
- os.path.join(os.path.sep, 'somewhere', 'else')))
+ root = os.path.normcase(
+ os.path.abspath(os.path.join(os.path.sep, "somewhere", "else"))
+ )
norm_scheme = _get_scheme_dict("example")
root_scheme = _get_scheme_dict("example", root=root)
@@ -96,11 +116,15 @@ class TestDistutilsScheme:
expected = os.path.join(root, path[1:])
assert os.path.abspath(root_scheme[key]) == expected
+ @pytest.mark.incompatible_with_sysconfig
@pytest.mark.incompatible_with_venv
- def test_distutils_config_file_read(self, tmpdir, monkeypatch):
+ def test_distutils_config_file_read(
+ self, tmpdir: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
# This deals with nt/posix path differences
- install_scripts = os.path.normcase(os.path.abspath(
- os.path.join(os.path.sep, 'somewhere', 'else')))
+ install_scripts = os.path.normcase(
+ os.path.abspath(os.path.join(os.path.sep, "somewhere", "else"))
+ )
f = tmpdir / "config" / "setup.cfg"
f.parent.mkdir()
f.write_text("[install]\ninstall-scripts=" + install_scripts)
@@ -109,20 +133,24 @@ class TestDistutilsScheme:
# patch the function that returns what config files are present
monkeypatch.setattr(
Distribution,
- 'find_config_files',
+ "find_config_files",
lambda self: [f],
)
- scheme = _get_scheme_dict('example')
- assert scheme['scripts'] == install_scripts
+ scheme = _get_scheme_dict("example")
+ assert scheme["scripts"] == install_scripts
+ @pytest.mark.incompatible_with_sysconfig
@pytest.mark.incompatible_with_venv
# when we request install-lib, we should install everything (.py &
# .so) into that path; i.e. ensure platlib & purelib are set to
- # this path
- def test_install_lib_takes_precedence(self, tmpdir, monkeypatch):
+ # this path. sysconfig does not support this.
+ def test_install_lib_takes_precedence(
+ self, tmpdir: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
# This deals with nt/posix path differences
- install_lib = os.path.normcase(os.path.abspath(
- os.path.join(os.path.sep, 'somewhere', 'else')))
+ install_lib = os.path.normcase(
+ os.path.abspath(os.path.join(os.path.sep, "somewhere", "else"))
+ )
f = tmpdir / "config" / "setup.cfg"
f.parent.mkdir()
f.write_text("[install]\ninstall-lib=" + install_lib)
@@ -131,25 +159,22 @@ class TestDistutilsScheme:
# patch the function that returns what config files are present
monkeypatch.setattr(
Distribution,
- 'find_config_files',
+ "find_config_files",
lambda self: [f],
)
- scheme = _get_scheme_dict('example')
- assert scheme['platlib'] == install_lib + os.path.sep
- assert scheme['purelib'] == install_lib + os.path.sep
+ scheme = _get_scheme_dict("example")
+ assert scheme["platlib"] == install_lib + os.path.sep
+ assert scheme["purelib"] == install_lib + os.path.sep
- def test_prefix_modifies_appropriately(self):
- prefix = os.path.abspath(os.path.join('somewhere', 'else'))
+ def test_prefix_modifies_appropriately(self) -> None:
+ prefix = os.path.abspath(os.path.join("somewhere", "else"))
normal_scheme = _get_scheme_dict("example")
prefix_scheme = _get_scheme_dict("example", prefix=prefix)
- def _calculate_expected(value):
+ def _calculate_expected(value: str) -> str:
path = os.path.join(prefix, os.path.relpath(value, sys.prefix))
return os.path.normpath(path)
- expected = {
- k: _calculate_expected(v)
- for k, v in normal_scheme.items()
- }
+ expected = {k: _calculate_expected(v) for k, v in normal_scheme.items()}
assert prefix_scheme == expected
diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py
index 20eaf9712..3ba6ed57c 100644
--- a/tests/unit/test_logging.py
+++ b/tests/unit/test_logging.py
@@ -1,4 +1,5 @@
import logging
+import time
from threading import Thread
from unittest.mock import patch
@@ -6,8 +7,8 @@ import pytest
from pip._internal.utils.logging import (
BrokenStdoutLoggingError,
- ColorizedStreamHandler,
IndentingFormatter,
+ RichPipStreamHandler,
indent_log,
)
from pip._internal.utils.misc import captured_stderr, captured_stdout
@@ -18,11 +19,11 @@ logger = logging.getLogger(__name__)
class TestIndentingFormatter:
"""Test ``pip._internal.utils.logging.IndentingFormatter``."""
- def make_record(self, msg, level_name):
+ def make_record(self, msg: str, level_name: str) -> logging.LogRecord:
level_number = getattr(logging, level_name)
attrs = dict(
msg=msg,
- created=1547704837.040001,
+ created=1547704837.040001 + time.timezone,
msecs=40,
levelname=level_name,
levelno=level_number,
@@ -31,59 +32,70 @@ class TestIndentingFormatter:
return record
- @pytest.mark.parametrize('level_name, expected', [
- ('DEBUG', 'hello\nworld'),
- ('INFO', 'hello\nworld'),
- ('WARNING', 'WARNING: hello\nworld'),
- ('ERROR', 'ERROR: hello\nworld'),
- ('CRITICAL', 'ERROR: hello\nworld'),
- ])
- def test_format(self, level_name, expected, utc):
+ @pytest.mark.parametrize(
+ "level_name, expected",
+ [
+ ("DEBUG", "hello\nworld"),
+ ("INFO", "hello\nworld"),
+ ("WARNING", "WARNING: hello\nworld"),
+ ("ERROR", "ERROR: hello\nworld"),
+ ("CRITICAL", "ERROR: hello\nworld"),
+ ],
+ )
+ def test_format(self, level_name: str, expected: str) -> None:
"""
Args:
level_name: a logging level name (e.g. "WARNING").
"""
- record = self.make_record('hello\nworld', level_name=level_name)
+ record = self.make_record("hello\nworld", level_name=level_name)
f = IndentingFormatter(fmt="%(message)s")
assert f.format(record) == expected
- @pytest.mark.parametrize('level_name, expected', [
- ('INFO',
- '2019-01-17T06:00:37,040 hello\n'
- '2019-01-17T06:00:37,040 world'),
- ('WARNING',
- '2019-01-17T06:00:37,040 WARNING: hello\n'
- '2019-01-17T06:00:37,040 world'),
- ])
- def test_format_with_timestamp(self, level_name, expected, utc):
- record = self.make_record('hello\nworld', level_name=level_name)
+ @pytest.mark.parametrize(
+ "level_name, expected",
+ [
+ ("INFO", "2019-01-17T06:00:37,040 hello\n2019-01-17T06:00:37,040 world"),
+ (
+ "WARNING",
+ "2019-01-17T06:00:37,040 WARNING: hello\n"
+ "2019-01-17T06:00:37,040 world",
+ ),
+ ],
+ )
+ def test_format_with_timestamp(self, level_name: str, expected: str) -> None:
+ record = self.make_record("hello\nworld", level_name=level_name)
f = IndentingFormatter(fmt="%(message)s", add_timestamp=True)
assert f.format(record) == expected
- @pytest.mark.parametrize('level_name, expected', [
- ('WARNING', 'DEPRECATION: hello\nworld'),
- ('ERROR', 'DEPRECATION: hello\nworld'),
- ('CRITICAL', 'DEPRECATION: hello\nworld'),
- ])
- def test_format_deprecated(self, level_name, expected, utc):
+ @pytest.mark.parametrize(
+ "level_name, expected",
+ [
+ ("WARNING", "DEPRECATION: hello\nworld"),
+ ("ERROR", "DEPRECATION: hello\nworld"),
+ ("CRITICAL", "DEPRECATION: hello\nworld"),
+ ],
+ )
+ def test_format_deprecated(self, level_name: str, expected: str) -> None:
"""
Test that logged deprecation warnings coming from deprecated()
don't get another prefix.
"""
record = self.make_record(
- 'DEPRECATION: hello\nworld', level_name=level_name,
+ "DEPRECATION: hello\nworld",
+ level_name=level_name,
)
f = IndentingFormatter(fmt="%(message)s")
assert f.format(record) == expected
- def test_thread_safety_base(self, utc):
+ def test_thread_safety_base(self) -> None:
record = self.make_record(
- 'DEPRECATION: hello\nworld', level_name='WARNING',
+ "DEPRECATION: hello\nworld",
+ level_name="WARNING",
)
f = IndentingFormatter(fmt="%(message)s")
results = []
- def thread_function():
+ def thread_function() -> None:
results.append(f.format(record))
thread_function()
@@ -92,14 +104,15 @@ class TestIndentingFormatter:
thread.join()
assert results[0] == results[1]
- def test_thread_safety_indent_log(self, utc):
+ def test_thread_safety_indent_log(self) -> None:
record = self.make_record(
- 'DEPRECATION: hello\nworld', level_name='WARNING',
+ "DEPRECATION: hello\nworld",
+ level_name="WARNING",
)
f = IndentingFormatter(fmt="%(message)s")
results = []
- def thread_function():
+ def thread_function() -> None:
with indent_log():
results.append(f.format(record))
@@ -111,16 +124,15 @@ class TestIndentingFormatter:
class TestColorizedStreamHandler:
-
- def _make_log_record(self):
+ def _make_log_record(self) -> logging.LogRecord:
attrs = {
- 'msg': 'my error',
+ "msg": "my error",
}
record = logging.makeLogRecord(attrs)
return record
- def test_broken_pipe_in_stderr_flush(self):
+ def test_broken_pipe_in_stderr_flush(self) -> None:
"""
Test sys.stderr.flush() raising BrokenPipeError.
@@ -129,21 +141,21 @@ class TestColorizedStreamHandler:
record = self._make_log_record()
with captured_stderr() as stderr:
- handler = ColorizedStreamHandler(stream=stderr)
- with patch('sys.stderr.flush') as mock_flush:
+ handler = RichPipStreamHandler(stream=stderr, no_color=True)
+ with patch("sys.stderr.flush") as mock_flush:
mock_flush.side_effect = BrokenPipeError()
# The emit() call raises no exception.
handler.emit(record)
err_text = stderr.getvalue()
- assert err_text.startswith('my error')
+ assert err_text.startswith("my error")
# Check that the logging framework tried to log the exception.
- assert 'Logging error' in err_text
- assert 'BrokenPipeError' in err_text
+ assert "Logging error" in err_text
+ assert "BrokenPipeError" in err_text
assert "Message: 'my error'" in err_text
- def test_broken_pipe_in_stdout_write(self):
+ def test_broken_pipe_in_stdout_write(self) -> None:
"""
Test sys.stdout.write() raising BrokenPipeError.
@@ -152,13 +164,13 @@ class TestColorizedStreamHandler:
record = self._make_log_record()
with captured_stdout() as stdout:
- handler = ColorizedStreamHandler(stream=stdout)
- with patch('sys.stdout.write') as mock_write:
+ handler = RichPipStreamHandler(stream=stdout, no_color=True)
+ with patch("sys.stdout.write") as mock_write:
mock_write.side_effect = BrokenPipeError()
with pytest.raises(BrokenStdoutLoggingError):
handler.emit(record)
- def test_broken_pipe_in_stdout_flush(self):
+ def test_broken_pipe_in_stdout_flush(self) -> None:
"""
Test sys.stdout.flush() raising BrokenPipeError.
@@ -167,8 +179,8 @@ class TestColorizedStreamHandler:
record = self._make_log_record()
with captured_stdout() as stdout:
- handler = ColorizedStreamHandler(stream=stdout)
- with patch('sys.stdout.flush') as mock_flush:
+ handler = RichPipStreamHandler(stream=stdout, no_color=True)
+ with patch("sys.stdout.flush") as mock_flush:
mock_flush.side_effect = BrokenPipeError()
with pytest.raises(BrokenStdoutLoggingError):
handler.emit(record)
@@ -177,4 +189,4 @@ class TestColorizedStreamHandler:
# Sanity check that the log record was written, since flush() happens
# after write().
- assert output.startswith('my error')
+ assert output.startswith("my error")
diff --git a/tests/unit/test_metadata.py b/tests/unit/test_metadata.py
deleted file mode 100644
index 325ce0094..000000000
--- a/tests/unit/test_metadata.py
+++ /dev/null
@@ -1,40 +0,0 @@
-import logging
-from unittest.mock import patch
-
-from pip._internal.metadata import BaseDistribution
-from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, ArchiveInfo
-
-
-@patch.object(BaseDistribution, "read_text", side_effect=FileNotFoundError)
-def test_dist_get_direct_url_no_metadata(mock_read_text):
- dist = BaseDistribution()
- assert dist.direct_url is None
- mock_read_text.assert_called_once_with(DIRECT_URL_METADATA_NAME)
-
-
-@patch.object(BaseDistribution, "read_text", return_value="{}")
-def test_dist_get_direct_url_invalid_json(mock_read_text, caplog):
- class FakeDistribution(BaseDistribution):
- canonical_name = "whatever" # Needed for error logging.
-
- dist = FakeDistribution()
- with caplog.at_level(logging.WARNING):
- assert dist.direct_url is None
-
- mock_read_text.assert_called_once_with(DIRECT_URL_METADATA_NAME)
- assert caplog.records[-1].getMessage().startswith(
- "Error parsing direct_url.json for whatever:",
- )
-
-
-@patch.object(
- BaseDistribution,
- "read_text",
- return_value='{"url": "https://e.c/p.tgz", "archive_info": {}}',
-)
-def test_dist_get_direct_url_valid_metadata(mock_read_text):
- dist = BaseDistribution()
- direct_url = dist.direct_url
- mock_read_text.assert_called_once_with(DIRECT_URL_METADATA_NAME)
- assert direct_url.url == "https://e.c/p.tgz"
- assert isinstance(direct_url.info, ArchiveInfo)
diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py
index 8e2975bd7..c5545e37d 100644
--- a/tests/unit/test_models.py
+++ b/tests/unit/test_models.py
@@ -4,13 +4,13 @@
from pip._vendor.packaging.version import parse as parse_version
from pip._internal.models import candidate, index
+from pip._internal.models.link import Link
class TestPackageIndex:
- """Tests for pip._internal.models.index.PackageIndex
- """
+ """Tests for pip._internal.models.index.PackageIndex"""
- def test_gives_right_urls(self):
+ def test_gives_right_urls(self) -> None:
url = "https://mypypi.internal/path/"
file_storage_domain = "files.mypypi.internal"
pack_index = index.PackageIndex(url, file_storage_domain)
@@ -22,7 +22,7 @@ class TestPackageIndex:
assert pack_index.simple_url == url + "simple"
assert pack_index.pypi_url == url + "pypi"
- def test_PyPI_urls_are_correct(self):
+ def test_PyPI_urls_are_correct(self) -> None:
pack_index = index.PyPI
assert pack_index.netloc == "pypi.org"
@@ -31,7 +31,7 @@ class TestPackageIndex:
assert pack_index.pypi_url == "https://pypi.org/pypi"
assert pack_index.file_storage_domain == "files.pythonhosted.org"
- def test_TestPyPI_urls_are_correct(self):
+ def test_TestPyPI_urls_are_correct(self) -> None:
pack_index = index.TestPyPI
assert pack_index.netloc == "test.pypi.org"
@@ -42,19 +42,18 @@ class TestPackageIndex:
class TestInstallationCandidate:
-
- def test_sets_correct_variables(self):
+ def test_sets_correct_variables(self) -> None:
obj = candidate.InstallationCandidate(
- "A", "1.0.0", "https://somewhere.com/path/A-1.0.0.tar.gz"
+ "A", "1.0.0", Link("https://somewhere.com/path/A-1.0.0.tar.gz")
)
assert obj.name == "A"
assert obj.version == parse_version("1.0.0")
- assert obj.link == "https://somewhere.com/path/A-1.0.0.tar.gz"
+ assert obj.link.url == "https://somewhere.com/path/A-1.0.0.tar.gz"
# NOTE: This isn't checking the ordering logic; only the data provided to
# it is correct.
- def test_sets_the_right_key(self):
+ def test_sets_the_right_key(self) -> None:
obj = candidate.InstallationCandidate(
- "A", "1.0.0", "https://somewhere.com/path/A-1.0.0.tar.gz"
+ "A", "1.0.0", Link("https://somewhere.com/path/A-1.0.0.tar.gz")
)
assert obj._compare_key == (obj.name, obj.version, obj.link)
diff --git a/tests/unit/test_models_wheel.py b/tests/unit/test_models_wheel.py
index e0d45f5c8..c06525e08 100644
--- a/tests/unit/test_models_wheel.py
+++ b/tests/unit/test_models_wheel.py
@@ -7,131 +7,122 @@ from pip._internal.utils import compatibility_tags
class TestWheelFile:
-
- def test_std_wheel_pattern(self):
- w = Wheel('simple-1.1.1-py2-none-any.whl')
- assert w.name == 'simple'
- assert w.version == '1.1.1'
- assert w.pyversions == ['py2']
- assert w.abis == ['none']
- assert w.plats == ['any']
-
- def test_wheel_pattern_multi_values(self):
- w = Wheel('simple-1.1-py2.py3-abi1.abi2-any.whl')
- assert w.name == 'simple'
- assert w.version == '1.1'
- assert w.pyversions == ['py2', 'py3']
- assert w.abis == ['abi1', 'abi2']
- assert w.plats == ['any']
-
- def test_wheel_with_build_tag(self):
+ def test_std_wheel_pattern(self) -> None:
+ w = Wheel("simple-1.1.1-py2-none-any.whl")
+ assert w.name == "simple"
+ assert w.version == "1.1.1"
+ assert w.pyversions == ["py2"]
+ assert w.abis == ["none"]
+ assert w.plats == ["any"]
+
+ def test_wheel_pattern_multi_values(self) -> None:
+ w = Wheel("simple-1.1-py2.py3-abi1.abi2-any.whl")
+ assert w.name == "simple"
+ assert w.version == "1.1"
+ assert w.pyversions == ["py2", "py3"]
+ assert w.abis == ["abi1", "abi2"]
+ assert w.plats == ["any"]
+
+ def test_wheel_with_build_tag(self) -> None:
# pip doesn't do anything with build tags, but theoretically, we might
# see one, in this case the build tag = '4'
- w = Wheel('simple-1.1-4-py2-none-any.whl')
- assert w.name == 'simple'
- assert w.version == '1.1'
- assert w.pyversions == ['py2']
- assert w.abis == ['none']
- assert w.plats == ['any']
-
- def test_single_digit_version(self):
- w = Wheel('simple-1-py2-none-any.whl')
- assert w.version == '1'
-
- def test_non_pep440_version(self):
- w = Wheel('simple-_invalid_-py2-none-any.whl')
- assert w.version == '-invalid-'
-
- def test_missing_version_raises(self):
+ w = Wheel("simple-1.1-4-py2-none-any.whl")
+ assert w.name == "simple"
+ assert w.version == "1.1"
+ assert w.pyversions == ["py2"]
+ assert w.abis == ["none"]
+ assert w.plats == ["any"]
+
+ def test_single_digit_version(self) -> None:
+ w = Wheel("simple-1-py2-none-any.whl")
+ assert w.version == "1"
+
+ def test_non_pep440_version(self) -> None:
+ w = Wheel("simple-_invalid_-py2-none-any.whl")
+ assert w.version == "-invalid-"
+
+ def test_missing_version_raises(self) -> None:
with pytest.raises(InvalidWheelFilename):
- Wheel('Cython-cp27-none-linux_x86_64.whl')
+ Wheel("Cython-cp27-none-linux_x86_64.whl")
- def test_invalid_filename_raises(self):
+ def test_invalid_filename_raises(self) -> None:
with pytest.raises(InvalidWheelFilename):
- Wheel('invalid.whl')
+ Wheel("invalid.whl")
- def test_supported_single_version(self):
+ def test_supported_single_version(self) -> None:
"""
Test single-version wheel is known to be supported
"""
- w = Wheel('simple-0.1-py2-none-any.whl')
- assert w.supported(tags=[Tag('py2', 'none', 'any')])
+ w = Wheel("simple-0.1-py2-none-any.whl")
+ assert w.supported(tags=[Tag("py2", "none", "any")])
- def test_supported_multi_version(self):
+ def test_supported_multi_version(self) -> None:
"""
Test multi-version wheel is known to be supported
"""
- w = Wheel('simple-0.1-py2.py3-none-any.whl')
- assert w.supported(tags=[Tag('py3', 'none', 'any')])
+ w = Wheel("simple-0.1-py2.py3-none-any.whl")
+ assert w.supported(tags=[Tag("py3", "none", "any")])
- def test_not_supported_version(self):
+ def test_not_supported_version(self) -> None:
"""
Test unsupported wheel is known to be unsupported
"""
- w = Wheel('simple-0.1-py2-none-any.whl')
- assert not w.supported(tags=[Tag('py1', 'none', 'any')])
+ w = Wheel("simple-0.1-py2-none-any.whl")
+ assert not w.supported(tags=[Tag("py1", "none", "any")])
- def test_supported_osx_version(self):
+ def test_supported_osx_version(self) -> None:
"""
Wheels built for macOS 10.6 are supported on 10.9
"""
tags = compatibility_tags.get_supported(
- '27', platforms=['macosx_10_9_intel'], impl='cp'
+ "27", platforms=["macosx_10_9_intel"], impl="cp"
)
- w = Wheel('simple-0.1-cp27-none-macosx_10_6_intel.whl')
+ w = Wheel("simple-0.1-cp27-none-macosx_10_6_intel.whl")
assert w.supported(tags=tags)
- w = Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl')
+ w = Wheel("simple-0.1-cp27-none-macosx_10_9_intel.whl")
assert w.supported(tags=tags)
- def test_not_supported_osx_version(self):
+ def test_not_supported_osx_version(self) -> None:
"""
Wheels built for macOS 10.9 are not supported on 10.6
"""
tags = compatibility_tags.get_supported(
- '27', platforms=['macosx_10_6_intel'], impl='cp'
+ "27", platforms=["macosx_10_6_intel"], impl="cp"
)
- w = Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl')
+ w = Wheel("simple-0.1-cp27-none-macosx_10_9_intel.whl")
assert not w.supported(tags=tags)
- @pytest.mark.xfail(
- reason=(
- "packaging.tags changed behaviour in this area, and @pradyunsg "
- "decided as the release manager that this behaviour change is less "
- "critical than Big Sur support for pip 20.3. See "
- "https://github.com/pypa/packaging/pull/361 for further discussion."
- )
- )
- def test_supported_multiarch_darwin(self):
+ def test_supported_multiarch_darwin(self) -> None:
"""
Multi-arch wheels (intel) are supported on components (i386, x86_64)
"""
universal = compatibility_tags.get_supported(
- '27', platforms=['macosx_10_5_universal'], impl='cp'
+ "27", platforms=["macosx_10_5_universal"], impl="cp"
)
intel = compatibility_tags.get_supported(
- '27', platforms=['macosx_10_5_intel'], impl='cp'
+ "27", platforms=["macosx_10_5_intel"], impl="cp"
)
x64 = compatibility_tags.get_supported(
- '27', platforms=['macosx_10_5_x86_64'], impl='cp'
+ "27", platforms=["macosx_10_5_x86_64"], impl="cp"
)
i386 = compatibility_tags.get_supported(
- '27', platforms=['macosx_10_5_i386'], impl='cp'
+ "27", platforms=["macosx_10_5_i386"], impl="cp"
)
ppc = compatibility_tags.get_supported(
- '27', platforms=['macosx_10_5_ppc'], impl='cp'
+ "27", platforms=["macosx_10_5_ppc"], impl="cp"
)
ppc64 = compatibility_tags.get_supported(
- '27', platforms=['macosx_10_5_ppc64'], impl='cp'
+ "27", platforms=["macosx_10_5_ppc64"], impl="cp"
)
- w = Wheel('simple-0.1-cp27-none-macosx_10_5_intel.whl')
+ w = Wheel("simple-0.1-cp27-none-macosx_10_5_intel.whl")
assert w.supported(tags=intel)
assert w.supported(tags=x64)
assert w.supported(tags=i386)
assert not w.supported(tags=universal)
assert not w.supported(tags=ppc)
assert not w.supported(tags=ppc64)
- w = Wheel('simple-0.1-cp27-none-macosx_10_5_universal.whl')
+ w = Wheel("simple-0.1-cp27-none-macosx_10_5_universal.whl")
assert w.supported(tags=universal)
assert w.supported(tags=intel)
assert w.supported(tags=x64)
@@ -139,50 +130,50 @@ class TestWheelFile:
assert w.supported(tags=ppc)
assert w.supported(tags=ppc64)
- def test_not_supported_multiarch_darwin(self):
+ def test_not_supported_multiarch_darwin(self) -> None:
"""
Single-arch wheels (x86_64) are not supported on multi-arch (intel)
"""
universal = compatibility_tags.get_supported(
- '27', platforms=['macosx_10_5_universal'], impl='cp'
+ "27", platforms=["macosx_10_5_universal"], impl="cp"
)
intel = compatibility_tags.get_supported(
- '27', platforms=['macosx_10_5_intel'], impl='cp'
+ "27", platforms=["macosx_10_5_intel"], impl="cp"
)
- w = Wheel('simple-0.1-cp27-none-macosx_10_5_i386.whl')
+ w = Wheel("simple-0.1-cp27-none-macosx_10_5_i386.whl")
assert not w.supported(tags=intel)
assert not w.supported(tags=universal)
- w = Wheel('simple-0.1-cp27-none-macosx_10_5_x86_64.whl')
+ w = Wheel("simple-0.1-cp27-none-macosx_10_5_x86_64.whl")
assert not w.supported(tags=intel)
assert not w.supported(tags=universal)
- def test_support_index_min(self):
+ def test_support_index_min(self) -> None:
"""
Test results from `support_index_min`
"""
tags = [
- Tag('py2', 'none', 'TEST'),
- Tag('py2', 'TEST', 'any'),
- Tag('py2', 'none', 'any'),
+ Tag("py2", "none", "TEST"),
+ Tag("py2", "TEST", "any"),
+ Tag("py2", "none", "any"),
]
- w = Wheel('simple-0.1-py2-none-any.whl')
+ w = Wheel("simple-0.1-py2-none-any.whl")
assert w.support_index_min(tags=tags) == 2
- w = Wheel('simple-0.1-py2-none-TEST.whl')
+ w = Wheel("simple-0.1-py2-none-TEST.whl")
assert w.support_index_min(tags=tags) == 0
- def test_support_index_min__none_supported(self):
+ def test_support_index_min__none_supported(self) -> None:
"""
Test a wheel not supported by the given tags.
"""
- w = Wheel('simple-0.1-py2-none-any.whl')
+ w = Wheel("simple-0.1-py2-none-any.whl")
with pytest.raises(ValueError):
w.support_index_min(tags=[])
- def test_version_underscore_conversion(self):
+ def test_version_underscore_conversion(self) -> None:
"""
Test that we convert '_' to '-' for versions parsed out of wheel
filenames
"""
- w = Wheel('simple-0.1_1-py2-none-any.whl')
- assert w.version == '0.1-1'
+ w = Wheel("simple-0.1_1-py2-none-any.whl")
+ assert w.version == "0.1-1"
diff --git a/tests/unit/test_network_auth.py b/tests/unit/test_network_auth.py
index 1ec8d19ec..5c0e57462 100644
--- a/tests/unit/test_network_auth.py
+++ b/tests/unit/test_network_auth.py
@@ -1,4 +1,5 @@
import functools
+from typing import Any, List, Optional, Tuple
import pytest
@@ -7,33 +8,38 @@ from pip._internal.network.auth import MultiDomainBasicAuth
from tests.lib.requests_mocks import MockConnection, MockRequest, MockResponse
-@pytest.mark.parametrize(["input_url", "url", "username", "password"], [
- (
- "http://user%40email.com:password@example.com/path",
- "http://example.com/path",
- "user@email.com",
- "password",
- ),
- (
- "http://username:password@example.com/path",
- "http://example.com/path",
- "username",
- "password",
- ),
- (
- "http://token@example.com/path",
- "http://example.com/path",
- "token",
- "",
- ),
- (
- "http://example.com/path",
- "http://example.com/path",
- None,
- None,
- ),
-])
-def test_get_credentials_parses_correctly(input_url, url, username, password):
+@pytest.mark.parametrize(
+ ["input_url", "url", "username", "password"],
+ [
+ (
+ "http://user%40email.com:password@example.com/path",
+ "http://example.com/path",
+ "user@email.com",
+ "password",
+ ),
+ (
+ "http://username:password@example.com/path",
+ "http://example.com/path",
+ "username",
+ "password",
+ ),
+ (
+ "http://token@example.com/path",
+ "http://example.com/path",
+ "token",
+ "",
+ ),
+ (
+ "http://example.com/path",
+ "http://example.com/path",
+ None,
+ None,
+ ),
+ ],
+)
+def test_get_credentials_parses_correctly(
+ input_url: str, url: str, username: Optional[str], password: Optional[str]
+) -> None:
auth = MultiDomainBasicAuth()
get = auth._get_url_and_credentials
@@ -41,51 +47,57 @@ def test_get_credentials_parses_correctly(input_url, url, username, password):
assert get(input_url) == (url, username, password)
assert (
# There are no credentials in the URL
- (username is None and password is None) or
+ (username is None and password is None)
+ or
# Credentials were found and "cached" appropriately
- auth.passwords['example.com'] == (username, password)
+ auth.passwords["example.com"] == (username, password)
)
-def test_get_credentials_not_to_uses_cached_credentials():
+def test_get_credentials_not_to_uses_cached_credentials() -> None:
auth = MultiDomainBasicAuth()
- auth.passwords['example.com'] = ('user', 'pass')
+ auth.passwords["example.com"] = ("user", "pass")
got = auth._get_url_and_credentials("http://foo:bar@example.com/path")
- expected = ('http://example.com/path', 'foo', 'bar')
+ expected = ("http://example.com/path", "foo", "bar")
assert got == expected
-def test_get_credentials_not_to_uses_cached_credentials_only_username():
+def test_get_credentials_not_to_uses_cached_credentials_only_username() -> None:
auth = MultiDomainBasicAuth()
- auth.passwords['example.com'] = ('user', 'pass')
+ auth.passwords["example.com"] = ("user", "pass")
got = auth._get_url_and_credentials("http://foo@example.com/path")
- expected = ('http://example.com/path', 'foo', '')
+ expected = ("http://example.com/path", "foo", "")
assert got == expected
-def test_get_credentials_uses_cached_credentials():
+def test_get_credentials_uses_cached_credentials() -> None:
auth = MultiDomainBasicAuth()
- auth.passwords['example.com'] = ('user', 'pass')
+ auth.passwords["example.com"] = ("user", "pass")
got = auth._get_url_and_credentials("http://example.com/path")
- expected = ('http://example.com/path', 'user', 'pass')
+ expected = ("http://example.com/path", "user", "pass")
assert got == expected
-def test_get_index_url_credentials():
- auth = MultiDomainBasicAuth(index_urls=[
- "http://foo:bar@example.com/path"
- ])
+def test_get_credentials_uses_cached_credentials_only_username() -> None:
+ auth = MultiDomainBasicAuth()
+ auth.passwords["example.com"] = ("user", "pass")
+
+ got = auth._get_url_and_credentials("http://user@example.com/path")
+ expected = ("http://example.com/path", "user", "pass")
+ assert got == expected
+
+
+def test_get_index_url_credentials() -> None:
+ auth = MultiDomainBasicAuth(index_urls=["http://foo:bar@example.com/path"])
get = functools.partial(
- auth._get_new_credentials,
- allow_netrc=False,
- allow_keyring=False
+ auth._get_new_credentials, allow_netrc=False, allow_keyring=False
)
# Check resolution of indexes
- assert get("http://example.com/path/path2") == ('foo', 'bar')
+ assert get("http://example.com/path/path2") == ("foo", "bar")
assert get("http://example.com/path3/path2") == (None, None)
@@ -94,125 +106,150 @@ class KeyringModuleV1:
was added.
"""
- def __init__(self):
- self.saved_passwords = []
+ def __init__(self) -> None:
+ self.saved_passwords: List[Tuple[str, str, str]] = []
- def get_password(self, system, username):
+ def get_password(self, system: str, username: str) -> Optional[str]:
if system == "example.com" and username:
return username + "!netloc"
if system == "http://example.com/path2" and username:
return username + "!url"
return None
- def set_password(self, system, username, password):
+ def set_password(self, system: str, username: str, password: str) -> None:
self.saved_passwords.append((system, username, password))
-@pytest.mark.parametrize('url, expect', (
- ("http://example.com/path1", (None, None)),
- # path1 URLs will be resolved by netloc
- ("http://user@example.com/path1", ("user", "user!netloc")),
- ("http://user2@example.com/path1", ("user2", "user2!netloc")),
- # path2 URLs will be resolved by index URL
- ("http://example.com/path2/path3", (None, None)),
- ("http://foo@example.com/path2/path3", ("foo", "foo!url")),
-))
-def test_keyring_get_password(monkeypatch, url, expect):
+@pytest.mark.parametrize(
+ "url, expect",
+ (
+ ("http://example.com/path1", (None, None)),
+ # path1 URLs will be resolved by netloc
+ ("http://user@example.com/path1", ("user", "user!netloc")),
+ ("http://user2@example.com/path1", ("user2", "user2!netloc")),
+ # path2 URLs will be resolved by index URL
+ ("http://example.com/path2/path3", (None, None)),
+ ("http://foo@example.com/path2/path3", ("foo", "foo!url")),
+ ),
+)
+def test_keyring_get_password(
+ monkeypatch: pytest.MonkeyPatch,
+ url: str,
+ expect: Tuple[Optional[str], Optional[str]],
+) -> None:
keyring = KeyringModuleV1()
- monkeypatch.setattr('pip._internal.network.auth.keyring', keyring)
+ monkeypatch.setattr("pip._internal.network.auth.keyring", keyring)
auth = MultiDomainBasicAuth(index_urls=["http://example.com/path2"])
- actual = auth._get_new_credentials(url, allow_netrc=False,
- allow_keyring=True)
+ actual = auth._get_new_credentials(url, allow_netrc=False, allow_keyring=True)
assert actual == expect
-def test_keyring_get_password_after_prompt(monkeypatch):
+def test_keyring_get_password_after_prompt(monkeypatch: pytest.MonkeyPatch) -> None:
keyring = KeyringModuleV1()
- monkeypatch.setattr('pip._internal.network.auth.keyring', keyring)
+ monkeypatch.setattr("pip._internal.network.auth.keyring", keyring)
auth = MultiDomainBasicAuth()
- def ask_input(prompt):
+ def ask_input(prompt: str) -> str:
assert prompt == "User for example.com: "
return "user"
- monkeypatch.setattr('pip._internal.network.auth.ask_input', ask_input)
+ monkeypatch.setattr("pip._internal.network.auth.ask_input", ask_input)
actual = auth._prompt_for_password("example.com")
assert actual == ("user", "user!netloc", False)
-def test_keyring_get_password_after_prompt_when_none(monkeypatch):
+def test_keyring_get_password_after_prompt_when_none(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
keyring = KeyringModuleV1()
- monkeypatch.setattr('pip._internal.network.auth.keyring', keyring)
+ monkeypatch.setattr("pip._internal.network.auth.keyring", keyring)
auth = MultiDomainBasicAuth()
- def ask_input(prompt):
+ def ask_input(prompt: str) -> str:
assert prompt == "User for unknown.com: "
return "user"
- def ask_password(prompt):
+ def ask_password(prompt: str) -> str:
assert prompt == "Password: "
return "fake_password"
- monkeypatch.setattr('pip._internal.network.auth.ask_input', ask_input)
- monkeypatch.setattr(
- 'pip._internal.network.auth.ask_password', ask_password)
+ monkeypatch.setattr("pip._internal.network.auth.ask_input", ask_input)
+ monkeypatch.setattr("pip._internal.network.auth.ask_password", ask_password)
actual = auth._prompt_for_password("unknown.com")
assert actual == ("user", "fake_password", True)
-def test_keyring_get_password_username_in_index(monkeypatch):
+def test_keyring_get_password_username_in_index(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
keyring = KeyringModuleV1()
- monkeypatch.setattr('pip._internal.network.auth.keyring', keyring)
+ monkeypatch.setattr("pip._internal.network.auth.keyring", keyring)
auth = MultiDomainBasicAuth(index_urls=["http://user@example.com/path2"])
get = functools.partial(
- auth._get_new_credentials,
- allow_netrc=False,
- allow_keyring=True
+ auth._get_new_credentials, allow_netrc=False, allow_keyring=True
)
assert get("http://example.com/path2/path3") == ("user", "user!url")
assert get("http://example.com/path4/path1") == (None, None)
-@pytest.mark.parametrize("response_status, creds, expect_save", (
- (403, ("user", "pass", True), False),
- (200, ("user", "pass", True), True,),
- (200, ("user", "pass", False), False,),
-))
-def test_keyring_set_password(monkeypatch, response_status, creds,
- expect_save):
+@pytest.mark.parametrize(
+ "response_status, creds, expect_save",
+ (
+ (403, ("user", "pass", True), False),
+ (
+ 200,
+ ("user", "pass", True),
+ True,
+ ),
+ (
+ 200,
+ ("user", "pass", False),
+ False,
+ ),
+ ),
+)
+def test_keyring_set_password(
+ monkeypatch: pytest.MonkeyPatch,
+ response_status: int,
+ creds: Tuple[str, str, bool],
+ expect_save: bool,
+) -> None:
keyring = KeyringModuleV1()
- monkeypatch.setattr('pip._internal.network.auth.keyring', keyring)
+ monkeypatch.setattr("pip._internal.network.auth.keyring", keyring)
auth = MultiDomainBasicAuth(prompting=True)
- monkeypatch.setattr(auth, '_get_url_and_credentials',
- lambda u: (u, None, None))
- monkeypatch.setattr(auth, '_prompt_for_password', lambda *a: creds)
+ monkeypatch.setattr(auth, "_get_url_and_credentials", lambda u: (u, None, None))
+ monkeypatch.setattr(auth, "_prompt_for_password", lambda *a: creds)
if creds[2]:
# when _prompt_for_password indicates to save, we should save
- def should_save_password_to_keyring(*a):
+ def should_save_password_to_keyring(*a: Any) -> bool:
return True
+
else:
# when _prompt_for_password indicates not to save, we should
# never call this function
- def should_save_password_to_keyring(*a):
+ def should_save_password_to_keyring(*a: Any) -> bool:
assert False, "_should_save_password_to_keyring should not be called"
- monkeypatch.setattr(auth, '_should_save_password_to_keyring',
- should_save_password_to_keyring)
+
+ monkeypatch.setattr(
+ auth, "_should_save_password_to_keyring", should_save_password_to_keyring
+ )
req = MockRequest("https://example.com")
resp = MockResponse(b"")
resp.url = req.url
connection = MockConnection()
- def _send(sent_req, **kwargs):
+ def _send(sent_req: MockRequest, **kwargs: Any) -> MockResponse:
assert sent_req is req
assert "Authorization" in sent_req.headers
r = MockResponse(b"")
r.status_code = response_status
return r
- connection._send = _send
+ # https://github.com/python/mypy/issues/2427
+ connection._send = _send # type: ignore[assignment]
resp.request = req
resp.status_code = 401
@@ -230,14 +267,14 @@ class KeyringModuleV2:
"""Represents the current supported API of keyring"""
class Credential:
- def __init__(self, username, password):
+ def __init__(self, username: str, password: str) -> None:
self.username = username
self.password = password
- def get_password(self, system, username):
+ def get_password(self, system: str, username: str) -> None:
assert False, "get_password should not ever be called"
- def get_credential(self, system, username):
+ def get_credential(self, system: str, username: str) -> Optional[Credential]:
if system == "http://example.com/path2":
return self.Credential("username", "url")
if system == "example.com":
@@ -245,36 +282,39 @@ class KeyringModuleV2:
return None
-@pytest.mark.parametrize('url, expect', (
- ("http://example.com/path1", ("username", "netloc")),
- ("http://example.com/path2/path3", ("username", "url")),
- ("http://user2@example.com/path2/path3", ("username", "url")),
-))
-def test_keyring_get_credential(monkeypatch, url, expect):
- monkeypatch.setattr(
- pip._internal.network.auth, 'keyring', KeyringModuleV2()
- )
+@pytest.mark.parametrize(
+ "url, expect",
+ (
+ ("http://example.com/path1", ("username", "netloc")),
+ ("http://example.com/path2/path3", ("username", "url")),
+ ("http://user2@example.com/path2/path3", ("username", "url")),
+ ),
+)
+def test_keyring_get_credential(
+ monkeypatch: pytest.MonkeyPatch, url: str, expect: str
+) -> None:
+ monkeypatch.setattr(pip._internal.network.auth, "keyring", KeyringModuleV2())
auth = MultiDomainBasicAuth(index_urls=["http://example.com/path2"])
- assert auth._get_new_credentials(
- url, allow_netrc=False, allow_keyring=True
- ) == expect
+ assert (
+ auth._get_new_credentials(url, allow_netrc=False, allow_keyring=True) == expect
+ )
class KeyringModuleBroken:
"""Represents the current supported API of keyring, but broken"""
- def __init__(self):
+ def __init__(self) -> None:
self._call_count = 0
- def get_credential(self, system, username):
+ def get_credential(self, system: str, username: str) -> None:
self._call_count += 1
raise Exception("This keyring is broken!")
-def test_broken_keyring_disables_keyring(monkeypatch):
+def test_broken_keyring_disables_keyring(monkeypatch: pytest.MonkeyPatch) -> None:
keyring_broken = KeyringModuleBroken()
- monkeypatch.setattr(pip._internal.network.auth, 'keyring', keyring_broken)
+ monkeypatch.setattr(pip._internal.network.auth, "keyring", keyring_broken)
auth = MultiDomainBasicAuth(index_urls=["http://example.com/"])
diff --git a/tests/unit/test_network_cache.py b/tests/unit/test_network_cache.py
index dc5e06557..8764b1343 100644
--- a/tests/unit/test_network_cache.py
+++ b/tests/unit/test_network_cache.py
@@ -1,4 +1,6 @@
import os
+from pathlib import Path
+from typing import Iterator
from unittest.mock import Mock
import pytest
@@ -8,7 +10,7 @@ from pip._internal.network.cache import SafeFileCache
@pytest.fixture(scope="function")
-def cache_tmpdir(tmpdir):
+def cache_tmpdir(tmpdir: Path) -> Iterator[Path]:
cache_dir = tmpdir.joinpath("cache")
cache_dir.mkdir(parents=True)
yield cache_dir
@@ -21,9 +23,9 @@ class TestSafeFileCache:
os.geteuid which is absent on Windows.
"""
- def test_cache_roundtrip(self, cache_tmpdir):
+ def test_cache_roundtrip(self, cache_tmpdir: Path) -> None:
- cache = SafeFileCache(cache_tmpdir)
+ cache = SafeFileCache(os.fspath(cache_tmpdir))
assert cache.get("test key") is None
cache.set("test key", b"a test string")
assert cache.get("test key") == b"a test string"
@@ -31,32 +33,32 @@ class TestSafeFileCache:
assert cache.get("test key") is None
@pytest.mark.skipif("sys.platform == 'win32'")
- def test_safe_get_no_perms(self, cache_tmpdir, monkeypatch):
+ def test_safe_get_no_perms(
+ self, cache_tmpdir: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
os.chmod(cache_tmpdir, 000)
monkeypatch.setattr(os.path, "exists", lambda x: True)
- cache = SafeFileCache(cache_tmpdir)
+ cache = SafeFileCache(os.fspath(cache_tmpdir))
cache.get("foo")
@pytest.mark.skipif("sys.platform == 'win32'")
- def test_safe_set_no_perms(self, cache_tmpdir):
+ def test_safe_set_no_perms(self, cache_tmpdir: Path) -> None:
os.chmod(cache_tmpdir, 000)
- cache = SafeFileCache(cache_tmpdir)
+ cache = SafeFileCache(os.fspath(cache_tmpdir))
cache.set("foo", b"bar")
@pytest.mark.skipif("sys.platform == 'win32'")
- def test_safe_delete_no_perms(self, cache_tmpdir):
+ def test_safe_delete_no_perms(self, cache_tmpdir: Path) -> None:
os.chmod(cache_tmpdir, 000)
- cache = SafeFileCache(cache_tmpdir)
+ cache = SafeFileCache(os.fspath(cache_tmpdir))
cache.delete("foo")
- def test_cache_hashes_are_same(self, cache_tmpdir):
- cache = SafeFileCache(cache_tmpdir)
+ def test_cache_hashes_are_same(self, cache_tmpdir: Path) -> None:
+ cache = SafeFileCache(os.fspath(cache_tmpdir))
key = "test key"
- fake_cache = Mock(
- FileCache, directory=cache.directory, encode=FileCache.encode
- )
+ fake_cache = Mock(FileCache, directory=cache.directory, encode=FileCache.encode)
assert cache._get_cache_path(key) == FileCache._fn(fake_cache, key)
diff --git a/tests/unit/test_network_download.py b/tests/unit/test_network_download.py
index 20f5513a2..53200f2e5 100644
--- a/tests/unit/test_network_download.py
+++ b/tests/unit/test_network_download.py
@@ -1,5 +1,6 @@
import logging
import sys
+from typing import Dict
import pytest
@@ -12,23 +13,51 @@ from pip._internal.network.download import (
from tests.lib.requests_mocks import MockResponse
-@pytest.mark.parametrize("url, headers, from_cache, expected", [
- ('http://example.com/foo.tgz', {}, False,
- "Downloading http://example.com/foo.tgz"),
- ('http://example.com/foo.tgz', {'content-length': 2}, False,
- "Downloading http://example.com/foo.tgz (2 bytes)"),
- ('http://example.com/foo.tgz', {'content-length': 2}, True,
- "Using cached http://example.com/foo.tgz (2 bytes)"),
- ('https://files.pythonhosted.org/foo.tgz', {}, False,
- "Downloading foo.tgz"),
- ('https://files.pythonhosted.org/foo.tgz', {'content-length': 2}, False,
- "Downloading foo.tgz (2 bytes)"),
- ('https://files.pythonhosted.org/foo.tgz', {'content-length': 2}, True,
- "Using cached foo.tgz"),
-])
-def test_prepare_download__log(caplog, url, headers, from_cache, expected):
+@pytest.mark.parametrize(
+ "url, headers, from_cache, expected",
+ [
+ (
+ "http://example.com/foo.tgz",
+ {},
+ False,
+ "Downloading http://example.com/foo.tgz",
+ ),
+ (
+ "http://example.com/foo.tgz",
+ {"content-length": "2"},
+ False,
+ "Downloading http://example.com/foo.tgz (2 bytes)",
+ ),
+ (
+ "http://example.com/foo.tgz",
+ {"content-length": "2"},
+ True,
+ "Using cached http://example.com/foo.tgz (2 bytes)",
+ ),
+ ("https://files.pythonhosted.org/foo.tgz", {}, False, "Downloading foo.tgz"),
+ (
+ "https://files.pythonhosted.org/foo.tgz",
+ {"content-length": "2"},
+ False,
+ "Downloading foo.tgz (2 bytes)",
+ ),
+ (
+ "https://files.pythonhosted.org/foo.tgz",
+ {"content-length": "2"},
+ True,
+ "Using cached foo.tgz",
+ ),
+ ],
+)
+def test_prepare_download__log(
+ caplog: pytest.LogCaptureFixture,
+ url: str,
+ headers: Dict[str, str],
+ from_cache: bool,
+ expected: str,
+) -> None:
caplog.set_level(logging.INFO)
- resp = MockResponse(b'')
+ resp = MockResponse(b"")
resp.url = url
resp.headers = headers
if from_cache:
@@ -38,55 +67,60 @@ def test_prepare_download__log(caplog, url, headers, from_cache, expected):
assert len(caplog.records) == 1
record = caplog.records[0]
- assert record.levelname == 'INFO'
+ assert record.levelname == "INFO"
assert expected in record.message
-@pytest.mark.parametrize("filename, expected", [
- ('dir/file', 'file'),
- ('../file', 'file'),
- ('../../file', 'file'),
- ('../', ''),
- ('../..', '..'),
- ('/', ''),
-])
-def test_sanitize_content_filename(filename, expected):
+@pytest.mark.parametrize(
+ "filename, expected",
+ [
+ ("dir/file", "file"),
+ ("../file", "file"),
+ ("../../file", "file"),
+ ("../", ""),
+ ("../..", ".."),
+ ("/", ""),
+ ],
+)
+def test_sanitize_content_filename(filename: str, expected: str) -> None:
"""
Test inputs where the result is the same for Windows and non-Windows.
"""
assert sanitize_content_filename(filename) == expected
-@pytest.mark.parametrize("filename, win_expected, non_win_expected", [
- ('dir\\file', 'file', 'dir\\file'),
- ('..\\file', 'file', '..\\file'),
- ('..\\..\\file', 'file', '..\\..\\file'),
- ('..\\', '', '..\\'),
- ('..\\..', '..', '..\\..'),
- ('\\', '', '\\'),
-])
+@pytest.mark.parametrize(
+ "filename, win_expected, non_win_expected",
+ [
+ ("dir\\file", "file", "dir\\file"),
+ ("..\\file", "file", "..\\file"),
+ ("..\\..\\file", "file", "..\\..\\file"),
+ ("..\\", "", "..\\"),
+ ("..\\..", "..", "..\\.."),
+ ("\\", "", "\\"),
+ ],
+)
def test_sanitize_content_filename__platform_dependent(
- filename,
- win_expected,
- non_win_expected
-):
+ filename: str, win_expected: str, non_win_expected: str
+) -> None:
"""
Test inputs where the result is different for Windows and non-Windows.
"""
- if sys.platform == 'win32':
+ if sys.platform == "win32":
expected = win_expected
else:
expected = non_win_expected
assert sanitize_content_filename(filename) == expected
-@pytest.mark.parametrize("content_disposition, default_filename, expected", [
- ('attachment;filename="../file"', 'df', 'file'),
-])
+@pytest.mark.parametrize(
+ "content_disposition, default_filename, expected",
+ [
+ ('attachment;filename="../file"', "df", "file"),
+ ],
+)
def test_parse_content_disposition(
- content_disposition,
- default_filename,
- expected
-):
+ content_disposition: str, default_filename: str, expected: str
+) -> None:
actual = parse_content_disposition(content_disposition, default_filename)
assert actual == expected
diff --git a/tests/unit/test_network_lazy_wheel.py b/tests/unit/test_network_lazy_wheel.py
index e6747a18e..79e863217 100644
--- a/tests/unit/test_network_lazy_wheel.py
+++ b/tests/unit/test_network_lazy_wheel.py
@@ -1,61 +1,66 @@
-from zipfile import BadZipfile
+from typing import Iterator
-from pip._vendor.pkg_resources import Requirement
+from pip._vendor.packaging.version import Version
from pytest import fixture, mark, raises
+from pip._internal.exceptions import InvalidWheel
from pip._internal.network.lazy_wheel import (
HTTPRangeRequestUnsupported,
dist_from_wheel_url,
)
from pip._internal.network.session import PipSession
-from tests.lib.server import file_response
+from tests.lib import TestData
+from tests.lib.server import MockServer, file_response
MYPY_0_782_WHL = (
- 'https://files.pythonhosted.org/packages/9d/65/'
- 'b96e844150ce18b9892b155b780248955ded13a2581d31872e7daa90a503/'
- 'mypy-0.782-py3-none-any.whl'
+ "https://files.pythonhosted.org/packages/9d/65/"
+ "b96e844150ce18b9892b155b780248955ded13a2581d31872e7daa90a503/"
+ "mypy-0.782-py3-none-any.whl"
)
MYPY_0_782_REQS = {
- Requirement('typed-ast (<1.5.0,>=1.4.0)'),
- Requirement('typing-extensions (>=3.7.4)'),
- Requirement('mypy-extensions (<0.5.0,>=0.4.3)'),
- Requirement('psutil (>=4.0); extra == "dmypy"'),
+ "typed-ast<1.5.0,>=1.4.0",
+ "typing-extensions>=3.7.4",
+ "mypy-extensions<0.5.0,>=0.4.3",
+ 'psutil>=4.0; extra == "dmypy"',
}
@fixture
-def session():
+def session() -> PipSession:
return PipSession()
@fixture
-def mypy_whl_no_range(mock_server, shared_data):
- mypy_whl = shared_data.packages / 'mypy-0.782-py3-none-any.whl'
+def mypy_whl_no_range(mock_server: MockServer, shared_data: TestData) -> Iterator[str]:
+ mypy_whl = shared_data.packages / "mypy-0.782-py3-none-any.whl"
mock_server.set_responses([file_response(mypy_whl)])
mock_server.start()
- base_address = f'http://{mock_server.host}:{mock_server.port}'
- yield "{}/{}".format(base_address, 'mypy-0.782-py3-none-any.whl')
+ base_address = f"http://{mock_server.host}:{mock_server.port}"
+ yield "{}/{}".format(base_address, "mypy-0.782-py3-none-any.whl")
mock_server.stop()
@mark.network
-def test_dist_from_wheel_url(session):
+def test_dist_from_wheel_url(session: PipSession) -> None:
"""Test if the acquired distribution contain correct information."""
- dist = dist_from_wheel_url('mypy', MYPY_0_782_WHL, session)
- assert dist.key == 'mypy'
- assert dist.version == '0.782'
- assert dist.extras == ['dmypy']
- assert set(dist.requires(dist.extras)) == MYPY_0_782_REQS
+ dist = dist_from_wheel_url("mypy", MYPY_0_782_WHL, session)
+ assert dist.canonical_name == "mypy"
+ assert dist.version == Version("0.782")
+ extras = list(dist.iter_provided_extras())
+ assert extras == ["dmypy"]
+ assert {str(d) for d in dist.iter_dependencies(extras)} == MYPY_0_782_REQS
-def test_dist_from_wheel_url_no_range(session, mypy_whl_no_range):
+def test_dist_from_wheel_url_no_range(
+ session: PipSession, mypy_whl_no_range: str
+) -> None:
"""Test handling when HTTP range requests are not supported."""
with raises(HTTPRangeRequestUnsupported):
- dist_from_wheel_url('mypy', mypy_whl_no_range, session)
+ dist_from_wheel_url("mypy", mypy_whl_no_range, session)
@mark.network
-def test_dist_from_wheel_url_not_zip(session):
+def test_dist_from_wheel_url_not_zip(session: PipSession) -> None:
"""Test handling with the given URL does not point to a ZIP."""
- with raises(BadZipfile):
- dist_from_wheel_url('python', 'https://www.python.org/', session)
+ with raises(InvalidWheel):
+ dist_from_wheel_url("python", "https://www.python.org/", session)
diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py
index 044d1fb92..2bb9317b7 100644
--- a/tests/unit/test_network_session.py
+++ b/tests/unit/test_network_session.py
@@ -1,30 +1,42 @@
import logging
+import os
+from pathlib import Path
+from typing import Any, List, Optional
+from urllib.parse import urlparse
+from urllib.request import getproxies
import pytest
+from pip._vendor import requests
from pip import __version__
+from pip._internal.models.link import Link
from pip._internal.network.session import CI_ENVIRONMENT_VARIABLES, PipSession
-def get_user_agent():
+def get_user_agent() -> str:
return PipSession().headers["User-Agent"]
-def test_user_agent():
+def test_user_agent() -> None:
user_agent = get_user_agent()
assert user_agent.startswith(f"pip/{__version__}")
-@pytest.mark.parametrize('name, expected_like_ci', [
- ('BUILD_BUILDID', True),
- ('BUILD_ID', True),
- ('CI', True),
- ('PIP_IS_CI', True),
- # Test a prefix substring of one of the variable names we use.
- ('BUILD', False),
-])
-def test_user_agent__ci(monkeypatch, name, expected_like_ci):
+@pytest.mark.parametrize(
+ "name, expected_like_ci",
+ [
+ ("BUILD_BUILDID", True),
+ ("BUILD_ID", True),
+ ("CI", True),
+ ("PIP_IS_CI", True),
+ # Test a prefix substring of one of the variable names we use.
+ ("BUILD", False),
+ ],
+)
+def test_user_agent__ci(
+ monkeypatch: pytest.MonkeyPatch, name: str, expected_like_ci: bool
+) -> None:
# Delete the variable names we use to check for CI to prevent the
# detection from always returning True in case the tests are being run
# under actual CI. It is okay to depend on CI_ENVIRONMENT_VARIABLES
@@ -38,43 +50,40 @@ def test_user_agent__ci(monkeypatch, name, expected_like_ci):
assert '"ci":null' in user_agent
assert '"ci":true' not in user_agent
- monkeypatch.setenv(name, 'true')
+ monkeypatch.setenv(name, "true")
user_agent = get_user_agent()
assert ('"ci":true' in user_agent) == expected_like_ci
assert ('"ci":null' in user_agent) == (not expected_like_ci)
-def test_user_agent_user_data(monkeypatch):
+def test_user_agent_user_data(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("PIP_USER_AGENT_USER_DATA", "some_string")
assert "some_string" in PipSession().headers["User-Agent"]
class TestPipSession:
-
- def test_cache_defaults_off(self):
+ def test_cache_defaults_off(self) -> None:
session = PipSession()
assert not hasattr(session.adapters["http://"], "cache")
assert not hasattr(session.adapters["https://"], "cache")
- def test_cache_is_enabled(self, tmpdir):
- cache_directory = tmpdir.joinpath("test-cache")
+ def test_cache_is_enabled(self, tmpdir: Path) -> None:
+ cache_directory = os.fspath(tmpdir.joinpath("test-cache"))
session = PipSession(cache=cache_directory)
assert hasattr(session.adapters["https://"], "cache")
- assert (
- session.adapters["https://"].cache.directory == cache_directory
- )
+ assert session.adapters["https://"].cache.directory == cache_directory
- def test_http_cache_is_not_enabled(self, tmpdir):
- session = PipSession(cache=tmpdir.joinpath("test-cache"))
+ def test_http_cache_is_not_enabled(self, tmpdir: Path) -> None:
+ session = PipSession(cache=os.fspath(tmpdir.joinpath("test-cache")))
assert not hasattr(session.adapters["http://"], "cache")
- def test_trusted_hosts_adapter(self, tmpdir):
+ def test_trusted_hosts_adapter(self, tmpdir: Path) -> None:
session = PipSession(
- cache=tmpdir.joinpath("test-cache"),
+ cache=os.fspath(tmpdir.joinpath("test-cache")),
trusted_hosts=["example.com"],
)
@@ -82,86 +91,102 @@ class TestPipSession:
# Check that the "port wildcard" is present.
assert "https://example.com:" in session.adapters
# Check that the cache is enabled.
+ assert hasattr(session.adapters["http://example.com/"], "cache")
assert hasattr(session.adapters["https://example.com/"], "cache")
- def test_add_trusted_host(self):
+ def test_add_trusted_host(self) -> None:
# Leave a gap to test how the ordering is affected.
- trusted_hosts = ['host1', 'host3']
+ trusted_hosts = ["host1", "host3"]
session = PipSession(trusted_hosts=trusted_hosts)
trusted_host_adapter = session._trusted_host_adapter
- prefix2 = 'https://host2/'
- prefix3 = 'https://host3/'
- prefix3_wildcard = 'https://host3:'
+ prefix2 = "https://host2/"
+ prefix3 = "https://host3/"
+ prefix3_wildcard = "https://host3:"
+
+ prefix2_http = "http://host2/"
+ prefix3_http = "http://host3/"
+ prefix3_wildcard_http = "http://host3:"
# Confirm some initial conditions as a baseline.
- assert session.pip_trusted_origins == [
- ('host1', None), ('host3', None)
- ]
+ assert session.pip_trusted_origins == [("host1", None), ("host3", None)]
assert session.adapters[prefix3] is trusted_host_adapter
assert session.adapters[prefix3_wildcard] is trusted_host_adapter
+ assert session.adapters[prefix3_http] is trusted_host_adapter
+ assert session.adapters[prefix3_wildcard_http] is trusted_host_adapter
+
assert prefix2 not in session.adapters
+ assert prefix2_http not in session.adapters
# Test adding a new host.
- session.add_trusted_host('host2')
+ session.add_trusted_host("host2")
assert session.pip_trusted_origins == [
- ('host1', None), ('host3', None), ('host2', None)
+ ("host1", None),
+ ("host3", None),
+ ("host2", None),
]
# Check that prefix3 is still present.
assert session.adapters[prefix3] is trusted_host_adapter
assert session.adapters[prefix2] is trusted_host_adapter
+ assert session.adapters[prefix2_http] is trusted_host_adapter
# Test that adding the same host doesn't create a duplicate.
- session.add_trusted_host('host3')
+ session.add_trusted_host("host3")
assert session.pip_trusted_origins == [
- ('host1', None), ('host3', None), ('host2', None)
- ], f'actual: {session.pip_trusted_origins}'
-
- session.add_trusted_host('host4:8080')
- prefix4 = 'https://host4:8080/'
+ ("host1", None),
+ ("host3", None),
+ ("host2", None),
+ ], f"actual: {session.pip_trusted_origins}"
+
+ session.add_trusted_host("host4:8080")
+ prefix4 = "https://host4:8080/"
+ prefix4_http = "http://host4:8080/"
assert session.pip_trusted_origins == [
- ('host1', None), ('host3', None),
- ('host2', None), ('host4', 8080)
+ ("host1", None),
+ ("host3", None),
+ ("host2", None),
+ ("host4", 8080),
]
assert session.adapters[prefix4] is trusted_host_adapter
+ assert session.adapters[prefix4_http] is trusted_host_adapter
- def test_add_trusted_host__logging(self, caplog):
+ def test_add_trusted_host__logging(self, caplog: pytest.LogCaptureFixture) -> None:
"""
Test logging when add_trusted_host() is called.
"""
- trusted_hosts = ['host0', 'host1']
+ trusted_hosts = ["host0", "host1"]
session = PipSession(trusted_hosts=trusted_hosts)
with caplog.at_level(logging.INFO):
# Test adding an existing host.
- session.add_trusted_host('host1', source='somewhere')
- session.add_trusted_host('host2')
+ session.add_trusted_host("host1", source="somewhere")
+ session.add_trusted_host("host2")
# Test calling add_trusted_host() on the same host twice.
- session.add_trusted_host('host2')
+ session.add_trusted_host("host2")
actual = [(r.levelname, r.message) for r in caplog.records]
# Observe that "host0" isn't included in the logs.
expected = [
- ('INFO', "adding trusted host: 'host1' (from somewhere)"),
- ('INFO', "adding trusted host: 'host2'"),
- ('INFO', "adding trusted host: 'host2'"),
+ ("INFO", "adding trusted host: 'host1' (from somewhere)"),
+ ("INFO", "adding trusted host: 'host2'"),
+ ("INFO", "adding trusted host: 'host2'"),
]
assert actual == expected
- def test_iter_secure_origins(self):
- trusted_hosts = ['host1', 'host2', 'host3:8080']
+ def test_iter_secure_origins(self) -> None:
+ trusted_hosts = ["host1", "host2", "host3:8080"]
session = PipSession(trusted_hosts=trusted_hosts)
actual = list(session.iter_secure_origins())
assert len(actual) == 9
# Spot-check that SECURE_ORIGINS is included.
- assert actual[0] == ('https', '*', '*')
+ assert actual[0] == ("https", "*", "*")
assert actual[-3:] == [
- ('*', 'host1', '*'),
- ('*', 'host2', '*'),
- ('*', 'host3', 8080)
+ ("*", "host1", "*"),
+ ("*", "host2", "*"),
+ ("*", "host3", 8080),
]
- def test_iter_secure_origins__trusted_hosts_empty(self):
+ def test_iter_secure_origins__trusted_hosts_empty(self) -> None:
"""
Test iter_secure_origins() after passing trusted_hosts=[].
"""
@@ -170,10 +195,10 @@ class TestPipSession:
actual = list(session.iter_secure_origins())
assert len(actual) == 6
# Spot-check that SECURE_ORIGINS is included.
- assert actual[0] == ('https', '*', '*')
+ assert actual[0] == ("https", "*", "*")
@pytest.mark.parametrize(
- 'location, trusted, expected',
+ "location, trusted, expected",
[
("http://pypi.org/something", [], False),
("https://pypi.org/something", [], True),
@@ -191,23 +216,25 @@ class TestPipSession:
# Test a trusted_host with a port.
("http://example.com:8080/something/", ["example.com:8080"], True),
("http://example.com/something/", ["example.com:8080"], False),
- (
- "http://example.com:8888/something/",
- ["example.com:8080"],
- False
- ),
+ ("http://example.com:8888/something/", ["example.com:8080"], False),
],
)
- def test_is_secure_origin(self, caplog, location, trusted, expected):
+ def test_is_secure_origin(
+ self,
+ caplog: pytest.LogCaptureFixture,
+ location: str,
+ trusted: List[str],
+ expected: bool,
+ ) -> None:
class MockLogger:
- def __init__(self):
+ def __init__(self) -> None:
self.called = False
- def warning(self, *args, **kwargs):
+ def warning(self, *args: Any, **kwargs: Any) -> None:
self.called = True
session = PipSession(trusted_hosts=trusted)
- actual = session.is_secure_origin(location)
+ actual = session.is_secure_origin(Link(location))
assert actual == expected
log_records = [(r.levelname, r.message) for r in caplog.records]
@@ -217,5 +244,33 @@ class TestPipSession:
assert len(log_records) == 1
actual_level, actual_message = log_records[0]
- assert actual_level == 'WARNING'
- assert 'is not a trusted or secure host' in actual_message
+ assert actual_level == "WARNING"
+ assert "is not a trusted or secure host" in actual_message
+
+ @pytest.mark.network
+ def test_proxy(self, proxy: Optional[str]) -> None:
+ session = PipSession(trusted_hosts=[])
+
+ if not proxy:
+ # if user didn't pass --proxy then try to get it from the system.
+ env_proxy = getproxies().get("http", None)
+ proxy = urlparse(env_proxy).netloc if env_proxy else None
+
+ if proxy:
+ # set proxy scheme to session.proxies
+ session.proxies = {
+ "http": f"{proxy}",
+ "https": f"{proxy}",
+ "ftp": f"{proxy}",
+ }
+
+ connection_error = None
+ try:
+ session.request("GET", "https://pypi.org", timeout=1)
+ except requests.exceptions.ConnectionError as e:
+ connection_error = e
+
+ assert connection_error is None, (
+ f"Invalid proxy {proxy} or session.proxies: "
+ f"{session.proxies} is not correctly passed to session.request."
+ )
diff --git a/tests/unit/test_network_utils.py b/tests/unit/test_network_utils.py
index 09f0684c5..cdc10b2ba 100644
--- a/tests/unit/test_network_utils.py
+++ b/tests/unit/test_network_utils.py
@@ -5,30 +5,31 @@ from pip._internal.network.utils import raise_for_status
from tests.lib.requests_mocks import MockResponse
-@pytest.mark.parametrize(("status_code", "error_type"), [
- (401, "Client Error"),
- (501, "Server Error"),
-])
-def test_raise_for_status_raises_exception(status_code, error_type):
- contents = b'downloaded'
+@pytest.mark.parametrize(
+ ("status_code", "error_type"),
+ [
+ (401, "Client Error"),
+ (501, "Server Error"),
+ ],
+)
+def test_raise_for_status_raises_exception(status_code: int, error_type: str) -> None:
+ contents = b"downloaded"
resp = MockResponse(contents)
resp.status_code = status_code
resp.url = "http://www.example.com/whatever.tgz"
resp.reason = "Network Error"
- with pytest.raises(NetworkConnectionError) as exc:
+ with pytest.raises(NetworkConnectionError) as excinfo:
raise_for_status(resp)
- assert str(exc.info) == (
- "{} {}: Network Error for url:"
- " http://www.example.com/whatever.tgz".format(
- status_code, error_type)
- )
+ assert str(excinfo.value) == (
+ "{} {}: Network Error for url:"
+ " http://www.example.com/whatever.tgz".format(status_code, error_type)
+ )
-def test_raise_for_status_does_not_raises_exception():
- contents = b'downloaded'
+def test_raise_for_status_does_not_raises_exception() -> None:
+ contents = b"downloaded"
resp = MockResponse(contents)
resp.status_code = 201
resp.url = "http://www.example.com/whatever.tgz"
resp.reason = "No error"
- return_value = raise_for_status(resp)
- assert return_value is None
+ raise_for_status(resp)
diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py
index 4d912fb6e..d06733e85 100644
--- a/tests/unit/test_operations_prepare.py
+++ b/tests/unit/test_operations_prepare.py
@@ -1,7 +1,9 @@
import os
import shutil
+from pathlib import Path
from shutil import rmtree
from tempfile import mkdtemp
+from typing import Any, Dict
from unittest.mock import Mock, patch
import pytest
@@ -10,21 +12,19 @@ from pip._internal.exceptions import HashMismatch
from pip._internal.models.link import Link
from pip._internal.network.download import Downloader
from pip._internal.network.session import PipSession
-from pip._internal.operations.prepare import _copy_source_tree, unpack_url
+from pip._internal.operations.prepare import unpack_url
from pip._internal.utils.hashes import Hashes
-from pip._internal.utils.urls import path_to_url
-from tests.lib.filesystem import get_filelist, make_socket_file, make_unreadable_file
-from tests.lib.path import Path
+from tests.lib import TestData
from tests.lib.requests_mocks import MockResponse
-def test_unpack_url_with_urllib_response_without_content_type(data):
+def test_unpack_url_with_urllib_response_without_content_type(data: TestData) -> None:
"""
It should download and unpack files even if no Content-Type header exists
"""
_real_session = PipSession()
- def _fake_session_get(*args, **kwargs):
+ def _fake_session_get(*args: Any, **kwargs: Any) -> Dict[str, str]:
resp = _real_session.get(*args, **kwargs)
del resp.headers["Content-Type"]
return resp
@@ -33,7 +33,7 @@ def test_unpack_url_with_urllib_response_without_content_type(data):
session.get = _fake_session_get
download = Downloader(session, progress_bar="on")
- uri = path_to_url(data.packages.joinpath("simple-1.0.tar.gz"))
+ uri = data.packages.joinpath("simple-1.0.tar.gz").as_uri()
link = Link(uri)
temp_dir = mkdtemp()
try:
@@ -42,23 +42,29 @@ def test_unpack_url_with_urllib_response_without_content_type(data):
temp_dir,
download=download,
download_dir=None,
+ verbosity=0,
)
assert set(os.listdir(temp_dir)) == {
- 'PKG-INFO', 'setup.cfg', 'setup.py', 'simple', 'simple.egg-info'
+ "PKG-INFO",
+ "setup.cfg",
+ "setup.py",
+ "simple",
+ "simple.egg-info",
}
finally:
rmtree(temp_dir)
@patch("pip._internal.network.download.raise_for_status")
-def test_download_http_url__no_directory_traversal(mock_raise_for_status,
- tmpdir):
+def test_download_http_url__no_directory_traversal(
+ mock_raise_for_status: Mock, tmpdir: Path
+) -> None:
"""
Test that directory traversal doesn't happen on download when the
Content-Disposition header contains a filename with a ".." path part.
"""
- mock_url = 'http://www.example.com/whatever.tgz'
- contents = b'downloaded'
+ mock_url = "http://www.example.com/whatever.tgz"
+ contents = b"downloaded"
link = Link(mock_url)
session = Mock()
@@ -67,177 +73,62 @@ def test_download_http_url__no_directory_traversal(mock_raise_for_status,
resp.headers = {
# Set the content-type to a random value to prevent
# mimetypes.guess_extension from guessing the extension.
- 'content-type': 'random',
- 'content-disposition': 'attachment;filename="../out_dir_file"'
+ "content-type": "random",
+ "content-disposition": 'attachment;filename="../out_dir_file"',
}
session.get.return_value = resp
download = Downloader(session, progress_bar="on")
- download_dir = tmpdir.joinpath('download')
+ download_dir = os.fspath(tmpdir.joinpath("download"))
os.mkdir(download_dir)
file_path, content_type = download(link, download_dir)
# The file should be downloaded to download_dir.
actual = os.listdir(download_dir)
- assert actual == ['out_dir_file']
+ assert actual == ["out_dir_file"]
mock_raise_for_status.assert_called_once_with(resp)
@pytest.fixture
-def clean_project(tmpdir_factory, data):
- tmpdir = Path(str(tmpdir_factory.mktemp("clean_project")))
+def clean_project(tmpdir_factory: pytest.TempPathFactory, data: TestData) -> Path:
+ tmpdir = tmpdir_factory.mktemp("clean_project")
new_project_dir = tmpdir.joinpath("FSPkg")
path = data.packages.joinpath("FSPkg")
shutil.copytree(path, new_project_dir)
return new_project_dir
-def test_copy_source_tree(clean_project, tmpdir):
- target = tmpdir.joinpath("target")
- expected_files = get_filelist(clean_project)
- assert len(expected_files) == 3
-
- _copy_source_tree(clean_project, target)
-
- copied_files = get_filelist(target)
- assert expected_files == copied_files
-
-
-@pytest.mark.skipif("sys.platform == 'win32'")
-def test_copy_source_tree_with_socket(clean_project, tmpdir, caplog):
- target = tmpdir.joinpath("target")
- expected_files = get_filelist(clean_project)
- socket_path = str(clean_project.joinpath("aaa"))
- make_socket_file(socket_path)
-
- _copy_source_tree(clean_project, target)
-
- copied_files = get_filelist(target)
- assert expected_files == copied_files
-
- # Warning should have been logged.
- assert len(caplog.records) == 1
- record = caplog.records[0]
- assert record.levelname == 'WARNING'
- assert socket_path in record.message
-
-
-@pytest.mark.skipif("sys.platform == 'win32'")
-def test_copy_source_tree_with_socket_fails_with_no_socket_error(
- clean_project, tmpdir
-):
- target = tmpdir.joinpath("target")
- expected_files = get_filelist(clean_project)
- make_socket_file(clean_project.joinpath("aaa"))
- unreadable_file = clean_project.joinpath("bbb")
- make_unreadable_file(unreadable_file)
-
- with pytest.raises(shutil.Error) as e:
- _copy_source_tree(clean_project, target)
-
- errored_files = [err[0] for err in e.value.args[0]]
- assert len(errored_files) == 1
- assert unreadable_file in errored_files
-
- copied_files = get_filelist(target)
- # All files without errors should have been copied.
- assert expected_files == copied_files
-
-
-def test_copy_source_tree_with_unreadable_dir_fails(clean_project, tmpdir):
- target = tmpdir.joinpath("target")
- expected_files = get_filelist(clean_project)
- unreadable_file = clean_project.joinpath("bbb")
- make_unreadable_file(unreadable_file)
-
- with pytest.raises(shutil.Error) as e:
- _copy_source_tree(clean_project, target)
-
- errored_files = [err[0] for err in e.value.args[0]]
- assert len(errored_files) == 1
- assert unreadable_file in errored_files
-
- copied_files = get_filelist(target)
- # All files without errors should have been copied.
- assert expected_files == copied_files
-
-
class Test_unpack_url:
-
- def prep(self, tmpdir, data):
- self.build_dir = tmpdir.joinpath('build')
- self.download_dir = tmpdir.joinpath('download')
+ def prep(self, tmpdir: Path, data: TestData) -> None:
+ self.build_dir = os.fspath(tmpdir.joinpath("build"))
+ self.download_dir = tmpdir.joinpath("download")
os.mkdir(self.build_dir)
os.mkdir(self.download_dir)
self.dist_file = "simple-1.0.tar.gz"
self.dist_file2 = "simple-2.0.tar.gz"
self.dist_path = data.packages.joinpath(self.dist_file)
self.dist_path2 = data.packages.joinpath(self.dist_file2)
- self.dist_url = Link(path_to_url(self.dist_path))
- self.dist_url2 = Link(path_to_url(self.dist_path2))
+ self.dist_url = Link(self.dist_path.as_uri())
+ self.dist_url2 = Link(self.dist_path2.as_uri())
self.no_download = Mock(side_effect=AssertionError)
- def test_unpack_url_no_download(self, tmpdir, data):
+ def test_unpack_url_no_download(self, tmpdir: Path, data: TestData) -> None:
self.prep(tmpdir, data)
- unpack_url(self.dist_url, self.build_dir, self.no_download)
- assert os.path.isdir(os.path.join(self.build_dir, 'simple'))
- assert not os.path.isfile(
- os.path.join(self.download_dir, self.dist_file))
+ unpack_url(self.dist_url, self.build_dir, self.no_download, verbosity=0)
+ assert os.path.isdir(os.path.join(self.build_dir, "simple"))
+ assert not os.path.isfile(os.path.join(self.download_dir, self.dist_file))
- def test_unpack_url_bad_hash(self, tmpdir, data,
- monkeypatch):
+ def test_unpack_url_bad_hash(self, tmpdir: Path, data: TestData) -> None:
"""
Test when the file url hash fragment is wrong
"""
self.prep(tmpdir, data)
- url = f'{self.dist_url.url}#md5=bogus'
+ url = f"{self.dist_url.url}#md5=bogus"
dist_url = Link(url)
with pytest.raises(HashMismatch):
- unpack_url(dist_url,
- self.build_dir,
- download=self.no_download,
- hashes=Hashes({'md5': ['bogus']}))
-
- def test_unpack_url_thats_a_dir(self, tmpdir, data):
- self.prep(tmpdir, data)
- dist_path = data.packages.joinpath("FSPkg")
- dist_url = Link(path_to_url(dist_path))
- unpack_url(dist_url, self.build_dir,
- download=self.no_download,
- download_dir=self.download_dir)
- assert os.path.isdir(os.path.join(self.build_dir, 'fspkg'))
-
-
-@pytest.mark.parametrize('exclude_dir', [
- '.nox',
- '.tox'
-])
-def test_unpack_url_excludes_expected_dirs(tmpdir, exclude_dir):
- src_dir = tmpdir / 'src'
- dst_dir = tmpdir / 'dst'
- src_included_file = src_dir.joinpath('file.txt')
- src_excluded_dir = src_dir.joinpath(exclude_dir)
- src_excluded_file = src_dir.joinpath(exclude_dir, 'file.txt')
- src_included_dir = src_dir.joinpath('subdir', exclude_dir)
-
- # set up source directory
- src_excluded_dir.mkdir(parents=True)
- src_included_dir.mkdir(parents=True)
- src_included_file.touch()
- src_excluded_file.touch()
-
- dst_included_file = dst_dir.joinpath('file.txt')
- dst_excluded_dir = dst_dir.joinpath(exclude_dir)
- dst_excluded_file = dst_dir.joinpath(exclude_dir, 'file.txt')
- dst_included_dir = dst_dir.joinpath('subdir', exclude_dir)
-
- src_link = Link(path_to_url(src_dir))
- unpack_url(
- src_link,
- dst_dir,
- Mock(side_effect=AssertionError),
- download_dir=None
- )
- assert not os.path.isdir(dst_excluded_dir)
- assert not os.path.isfile(dst_excluded_file)
- assert os.path.isfile(dst_included_file)
- assert os.path.isdir(dst_included_dir)
+ unpack_url(
+ dist_url,
+ self.build_dir,
+ download=self.no_download,
+ hashes=Hashes({"md5": ["bogus"]}),
+ verbosity=0,
+ )
diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py
index 6cde2e212..ada5e1c30 100644
--- a/tests/unit/test_options.py
+++ b/tests/unit/test_options.py
@@ -1,18 +1,23 @@
import os
from contextlib import contextmanager
+from optparse import Values
from tempfile import NamedTemporaryFile
+from typing import Any, Dict, Iterator, List, Tuple, Union, cast
import pytest
import pip._internal.configuration
from pip._internal.cli.main import main
from pip._internal.commands import create_command
+from pip._internal.commands.configuration import ConfigurationCommand
from pip._internal.exceptions import PipError
from tests.lib.options_helpers import AddFakeCommandMixin
@contextmanager
-def assert_option_error(capsys, expected):
+def assert_option_error(
+ capsys: pytest.CaptureFixture[str], expected: str
+) -> Iterator[None]:
"""
Assert that a SystemExit occurred because of a parsing error.
@@ -27,10 +32,10 @@ def assert_option_error(capsys, expected):
assert expected in stderr
-def assert_is_default_cache_dir(value):
+def assert_is_default_cache_dir(value: str) -> None:
# This path looks different on different platforms, but the path always
# has the substring "pip".
- assert 'pip' in value
+ assert "pip" in value
class TestOptionPrecedence(AddFakeCommandMixin):
@@ -40,124 +45,153 @@ class TestOptionPrecedence(AddFakeCommandMixin):
defaults
"""
- def get_config_section(self, section):
+ def get_config_section(self, section: str) -> List[Tuple[str, str]]:
config = {
- 'global': [('timeout', '-3')],
- 'fake': [('timeout', '-2')],
+ "global": [("timeout", "-3")],
+ "fake": [("timeout", "-2")],
}
return config[section]
- def get_config_section_global(self, section):
- config = {
- 'global': [('timeout', '-3')],
- 'fake': [],
+ def get_config_section_global(self, section: str) -> List[Tuple[str, str]]:
+ config: Dict[str, List[Tuple[str, str]]] = {
+ "global": [("timeout", "-3")],
+ "fake": [],
}
return config[section]
- def test_env_override_default_int(self, monkeypatch):
+ def test_env_override_default_int(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""
Test that environment variable overrides an int option default.
"""
- monkeypatch.setenv('PIP_TIMEOUT', '-1')
- options, args = main(['fake'])
+ monkeypatch.setenv("PIP_TIMEOUT", "-1")
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert options.timeout == -1
- @pytest.mark.parametrize('values', (['F1'], ['F1', 'F2']))
- def test_env_override_default_append(self, values, monkeypatch):
+ @pytest.mark.parametrize("values", (["F1"], ["F1", "F2"]))
+ def test_env_override_default_append(
+ self, values: List[str], monkeypatch: pytest.MonkeyPatch
+ ) -> None:
"""
Test that environment variable overrides an append option default.
"""
- monkeypatch.setenv('PIP_FIND_LINKS', ' '.join(values))
- options, args = main(['fake'])
+ monkeypatch.setenv("PIP_FIND_LINKS", " ".join(values))
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert options.find_links == values
- @pytest.mark.parametrize('choises', (['w'], ['s', 'w']))
- def test_env_override_default_choice(self, choises, monkeypatch):
+ @pytest.mark.parametrize("choices", (["w"], ["s", "w"]))
+ def test_env_override_default_choice(
+ self, choices: List[str], monkeypatch: pytest.MonkeyPatch
+ ) -> None:
"""
Test that environment variable overrides a choice option default.
"""
- monkeypatch.setenv('PIP_EXISTS_ACTION', ' '.join(choises))
- options, args = main(['fake'])
- assert options.exists_action == choises
+ monkeypatch.setenv("PIP_EXISTS_ACTION", " ".join(choices))
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake"]))
+ assert options.exists_action == choices
- @pytest.mark.parametrize('name', ('PIP_LOG_FILE', 'PIP_LOCAL_LOG'))
- def test_env_alias_override_default(self, name, monkeypatch):
+ @pytest.mark.parametrize("name", ("PIP_LOG_FILE", "PIP_LOCAL_LOG"))
+ def test_env_alias_override_default(
+ self, name: str, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
"""
When an option has multiple long forms, test that the technique of
using the env variable, "PIP_<long form>" works for all cases.
(e.g. PIP_LOG_FILE and PIP_LOCAL_LOG should all work)
"""
- monkeypatch.setenv(name, 'override.log')
- options, args = main(['fake'])
- assert options.log == 'override.log'
+ monkeypatch.setenv(name, "override.log")
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake"]))
+ assert options.log == "override.log"
- def test_cli_override_environment(self, monkeypatch):
+ def test_cli_override_environment(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""
Test the cli overrides and environment variable
"""
- monkeypatch.setenv('PIP_TIMEOUT', '-1')
- options, args = main(['fake', '--timeout', '-2'])
+ monkeypatch.setenv("PIP_TIMEOUT", "-1")
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(
+ Tuple[Values, List[str]], main(["fake", "--timeout", "-2"])
+ )
assert options.timeout == -2
- @pytest.mark.parametrize('pip_no_cache_dir', [
- # Enabling --no-cache-dir means no cache directory.
- '1',
- 'true',
- 'on',
- 'yes',
- # For historical / backwards compatibility reasons, we also disable
- # the cache directory if provided a value that translates to 0.
- '0',
- 'false',
- 'off',
- 'no',
- ])
- def test_cache_dir__PIP_NO_CACHE_DIR(self, pip_no_cache_dir, monkeypatch):
+ @pytest.mark.parametrize(
+ "pip_no_cache_dir",
+ [
+ # Enabling --no-cache-dir means no cache directory.
+ "1",
+ "true",
+ "on",
+ "yes",
+ # For historical / backwards compatibility reasons, we also disable
+ # the cache directory if provided a value that translates to 0.
+ "0",
+ "false",
+ "off",
+ "no",
+ ],
+ )
+ def test_cache_dir__PIP_NO_CACHE_DIR(
+ self, pip_no_cache_dir: str, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
"""
Test setting the PIP_NO_CACHE_DIR environment variable without
passing any command-line flags.
"""
- monkeypatch.setenv('PIP_NO_CACHE_DIR', pip_no_cache_dir)
- options, args = main(['fake'])
+ monkeypatch.setenv("PIP_NO_CACHE_DIR", pip_no_cache_dir)
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake"]))
assert options.cache_dir is False
- @pytest.mark.parametrize('pip_no_cache_dir', ['yes', 'no'])
+ @pytest.mark.parametrize("pip_no_cache_dir", ["yes", "no"])
def test_cache_dir__PIP_NO_CACHE_DIR__with_cache_dir(
- self, pip_no_cache_dir, monkeypatch,
- ):
+ self,
+ pip_no_cache_dir: str,
+ monkeypatch: pytest.MonkeyPatch,
+ ) -> None:
"""
Test setting PIP_NO_CACHE_DIR while also passing an explicit
--cache-dir value.
"""
- monkeypatch.setenv('PIP_NO_CACHE_DIR', pip_no_cache_dir)
- options, args = main(['--cache-dir', '/cache/dir', 'fake'])
+ monkeypatch.setenv("PIP_NO_CACHE_DIR", pip_no_cache_dir)
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(
+ Tuple[Values, List[str]], main(["--cache-dir", "/cache/dir", "fake"])
+ )
# The command-line flag takes precedence.
- assert options.cache_dir == '/cache/dir'
+ assert options.cache_dir == "/cache/dir"
- @pytest.mark.parametrize('pip_no_cache_dir', ['yes', 'no'])
+ @pytest.mark.parametrize("pip_no_cache_dir", ["yes", "no"])
def test_cache_dir__PIP_NO_CACHE_DIR__with_no_cache_dir(
- self, pip_no_cache_dir, monkeypatch,
- ):
+ self,
+ pip_no_cache_dir: str,
+ monkeypatch: pytest.MonkeyPatch,
+ ) -> None:
"""
Test setting PIP_NO_CACHE_DIR while also passing --no-cache-dir.
"""
- monkeypatch.setenv('PIP_NO_CACHE_DIR', pip_no_cache_dir)
- options, args = main(['--no-cache-dir', 'fake'])
+ monkeypatch.setenv("PIP_NO_CACHE_DIR", pip_no_cache_dir)
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["--no-cache-dir", "fake"]))
# The command-line flag should take precedence (which has the same
# value in this case).
assert options.cache_dir is False
def test_cache_dir__PIP_NO_CACHE_DIR_invalid__with_no_cache_dir(
- self, monkeypatch, capsys,
- ):
+ self,
+ monkeypatch: pytest.MonkeyPatch,
+ capsys: pytest.CaptureFixture[str],
+ ) -> None:
"""
Test setting PIP_NO_CACHE_DIR to an invalid value while also passing
--no-cache-dir.
"""
- monkeypatch.setenv('PIP_NO_CACHE_DIR', 'maybe')
+ monkeypatch.setenv("PIP_NO_CACHE_DIR", "maybe")
expected_err = "--no-cache-dir error: invalid truth value 'maybe'"
with assert_option_error(capsys, expected=expected_err):
- main(['--no-cache-dir', 'fake'])
+ main(["--no-cache-dir", "fake"])
class TestUsePEP517Options:
@@ -166,103 +200,115 @@ class TestUsePEP517Options:
Test options related to using --use-pep517.
"""
- def parse_args(self, args):
+ def parse_args(self, args: List[str]) -> Values:
# We use DownloadCommand since that is one of the few Command
# classes with the use_pep517 options.
- command = create_command('download')
+ command = create_command("download")
options, args = command.parse_args(args)
return options
- def test_no_option(self):
+ def test_no_option(self) -> None:
"""
Test passing no option.
"""
options = self.parse_args([])
assert options.use_pep517 is None
- def test_use_pep517(self):
+ def test_use_pep517(self) -> None:
"""
Test passing --use-pep517.
"""
- options = self.parse_args(['--use-pep517'])
+ options = self.parse_args(["--use-pep517"])
assert options.use_pep517 is True
- def test_no_use_pep517(self):
+ def test_no_use_pep517(self) -> None:
"""
Test passing --no-use-pep517.
"""
- options = self.parse_args(['--no-use-pep517'])
+ options = self.parse_args(["--no-use-pep517"])
assert options.use_pep517 is False
- def test_PIP_USE_PEP517_true(self, monkeypatch):
+ def test_PIP_USE_PEP517_true(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""
Test setting PIP_USE_PEP517 to "true".
"""
- monkeypatch.setenv('PIP_USE_PEP517', 'true')
+ monkeypatch.setenv("PIP_USE_PEP517", "true")
options = self.parse_args([])
# This is an int rather than a boolean because strtobool() in pip's
# configuration code returns an int.
assert options.use_pep517 == 1
- def test_PIP_USE_PEP517_false(self, monkeypatch):
+ def test_PIP_USE_PEP517_false(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""
Test setting PIP_USE_PEP517 to "false".
"""
- monkeypatch.setenv('PIP_USE_PEP517', 'false')
+ monkeypatch.setenv("PIP_USE_PEP517", "false")
options = self.parse_args([])
# This is an int rather than a boolean because strtobool() in pip's
# configuration code returns an int.
assert options.use_pep517 == 0
- def test_use_pep517_and_PIP_USE_PEP517_false(self, monkeypatch):
+ def test_use_pep517_and_PIP_USE_PEP517_false(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
"""
Test passing --use-pep517 and setting PIP_USE_PEP517 to "false".
"""
- monkeypatch.setenv('PIP_USE_PEP517', 'false')
- options = self.parse_args(['--use-pep517'])
+ monkeypatch.setenv("PIP_USE_PEP517", "false")
+ options = self.parse_args(["--use-pep517"])
assert options.use_pep517 is True
- def test_no_use_pep517_and_PIP_USE_PEP517_true(self, monkeypatch):
+ def test_no_use_pep517_and_PIP_USE_PEP517_true(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
"""
Test passing --no-use-pep517 and setting PIP_USE_PEP517 to "true".
"""
- monkeypatch.setenv('PIP_USE_PEP517', 'true')
- options = self.parse_args(['--no-use-pep517'])
+ monkeypatch.setenv("PIP_USE_PEP517", "true")
+ options = self.parse_args(["--no-use-pep517"])
assert options.use_pep517 is False
- def test_PIP_NO_USE_PEP517(self, monkeypatch, capsys):
+ def test_PIP_NO_USE_PEP517(
+ self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
+ ) -> None:
"""
Test setting PIP_NO_USE_PEP517, which isn't allowed.
"""
- monkeypatch.setenv('PIP_NO_USE_PEP517', 'true')
- with assert_option_error(capsys, expected='--no-use-pep517 error'):
+ monkeypatch.setenv("PIP_NO_USE_PEP517", "true")
+ with assert_option_error(capsys, expected="--no-use-pep517 error"):
self.parse_args([])
class TestOptionsInterspersed(AddFakeCommandMixin):
-
- def test_general_option_after_subcommand(self):
- options, args = main(['fake', '--timeout', '-1'])
+ def test_general_option_after_subcommand(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(
+ Tuple[Values, List[str]], main(["fake", "--timeout", "-1"])
+ )
assert options.timeout == -1
- def test_option_after_subcommand_arg(self):
- options, args = main(['fake', 'arg', '--timeout', '-1'])
+ def test_option_after_subcommand_arg(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(
+ Tuple[Values, List[str]], main(["fake", "arg", "--timeout", "-1"])
+ )
assert options.timeout == -1
- def test_additive_before_after_subcommand(self):
- options, args = main(['-v', 'fake', '-v'])
+ def test_additive_before_after_subcommand(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["-v", "fake", "-v"]))
assert options.verbose == 2
- def test_subcommand_option_before_subcommand_fails(self):
+ def test_subcommand_option_before_subcommand_fails(self) -> None:
with pytest.raises(SystemExit):
- main(['--find-links', 'F1', 'fake'])
+ main(["--find-links", "F1", "fake"])
@contextmanager
-def tmpconfig(option, value, section='global'):
- with NamedTemporaryFile(mode='w', delete=False) as f:
- f.write(f'[{section}]\n{option}={value}\n')
+def tmpconfig(option: str, value: Any, section: str = "global") -> Iterator[str]:
+ with NamedTemporaryFile(mode="w", delete=False) as f:
+ f.write(f"[{section}]\n{option}={value}\n")
name = f.name
try:
yield name
@@ -271,93 +317,140 @@ def tmpconfig(option, value, section='global'):
class TestCountOptions(AddFakeCommandMixin):
-
- @pytest.mark.parametrize('option', ('verbose', 'quiet'))
- @pytest.mark.parametrize('value', range(4))
- def test_cli_long(self, option, value):
- flags = [f'--{option}'] * value
- opt1, args1 = main(flags+['fake'])
- opt2, args2 = main(['fake']+flags)
+ @pytest.mark.parametrize("option", ("verbose", "quiet"))
+ @pytest.mark.parametrize("value", range(4))
+ def test_cli_long(self, option: str, value: int) -> None:
+ flags = [f"--{option}"] * value
+ # FakeCommand intentionally returns the wrong type.
+ opt1, args1 = cast(Tuple[Values, List[str]], main(flags + ["fake"]))
+ opt2, args2 = cast(Tuple[Values, List[str]], main(["fake"] + flags))
assert getattr(opt1, option) == getattr(opt2, option) == value
- @pytest.mark.parametrize('option', ('verbose', 'quiet'))
- @pytest.mark.parametrize('value', range(1, 4))
- def test_cli_short(self, option, value):
- flag = '-' + option[0]*value
- opt1, args1 = main([flag, 'fake'])
- opt2, args2 = main(['fake', flag])
+ @pytest.mark.parametrize("option", ("verbose", "quiet"))
+ @pytest.mark.parametrize("value", range(1, 4))
+ def test_cli_short(self, option: str, value: int) -> None:
+ flag = "-" + option[0] * value
+ # FakeCommand intentionally returns the wrong type.
+ opt1, args1 = cast(Tuple[Values, List[str]], main([flag, "fake"]))
+ opt2, args2 = cast(Tuple[Values, List[str]], main(["fake", flag]))
assert getattr(opt1, option) == getattr(opt2, option) == value
- @pytest.mark.parametrize('option', ('verbose', 'quiet'))
- @pytest.mark.parametrize('value', range(4))
- def test_env_var(self, option, value, monkeypatch):
- monkeypatch.setenv('PIP_'+option.upper(), str(value))
- assert getattr(main(['fake'])[0], option) == value
-
- @pytest.mark.parametrize('option', ('verbose', 'quiet'))
- @pytest.mark.parametrize('value', range(3))
- def test_env_var_integrate_cli(self, option, value, monkeypatch):
- monkeypatch.setenv('PIP_'+option.upper(), str(value))
- assert getattr(main(['fake', '--'+option])[0], option) == value + 1
-
- @pytest.mark.parametrize('option', ('verbose', 'quiet'))
- @pytest.mark.parametrize('value', (-1, 'foobar'))
- def test_env_var_invalid(self, option, value, monkeypatch, capsys):
- monkeypatch.setenv('PIP_'+option.upper(), str(value))
- with assert_option_error(capsys, expected='a non-negative integer'):
- main(['fake'])
+ @pytest.mark.parametrize("option", ("verbose", "quiet"))
+ @pytest.mark.parametrize("value", range(4))
+ def test_env_var(
+ self, option: str, value: int, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ monkeypatch.setenv("PIP_" + option.upper(), str(value))
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake"]))
+ assert getattr(options, option) == value
+
+ @pytest.mark.parametrize("option", ("verbose", "quiet"))
+ @pytest.mark.parametrize("value", range(3))
+ def test_env_var_integrate_cli(
+ self, option: str, value: int, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ monkeypatch.setenv("PIP_" + option.upper(), str(value))
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake", "--" + option]))
+ assert getattr(options, option) == value + 1
+
+ @pytest.mark.parametrize("option", ("verbose", "quiet"))
+ @pytest.mark.parametrize("value", (-1, "foobar"))
+ def test_env_var_invalid(
+ self,
+ option: str,
+ value: Any,
+ monkeypatch: pytest.MonkeyPatch,
+ capsys: pytest.CaptureFixture[str],
+ ) -> None:
+ monkeypatch.setenv("PIP_" + option.upper(), str(value))
+ with assert_option_error(capsys, expected="a non-negative integer"):
+ main(["fake"])
# Undocumented, support for backward compatibility
- @pytest.mark.parametrize('option', ('verbose', 'quiet'))
- @pytest.mark.parametrize('value', ('no', 'false'))
- def test_env_var_false(self, option, value, monkeypatch):
- monkeypatch.setenv('PIP_'+option.upper(), str(value))
- assert getattr(main(['fake'])[0], option) == 0
+ @pytest.mark.parametrize("option", ("verbose", "quiet"))
+ @pytest.mark.parametrize("value", ("no", "false"))
+ def test_env_var_false(
+ self, option: str, value: str, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ monkeypatch.setenv("PIP_" + option.upper(), str(value))
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake"]))
+ assert getattr(options, option) == 0
# Undocumented, support for backward compatibility
- @pytest.mark.parametrize('option', ('verbose', 'quiet'))
- @pytest.mark.parametrize('value', ('yes', 'true'))
- def test_env_var_true(self, option, value, monkeypatch):
- monkeypatch.setenv('PIP_'+option.upper(), str(value))
- assert getattr(main(['fake'])[0], option) == 1
-
- @pytest.mark.parametrize('option', ('verbose', 'quiet'))
- @pytest.mark.parametrize('value', range(4))
- def test_config_file(self, option, value, monkeypatch):
+ @pytest.mark.parametrize("option", ("verbose", "quiet"))
+ @pytest.mark.parametrize("value", ("yes", "true"))
+ def test_env_var_true(
+ self, option: str, value: str, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ monkeypatch.setenv("PIP_" + option.upper(), str(value))
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake"]))
+ assert getattr(options, option) == 1
+
+ @pytest.mark.parametrize("option", ("verbose", "quiet"))
+ @pytest.mark.parametrize("value", range(4))
+ def test_config_file(
+ self, option: str, value: int, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
with tmpconfig(option, value) as name:
- monkeypatch.setenv('PIP_CONFIG_FILE', name)
- assert getattr(main(['fake'])[0], option) == value
-
- @pytest.mark.parametrize('option', ('verbose', 'quiet'))
- @pytest.mark.parametrize('value', range(3))
- def test_config_file_integrate_cli(self, option, value, monkeypatch):
+ monkeypatch.setenv("PIP_CONFIG_FILE", name)
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake"]))
+ assert getattr(options, option) == value
+
+ @pytest.mark.parametrize("option", ("verbose", "quiet"))
+ @pytest.mark.parametrize("value", range(3))
+ def test_config_file_integrate_cli(
+ self, option: str, value: int, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
with tmpconfig(option, value) as name:
- monkeypatch.setenv('PIP_CONFIG_FILE', name)
- assert getattr(main(['fake', '--'+option])[0], option) == value + 1
-
- @pytest.mark.parametrize('option', ('verbose', 'quiet'))
- @pytest.mark.parametrize('value', (-1, 'foobar'))
- def test_config_file_invalid(self, option, value, monkeypatch, capsys):
+ monkeypatch.setenv("PIP_CONFIG_FILE", name)
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(
+ Tuple[Values, List[str]], main(["fake", "--" + option])
+ )
+ assert getattr(options, option) == value + 1
+
+ @pytest.mark.parametrize("option", ("verbose", "quiet"))
+ @pytest.mark.parametrize("value", (-1, "foobar"))
+ def test_config_file_invalid(
+ self,
+ option: str,
+ value: Any,
+ monkeypatch: pytest.MonkeyPatch,
+ capsys: pytest.CaptureFixture[str],
+ ) -> None:
with tmpconfig(option, value) as name:
- monkeypatch.setenv('PIP_CONFIG_FILE', name)
- with assert_option_error(capsys, expected='non-negative integer'):
- main(['fake'])
+ monkeypatch.setenv("PIP_CONFIG_FILE", name)
+ with assert_option_error(capsys, expected="non-negative integer"):
+ main(["fake"])
# Undocumented, support for backward compatibility
- @pytest.mark.parametrize('option', ('verbose', 'quiet'))
- @pytest.mark.parametrize('value', ('no', 'false'))
- def test_config_file_false(self, option, value, monkeypatch):
+ @pytest.mark.parametrize("option", ("verbose", "quiet"))
+ @pytest.mark.parametrize("value", ("no", "false"))
+ def test_config_file_false(
+ self, option: str, value: str, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
with tmpconfig(option, value) as name:
- monkeypatch.setenv('PIP_CONFIG_FILE', name)
- assert getattr(main(['fake'])[0], option) == 0
+ monkeypatch.setenv("PIP_CONFIG_FILE", name)
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake"]))
+ assert getattr(options, option) == 0
# Undocumented, support for backward compatibility
- @pytest.mark.parametrize('option', ('verbose', 'quiet'))
- @pytest.mark.parametrize('value', ('yes', 'true'))
- def test_config_file_true(self, option, value, monkeypatch):
+ @pytest.mark.parametrize("option", ("verbose", "quiet"))
+ @pytest.mark.parametrize("value", ("yes", "true"))
+ def test_config_file_true(
+ self, option: str, value: str, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
with tmpconfig(option, value) as name:
- monkeypatch.setenv('PIP_CONFIG_FILE', name)
- assert getattr(main(['fake'])[0], option) == 1
+ monkeypatch.setenv("PIP_CONFIG_FILE", name)
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake"]))
+ assert getattr(options, option) == 1
class TestGeneralOptions(AddFakeCommandMixin):
@@ -365,79 +458,128 @@ class TestGeneralOptions(AddFakeCommandMixin):
# the reason to specifically test general options is due to the
# extra processing they receive, and the number of bugs we've had
- def test_cache_dir__default(self):
- options, args = main(['fake'])
+ def test_cache_dir__default(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["fake"]))
# With no options the default cache dir should be used.
assert_is_default_cache_dir(options.cache_dir)
- def test_cache_dir__provided(self):
- options, args = main(['--cache-dir', '/cache/dir', 'fake'])
- assert options.cache_dir == '/cache/dir'
+ def test_cache_dir__provided(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(
+ Tuple[Values, List[str]], main(["--cache-dir", "/cache/dir", "fake"])
+ )
+ assert options.cache_dir == "/cache/dir"
- def test_no_cache_dir__provided(self):
- options, args = main(['--no-cache-dir', 'fake'])
+ def test_no_cache_dir__provided(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(Tuple[Values, List[str]], main(["--no-cache-dir", "fake"]))
assert options.cache_dir is False
- def test_require_virtualenv(self):
- options1, args1 = main(['--require-virtualenv', 'fake'])
- options2, args2 = main(['fake', '--require-virtualenv'])
+ def test_require_virtualenv(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options1, args1 = cast(
+ Tuple[Values, List[str]], main(["--require-virtualenv", "fake"])
+ )
+ options2, args2 = cast(
+ Tuple[Values, List[str]], main(["fake", "--require-virtualenv"])
+ )
assert options1.require_venv
assert options2.require_venv
- def test_log(self):
- options1, args1 = main(['--log', 'path', 'fake'])
- options2, args2 = main(['fake', '--log', 'path'])
- assert options1.log == options2.log == 'path'
+ def test_log(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options1, args1 = cast(
+ Tuple[Values, List[str]], main(["--log", "path", "fake"])
+ )
+ options2, args2 = cast(
+ Tuple[Values, List[str]], main(["fake", "--log", "path"])
+ )
+ assert options1.log == options2.log == "path"
- def test_local_log(self):
- options1, args1 = main(['--local-log', 'path', 'fake'])
- options2, args2 = main(['fake', '--local-log', 'path'])
- assert options1.log == options2.log == 'path'
+ def test_local_log(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options1, args1 = cast(
+ Tuple[Values, List[str]], main(["--local-log", "path", "fake"])
+ )
+ options2, args2 = cast(
+ Tuple[Values, List[str]], main(["fake", "--local-log", "path"])
+ )
+ assert options1.log == options2.log == "path"
- def test_no_input(self):
- options1, args1 = main(['--no-input', 'fake'])
- options2, args2 = main(['fake', '--no-input'])
+ def test_no_input(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options1, args1 = cast(Tuple[Values, List[str]], main(["--no-input", "fake"]))
+ options2, args2 = cast(Tuple[Values, List[str]], main(["fake", "--no-input"]))
assert options1.no_input
assert options2.no_input
- def test_proxy(self):
- options1, args1 = main(['--proxy', 'path', 'fake'])
- options2, args2 = main(['fake', '--proxy', 'path'])
- assert options1.proxy == options2.proxy == 'path'
+ def test_proxy(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options1, args1 = cast(
+ Tuple[Values, List[str]], main(["--proxy", "path", "fake"])
+ )
+ options2, args2 = cast(
+ Tuple[Values, List[str]], main(["fake", "--proxy", "path"])
+ )
+ assert options1.proxy == options2.proxy == "path"
- def test_retries(self):
- options1, args1 = main(['--retries', '-1', 'fake'])
- options2, args2 = main(['fake', '--retries', '-1'])
+ def test_retries(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options1, args1 = cast(
+ Tuple[Values, List[str]], main(["--retries", "-1", "fake"])
+ )
+ options2, args2 = cast(
+ Tuple[Values, List[str]], main(["fake", "--retries", "-1"])
+ )
assert options1.retries == options2.retries == -1
- def test_timeout(self):
- options1, args1 = main(['--timeout', '-1', 'fake'])
- options2, args2 = main(['fake', '--timeout', '-1'])
+ def test_timeout(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options1, args1 = cast(
+ Tuple[Values, List[str]], main(["--timeout", "-1", "fake"])
+ )
+ options2, args2 = cast(
+ Tuple[Values, List[str]], main(["fake", "--timeout", "-1"])
+ )
assert options1.timeout == options2.timeout == -1
- def test_exists_action(self):
- options1, args1 = main(['--exists-action', 'w', 'fake'])
- options2, args2 = main(['fake', '--exists-action', 'w'])
- assert options1.exists_action == options2.exists_action == ['w']
+ def test_exists_action(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options1, args1 = cast(
+ Tuple[Values, List[str]], main(["--exists-action", "w", "fake"])
+ )
+ options2, args2 = cast(
+ Tuple[Values, List[str]], main(["fake", "--exists-action", "w"])
+ )
+ assert options1.exists_action == options2.exists_action == ["w"]
- def test_cert(self):
- options1, args1 = main(['--cert', 'path', 'fake'])
- options2, args2 = main(['fake', '--cert', 'path'])
- assert options1.cert == options2.cert == 'path'
+ def test_cert(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options1, args1 = cast(
+ Tuple[Values, List[str]], main(["--cert", "path", "fake"])
+ )
+ options2, args2 = cast(
+ Tuple[Values, List[str]], main(["fake", "--cert", "path"])
+ )
+ assert options1.cert == options2.cert == "path"
- def test_client_cert(self):
- options1, args1 = main(['--client-cert', 'path', 'fake'])
- options2, args2 = main(['fake', '--client-cert', 'path'])
- assert options1.client_cert == options2.client_cert == 'path'
+ def test_client_cert(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options1, args1 = cast(
+ Tuple[Values, List[str]], main(["--client-cert", "path", "fake"])
+ )
+ options2, args2 = cast(
+ Tuple[Values, List[str]], main(["fake", "--client-cert", "path"])
+ )
+ assert options1.client_cert == options2.client_cert == "path"
class TestOptionsConfigFiles:
-
- def test_venv_config_file_found(self, monkeypatch):
+ def test_venv_config_file_found(self, monkeypatch: pytest.MonkeyPatch) -> None:
# strict limit on the global config files list
monkeypatch.setattr(
- pip._internal.utils.appdirs, 'site_config_dirs',
- lambda _: ['/a/place']
+ pip._internal.utils.appdirs, "site_config_dirs", lambda _: ["/a/place"]
)
cp = pip._internal.configuration.Configuration(isolated=False)
@@ -458,10 +600,15 @@ class TestOptionsConfigFiles:
(["--global", "--user"], PipError),
(["--global", "--site"], PipError),
(["--global", "--site", "--user"], PipError),
- )
+ ),
)
- def test_config_file_options(self, monkeypatch, args, expect):
- cmd = create_command('config')
+ def test_config_file_options(
+ self,
+ monkeypatch: pytest.MonkeyPatch,
+ args: List[str],
+ expect: Union[None, str, PipError],
+ ) -> None:
+ cmd = cast(ConfigurationCommand, create_command("config"))
# Replace a handler with a no-op to avoid side effects
monkeypatch.setattr(cmd, "get_name", lambda *a: None)
@@ -474,22 +621,37 @@ class TestOptionsConfigFiles:
class TestOptionsExpandUser(AddFakeCommandMixin):
- def test_cache_dir(self):
- options, args = main(['--cache-dir', '~/cache/dir', 'fake'])
- assert options.cache_dir == os.path.expanduser('~/cache/dir')
+ def test_cache_dir(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(
+ Tuple[Values, List[str]], main(["--cache-dir", "~/cache/dir", "fake"])
+ )
+ assert options.cache_dir == os.path.expanduser("~/cache/dir")
- def test_log(self):
- options, args = main(['--log', '~/path', 'fake'])
- assert options.log == os.path.expanduser('~/path')
+ def test_log(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(
+ Tuple[Values, List[str]], main(["--log", "~/path", "fake"])
+ )
+ assert options.log == os.path.expanduser("~/path")
- def test_local_log(self):
- options, args = main(['--local-log', '~/path', 'fake'])
- assert options.log == os.path.expanduser('~/path')
+ def test_local_log(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(
+ Tuple[Values, List[str]], main(["--local-log", "~/path", "fake"])
+ )
+ assert options.log == os.path.expanduser("~/path")
- def test_cert(self):
- options, args = main(['--cert', '~/path', 'fake'])
- assert options.cert == os.path.expanduser('~/path')
+ def test_cert(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(
+ Tuple[Values, List[str]], main(["--cert", "~/path", "fake"])
+ )
+ assert options.cert == os.path.expanduser("~/path")
- def test_client_cert(self):
- options, args = main(['--client-cert', '~/path', 'fake'])
- assert options.client_cert == os.path.expanduser('~/path')
+ def test_client_cert(self) -> None:
+ # FakeCommand intentionally returns the wrong type.
+ options, args = cast(
+ Tuple[Values, List[str]], main(["--client-cert", "~/path", "fake"])
+ )
+ assert options.client_cert == os.path.expanduser("~/path")
diff --git a/tests/unit/test_packaging.py b/tests/unit/test_packaging.py
index 448e38063..88277448c 100644
--- a/tests/unit/test_packaging.py
+++ b/tests/unit/test_packaging.py
@@ -1,22 +1,44 @@
+from typing import Optional, Tuple
+
import pytest
from pip._vendor.packaging import specifiers
+from pip._vendor.packaging.requirements import Requirement
-from pip._internal.utils.packaging import check_requires_python
+from pip._internal.utils.packaging import check_requires_python, get_requirement
-@pytest.mark.parametrize('version_info, requires_python, expected', [
- ((3, 6, 5), '== 3.6.4', False),
- ((3, 6, 5), '== 3.6.5', True),
- ((3, 6, 5), None, True),
-])
-def test_check_requires_python(version_info, requires_python, expected):
+@pytest.mark.parametrize(
+ "version_info, requires_python, expected",
+ [
+ ((3, 6, 5), "== 3.6.4", False),
+ ((3, 6, 5), "== 3.6.5", True),
+ ((3, 6, 5), None, True),
+ ],
+)
+def test_check_requires_python(
+ version_info: Tuple[int, int, int], requires_python: Optional[str], expected: bool
+) -> None:
actual = check_requires_python(requires_python, version_info)
assert actual == expected
-def test_check_requires_python__invalid():
+def test_check_requires_python__invalid() -> None:
"""
Test an invalid Requires-Python value.
"""
with pytest.raises(specifiers.InvalidSpecifier):
- check_requires_python('invalid', (3, 6, 5))
+ check_requires_python("invalid", (3, 6, 5))
+
+
+def test_get_or_create_caching() -> None:
+ """test caching of get_or_create requirement"""
+ teststr = "affinegap==1.10"
+ from_helper = get_requirement(teststr)
+ freshly_made = Requirement(teststr)
+
+ # Requirement doesn't have an equality operator (yet) so test
+ # equality of attribute for list of attributes
+ for iattr in ["name", "url", "extras", "specifier", "marker"]:
+ assert getattr(from_helper, iattr) == getattr(freshly_made, iattr)
+ assert get_requirement(teststr) is not Requirement(teststr)
+ assert get_requirement(teststr) is get_requirement(teststr)
diff --git a/tests/unit/test_pep517.py b/tests/unit/test_pep517.py
index 18cb178bb..5eefbf4e7 100644
--- a/tests/unit/test_pep517.py
+++ b/tests/unit/test_pep517.py
@@ -1,38 +1,63 @@
+import os
+from pathlib import Path
from textwrap import dedent
import pytest
-from pip._internal.exceptions import InstallationError
+from pip._internal.exceptions import InstallationError, InvalidPyProjectBuildRequires
from pip._internal.req import InstallRequirement
+from tests.lib import TestData
-@pytest.mark.parametrize(('source', 'expected'), [
- ("pep517_setup_and_pyproject", True),
- ("pep517_setup_only", False),
- ("pep517_pyproject_only", True),
-])
-def test_use_pep517(shared_data, source, expected):
+@pytest.mark.parametrize(
+ ("source", "expected"),
+ [
+ ("pep517_setup_and_pyproject", True),
+ ("pep517_setup_only", False),
+ ("pep517_pyproject_only", True),
+ ],
+)
+def test_use_pep517(shared_data: TestData, source: str, expected: bool) -> None:
"""
Test that we choose correctly between PEP 517 and legacy code paths
"""
src = shared_data.src.joinpath(source)
req = InstallRequirement(None, None)
- req.source_dir = src # make req believe it has been unpacked
+ req.source_dir = os.fspath(src) # make req believe it has been unpacked
req.load_pyproject_toml()
assert req.use_pep517 is expected
-@pytest.mark.parametrize(('source', 'msg'), [
- ("pep517_setup_and_pyproject", "specifies a build backend"),
- ("pep517_pyproject_only", "does not have a setup.py"),
-])
-def test_disabling_pep517_invalid(shared_data, source, msg):
+def test_use_pep517_rejects_setup_cfg_only(shared_data: TestData) -> None:
+ """
+ Test that projects with setup.cfg but no pyproject.toml are rejected.
+ """
+ src = shared_data.src.joinpath("pep517_setup_cfg_only")
+ req = InstallRequirement(None, None)
+ req.source_dir = os.fspath(src) # make req believe it has been unpacked
+ with pytest.raises(InstallationError) as e:
+ req.load_pyproject_toml()
+ err_msg = e.value.args[0]
+ assert (
+ "does not appear to be a Python project: "
+ "neither 'setup.py' nor 'pyproject.toml' found" in err_msg
+ )
+
+
+@pytest.mark.parametrize(
+ ("source", "msg"),
+ [
+ ("pep517_setup_and_pyproject", "specifies a build backend"),
+ ("pep517_pyproject_only", "does not have a setup.py"),
+ ],
+)
+def test_disabling_pep517_invalid(shared_data: TestData, source: str, msg: str) -> None:
"""
Test that we fail if we try to disable PEP 517 when it's not acceptable
"""
src = shared_data.src.joinpath(source)
req = InstallRequirement(None, None)
- req.source_dir = src # make req believe it has been unpacked
+ req.source_dir = os.fspath(src) # make req believe it has been unpacked
# Simulate --no-use-pep517
req.use_pep517 = False
@@ -48,19 +73,27 @@ def test_disabling_pep517_invalid(shared_data, source, msg):
@pytest.mark.parametrize(
("spec",), [("./foo",), ("git+https://example.com/pkg@dev#egg=myproj",)]
)
-def test_pep517_parsing_checks_requirements(tmpdir, spec):
- tmpdir.joinpath("pyproject.toml").write_text(dedent(
- """
- [build-system]
- requires = [{!r}]
- build-backend = "foo"
- """.format(spec)
- ))
+def test_pep517_parsing_checks_requirements(tmpdir: Path, spec: str) -> None:
+ tmpdir.joinpath("pyproject.toml").write_text(
+ dedent(
+ f"""
+ [build-system]
+ requires = [{spec!r}]
+ build-backend = "foo"
+ """
+ )
+ )
req = InstallRequirement(None, None)
- req.source_dir = tmpdir # make req believe it has been unpacked
+ req.source_dir = os.fspath(tmpdir) # make req believe it has been unpacked
- with pytest.raises(InstallationError) as e:
+ with pytest.raises(InvalidPyProjectBuildRequires) as e:
req.load_pyproject_toml()
- err_msg = e.value.args[0]
- assert "contains an invalid requirement" in err_msg
+ error = e.value
+
+ assert str(req) in error.message
+ assert error.context
+ assert "build-system.requires" in error.context
+ assert "contains an invalid requirement" in error.context
+ assert error.hint_stmt
+ assert "PEP 518" in error.hint_stmt
diff --git a/tests/unit/test_pyproject_config.py b/tests/unit/test_pyproject_config.py
new file mode 100644
index 000000000..9937f3880
--- /dev/null
+++ b/tests/unit/test_pyproject_config.py
@@ -0,0 +1,44 @@
+import pytest
+
+from pip._internal.commands import create_command
+
+
+@pytest.mark.parametrize(
+ ("command", "expected"),
+ [
+ ("install", True),
+ ("wheel", True),
+ ("freeze", False),
+ ],
+)
+def test_supports_config(command: str, expected: bool) -> None:
+ c = create_command(command)
+ options, _ = c.parse_args([])
+ assert hasattr(options, "config_settings") == expected
+
+
+def test_set_config_value_true() -> None:
+ i = create_command("install")
+ # Invalid argument exits with an error
+ with pytest.raises(SystemExit):
+ options, _ = i.parse_args(["xxx", "--config-settings", "x"])
+
+
+def test_set_config_value() -> None:
+ i = create_command("install")
+ options, _ = i.parse_args(["xxx", "--config-settings", "x=hello"])
+ assert options.config_settings == {"x": "hello"}
+
+
+def test_set_config_empty_value() -> None:
+ i = create_command("install")
+ options, _ = i.parse_args(["xxx", "--config-settings", "x="])
+ assert options.config_settings == {"x": ""}
+
+
+def test_replace_config_value() -> None:
+ i = create_command("install")
+ options, _ = i.parse_args(
+ ["xxx", "--config-settings", "x=hello", "--config-settings", "x=world"]
+ )
+ assert options.config_settings == {"x": "world"}
diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py
index 06b7d4a02..bd8289165 100644
--- a/tests/unit/test_req.py
+++ b/tests/unit/test_req.py
@@ -1,24 +1,34 @@
import contextlib
+import email.message
import os
import shutil
import sys
import tempfile
from functools import partial
-from unittest.mock import patch
+from pathlib import Path
+from typing import Iterator, Optional, Tuple, cast
+from unittest import mock
import pytest
-from pip._vendor import pkg_resources
from pip._vendor.packaging.markers import Marker
from pip._vendor.packaging.requirements import Requirement
+from pip._internal.cache import WheelCache
from pip._internal.commands import create_command
+from pip._internal.commands.install import InstallCommand
from pip._internal.exceptions import (
HashErrors,
InstallationError,
InvalidWheelFilename,
PreviousBuildDirError,
)
+from pip._internal.index.package_finder import PackageFinder
+from pip._internal.metadata import select_backend
+from pip._internal.models.direct_url import ArchiveInfo, DirectUrl, DirInfo, VcsInfo
+from pip._internal.models.format_control import FormatControl
+from pip._internal.models.link import Link
from pip._internal.network.session import PipSession
+from pip._internal.operations.build.build_tracker import get_build_tracker
from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req import InstallRequirement, RequirementSet
from pip._internal.req.constructors import (
@@ -35,13 +45,13 @@ from pip._internal.req.req_file import (
get_line_parser,
handle_requirement_line,
)
-from pip._internal.req.req_tracker import get_requirement_tracker
from pip._internal.resolution.legacy.resolver import Resolver
-from pip._internal.utils.urls import path_to_url
-from tests.lib import assert_raises_regexp, make_test_finder, requirements_file
+from tests.lib import TestData, make_test_finder, requirements_file, wheel
-def get_processed_req_from_line(line, fname='file', lineno=1):
+def get_processed_req_from_line(
+ line: str, fname: str = "file", lineno: int = 1
+) -> InstallRequirement:
line_parser = get_line_parser(None)
args_str, opts = line_parser(line)
parsed_line = ParsedLine(
@@ -61,14 +71,19 @@ def get_processed_req_from_line(line, fname='file', lineno=1):
class TestRequirementSet:
"""RequirementSet tests"""
- def setup(self):
+ def setup(self) -> None:
self.tempdir = tempfile.mkdtemp()
- def teardown(self):
+ def teardown(self) -> None:
shutil.rmtree(self.tempdir, ignore_errors=True)
@contextlib.contextmanager
- def _basic_resolver(self, finder, require_hashes=False):
+ def _basic_resolver(
+ self,
+ finder: PackageFinder,
+ require_hashes: bool = False,
+ wheel_cache: Optional[WheelCache] = None,
+ ) -> Iterator[Resolver]:
make_install_req = partial(
install_req_from_req_string,
isolated=False,
@@ -76,111 +91,112 @@ class TestRequirementSet:
)
session = PipSession()
- with get_requirement_tracker() as tracker:
+ with get_build_tracker() as tracker:
preparer = RequirementPreparer(
- build_dir=os.path.join(self.tempdir, 'build'),
- src_dir=os.path.join(self.tempdir, 'src'),
+ build_dir=os.path.join(self.tempdir, "build"),
+ src_dir=os.path.join(self.tempdir, "src"),
download_dir=None,
build_isolation=True,
- req_tracker=tracker,
+ check_build_deps=False,
+ build_tracker=tracker,
session=session,
- progress_bar='on',
+ progress_bar="on",
finder=finder,
require_hashes=require_hashes,
use_user_site=False,
lazy_wheel=False,
- in_tree_build=False,
+ verbosity=0,
)
yield Resolver(
preparer=preparer,
make_install_req=make_install_req,
finder=finder,
- wheel_cache=None,
- use_user_site=False, upgrade_strategy="to-satisfy-only",
- ignore_dependencies=False, ignore_installed=False,
- ignore_requires_python=False, force_reinstall=False,
+ wheel_cache=wheel_cache,
+ use_user_site=False,
+ upgrade_strategy="to-satisfy-only",
+ ignore_dependencies=False,
+ ignore_installed=False,
+ ignore_requires_python=False,
+ force_reinstall=False,
)
- def test_no_reuse_existing_build_dir(self, data):
+ def test_no_reuse_existing_build_dir(self, data: TestData) -> None:
"""Test prepare_files raise exception with previous build dir"""
- build_dir = os.path.join(self.tempdir, 'build', 'simple')
+ build_dir = os.path.join(self.tempdir, "build", "simple")
os.makedirs(build_dir)
- with open(os.path.join(build_dir, "setup.py"), 'w'):
+ with open(os.path.join(build_dir, "setup.py"), "w"):
pass
reqset = RequirementSet()
- req = install_req_from_line('simple')
+ req = install_req_from_line("simple")
req.user_supplied = True
- reqset.add_requirement(req)
+ reqset.add_named_requirement(req)
finder = make_test_finder(find_links=[data.find_links])
with self._basic_resolver(finder) as resolver:
- assert_raises_regexp(
+ with pytest.raises(
PreviousBuildDirError,
- r"pip can't proceed with [\s\S]*{req}[\s\S]*{build_dir_esc}"
- .format(
- build_dir_esc=build_dir.replace('\\', '\\\\'), req=req),
- resolver.resolve,
- reqset.all_requirements,
- True,
- )
-
- # TODO: Update test when Python 2.7 is dropped.
- def test_environment_marker_extras(self, data):
+ match=(
+ r"pip can't proceed with [\s\S]*{req}[\s\S]*{build_dir_esc}".format(
+ build_dir_esc=build_dir.replace("\\", "\\\\"), req=req
+ )
+ ),
+ ):
+ resolver.resolve(reqset.all_requirements, True)
+
+ def test_environment_marker_extras(self, data: TestData) -> None:
"""
Test that the environment marker extras are used with
non-wheel installs.
"""
reqset = RequirementSet()
req = install_req_from_editable(
- data.packages.joinpath("LocalEnvironMarker")
+ os.fspath(data.packages.joinpath("LocalEnvironMarker")),
)
req.user_supplied = True
- reqset.add_requirement(req)
+ reqset.add_unnamed_requirement(req)
finder = make_test_finder(find_links=[data.find_links])
with self._basic_resolver(finder) as resolver:
reqset = resolver.resolve(reqset.all_requirements, True)
- # This is hacky but does test both case in py2 and py3
- if sys.version_info[:2] == (2, 7):
- assert reqset.has_requirement('simple')
- else:
- assert not reqset.has_requirement('simple')
+ assert not reqset.has_requirement("simple")
- def test_missing_hash_with_require_hashes(self, data):
+ def test_missing_hash_with_require_hashes(self, data: TestData) -> None:
"""Setting --require-hashes explicitly should raise errors if hashes
are missing.
"""
reqset = RequirementSet()
- reqset.add_requirement(get_processed_req_from_line(
- 'simple==1.0', lineno=1
- ))
+ reqset.add_named_requirement(
+ get_processed_req_from_line("simple==1.0", lineno=1)
+ )
finder = make_test_finder(find_links=[data.find_links])
with self._basic_resolver(finder, require_hashes=True) as resolver:
- assert_raises_regexp(
+ with pytest.raises(
HashErrors,
- r'Hashes are required in --require-hashes mode, but they are '
- r'missing .*\n'
- r' simple==1.0 --hash=sha256:393043e672415891885c9a2a0929b1'
- r'af95fb866d6ca016b42d2e6ce53619b653$',
- resolver.resolve,
- reqset.all_requirements,
- True,
- )
-
- def test_missing_hash_with_require_hashes_in_reqs_file(self, data, tmpdir):
+ match=(
+ r"Hashes are required in --require-hashes mode, but they are "
+ r"missing .*\n"
+ r" simple==1.0 --hash=sha256:393043e672415891885c9a2a0929b1"
+ r"af95fb866d6ca016b42d2e6ce53619b653$"
+ ),
+ ):
+ resolver.resolve(reqset.all_requirements, True)
+
+ def test_missing_hash_with_require_hashes_in_reqs_file(
+ self, data: TestData, tmpdir: Path
+ ) -> None:
"""--require-hashes in a requirements file should make its way to the
RequirementSet.
"""
finder = make_test_finder(find_links=[data.find_links])
session = finder._link_collector.session
- command = create_command('install')
- with requirements_file('--require-hashes', tmpdir) as reqs_file:
- options, args = command.parse_args(['-r', reqs_file])
+ command = cast(InstallCommand, create_command("install"))
+ with requirements_file("--require-hashes", tmpdir) as reqs_file:
+ options, args = command.parse_args(["-r", os.fspath(reqs_file)])
command.get_requirements(args, options, finder, session)
assert options.require_hashes
- def test_unsupported_hashes(self, data):
+ def test_unsupported_hashes(self, data: TestData) -> None:
"""VCS and dir links should raise errors when --require-hashes is
on.
@@ -189,112 +205,125 @@ class TestRequirementSet:
"""
reqset = RequirementSet()
- reqset.add_requirement(get_processed_req_from_line(
- 'git+git://github.com/pypa/pip-test-package --hash=sha256:123',
- lineno=1,
- ))
- dir_path = data.packages.joinpath('FSPkg')
- reqset.add_requirement(get_processed_req_from_line(
- f'file://{dir_path}',
- lineno=2,
- ))
+ reqset.add_unnamed_requirement(
+ get_processed_req_from_line(
+ "git+git://github.com/pypa/pip-test-package --hash=sha256:123",
+ lineno=1,
+ )
+ )
+ dir_path = data.packages.joinpath("FSPkg")
+ reqset.add_unnamed_requirement(
+ get_processed_req_from_line(
+ f"file://{dir_path}",
+ lineno=2,
+ )
+ )
finder = make_test_finder(find_links=[data.find_links])
sep = os.path.sep
- if sep == '\\':
- sep = '\\\\' # This needs to be escaped for the regex
+ if sep == "\\":
+ sep = "\\\\" # This needs to be escaped for the regex
with self._basic_resolver(finder, require_hashes=True) as resolver:
- assert_raises_regexp(
+ with pytest.raises(
HashErrors,
- r"Can't verify hashes for these requirements because we don't "
- r"have a way to hash version control repositories:\n"
- r" git\+git://github\.com/pypa/pip-test-package \(from -r "
- r"file \(line 1\)\)\n"
- r"Can't verify hashes for these file:// requirements because "
- r"they point to directories:\n"
- r" file://.*{sep}data{sep}packages{sep}FSPkg "
- r"\(from -r file \(line 2\)\)".format(sep=sep),
- resolver.resolve,
- reqset.all_requirements,
- True,
- )
-
- def test_unpinned_hash_checking(self, data):
+ match=(
+ r"Can't verify hashes for these requirements because we don't "
+ r"have a way to hash version control repositories:\n"
+ r" git\+git://github\.com/pypa/pip-test-package \(from -r "
+ r"file \(line 1\)\)\n"
+ r"Can't verify hashes for these file:// requirements because "
+ r"they point to directories:\n"
+ r" file://.*{sep}data{sep}packages{sep}FSPkg "
+ r"\(from -r file \(line 2\)\)".format(sep=sep)
+ ),
+ ):
+ resolver.resolve(reqset.all_requirements, True)
+
+ def test_unpinned_hash_checking(self, data: TestData) -> None:
"""Make sure prepare_files() raises an error when a requirement is not
version-pinned in hash-checking mode.
"""
reqset = RequirementSet()
# Test that there must be exactly 1 specifier:
- reqset.add_requirement(get_processed_req_from_line(
- 'simple --hash=sha256:a90427ae31f5d1d0d7ec06ee97d9fcf2d0fc9a786985'
- '250c1c83fd68df5911dd', lineno=1,
- ))
+ reqset.add_named_requirement(
+ get_processed_req_from_line(
+ "simple --hash=sha256:a90427ae31f5d1d0d7ec06ee97d9fcf2d0fc9a786985"
+ "250c1c83fd68df5911dd",
+ lineno=1,
+ )
+ )
# Test that the operator must be ==:
- reqset.add_requirement(get_processed_req_from_line(
- 'simple2>1.0 --hash=sha256:3ad45e1e9aa48b4462af0'
- '123f6a7e44a9115db1ef945d4d92c123dfe21815a06',
- lineno=2,
- ))
+ reqset.add_named_requirement(
+ get_processed_req_from_line(
+ "simple2>1.0 --hash=sha256:3ad45e1e9aa48b4462af0"
+ "123f6a7e44a9115db1ef945d4d92c123dfe21815a06",
+ lineno=2,
+ )
+ )
finder = make_test_finder(find_links=[data.find_links])
with self._basic_resolver(finder, require_hashes=True) as resolver:
- assert_raises_regexp(
+ with pytest.raises(
HashErrors,
# Make sure all failing requirements are listed:
- r'versions pinned with ==. These do not:\n'
- r' simple .* \(from -r file \(line 1\)\)\n'
- r' simple2>1.0 .* \(from -r file \(line 2\)\)',
- resolver.resolve,
- reqset.all_requirements,
- True,
- )
-
- def test_hash_mismatch(self, data):
+ match=(
+ r"versions pinned with ==. These do not:\n"
+ r" simple .* \(from -r file \(line 1\)\)\n"
+ r" simple2>1.0 .* \(from -r file \(line 2\)\)"
+ ),
+ ):
+ resolver.resolve(reqset.all_requirements, True)
+
+ def test_hash_mismatch(self, data: TestData) -> None:
"""A hash mismatch should raise an error."""
- file_url = path_to_url(
- (data.packages / 'simple-1.0.tar.gz').resolve())
+ file_url = data.packages.joinpath("simple-1.0.tar.gz").resolve().as_uri()
reqset = RequirementSet()
- reqset.add_requirement(get_processed_req_from_line(
- f'{file_url} --hash=sha256:badbad', lineno=1,
- ))
+ reqset.add_unnamed_requirement(
+ get_processed_req_from_line(
+ f"{file_url} --hash=sha256:badbad",
+ lineno=1,
+ )
+ )
finder = make_test_finder(find_links=[data.find_links])
with self._basic_resolver(finder, require_hashes=True) as resolver:
- assert_raises_regexp(
+ with pytest.raises(
HashErrors,
- r'THESE PACKAGES DO NOT MATCH THE HASHES.*\n'
- r' file:///.*/data/packages/simple-1\.0\.tar\.gz .*:\n'
- r' Expected sha256 badbad\n'
- r' Got 393043e672415891885c9a2a0929b1af95fb'
- r'866d6ca016b42d2e6ce53619b653$',
- resolver.resolve,
- reqset.all_requirements,
- True,
- )
-
- def test_unhashed_deps_on_require_hashes(self, data):
+ match=(
+ r"THESE PACKAGES DO NOT MATCH THE HASHES.*\n"
+ r" file:///.*/data/packages/simple-1\.0\.tar\.gz .*:\n"
+ r" Expected sha256 badbad\n"
+ r" Got 393043e672415891885c9a2a0929b1af95fb"
+ r"866d6ca016b42d2e6ce53619b653$"
+ ),
+ ):
+ resolver.resolve(reqset.all_requirements, True)
+
+ def test_unhashed_deps_on_require_hashes(self, data: TestData) -> None:
"""Make sure unhashed, unpinned, or otherwise unrepeatable
dependencies get complained about when --require-hashes is on."""
reqset = RequirementSet()
finder = make_test_finder(find_links=[data.find_links])
- reqset.add_requirement(get_processed_req_from_line(
- 'TopoRequires2==0.0.1 ' # requires TopoRequires
- '--hash=sha256:eaf9a01242c9f2f42cf2bd82a6a848cd'
- 'e3591d14f7896bdbefcf48543720c970',
- lineno=1
- ))
+ reqset.add_named_requirement(
+ get_processed_req_from_line(
+ "TopoRequires2==0.0.1 " # requires TopoRequires
+ "--hash=sha256:eaf9a01242c9f2f42cf2bd82a6a848cd"
+ "e3591d14f7896bdbefcf48543720c970",
+ lineno=1,
+ )
+ )
with self._basic_resolver(finder, require_hashes=True) as resolver:
- assert_raises_regexp(
+ with pytest.raises(
HashErrors,
- r'In --require-hashes mode, all requirements must have their '
- r'versions pinned.*\n'
- r' TopoRequires from .*$',
- resolver.resolve,
- reqset.all_requirements,
- True,
- )
-
- def test_hashed_deps_on_require_hashes(self):
+ match=(
+ r"In --require-hashes mode, all requirements must have their "
+ r"versions pinned.*\n"
+ r" TopoRequires from .*$"
+ ),
+ ):
+ resolver.resolve(reqset.all_requirements, True)
+
+ def test_hashed_deps_on_require_hashes(self) -> None:
"""Make sure hashed dependencies get installed when --require-hashes
is on.
@@ -304,130 +333,269 @@ class TestRequirementSet:
"""
reqset = RequirementSet()
- reqset.add_requirement(get_processed_req_from_line(
- 'TopoRequires2==0.0.1 ' # requires TopoRequires
- '--hash=sha256:eaf9a01242c9f2f42cf2bd82a6a848cd'
- 'e3591d14f7896bdbefcf48543720c970',
- lineno=1
- ))
- reqset.add_requirement(get_processed_req_from_line(
- 'TopoRequires==0.0.1 '
- '--hash=sha256:d6dd1e22e60df512fdcf3640ced3039b3b02a56ab2cee81ebcb'
- '3d0a6d4e8bfa6',
- lineno=2
- ))
+ reqset.add_named_requirement(
+ get_processed_req_from_line(
+ "TopoRequires2==0.0.1 " # requires TopoRequires
+ "--hash=sha256:eaf9a01242c9f2f42cf2bd82a6a848cd"
+ "e3591d14f7896bdbefcf48543720c970",
+ lineno=1,
+ )
+ )
+ reqset.add_named_requirement(
+ get_processed_req_from_line(
+ "TopoRequires==0.0.1 "
+ "--hash=sha256:d6dd1e22e60df512fdcf3640ced3039b3b02a56ab2cee81ebcb"
+ "3d0a6d4e8bfa6",
+ lineno=2,
+ )
+ )
+
+ def test_download_info_find_links(self, data: TestData) -> None:
+ """Test that download_info is set for requirements via find_links."""
+ finder = make_test_finder(find_links=[data.find_links])
+ with self._basic_resolver(finder) as resolver:
+ ireq = get_processed_req_from_line("simple")
+ reqset = resolver.resolve([ireq], True)
+ assert len(reqset.all_requirements) == 1
+ req = reqset.all_requirements[0]
+ assert req.download_info
+ assert isinstance(req.download_info.info, ArchiveInfo)
+ assert req.download_info.info.hash
+
+ @pytest.mark.network
+ def test_download_info_index_url(self) -> None:
+ """Test that download_info is set for requirements via index."""
+ finder = make_test_finder(index_urls=["https://pypi.org/simple"])
+ with self._basic_resolver(finder) as resolver:
+ ireq = get_processed_req_from_line("initools")
+ reqset = resolver.resolve([ireq], True)
+ assert len(reqset.all_requirements) == 1
+ req = reqset.all_requirements[0]
+ assert req.download_info
+ assert isinstance(req.download_info.info, ArchiveInfo)
+
+ @pytest.mark.network
+ def test_download_info_web_archive(self) -> None:
+ """Test that download_info is set for requirements from a web archive."""
+ finder = make_test_finder()
+ with self._basic_resolver(finder) as resolver:
+ ireq = get_processed_req_from_line(
+ "pip-test-package @ "
+ "https://github.com/pypa/pip-test-package/tarball/0.1.1"
+ )
+ reqset = resolver.resolve([ireq], True)
+ assert len(reqset.all_requirements) == 1
+ req = reqset.all_requirements[0]
+ assert req.download_info
+ assert (
+ req.download_info.url
+ == "https://github.com/pypa/pip-test-package/tarball/0.1.1"
+ )
+ assert isinstance(req.download_info.info, ArchiveInfo)
+ assert (
+ req.download_info.info.hash == "sha256="
+ "ad977496000576e1b6c41f6449a9897087ce9da6db4f15b603fe8372af4bf3c6"
+ )
+
+ def test_download_info_archive_legacy_cache(
+ self, tmp_path: Path, shared_data: TestData
+ ) -> None:
+ """Test download_info hash is not set for an archive with legacy cache entry."""
+ url = shared_data.packages.joinpath("simple-1.0.tar.gz").as_uri()
+ finder = make_test_finder()
+ wheel_cache = WheelCache(str(tmp_path / "cache"), FormatControl())
+ cache_entry_dir = wheel_cache.get_path_for_link(Link(url))
+ Path(cache_entry_dir).mkdir(parents=True)
+ wheel.make_wheel(name="simple", version="1.0").save_to_dir(cache_entry_dir)
+ with self._basic_resolver(finder, wheel_cache=wheel_cache) as resolver:
+ ireq = get_processed_req_from_line(f"simple @ {url}")
+ reqset = resolver.resolve([ireq], True)
+ assert len(reqset.all_requirements) == 1
+ req = reqset.all_requirements[0]
+ assert req.original_link_is_in_wheel_cache
+ assert req.download_info
+ assert req.download_info.url == url
+ assert isinstance(req.download_info.info, ArchiveInfo)
+ assert not req.download_info.info.hash
+
+ def test_download_info_archive_cache_with_origin(
+ self, tmp_path: Path, shared_data: TestData
+ ) -> None:
+ """Test download_info hash is set for a web archive with cache entry
+ that has origin.json."""
+ url = shared_data.packages.joinpath("simple-1.0.tar.gz").as_uri()
+ hash = "sha256=ad977496000576e1b6c41f6449a9897087ce9da6db4f15b603fe8372af4bf3c6"
+ finder = make_test_finder()
+ wheel_cache = WheelCache(str(tmp_path / "cache"), FormatControl())
+ cache_entry_dir = wheel_cache.get_path_for_link(Link(url))
+ Path(cache_entry_dir).mkdir(parents=True)
+ Path(cache_entry_dir).joinpath("origin.json").write_text(
+ DirectUrl(url, ArchiveInfo(hash=hash)).to_json()
+ )
+ wheel.make_wheel(name="simple", version="1.0").save_to_dir(cache_entry_dir)
+ with self._basic_resolver(finder, wheel_cache=wheel_cache) as resolver:
+ ireq = get_processed_req_from_line(f"simple @ {url}")
+ reqset = resolver.resolve([ireq], True)
+ assert len(reqset.all_requirements) == 1
+ req = reqset.all_requirements[0]
+ assert req.original_link_is_in_wheel_cache
+ assert req.download_info
+ assert req.download_info.url == url
+ assert isinstance(req.download_info.info, ArchiveInfo)
+ assert req.download_info.info.hash == hash
+
+ def test_download_info_local_wheel(self, data: TestData) -> None:
+ """Test that download_info is set for requirements from a local wheel."""
+ finder = make_test_finder()
+ with self._basic_resolver(finder) as resolver:
+ ireq = get_processed_req_from_line(
+ f"{data.packages}/simplewheel-1.0-py2.py3-none-any.whl"
+ )
+ reqset = resolver.resolve([ireq], True)
+ assert len(reqset.all_requirements) == 1
+ req = reqset.all_requirements[0]
+ assert req.download_info
+ assert req.download_info.url.startswith("file://")
+ assert isinstance(req.download_info.info, ArchiveInfo)
+ assert (
+ req.download_info.info.hash == "sha256="
+ "e63aa139caee941ec7f33f057a5b987708c2128238357cf905429846a2008718"
+ )
+
+ def test_download_info_local_dir(self, data: TestData) -> None:
+ """Test that download_info is set for requirements from a local dir."""
+ finder = make_test_finder()
+ with self._basic_resolver(finder) as resolver:
+ ireq_url = data.packages.joinpath("FSPkg").as_uri()
+ ireq = get_processed_req_from_line(f"FSPkg @ {ireq_url}")
+ reqset = resolver.resolve([ireq], True)
+ assert len(reqset.all_requirements) == 1
+ req = reqset.all_requirements[0]
+ assert req.download_info
+ assert req.download_info.url.startswith("file://")
+ assert isinstance(req.download_info.info, DirInfo)
+
+ def test_download_info_local_editable_dir(self, data: TestData) -> None:
+ """Test that download_info is set for requirements from a local editable dir."""
+ finder = make_test_finder()
+ with self._basic_resolver(finder) as resolver:
+ ireq_url = data.packages.joinpath("FSPkg").as_uri()
+ ireq = get_processed_req_from_line(f"-e {ireq_url}#egg=FSPkg")
+ reqset = resolver.resolve([ireq], True)
+ assert len(reqset.all_requirements) == 1
+ req = reqset.all_requirements[0]
+ assert req.download_info
+ assert req.download_info.url.startswith("file://")
+ assert isinstance(req.download_info.info, DirInfo)
+ assert req.download_info.info.editable
+
+ @pytest.mark.network
+ def test_download_info_vcs(self) -> None:
+ """Test that download_info is set for requirements from git."""
+ finder = make_test_finder()
+ with self._basic_resolver(finder) as resolver:
+ ireq = get_processed_req_from_line(
+ "pip-test-package @ git+https://github.com/pypa/pip-test-package"
+ )
+ reqset = resolver.resolve([ireq], True)
+ assert len(reqset.all_requirements) == 1
+ req = reqset.all_requirements[0]
+ assert req.download_info
+ assert isinstance(req.download_info.info, VcsInfo)
+ assert req.download_info.url == "https://github.com/pypa/pip-test-package"
+ assert req.download_info.info.vcs == "git"
class TestInstallRequirement:
- def setup(self):
+ def setup(self) -> None:
self.tempdir = tempfile.mkdtemp()
- def teardown(self):
+ def teardown(self) -> None:
shutil.rmtree(self.tempdir, ignore_errors=True)
- def test_url_with_query(self):
+ def test_url_with_query(self) -> None:
"""InstallRequirement should strip the fragment, but not the query."""
- url = 'http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz'
- fragment = '#egg=bar'
+ url = "http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz"
+ fragment = "#egg=bar"
req = install_req_from_line(url + fragment)
+ assert req.link is not None
assert req.link.url == url + fragment, req.link
- def test_pep440_wheel_link_requirement(self):
- url = 'https://whatever.com/test-0.4-py2.py3-bogus-any.whl'
- line = 'test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl'
+ def test_pep440_wheel_link_requirement(self) -> None:
+ url = "https://whatever.com/test-0.4-py2.py3-bogus-any.whl"
+ line = "test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl"
req = install_req_from_line(line)
- parts = str(req.req).split('@', 1)
+ parts = str(req.req).split("@", 1)
assert len(parts) == 2
- assert parts[0].strip() == 'test'
+ assert parts[0].strip() == "test"
assert parts[1].strip() == url
- def test_pep440_url_link_requirement(self):
- url = 'git+http://foo.com@ref#egg=foo'
- line = 'foo @ git+http://foo.com@ref#egg=foo'
+ def test_pep440_url_link_requirement(self) -> None:
+ url = "git+http://foo.com@ref#egg=foo"
+ line = "foo @ git+http://foo.com@ref#egg=foo"
req = install_req_from_line(line)
- parts = str(req.req).split('@', 1)
+ parts = str(req.req).split("@", 1)
assert len(parts) == 2
- assert parts[0].strip() == 'foo'
+ assert parts[0].strip() == "foo"
assert parts[1].strip() == url
- def test_url_with_authentication_link_requirement(self):
- url = 'https://what@whatever.com/test-0.4-py2.py3-bogus-any.whl'
- line = 'https://what@whatever.com/test-0.4-py2.py3-bogus-any.whl'
+ def test_url_with_authentication_link_requirement(self) -> None:
+ url = "https://what@whatever.com/test-0.4-py2.py3-bogus-any.whl"
+ line = "https://what@whatever.com/test-0.4-py2.py3-bogus-any.whl"
req = install_req_from_line(line)
assert req.link is not None
assert req.link.is_wheel
assert req.link.scheme == "https"
assert req.link.url == url
- def test_unsupported_wheel_link_requirement_raises(self):
- reqset = RequirementSet()
- req = install_req_from_line(
- 'https://whatever.com/peppercorn-0.4-py2.py3-bogus-any.whl',
- )
- assert req.link is not None
- assert req.link.is_wheel
- assert req.link.scheme == "https"
-
- with pytest.raises(InstallationError):
- reqset.add_requirement(req)
-
- def test_unsupported_wheel_local_file_requirement_raises(self, data):
- reqset = RequirementSet()
- req = install_req_from_line(
- data.packages.joinpath('simple.dist-0.1-py1-none-invalid.whl'),
- )
- assert req.link is not None
- assert req.link.is_wheel
- assert req.link.scheme == "file"
-
- with pytest.raises(InstallationError):
- reqset.add_requirement(req)
-
- def test_str(self):
- req = install_req_from_line('simple==0.1')
- assert str(req) == 'simple==0.1'
+ def test_str(self) -> None:
+ req = install_req_from_line("simple==0.1")
+ assert str(req) == "simple==0.1"
- def test_repr(self):
- req = install_req_from_line('simple==0.1')
- assert repr(req) == (
- '<InstallRequirement object: simple==0.1 editable=False>'
- )
+ def test_repr(self) -> None:
+ req = install_req_from_line("simple==0.1")
+ assert repr(req) == ("<InstallRequirement object: simple==0.1 editable=False>")
- def test_invalid_wheel_requirement_raises(self):
+ def test_invalid_wheel_requirement_raises(self) -> None:
with pytest.raises(InvalidWheelFilename):
- install_req_from_line('invalid.whl')
+ install_req_from_line("invalid.whl")
- def test_wheel_requirement_sets_req_attribute(self):
- req = install_req_from_line('simple-0.1-py2.py3-none-any.whl')
+ def test_wheel_requirement_sets_req_attribute(self) -> None:
+ req = install_req_from_line("simple-0.1-py2.py3-none-any.whl")
assert isinstance(req.req, Requirement)
- assert str(req.req) == 'simple==0.1'
+ assert str(req.req) == "simple==0.1"
- def test_url_preserved_line_req(self):
+ def test_url_preserved_line_req(self) -> None:
"""Confirm the url is preserved in a non-editable requirement"""
- url = 'git+http://foo.com@ref#egg=foo'
+ url = "git+http://foo.com@ref#egg=foo"
req = install_req_from_line(url)
+ assert req.link is not None
assert req.link.url == url
- def test_url_preserved_editable_req(self):
+ def test_url_preserved_editable_req(self) -> None:
"""Confirm the url is preserved in a editable requirement"""
- url = 'git+http://foo.com@ref#egg=foo'
+ url = "git+http://foo.com@ref#egg=foo"
req = install_req_from_editable(url)
+ assert req.link is not None
assert req.link.url == url
- @pytest.mark.parametrize('path', (
- '/path/to/foo.egg-info'.replace('/', os.path.sep),
- # Tests issue fixed by https://github.com/pypa/pip/pull/2530
- '/path/to/foo.egg-info/'.replace('/', os.path.sep),
- ))
- def test_get_dist(self, path):
- req = install_req_from_line('foo')
+ @pytest.mark.parametrize(
+ "path",
+ (
+ "/path/to/foo.egg-info".replace("/", os.path.sep),
+ # Tests issue fixed by https://github.com/pypa/pip/pull/2530
+ "/path/to/foo.egg-info/".replace("/", os.path.sep),
+ ),
+ )
+ def test_get_dist(self, path: str) -> None:
+ req = install_req_from_line("foo")
req.metadata_directory = path
dist = req.get_dist()
- assert isinstance(dist, pkg_resources.Distribution)
- assert dist.project_name == 'foo'
- assert dist.location == '/path/to'.replace('/', os.path.sep)
+ assert isinstance(dist, select_backend().Distribution)
+ assert dist.raw_name == dist.canonical_name == "foo"
+ assert dist.location == "/path/to".replace("/", os.path.sep)
- def test_markers(self):
+ def test_markers(self) -> None:
for line in (
# recommended syntax
'mock3; python_version >= "3"',
@@ -437,39 +605,43 @@ class TestInstallRequirement:
'mock3;python_version >= "3"',
):
req = install_req_from_line(line)
- assert req.req.name == 'mock3'
- assert str(req.req.specifier) == ''
+ assert req.req is not None
+ assert req.req.name == "mock3"
+ assert str(req.req.specifier) == ""
assert str(req.markers) == 'python_version >= "3"'
- def test_markers_semicolon(self):
+ def test_markers_semicolon(self) -> None:
# check that the markers can contain a semicolon
req = install_req_from_line('semicolon; os_name == "a; b"')
- assert req.req.name == 'semicolon'
- assert str(req.req.specifier) == ''
+ assert req.req is not None
+ assert req.req.name == "semicolon"
+ assert str(req.req.specifier) == ""
assert str(req.markers) == 'os_name == "a; b"'
- def test_markers_url(self):
+ def test_markers_url(self) -> None:
# test "URL; markers" syntax
- url = 'http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz'
+ url = "http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz"
line = f'{url}; python_version >= "3"'
req = install_req_from_line(line)
- assert req.link.url == url, req.url
+ assert req.link is not None
+ assert req.link.url == url, req.link.url
assert str(req.markers) == 'python_version >= "3"'
# without space, markers are part of the URL
- url = 'http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz'
+ url = "http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz"
line = f'{url};python_version >= "3"'
req = install_req_from_line(line)
- assert req.link.url == line, req.url
+ assert req.link is not None
+ assert req.link.url == line, req.link.url
assert req.markers is None
- def test_markers_match_from_line(self):
+ def test_markers_match_from_line(self) -> None:
# match
for markers in (
'python_version >= "1.0"',
- f'sys_platform == {sys.platform!r}',
+ f"sys_platform == {sys.platform!r}",
):
- line = 'name; ' + markers
+ line = "name; " + markers
req = install_req_from_line(line)
assert str(req.markers) == str(Marker(markers))
assert req.match_markers()
@@ -477,92 +649,91 @@ class TestInstallRequirement:
# don't match
for markers in (
'python_version >= "5.0"',
- f'sys_platform != {sys.platform!r}',
+ f"sys_platform != {sys.platform!r}",
):
- line = 'name; ' + markers
+ line = "name; " + markers
req = install_req_from_line(line)
assert str(req.markers) == str(Marker(markers))
assert not req.match_markers()
- def test_markers_match(self):
+ def test_markers_match(self) -> None:
# match
for markers in (
'python_version >= "1.0"',
- f'sys_platform == {sys.platform!r}',
+ f"sys_platform == {sys.platform!r}",
):
- line = 'name; ' + markers
- req = install_req_from_line(line, comes_from='')
+ line = "name; " + markers
+ req = install_req_from_line(line, comes_from="")
assert str(req.markers) == str(Marker(markers))
assert req.match_markers()
# don't match
for markers in (
'python_version >= "5.0"',
- f'sys_platform != {sys.platform!r}',
+ f"sys_platform != {sys.platform!r}",
):
- line = 'name; ' + markers
- req = install_req_from_line(line, comes_from='')
+ line = "name; " + markers
+ req = install_req_from_line(line, comes_from="")
assert str(req.markers) == str(Marker(markers))
assert not req.match_markers()
- def test_extras_for_line_path_requirement(self):
- line = 'SomeProject[ex1,ex2]'
- filename = 'filename'
- comes_from = f'-r {filename} (line 1)'
+ def test_extras_for_line_path_requirement(self) -> None:
+ line = "SomeProject[ex1,ex2]"
+ filename = "filename"
+ comes_from = f"-r {filename} (line 1)"
req = install_req_from_line(line, comes_from=comes_from)
assert len(req.extras) == 2
- assert req.extras == {'ex1', 'ex2'}
+ assert req.extras == {"ex1", "ex2"}
- def test_extras_for_line_url_requirement(self):
- line = 'git+https://url#egg=SomeProject[ex1,ex2]'
- filename = 'filename'
- comes_from = f'-r {filename} (line 1)'
+ def test_extras_for_line_url_requirement(self) -> None:
+ line = "git+https://url#egg=SomeProject[ex1,ex2]"
+ filename = "filename"
+ comes_from = f"-r {filename} (line 1)"
req = install_req_from_line(line, comes_from=comes_from)
assert len(req.extras) == 2
- assert req.extras == {'ex1', 'ex2'}
+ assert req.extras == {"ex1", "ex2"}
- def test_extras_for_editable_path_requirement(self):
- url = '.[ex1,ex2]'
- filename = 'filename'
- comes_from = f'-r {filename} (line 1)'
+ def test_extras_for_editable_path_requirement(self) -> None:
+ url = ".[ex1,ex2]"
+ filename = "filename"
+ comes_from = f"-r {filename} (line 1)"
req = install_req_from_editable(url, comes_from=comes_from)
assert len(req.extras) == 2
- assert req.extras == {'ex1', 'ex2'}
+ assert req.extras == {"ex1", "ex2"}
- def test_extras_for_editable_url_requirement(self):
- url = 'git+https://url#egg=SomeProject[ex1,ex2]'
- filename = 'filename'
- comes_from = f'-r {filename} (line 1)'
+ def test_extras_for_editable_url_requirement(self) -> None:
+ url = "git+https://url#egg=SomeProject[ex1,ex2]"
+ filename = "filename"
+ comes_from = f"-r {filename} (line 1)"
req = install_req_from_editable(url, comes_from=comes_from)
assert len(req.extras) == 2
- assert req.extras == {'ex1', 'ex2'}
+ assert req.extras == {"ex1", "ex2"}
- def test_unexisting_path(self):
+ def test_unexisting_path(self) -> None:
with pytest.raises(InstallationError) as e:
- install_req_from_line(
- os.path.join('this', 'path', 'does', 'not', 'exist'))
+ install_req_from_line(os.path.join("this", "path", "does", "not", "exist"))
err_msg = e.value.args[0]
assert "Invalid requirement" in err_msg
assert "It looks like a path." in err_msg
- def test_single_equal_sign(self):
+ def test_single_equal_sign(self) -> None:
with pytest.raises(InstallationError) as e:
- install_req_from_line('toto=42')
+ install_req_from_line("toto=42")
err_msg = e.value.args[0]
assert "Invalid requirement" in err_msg
assert "= is not a valid operator. Did you mean == ?" in err_msg
- def test_unidentifiable_name(self):
- test_name = '-'
+ def test_unidentifiable_name(self) -> None:
+ test_name = "-"
with pytest.raises(InstallationError) as e:
install_req_from_line(test_name)
err_msg = e.value.args[0]
assert f"Invalid requirement: '{test_name}'" == err_msg
- def test_requirement_file(self):
- req_file_path = os.path.join(self.tempdir, 'test.txt')
- with open(req_file_path, 'w') as req_file:
- req_file.write('pip\nsetuptools')
+ def test_requirement_file(self) -> None:
+ req_file_path = os.path.join(self.tempdir, "test.txt")
+ with open(req_file_path, "w") as req_file:
+ req_file.write("pip\nsetuptools")
with pytest.raises(InstallationError) as e:
install_req_from_line(req_file_path)
err_msg = e.value.args[0]
@@ -572,168 +743,194 @@ class TestInstallRequirement:
assert "If that is the case, use the '-r' flag to install" in err_msg
-@patch('pip._internal.req.req_install.os.path.abspath')
-@patch('pip._internal.req.req_install.os.path.exists')
-@patch('pip._internal.req.req_install.os.path.isdir')
+@mock.patch("pip._internal.req.req_install.os.path.abspath")
+@mock.patch("pip._internal.req.req_install.os.path.exists")
+@mock.patch("pip._internal.req.req_install.os.path.isdir")
def test_parse_editable_local(
- isdir_mock, exists_mock, abspath_mock):
+ isdir_mock: mock.Mock, exists_mock: mock.Mock, abspath_mock: mock.Mock
+) -> None:
exists_mock.return_value = isdir_mock.return_value = True
# mocks needed to support path operations on windows tests
abspath_mock.return_value = "/some/path"
- assert parse_editable('.') == (None, 'file:///some/path', set())
+ assert parse_editable(".") == (None, "file:///some/path", set())
abspath_mock.return_value = "/some/path/foo"
- assert parse_editable('foo') == (
- None, 'file:///some/path/foo', set(),
+ assert parse_editable("foo") == (
+ None,
+ "file:///some/path/foo",
+ set(),
)
-def test_parse_editable_explicit_vcs():
- assert parse_editable('svn+https://foo#egg=foo') == (
- 'foo',
- 'svn+https://foo#egg=foo',
+def test_parse_editable_explicit_vcs() -> None:
+ assert parse_editable("svn+https://foo#egg=foo") == (
+ "foo",
+ "svn+https://foo#egg=foo",
set(),
)
-def test_parse_editable_vcs_extras():
- assert parse_editable('svn+https://foo#egg=foo[extras]') == (
- 'foo[extras]',
- 'svn+https://foo#egg=foo[extras]',
+def test_parse_editable_vcs_extras() -> None:
+ assert parse_editable("svn+https://foo#egg=foo[extras]") == (
+ "foo[extras]",
+ "svn+https://foo#egg=foo[extras]",
set(),
)
-@patch('pip._internal.req.req_install.os.path.abspath')
-@patch('pip._internal.req.req_install.os.path.exists')
-@patch('pip._internal.req.req_install.os.path.isdir')
+@mock.patch("pip._internal.req.req_install.os.path.abspath")
+@mock.patch("pip._internal.req.req_install.os.path.exists")
+@mock.patch("pip._internal.req.req_install.os.path.isdir")
def test_parse_editable_local_extras(
- isdir_mock, exists_mock, abspath_mock):
+ isdir_mock: mock.Mock, exists_mock: mock.Mock, abspath_mock: mock.Mock
+) -> None:
exists_mock.return_value = isdir_mock.return_value = True
abspath_mock.return_value = "/some/path"
- assert parse_editable('.[extras]') == (
- None, "file:///some/path", {'extras'},
+ assert parse_editable(".[extras]") == (
+ None,
+ "file:///some/path",
+ {"extras"},
)
abspath_mock.return_value = "/some/path/foo"
- assert parse_editable('foo[bar,baz]') == (
- None, 'file:///some/path/foo', {'bar', 'baz'},
+ assert parse_editable("foo[bar,baz]") == (
+ None,
+ "file:///some/path/foo",
+ {"bar", "baz"},
)
-def test_exclusive_environment_markers():
- """Make sure RequirementSet accepts several excluding env markers"""
- eq36 = install_req_from_line(
- "Django>=1.6.10,<1.7 ; python_version == '3.6'")
- eq36.user_supplied = True
- ne36 = install_req_from_line(
- "Django>=1.6.10,<1.8 ; python_version != '3.6'")
- ne36.user_supplied = True
-
- req_set = RequirementSet()
- req_set.add_requirement(eq36)
- req_set.add_requirement(ne36)
- assert req_set.has_requirement('Django')
-
-
-def test_mismatched_versions(caplog):
+def test_mismatched_versions(caplog: pytest.LogCaptureFixture) -> None:
req = InstallRequirement(
- req=Requirement('simplewheel==2.0'),
+ req=Requirement("simplewheel==2.0"),
comes_from=None,
)
req.source_dir = "/tmp/somewhere" # make req believe it has been unpacked
# Monkeypatch!
- req._metadata = {"name": "simplewheel", "version": "1.0"}
+ metadata = email.message.Message()
+ metadata["name"] = "simplewheel"
+ metadata["version"] = "1.0"
+ req._metadata = metadata
+
req.assert_source_matches_version()
assert caplog.records[-1].message == (
- 'Requested simplewheel==2.0, but installing version 1.0'
+ "Requested simplewheel==2.0, but installing version 1.0"
)
-@pytest.mark.parametrize('args, expected', [
- # Test UNIX-like paths
- (('/path/to/installable'), True),
- # Test relative paths
- (('./path/to/installable'), True),
- # Test current path
- (('.'), True),
- # Test url paths
- (('https://whatever.com/test-0.4-py2.py3-bogus-any.whl'), True),
- # Test pep440 paths
- (('test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl'), True),
- # Test wheel
- (('simple-0.1-py2.py3-none-any.whl'), False),
-])
-def test_looks_like_path(args, expected):
+@pytest.mark.parametrize(
+ "args, expected",
+ [
+ # Test UNIX-like paths
+ (("/path/to/installable"), True),
+ # Test relative paths
+ (("./path/to/installable"), True),
+ # Test current path
+ (("."), True),
+ # Test url paths
+ (("https://whatever.com/test-0.4-py2.py3-bogus-any.whl"), True),
+ # Test pep440 paths
+ (("test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl"), True),
+ # Test wheel
+ (("simple-0.1-py2.py3-none-any.whl"), False),
+ ],
+)
+def test_looks_like_path(args: str, expected: bool) -> None:
assert _looks_like_path(args) == expected
@pytest.mark.skipif(
- not sys.platform.startswith("win"),
- reason='Test only available on Windows'
+ not sys.platform.startswith("win"), reason="Test only available on Windows"
)
-@pytest.mark.parametrize('args, expected', [
- # Test relative paths
- (('.\\path\\to\\installable'), True),
- (('relative\\path'), True),
- # Test absolute paths
- (('C:\\absolute\\path'), True),
-])
-def test_looks_like_path_win(args, expected):
+@pytest.mark.parametrize(
+ "args, expected",
+ [
+ # Test relative paths
+ ((".\\path\\to\\installable"), True),
+ (("relative\\path"), True),
+ # Test absolute paths
+ (("C:\\absolute\\path"), True),
+ ],
+)
+def test_looks_like_path_win(args: str, expected: bool) -> None:
assert _looks_like_path(args) == expected
-@pytest.mark.parametrize('args, mock_returns, expected', [
- # Test pep440 urls
- (('/path/to/foo @ git+http://foo.com@ref#egg=foo',
- 'foo @ git+http://foo.com@ref#egg=foo'), (False, False), None),
- # Test pep440 urls without spaces
- (('/path/to/foo@git+http://foo.com@ref#egg=foo',
- 'foo @ git+http://foo.com@ref#egg=foo'), (False, False), None),
- # Test pep440 wheel
- (('/path/to/test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl',
- 'test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl'),
- (False, False), None),
- # Test name is not a file
- (('/path/to/simple==0.1',
- 'simple==0.1'),
- (False, False), None),
-])
-@patch('pip._internal.req.req_install.os.path.isdir')
-@patch('pip._internal.req.req_install.os.path.isfile')
+@pytest.mark.parametrize(
+ "args, mock_returns, expected",
+ [
+ # Test pep440 urls
+ (
+ (
+ "/path/to/foo @ git+http://foo.com@ref#egg=foo",
+ "foo @ git+http://foo.com@ref#egg=foo",
+ ),
+ (False, False),
+ None,
+ ),
+ # Test pep440 urls without spaces
+ (
+ (
+ "/path/to/foo@git+http://foo.com@ref#egg=foo",
+ "foo @ git+http://foo.com@ref#egg=foo",
+ ),
+ (False, False),
+ None,
+ ),
+ # Test pep440 wheel
+ (
+ (
+ "/path/to/test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl",
+ "test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl",
+ ),
+ (False, False),
+ None,
+ ),
+ # Test name is not a file
+ (("/path/to/simple==0.1", "simple==0.1"), (False, False), None),
+ ],
+)
+@mock.patch("pip._internal.req.req_install.os.path.isdir")
+@mock.patch("pip._internal.req.req_install.os.path.isfile")
def test_get_url_from_path(
- isdir_mock, isfile_mock, args, mock_returns, expected
-):
+ isdir_mock: mock.Mock,
+ isfile_mock: mock.Mock,
+ args: Tuple[str, str],
+ mock_returns: Tuple[bool, bool],
+ expected: None,
+) -> None:
isdir_mock.return_value = mock_returns[0]
isfile_mock.return_value = mock_returns[1]
assert _get_url_from_path(*args) is expected
-@patch('pip._internal.req.req_install.os.path.isdir')
-@patch('pip._internal.req.req_install.os.path.isfile')
-def test_get_url_from_path__archive_file(isdir_mock, isfile_mock):
+@mock.patch("pip._internal.req.req_install.os.path.isdir")
+@mock.patch("pip._internal.req.req_install.os.path.isfile")
+def test_get_url_from_path__archive_file(
+ isdir_mock: mock.Mock, isfile_mock: mock.Mock
+) -> None:
isdir_mock.return_value = False
isfile_mock.return_value = True
- name = 'simple-0.1-py2.py3-none-any.whl'
- path = os.path.join('/path/to/' + name)
- url = path_to_url(path)
- assert _get_url_from_path(path, name) == url
+ name = "simple-0.1-py2.py3-none-any.whl"
+ url = Path(f"/path/to/{name}").resolve(strict=False).as_uri()
+ assert _get_url_from_path(f"/path/to/{name}", name) == url
-@patch('pip._internal.req.req_install.os.path.isdir')
-@patch('pip._internal.req.req_install.os.path.isfile')
-def test_get_url_from_path__installable_dir(isdir_mock, isfile_mock):
+@mock.patch("pip._internal.req.req_install.os.path.isdir")
+@mock.patch("pip._internal.req.req_install.os.path.isfile")
+def test_get_url_from_path__installable_dir(
+ isdir_mock: mock.Mock, isfile_mock: mock.Mock
+) -> None:
isdir_mock.return_value = True
isfile_mock.return_value = True
- name = 'some/setuptools/project'
- path = os.path.join('/path/to/' + name)
- url = path_to_url(path)
- assert _get_url_from_path(path, name) == url
+ name = "some/setuptools/project"
+ url = Path(f"/path/to/{name}").resolve(strict=False).as_uri()
+ assert _get_url_from_path(f"/path/to/{name}", name) == url
-@patch('pip._internal.req.req_install.os.path.isdir')
-def test_get_url_from_path__installable_error(isdir_mock):
+@mock.patch("pip._internal.req.req_install.os.path.isdir")
+def test_get_url_from_path__installable_error(isdir_mock: mock.Mock) -> None:
isdir_mock.return_value = True
- name = 'some/setuptools/project'
- path = os.path.join('/path/to/' + name)
+ name = "some/setuptools/project"
+ path = os.path.join("/path/to/" + name)
with pytest.raises(InstallationError) as e:
_get_url_from_path(path, name)
err_msg = e.value.args[0]
diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py
index e86a09477..6b3712b0c 100644
--- a/tests/unit/test_req_file.py
+++ b/tests/unit/test_req_file.py
@@ -3,13 +3,16 @@ import logging
import os
import subprocess
import textwrap
-from unittest.mock import patch
+from optparse import Values
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Tuple, Union
+from unittest import mock
import pytest
-from pretend import stub
import pip._internal.req.req_file # this will be monkeypatched
from pip._internal.exceptions import InstallationError, RequirementsFileParseError
+from pip._internal.index.package_finder import PackageFinder
from pip._internal.models.format_control import FormatControl
from pip._internal.network.session import PipSession
from pip._internal.req.constructors import (
@@ -24,50 +27,57 @@ from pip._internal.req.req_file import (
parse_requirements,
preprocess,
)
-from tests.lib import make_test_finder, requirements_file
+from pip._internal.req.req_install import InstallRequirement
+from tests.lib import TestData, make_test_finder, requirements_file
+
+if TYPE_CHECKING:
+ from typing import Protocol
+else:
+ # Protocol was introduced in Python 3.8.
+ Protocol = object
@pytest.fixture
-def session():
+def session() -> PipSession:
return PipSession()
@pytest.fixture
-def finder(session):
+def finder(session: PipSession) -> PackageFinder:
return make_test_finder(session=session)
@pytest.fixture
-def options(session):
- return stub(
+def options(session: PipSession) -> mock.Mock:
+ return mock.Mock(
isolated_mode=False,
- index_url='default_url',
+ index_url="default_url",
format_control=FormatControl(set(), set()),
features_enabled=[],
)
def parse_reqfile(
- filename,
- session,
- finder=None,
- options=None,
- constraint=False,
- isolated=False,
-):
+ filename: Union[Path, str],
+ session: PipSession,
+ finder: Optional[PackageFinder] = None,
+ options: Optional[Values] = None,
+ constraint: bool = False,
+ isolated: bool = False,
+) -> Iterator[InstallRequirement]:
# Wrap parse_requirements/install_req_from_parsed_requirement to
# avoid having to write the same chunk of code in lots of tests.
for parsed_req in parse_requirements(
- filename, session, finder=finder,
- options=options, constraint=constraint,
+ os.fspath(filename),
+ session,
+ finder=finder,
+ options=options,
+ constraint=constraint,
):
- yield install_req_from_parsed_requirement(
- parsed_req,
- isolated=isolated
- )
+ yield install_req_from_parsed_requirement(parsed_req, isolated=isolated)
-def test_read_file_url(tmp_path):
+def test_read_file_url(tmp_path: Path, session: PipSession) -> None:
reqs = tmp_path.joinpath("requirements.txt")
reqs.write_text("foo")
result = list(parse_requirements(reqs.as_posix(), session))
@@ -86,115 +96,140 @@ def test_read_file_url(tmp_path):
class TestPreprocess:
"""tests for `preprocess`"""
- def test_comments_and_joins_case1(self):
- content = textwrap.dedent("""\
+ def test_comments_and_joins_case1(self) -> None:
+ content = textwrap.dedent(
+ """\
req1 \\
# comment \\
req2
- """)
+ """
+ )
result = preprocess(content)
- assert list(result) == [(1, 'req1'), (3, 'req2')]
+ assert list(result) == [(1, "req1"), (3, "req2")]
- def test_comments_and_joins_case2(self):
- content = textwrap.dedent("""\
+ def test_comments_and_joins_case2(self) -> None:
+ content = textwrap.dedent(
+ """\
req1\\
# comment
- """)
+ """
+ )
result = preprocess(content)
- assert list(result) == [(1, 'req1')]
+ assert list(result) == [(1, "req1")]
- def test_comments_and_joins_case3(self):
- content = textwrap.dedent("""\
+ def test_comments_and_joins_case3(self) -> None:
+ content = textwrap.dedent(
+ """\
req1 \\
# comment
req2
- """)
+ """
+ )
result = preprocess(content)
- assert list(result) == [(1, 'req1'), (3, 'req2')]
+ assert list(result) == [(1, "req1"), (3, "req2")]
class TestIgnoreComments:
"""tests for `ignore_comment`"""
- def test_ignore_line(self):
- lines = [(1, ''), (2, 'req1'), (3, 'req2')]
+ def test_ignore_line(self) -> None:
+ lines = [(1, ""), (2, "req1"), (3, "req2")]
result = ignore_comments(lines)
- assert list(result) == [(2, 'req1'), (3, 'req2')]
+ assert list(result) == [(2, "req1"), (3, "req2")]
- def test_ignore_comment(self):
- lines = [(1, 'req1'), (2, '# comment'), (3, 'req2')]
+ def test_ignore_comment(self) -> None:
+ lines = [(1, "req1"), (2, "# comment"), (3, "req2")]
result = ignore_comments(lines)
- assert list(result) == [(1, 'req1'), (3, 'req2')]
+ assert list(result) == [(1, "req1"), (3, "req2")]
- def test_strip_comment(self):
- lines = [(1, 'req1'), (2, 'req # comment'), (3, 'req2')]
+ def test_strip_comment(self) -> None:
+ lines = [(1, "req1"), (2, "req # comment"), (3, "req2")]
result = ignore_comments(lines)
- assert list(result) == [(1, 'req1'), (2, 'req'), (3, 'req2')]
+ assert list(result) == [(1, "req1"), (2, "req"), (3, "req2")]
class TestJoinLines:
"""tests for `join_lines`"""
- def test_join_lines(self):
- lines = enumerate([
- 'line 1',
- 'line 2:1 \\',
- 'line 2:2',
- 'line 3:1 \\',
- 'line 3:2 \\',
- 'line 3:3',
- 'line 4'
- ], start=1)
+ def test_join_lines(self) -> None:
+ lines = enumerate(
+ [
+ "line 1",
+ "line 2:1 \\",
+ "line 2:2",
+ "line 3:1 \\",
+ "line 3:2 \\",
+ "line 3:3",
+ "line 4",
+ ],
+ start=1,
+ )
expect = [
- (1, 'line 1'),
- (2, 'line 2:1 line 2:2'),
- (4, 'line 3:1 line 3:2 line 3:3'),
- (7, 'line 4'),
+ (1, "line 1"),
+ (2, "line 2:1 line 2:2"),
+ (4, "line 3:1 line 3:2 line 3:3"),
+ (7, "line 4"),
]
assert expect == list(join_lines(lines))
- def test_last_line_with_escape(self):
- lines = enumerate([
- 'line 1',
- 'line 2 \\',
- ], start=1)
+ def test_last_line_with_escape(self) -> None:
+ lines = enumerate(
+ [
+ "line 1",
+ "line 2 \\",
+ ],
+ start=1,
+ )
expect = [
- (1, 'line 1'),
- (2, 'line 2 '),
+ (1, "line 1"),
+ (2, "line 2 "),
]
assert expect == list(join_lines(lines))
+class LineProcessor(Protocol):
+ def __call__(
+ self,
+ line: str,
+ filename: str,
+ line_number: int,
+ finder: Optional[PackageFinder] = None,
+ options: Optional[Values] = None,
+ session: Optional[PipSession] = None,
+ constraint: bool = False,
+ ) -> List[InstallRequirement]:
+ ...
+
+
@pytest.fixture
-def line_processor(
- monkeypatch,
- tmpdir,
-):
+def line_processor(monkeypatch: pytest.MonkeyPatch, tmpdir: Path) -> LineProcessor:
def process_line(
- line,
- filename,
- line_number,
- finder=None,
- options=None,
- session=None,
- constraint=False,
- ):
+ line: str,
+ filename: str,
+ line_number: int,
+ finder: Optional[PackageFinder] = None,
+ options: Optional[Values] = None,
+ session: Optional[PipSession] = None,
+ constraint: bool = False,
+ ) -> List[InstallRequirement]:
if session is None:
session = PipSession()
- prefix = '\n' * (line_number - 1)
+ prefix = "\n" * (line_number - 1)
path = tmpdir.joinpath(filename)
path.parent.mkdir(exist_ok=True)
path.write_text(prefix + line)
monkeypatch.chdir(str(tmpdir))
- return list(parse_reqfile(
- filename,
- finder=finder,
- options=options,
- session=session,
- constraint=constraint,
- isolated=options.isolated_mode if options else False
- ))
+ return list(
+ parse_reqfile(
+ filename,
+ finder=finder,
+ options=options,
+ session=session,
+ constraint=constraint,
+ isolated=options.isolated_mode if options else False,
+ )
+ )
return process_line
@@ -202,258 +237,284 @@ def line_processor(
class TestProcessLine:
"""tests for `process_line`"""
- def test_parser_error(self, line_processor):
+ def test_parser_error(self, line_processor: LineProcessor) -> None:
with pytest.raises(RequirementsFileParseError):
line_processor("--bogus", "file", 1)
- def test_parser_offending_line(self, line_processor):
- line = 'pkg==1.0.0 --hash=somehash'
+ def test_parser_offending_line(self, line_processor: LineProcessor) -> None:
+ line = "pkg==1.0.0 --hash=somehash"
with pytest.raises(RequirementsFileParseError) as err:
- line_processor(line, 'file', 1)
+ line_processor(line, "file", 1)
assert line in str(err.value)
- def test_parser_non_offending_line(self, line_processor):
+ def test_parser_non_offending_line(self, line_processor: LineProcessor) -> None:
try:
- line_processor('pkg==1.0.0 --hash=sha256:somehash', 'file', 1)
+ line_processor("pkg==1.0.0 --hash=sha256:somehash", "file", 1)
except RequirementsFileParseError:
- pytest.fail('Reported offending line where it should not.')
+ pytest.fail("Reported offending line where it should not.")
- def test_only_one_req_per_line(self, line_processor):
+ def test_only_one_req_per_line(self, line_processor: LineProcessor) -> None:
# pkg_resources raises the ValueError
with pytest.raises(InstallationError):
line_processor("req1 req2", "file", 1)
- def test_error_message(self, line_processor):
+ def test_error_message(self, line_processor: LineProcessor) -> None:
"""
Test the error message if a parsing error occurs (all of path,
line number, and hint).
"""
with pytest.raises(InstallationError) as exc:
line_processor(
- 'my-package=1.0',
- filename='path/requirements.txt',
- line_number=3
+ "my-package=1.0", filename="path/requirements.txt", line_number=3
)
expected = (
"Invalid requirement: 'my-package=1.0' "
- '(from line 3 of path/requirements.txt)\n'
- 'Hint: = is not a valid operator. Did you mean == ?'
+ "(from line 3 of path/requirements.txt)\n"
+ "Hint: = is not a valid operator. Did you mean == ?"
)
assert str(exc.value) == expected
- def test_yield_line_requirement(self, line_processor):
- line = 'SomeProject'
- filename = 'filename'
- comes_from = f'-r {filename} (line 1)'
+ def test_yield_line_requirement(self, line_processor: LineProcessor) -> None:
+ line = "SomeProject"
+ filename = "filename"
+ comes_from = f"-r {filename} (line 1)"
req = install_req_from_line(line, comes_from=comes_from)
assert repr(line_processor(line, filename, 1)[0]) == repr(req)
- def test_yield_pep440_line_requirement(self, line_processor):
- line = 'SomeProject @ https://url/SomeProject-py2-py3-none-any.whl'
- filename = 'filename'
- comes_from = f'-r {filename} (line 1)'
+ def test_yield_pep440_line_requirement(self, line_processor: LineProcessor) -> None:
+ line = "SomeProject @ https://url/SomeProject-py2-py3-none-any.whl"
+ filename = "filename"
+ comes_from = f"-r {filename} (line 1)"
req = install_req_from_line(line, comes_from=comes_from)
assert repr(line_processor(line, filename, 1)[0]) == repr(req)
- def test_yield_line_constraint(self, line_processor):
- line = 'SomeProject'
- filename = 'filename'
- comes_from = '-c {} (line {})'.format(filename, 1)
- req = install_req_from_line(
- line, comes_from=comes_from, constraint=True)
+ def test_yield_line_constraint(self, line_processor: LineProcessor) -> None:
+ line = "SomeProject"
+ filename = "filename"
+ comes_from = "-c {} (line {})".format(filename, 1)
+ req = install_req_from_line(line, comes_from=comes_from, constraint=True)
found_req = line_processor(line, filename, 1, constraint=True)[0]
assert repr(found_req) == repr(req)
assert found_req.constraint is True
def test_yield_line_requirement_with_spaces_in_specifier(
- self, line_processor
- ):
- line = 'SomeProject >= 2'
- filename = 'filename'
- comes_from = f'-r {filename} (line 1)'
+ self, line_processor: LineProcessor
+ ) -> None:
+ line = "SomeProject >= 2"
+ filename = "filename"
+ comes_from = f"-r {filename} (line 1)"
req = install_req_from_line(line, comes_from=comes_from)
assert repr(line_processor(line, filename, 1)[0]) == repr(req)
- assert str(req.req.specifier) == '>=2'
-
- def test_yield_editable_requirement(self, line_processor):
- url = 'git+https://url#egg=SomeProject'
- line = f'-e {url}'
- filename = 'filename'
- comes_from = f'-r {filename} (line 1)'
+ assert req.req is not None
+ assert str(req.req.specifier) == ">=2"
+
+ def test_yield_editable_requirement(self, line_processor: LineProcessor) -> None:
+ url = "git+https://url#egg=SomeProject"
+ line = f"-e {url}"
+ filename = "filename"
+ comes_from = f"-r {filename} (line 1)"
req = install_req_from_editable(url, comes_from=comes_from)
assert repr(line_processor(line, filename, 1)[0]) == repr(req)
- def test_yield_editable_constraint(self, line_processor):
- url = 'git+https://url#egg=SomeProject'
- line = f'-e {url}'
- filename = 'filename'
- comes_from = '-c {} (line {})'.format(filename, 1)
- req = install_req_from_editable(
- url, comes_from=comes_from, constraint=True)
+ def test_yield_editable_constraint(self, line_processor: LineProcessor) -> None:
+ url = "git+https://url#egg=SomeProject"
+ line = f"-e {url}"
+ filename = "filename"
+ comes_from = "-c {} (line {})".format(filename, 1)
+ req = install_req_from_editable(url, comes_from=comes_from, constraint=True)
found_req = line_processor(line, filename, 1, constraint=True)[0]
assert repr(found_req) == repr(req)
assert found_req.constraint is True
- def test_nested_constraints_file(self, monkeypatch, tmpdir):
- req_name = 'hello'
- req_file = tmpdir / 'parent' / 'req_file.txt'
+ def test_nested_constraints_file(
+ self, monkeypatch: pytest.MonkeyPatch, tmpdir: Path, session: PipSession
+ ) -> None:
+ req_name = "hello"
+ req_file = tmpdir / "parent" / "req_file.txt"
req_file.parent.mkdir()
- req_file.write_text('-c reqs.txt')
- req_file.parent.joinpath('reqs.txt').write_text(req_name)
+ req_file.write_text("-c reqs.txt")
+ req_file.parent.joinpath("reqs.txt").write_text(req_name)
monkeypatch.chdir(str(tmpdir))
- reqs = list(
- parse_reqfile('./parent/req_file.txt', session=session)
- )
+ reqs = list(parse_reqfile("./parent/req_file.txt", session=session))
assert len(reqs) == 1
assert reqs[0].name == req_name
assert reqs[0].constraint
- def test_options_on_a_requirement_line(self, line_processor):
+ def test_options_on_a_requirement_line(self, line_processor: LineProcessor) -> None:
line = (
- 'SomeProject --install-option=yo1 --install-option yo2 '
+ "SomeProject --install-option=yo1 --install-option yo2 "
'--global-option="yo3" --global-option "yo4"'
)
- filename = 'filename'
+ filename = "filename"
req = line_processor(line, filename, 1)[0]
- assert req.global_options == ['yo3', 'yo4']
- assert req.install_options == ['yo1', 'yo2']
+ assert req.global_options == ["yo3", "yo4"]
+ assert req.install_options == ["yo1", "yo2"]
- def test_hash_options(self, line_processor):
+ def test_hash_options(self, line_processor: LineProcessor) -> None:
"""Test the --hash option: mostly its value storage.
Make sure it reads and preserve multiple hashes.
"""
- line = ('SomeProject --hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b1'
- '61e5c1fa7425e73043362938b9824 '
- '--hash=sha384:59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c'
- '3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f '
- '--hash=sha256:486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8'
- 'e5a6c65260e9cb8a7')
- filename = 'filename'
+ line = (
+ "SomeProject --hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b1"
+ "61e5c1fa7425e73043362938b9824 "
+ "--hash=sha384:59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c"
+ "3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f "
+ "--hash=sha256:486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8"
+ "e5a6c65260e9cb8a7"
+ )
+ filename = "filename"
req = line_processor(line, filename, 1)[0]
assert req.hash_options == {
- 'sha256': ['2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e730433'
- '62938b9824',
- '486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65'
- '260e9cb8a7'],
- 'sha384': ['59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcd'
- 'b9c666fa90125a3c79f90397bdf5f6a13de828684f']}
-
- def test_set_isolated(self, line_processor, options):
- line = 'SomeProject'
- filename = 'filename'
+ "sha256": [
+ "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
+ "486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7",
+ ],
+ "sha384": [
+ "59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcd"
+ "b9c666fa90125a3c79f90397bdf5f6a13de828684f"
+ ],
+ }
+
+ def test_set_isolated(
+ self, line_processor: LineProcessor, options: mock.Mock
+ ) -> None:
+ line = "SomeProject"
+ filename = "filename"
options.isolated_mode = True
result = line_processor(line, filename, 1, options=options)
assert result[0].isolated
- def test_set_finder_no_index(self, line_processor, finder):
+ def test_set_finder_no_index(
+ self, line_processor: LineProcessor, finder: PackageFinder
+ ) -> None:
line_processor("--no-index", "file", 1, finder=finder)
assert finder.index_urls == []
- def test_set_finder_index_url(self, line_processor, finder, session):
- line_processor(
- "--index-url=url", "file", 1, finder=finder, session=session)
- assert finder.index_urls == ['url']
- assert session.auth.index_urls == ['url']
+ def test_set_finder_no_index_is_remembered_for_later_invocations(
+ self, line_processor: LineProcessor, finder: PackageFinder
+ ) -> None:
+ line_processor("--no-index", "file", 1, finder=finder)
+ line_processor("--index-url=url", "file", 1, finder=finder)
+ assert finder.index_urls == []
+
+ def test_set_finder_index_url(
+ self, line_processor: LineProcessor, finder: PackageFinder, session: PipSession
+ ) -> None:
+ line_processor("--index-url=url", "file", 1, finder=finder, session=session)
+ assert finder.index_urls == ["url"]
+ assert session.auth.index_urls == ["url"]
- def test_set_finder_find_links(self, line_processor, finder):
+ def test_set_finder_find_links(
+ self, line_processor: LineProcessor, finder: PackageFinder
+ ) -> None:
line_processor("--find-links=url", "file", 1, finder=finder)
- assert finder.find_links == ['url']
+ assert finder.find_links == ["url"]
def test_set_finder_extra_index_urls(
- self, line_processor, finder, session):
+ self, line_processor: LineProcessor, finder: PackageFinder, session: PipSession
+ ) -> None:
line_processor(
- "--extra-index-url=url", "file", 1, finder=finder, session=session)
- assert finder.index_urls == ['url']
- assert session.auth.index_urls == ['url']
+ "--extra-index-url=url", "file", 1, finder=finder, session=session
+ )
+ assert finder.index_urls == ["url"]
+ assert session.auth.index_urls == ["url"]
def test_set_finder_trusted_host(
- self, line_processor, caplog, session, finder
- ):
+ self,
+ line_processor: LineProcessor,
+ caplog: pytest.LogCaptureFixture,
+ session: PipSession,
+ finder: PackageFinder,
+ ) -> None:
with caplog.at_level(logging.INFO):
line_processor(
"--trusted-host=host1 --trusted-host=host2:8080",
- "file.txt", 1, finder=finder, session=session,
+ "file.txt",
+ 1,
+ finder=finder,
+ session=session,
)
- assert list(finder.trusted_hosts) == ['host1', 'host2:8080']
+ assert list(finder.trusted_hosts) == ["host1", "host2:8080"]
session = finder._link_collector.session
- assert (
- session.adapters['https://host1/']
- is session._trusted_host_adapter
- )
- assert (
- session.adapters['https://host2:8080/']
- is session._trusted_host_adapter
- )
+ assert session.adapters["https://host1/"] is session._trusted_host_adapter
+ assert session.adapters["https://host2:8080/"] is session._trusted_host_adapter
# Test the log message.
actual = [(r.levelname, r.message) for r in caplog.records]
- expected = (
- 'INFO', "adding trusted host: 'host1' (from line 1 of file.txt)"
- )
+ expected = ("INFO", "adding trusted host: 'host1' (from line 1 of file.txt)")
assert expected in actual
- def test_set_finder_allow_all_prereleases(self, line_processor, finder):
+ def test_set_finder_allow_all_prereleases(
+ self, line_processor: LineProcessor, finder: PackageFinder
+ ) -> None:
line_processor("--pre", "file", 1, finder=finder)
assert finder.allow_all_prereleases
- def test_use_feature(self, line_processor, options):
+ def test_use_feature(
+ self, line_processor: LineProcessor, options: mock.Mock
+ ) -> None:
"""--use-feature can be set in requirements files."""
- line_processor(
- "--use-feature=2020-resolver", "filename", 1, options=options
- )
+ line_processor("--use-feature=2020-resolver", "filename", 1, options=options)
assert "2020-resolver" in options.features_enabled
def test_relative_local_find_links(
- self, line_processor, finder, monkeypatch, tmpdir
- ):
+ self,
+ line_processor: LineProcessor,
+ finder: PackageFinder,
+ monkeypatch: pytest.MonkeyPatch,
+ tmpdir: Path,
+ ) -> None:
"""
Test a relative find_links path is joined with the req file directory
"""
- base_path = tmpdir / 'path'
+ base_path = tmpdir / "path"
- def normalize(path):
- return os.path.normcase(
- os.path.abspath(os.path.normpath(str(path)))
- )
+ def normalize(path: Path) -> str:
+ return os.path.normcase(os.path.abspath(os.path.normpath(str(path))))
# Make sure the test also passes on windows
- req_file = normalize(base_path / 'req_file.txt')
- nested_link = normalize(base_path / 'rel_path')
+ req_file = normalize(base_path / "req_file.txt")
+ nested_link = normalize(base_path / "rel_path")
exists_ = os.path.exists
- def exists(path):
+ def exists(path: str) -> bool:
if path == nested_link:
return True
else:
- exists_(path)
+ return exists_(path)
- monkeypatch.setattr(os.path, 'exists', exists)
+ monkeypatch.setattr(os.path, "exists", exists)
line_processor("--find-links=rel_path", req_file, 1, finder=finder)
assert finder.find_links == [nested_link]
def test_relative_http_nested_req_files(
- self, finder, session, monkeypatch
- ):
+ self,
+ finder: PackageFinder,
+ session: PipSession,
+ monkeypatch: pytest.MonkeyPatch,
+ ) -> None:
"""
Test a relative nested req file path is joined with the req file url
"""
- req_name = 'hello'
- req_file = 'http://me.com/me/req_file.txt'
+ req_name = "hello"
+ req_file = "http://me.com/me/req_file.txt"
- def get_file_content(filename, *args, **kwargs):
+ def get_file_content(
+ filename: str, *args: Any, **kwargs: Any
+ ) -> Tuple[None, str]:
if filename == req_file:
- return None, '-r reqs.txt'
- elif filename == 'http://me.com/me/reqs.txt':
+ return None, "-r reqs.txt"
+ elif filename == "http://me.com/me/reqs.txt":
return None, req_name
- assert False, f'Unexpected file requested {filename}'
+ assert False, f"Unexpected file requested {filename}"
monkeypatch.setattr(
- pip._internal.req.req_file, 'get_file_content', get_file_content
+ pip._internal.req.req_file, "get_file_content", get_file_content
)
result = list(parse_reqfile(req_file, session=session))
@@ -462,41 +523,39 @@ class TestProcessLine:
assert not result[0].constraint
def test_relative_local_nested_req_files(
- self, session, monkeypatch, tmpdir
- ):
+ self, session: PipSession, monkeypatch: pytest.MonkeyPatch, tmpdir: Path
+ ) -> None:
"""
Test a relative nested req file path is joined with the req file dir
"""
- req_name = 'hello'
- req_file = tmpdir / 'parent' / 'req_file.txt'
+ req_name = "hello"
+ req_file = tmpdir / "parent" / "req_file.txt"
req_file.parent.mkdir()
- req_file.write_text('-r reqs.txt')
- req_file.parent.joinpath('reqs.txt').write_text(req_name)
+ req_file.write_text("-r reqs.txt")
+ req_file.parent.joinpath("reqs.txt").write_text(req_name)
monkeypatch.chdir(str(tmpdir))
- reqs = list(
- parse_reqfile('./parent/req_file.txt', session=session)
- )
+ reqs = list(parse_reqfile("./parent/req_file.txt", session=session))
assert len(reqs) == 1
assert reqs[0].name == req_name
assert not reqs[0].constraint
def test_absolute_local_nested_req_files(
- self, session, monkeypatch, tmpdir
- ):
+ self, session: PipSession, tmpdir: Path
+ ) -> None:
"""
Test an absolute nested req file path
"""
- req_name = 'hello'
- req_file = tmpdir / 'parent' / 'req_file.txt'
+ req_name = "hello"
+ req_file = tmpdir / "parent" / "req_file.txt"
req_file.parent.mkdir()
- other_req_file = tmpdir / 'other' / 'reqs.txt'
+ other_req_file = tmpdir / "other" / "reqs.txt"
other_req_file.parent.mkdir()
# POSIX-ify the path, since Windows backslashes aren't supported.
- other_req_file_str = str(other_req_file).replace('\\', '/')
+ other_req_file_str = str(other_req_file).replace("\\", "/")
- req_file.write_text(f'-r {other_req_file_str}')
+ req_file.write_text(f"-r {other_req_file_str}")
other_req_file.write_text(req_name)
reqs = list(parse_reqfile(str(req_file), session=session))
@@ -505,24 +564,26 @@ class TestProcessLine:
assert not reqs[0].constraint
def test_absolute_http_nested_req_file_in_local(
- self, session, monkeypatch, tmpdir
- ):
+ self, session: PipSession, monkeypatch: pytest.MonkeyPatch, tmpdir: Path
+ ) -> None:
"""
Test a nested req file url in a local req file
"""
- req_name = 'hello'
- req_file = tmpdir / 'req_file.txt'
- nested_req_file = 'http://me.com/me/req_file.txt'
+ req_name = "hello"
+ req_file = tmpdir / "req_file.txt"
+ nested_req_file = "http://me.com/me/req_file.txt"
- def get_file_content(filename, *args, **kwargs):
+ def get_file_content(
+ filename: str, *args: Any, **kwargs: Any
+ ) -> Tuple[None, str]:
if filename == str(req_file):
- return None, f'-r {nested_req_file}'
+ return None, f"-r {nested_req_file}"
elif filename == nested_req_file:
return None, req_name
- assert False, f'Unexpected file requested {filename}'
+ assert False, f"Unexpected file requested {filename}"
monkeypatch.setattr(
- pip._internal.req.req_file, 'get_file_content', get_file_content
+ pip._internal.req.req_file, "get_file_content", get_file_content
)
result = list(parse_reqfile(req_file, session=session))
@@ -532,227 +593,280 @@ class TestProcessLine:
class TestBreakOptionsArgs:
+ def test_no_args(self) -> None:
+ assert ("", "--option") == break_args_options("--option")
- def test_no_args(self):
- assert ('', '--option') == break_args_options('--option')
-
- def test_no_options(self):
- assert ('arg arg', '') == break_args_options('arg arg')
+ def test_no_options(self) -> None:
+ assert ("arg arg", "") == break_args_options("arg arg")
- def test_args_short_options(self):
- result = break_args_options('arg arg -s')
- assert ('arg arg', '-s') == result
+ def test_args_short_options(self) -> None:
+ result = break_args_options("arg arg -s")
+ assert ("arg arg", "-s") == result
- def test_args_long_options(self):
- result = break_args_options('arg arg --long')
- assert ('arg arg', '--long') == result
+ def test_args_long_options(self) -> None:
+ result = break_args_options("arg arg --long")
+ assert ("arg arg", "--long") == result
class TestOptionVariants:
# this suite is really just testing optparse, but added it anyway
- def test_variant1(self, line_processor, finder):
+ def test_variant1(
+ self, line_processor: LineProcessor, finder: PackageFinder
+ ) -> None:
line_processor("-i url", "file", 1, finder=finder)
- assert finder.index_urls == ['url']
+ assert finder.index_urls == ["url"]
- def test_variant2(self, line_processor, finder):
+ def test_variant2(
+ self, line_processor: LineProcessor, finder: PackageFinder
+ ) -> None:
line_processor("-i 'url'", "file", 1, finder=finder)
- assert finder.index_urls == ['url']
+ assert finder.index_urls == ["url"]
- def test_variant3(self, line_processor, finder):
+ def test_variant3(
+ self, line_processor: LineProcessor, finder: PackageFinder
+ ) -> None:
line_processor("--index-url=url", "file", 1, finder=finder)
- assert finder.index_urls == ['url']
+ assert finder.index_urls == ["url"]
- def test_variant4(self, line_processor, finder):
+ def test_variant4(
+ self, line_processor: LineProcessor, finder: PackageFinder
+ ) -> None:
line_processor("--index-url url", "file", 1, finder=finder)
- assert finder.index_urls == ['url']
+ assert finder.index_urls == ["url"]
- def test_variant5(self, line_processor, finder):
+ def test_variant5(
+ self, line_processor: LineProcessor, finder: PackageFinder
+ ) -> None:
line_processor("--index-url='url'", "file", 1, finder=finder)
- assert finder.index_urls == ['url']
+ assert finder.index_urls == ["url"]
class TestParseRequirements:
"""tests for `parse_reqfile`"""
@pytest.mark.network
- def test_remote_reqs_parse(self):
+ def test_remote_reqs_parse(self) -> None:
"""
Test parsing a simple remote requirements file
"""
# this requirements file just contains a comment previously this has
# failed in py3: https://github.com/pypa/pip/issues/760
for _ in parse_reqfile(
- 'https://raw.githubusercontent.com/pypa/'
- 'pip-test-package/master/'
- 'tests/req_just_comment.txt', session=PipSession()):
+ "https://raw.githubusercontent.com/pypa/"
+ "pip-test-package/master/"
+ "tests/req_just_comment.txt",
+ session=PipSession(),
+ ):
pass
- def test_multiple_appending_options(self, tmpdir, finder, options):
+ def test_multiple_appending_options(
+ self, tmpdir: Path, finder: PackageFinder, options: mock.Mock
+ ) -> None:
with open(tmpdir.joinpath("req1.txt"), "w") as fp:
fp.write("--extra-index-url url1 \n")
fp.write("--extra-index-url url2 ")
- list(parse_reqfile(tmpdir.joinpath("req1.txt"), finder=finder,
- session=PipSession(), options=options))
+ list(
+ parse_reqfile(
+ tmpdir.joinpath("req1.txt"),
+ finder=finder,
+ session=PipSession(),
+ options=options,
+ )
+ )
- assert finder.index_urls == ['url1', 'url2']
+ assert finder.index_urls == ["url1", "url2"]
- def test_expand_existing_env_variables(self, tmpdir, finder):
- template = (
- 'https://{}:x-oauth-basic@github.com/'
- 'user/{}/archive/master.zip'
- )
+ def test_expand_existing_env_variables(
+ self, tmpdir: Path, finder: PackageFinder
+ ) -> None:
+ template = "https://{}:x-oauth-basic@github.com/user/{}/archive/master.zip"
- def make_var(name):
- return f'${{{name}}}'
+ def make_var(name: str) -> str:
+ return f"${{{name}}}"
- env_vars = collections.OrderedDict([
- ('GITHUB_TOKEN', 'notarealtoken'),
- ('DO_12_FACTOR', 'awwyeah'),
- ])
+ env_vars = collections.OrderedDict(
+ [
+ ("GITHUB_TOKEN", "notarealtoken"),
+ ("DO_12_FACTOR", "awwyeah"),
+ ]
+ )
- with open(tmpdir.joinpath('req1.txt'), 'w') as fp:
+ with open(tmpdir.joinpath("req1.txt"), "w") as fp:
fp.write(template.format(*map(make_var, env_vars)))
# Construct the session outside the monkey-patch, since it access the
# env
session = PipSession()
- with patch('pip._internal.req.req_file.os.getenv') as getenv:
+ with mock.patch("pip._internal.req.req_file.os.getenv") as getenv:
getenv.side_effect = lambda n: env_vars[n]
- reqs = list(parse_reqfile(
- tmpdir.joinpath('req1.txt'),
- finder=finder,
- session=session
- ))
+ reqs = list(
+ parse_reqfile(
+ tmpdir.joinpath("req1.txt"), finder=finder, session=session
+ )
+ )
- assert len(reqs) == 1, \
- 'parsing requirement file with env variable failed'
+ assert len(reqs) == 1, "parsing requirement file with env variable failed"
expected_url = template.format(*env_vars.values())
- assert reqs[0].link.url == expected_url, \
- 'variable expansion in req file failed'
+ assert reqs[0].link is not None
+ assert reqs[0].link.url == expected_url, "variable expansion in req file failed"
- def test_expand_missing_env_variables(self, tmpdir, finder):
+ def test_expand_missing_env_variables(
+ self, tmpdir: Path, finder: PackageFinder
+ ) -> None:
req_url = (
- 'https://${NON_EXISTENT_VARIABLE}:$WRONG_FORMAT@'
- '%WINDOWS_FORMAT%github.com/user/repo/archive/master.zip'
+ "https://${NON_EXISTENT_VARIABLE}:$WRONG_FORMAT@"
+ "%WINDOWS_FORMAT%github.com/user/repo/archive/master.zip"
)
- with open(tmpdir.joinpath('req1.txt'), 'w') as fp:
+ with open(tmpdir.joinpath("req1.txt"), "w") as fp:
fp.write(req_url)
# Construct the session outside the monkey-patch, since it access the
# env
session = PipSession()
- with patch('pip._internal.req.req_file.os.getenv') as getenv:
- getenv.return_value = ''
+ with mock.patch("pip._internal.req.req_file.os.getenv") as getenv:
+ getenv.return_value = ""
- reqs = list(parse_reqfile(
- tmpdir.joinpath('req1.txt'),
- finder=finder,
- session=session
- ))
+ reqs = list(
+ parse_reqfile(
+ tmpdir.joinpath("req1.txt"), finder=finder, session=session
+ )
+ )
- assert len(reqs) == 1, \
- 'parsing requirement file with env variable failed'
- assert reqs[0].link.url == req_url, \
- 'ignoring invalid env variable in req file failed'
+ assert len(reqs) == 1, "parsing requirement file with env variable failed"
+ assert reqs[0].link is not None
+ assert (
+ reqs[0].link.url == req_url
+ ), "ignoring invalid env variable in req file failed"
- def test_join_lines(self, tmpdir, finder):
+ def test_join_lines(self, tmpdir: Path, finder: PackageFinder) -> None:
with open(tmpdir.joinpath("req1.txt"), "w") as fp:
fp.write("--extra-index-url url1 \\\n--extra-index-url url2")
- list(parse_reqfile(tmpdir.joinpath("req1.txt"), finder=finder,
- session=PipSession()))
+ list(
+ parse_reqfile(
+ tmpdir.joinpath("req1.txt"), finder=finder, session=PipSession()
+ )
+ )
- assert finder.index_urls == ['url1', 'url2']
+ assert finder.index_urls == ["url1", "url2"]
- def test_req_file_parse_no_only_binary(self, data, finder):
- list(parse_reqfile(
- data.reqfiles.joinpath("supported_options2.txt"),
- finder=finder,
- session=PipSession()))
- expected = FormatControl({'fred'}, {'wilma'})
+ def test_req_file_parse_no_only_binary(
+ self, data: TestData, finder: PackageFinder
+ ) -> None:
+ list(
+ parse_reqfile(
+ data.reqfiles.joinpath("supported_options2.txt"),
+ finder=finder,
+ session=PipSession(),
+ )
+ )
+ expected = FormatControl({"fred"}, {"wilma"})
assert finder.format_control == expected
- def test_req_file_parse_comment_start_of_line(self, tmpdir, finder):
+ def test_req_file_parse_comment_start_of_line(
+ self, tmpdir: Path, finder: PackageFinder
+ ) -> None:
"""
Test parsing comments in a requirements file
"""
with open(tmpdir.joinpath("req1.txt"), "w") as fp:
fp.write("# Comment ")
- reqs = list(parse_reqfile(tmpdir.joinpath("req1.txt"),
- finder=finder,
- session=PipSession()))
+ reqs = list(
+ parse_reqfile(
+ tmpdir.joinpath("req1.txt"), finder=finder, session=PipSession()
+ )
+ )
assert not reqs
- def test_req_file_parse_comment_end_of_line_with_url(self, tmpdir, finder):
+ def test_req_file_parse_comment_end_of_line_with_url(
+ self, tmpdir: Path, finder: PackageFinder
+ ) -> None:
"""
Test parsing comments in a requirements file
"""
with open(tmpdir.joinpath("req1.txt"), "w") as fp:
fp.write("https://example.com/foo.tar.gz # Comment ")
- reqs = list(parse_reqfile(tmpdir.joinpath("req1.txt"),
- finder=finder,
- session=PipSession()))
+ reqs = list(
+ parse_reqfile(
+ tmpdir.joinpath("req1.txt"), finder=finder, session=PipSession()
+ )
+ )
assert len(reqs) == 1
+ assert reqs[0].link is not None
assert reqs[0].link.url == "https://example.com/foo.tar.gz"
- def test_req_file_parse_egginfo_end_of_line_with_url(self, tmpdir, finder):
+ def test_req_file_parse_egginfo_end_of_line_with_url(
+ self, tmpdir: Path, finder: PackageFinder
+ ) -> None:
"""
Test parsing comments in a requirements file
"""
with open(tmpdir.joinpath("req1.txt"), "w") as fp:
fp.write("https://example.com/foo.tar.gz#egg=wat")
- reqs = list(parse_reqfile(tmpdir.joinpath("req1.txt"),
- finder=finder,
- session=PipSession()))
+ reqs = list(
+ parse_reqfile(
+ tmpdir.joinpath("req1.txt"), finder=finder, session=PipSession()
+ )
+ )
assert len(reqs) == 1
assert reqs[0].name == "wat"
- def test_req_file_no_finder(self, tmpdir):
+ def test_req_file_no_finder(self, tmpdir: Path) -> None:
"""
Test parsing a requirements file without a finder
"""
with open(tmpdir.joinpath("req.txt"), "w") as fp:
- fp.write("""
+ fp.write(
+ """
--find-links https://example.com/
--index-url https://example.com/
--extra-index-url https://two.example.com/
--no-use-wheel
--no-index
- """)
+ """
+ )
parse_reqfile(tmpdir.joinpath("req.txt"), session=PipSession())
- def test_install_requirements_with_options(self, tmpdir, finder, session,
- options):
- global_option = '--dry-run'
- install_option = '--prefix=/opt'
-
- content = '''
+ def test_install_requirements_with_options(
+ self,
+ tmpdir: Path,
+ finder: PackageFinder,
+ session: PipSession,
+ options: mock.Mock,
+ ) -> None:
+ global_option = "--dry-run"
+ install_option = "--prefix=/opt"
+
+ content = """
--only-binary :all:
INITools==2.0 --global-option="{global_option}" \
--install-option "{install_option}"
- '''.format(global_option=global_option, install_option=install_option)
+ """.format(
+ global_option=global_option, install_option=install_option
+ )
with requirements_file(content, tmpdir) as reqs_file:
- req = next(parse_reqfile(reqs_file.resolve(),
- finder=finder,
- options=options,
- session=session))
+ req = next(
+ parse_reqfile(
+ reqs_file.resolve(), finder=finder, options=options, session=session
+ )
+ )
req.source_dir = os.curdir
- with patch.object(subprocess, 'Popen') as popen:
+ with mock.patch.object(subprocess, "Popen") as popen:
popen.return_value.stdout.readline.return_value = b""
try:
req.install([])
@@ -762,8 +876,10 @@ class TestParseRequirements:
last_call = popen.call_args_list[-1]
args = last_call[0][0]
assert (
- 0 < args.index(global_option) < args.index('install') <
- args.index(install_option)
+ 0
+ < args.index(global_option)
+ < args.index("install")
+ < args.index(install_option)
)
- assert options.format_control.no_binary == {':all:'}
+ assert options.format_control.no_binary == {":all:"}
assert options.format_control.only_binary == set()
diff --git a/tests/unit/test_req_install.py b/tests/unit/test_req_install.py
index d8eee8d13..4bb71a743 100644
--- a/tests/unit/test_req_install.py
+++ b/tests/unit/test_req_install.py
@@ -1,5 +1,6 @@
import os
import tempfile
+from pathlib import Path
import pytest
from pip._vendor.packaging.requirements import Requirement
@@ -15,17 +16,18 @@ from pip._internal.req.req_install import InstallRequirement
class TestInstallRequirementBuildDirectory:
# no need to test symlinks on Windows
@pytest.mark.skipif("sys.platform == 'win32'")
- def test_tmp_build_directory(self):
+ def test_tmp_build_directory(self) -> None:
# when req is None, we can produce a temporary directory
# Make sure we're handling it correctly with real path.
requirement = InstallRequirement(None, None)
- tmp_dir = tempfile.mkdtemp('-build', 'pip-')
+ tmp_dir = tempfile.mkdtemp("-build", "pip-")
tmp_build_dir = requirement.ensure_build_location(
- tmp_dir, autodelete=False, parallel_builds=False,
+ tmp_dir,
+ autodelete=False,
+ parallel_builds=False,
)
- assert (
- os.path.dirname(tmp_build_dir) ==
- os.path.realpath(os.path.dirname(tmp_dir))
+ assert os.path.dirname(tmp_build_dir) == os.path.realpath(
+ os.path.dirname(tmp_dir)
)
# are we on a system where /tmp is a symlink
if os.path.realpath(tmp_dir) != os.path.abspath(tmp_dir):
@@ -35,25 +37,22 @@ class TestInstallRequirementBuildDirectory:
os.rmdir(tmp_dir)
assert not os.path.exists(tmp_dir)
- def test_forward_slash_results_in_a_link(self, tmpdir):
+ def test_forward_slash_results_in_a_link(self, tmpdir: Path) -> None:
install_dir = tmpdir / "foo" / "bar"
# Just create a file for letting the logic work
setup_py_path = install_dir / "setup.py"
os.makedirs(str(install_dir))
- with open(setup_py_path, 'w') as f:
- f.write('')
+ with open(setup_py_path, "w") as f:
+ f.write("")
- requirement = install_req_from_line(
- str(install_dir).replace(os.sep, os.altsep or os.sep)
- )
+ requirement = install_req_from_line(install_dir.as_posix())
assert requirement.link is not None
class TestInstallRequirementFrom:
-
- def test_install_req_from_string_invalid_requirement(self):
+ def test_install_req_from_string_invalid_requirement(self) -> None:
"""
Requirement strings that cannot be parsed by
packaging.requirements.Requirement raise an InstallationError.
@@ -61,50 +60,53 @@ class TestInstallRequirementFrom:
with pytest.raises(InstallationError) as excinfo:
install_req_from_req_string("http:/this/is/invalid")
- assert str(excinfo.value) == (
- "Invalid requirement: 'http:/this/is/invalid'"
- )
+ assert str(excinfo.value) == ("Invalid requirement: 'http:/this/is/invalid'")
- def test_install_req_from_string_without_comes_from(self):
+ def test_install_req_from_string_without_comes_from(self) -> None:
"""
Test to make sure that install_req_from_string succeeds
when called with URL (PEP 508) but without comes_from.
"""
# Test with a PEP 508 url install string:
- wheel_url = ("https://download.pytorch.org/whl/cu90/"
- "torch-1.0.0-cp36-cp36m-win_amd64.whl")
+ wheel_url = (
+ "https://download.pytorch.org/whl/cu90/"
+ "torch-1.0.0-cp36-cp36m-win_amd64.whl"
+ )
install_str = "torch@ " + wheel_url
install_req = install_req_from_req_string(install_str)
assert isinstance(install_req, InstallRequirement)
+ assert install_req.link is not None
assert install_req.link.url == wheel_url
+ assert install_req.req is not None
assert install_req.req.url == wheel_url
assert install_req.comes_from is None
assert install_req.is_wheel
- def test_install_req_from_string_with_comes_from_without_link(self):
+ def test_install_req_from_string_with_comes_from_without_link(self) -> None:
"""
Test to make sure that install_req_from_string succeeds
when called with URL (PEP 508) and comes_from
does not have a link.
"""
# Test with a PEP 508 url install string:
- wheel_url = ("https://download.pytorch.org/whl/cu90/"
- "torch-1.0.0-cp36-cp36m-win_amd64.whl")
+ wheel_url = (
+ "https://download.pytorch.org/whl/cu90/"
+ "torch-1.0.0-cp36-cp36m-win_amd64.whl"
+ )
install_str = "torch@ " + wheel_url
# Dummy numpy "comes_from" requirement without link:
- comes_from = InstallRequirement(
- Requirement("numpy>=1.15.0"), comes_from=None
- )
+ comes_from = InstallRequirement(Requirement("numpy>=1.15.0"), comes_from=None)
# Attempt install from install string comes:
- install_req = install_req_from_req_string(
- install_str, comes_from=comes_from
- )
+ install_req = install_req_from_req_string(install_str, comes_from=comes_from)
assert isinstance(install_req, InstallRequirement)
+ assert isinstance(install_req.comes_from, InstallRequirement)
assert install_req.comes_from.link is None
+ assert install_req.link is not None
assert install_req.link.url == wheel_url
+ assert install_req.req is not None
assert install_req.req.url == wheel_url
assert install_req.is_wheel
diff --git a/tests/unit/test_req_uninstall.py b/tests/unit/test_req_uninstall.py
index 8de2ae9bc..4d99acfd3 100644
--- a/tests/unit/test_req_uninstall.py
+++ b/tests/unit/test_req_uninstall.py
@@ -1,5 +1,7 @@
import os
import sys
+from pathlib import Path
+from typing import Iterator, List, Optional, Tuple
from unittest.mock import Mock
import pytest
@@ -19,30 +21,33 @@ from tests.lib import create_file
# Pretend all files are local, so UninstallPathSet accepts files in the tmpdir,
# outside the virtualenv
-def mock_is_local(path):
+def mock_is_local(path: str) -> bool:
return True
-def test_uninstallation_paths():
+def test_uninstallation_paths() -> None:
class dist:
- def get_metadata_lines(self, record):
- return ['file.py,,',
- 'file.pyc,,',
- 'file.so,,',
- 'nopyc.py']
- location = ''
+ def iter_declared_entries(self) -> Optional[Iterator[str]]:
+ yield "file.py"
+ yield "file.pyc"
+ yield "file.so"
+ yield "nopyc.py"
+
+ location = ""
d = dist()
paths = list(uninstallation_paths(d))
- expected = ['file.py',
- 'file.pyc',
- 'file.pyo',
- 'file.so',
- 'nopyc.py',
- 'nopyc.pyc',
- 'nopyc.pyo']
+ expected = [
+ "file.py",
+ "file.pyc",
+ "file.pyo",
+ "file.so",
+ "nopyc.py",
+ "nopyc.pyc",
+ "nopyc.pyo",
+ ]
assert paths == expected
@@ -52,30 +57,30 @@ def test_uninstallation_paths():
assert paths2 == paths
-def test_compressed_listing(tmpdir):
- def in_tmpdir(paths):
+def test_compressed_listing(tmpdir: Path) -> None:
+ def in_tmpdir(paths: List[str]) -> List[str]:
li = []
for path in paths:
- li.append(
- str(os.path.join(tmpdir, path.replace("/", os.path.sep)))
- )
+ li.append(str(os.path.join(tmpdir, path.replace("/", os.path.sep))))
return li
- sample = in_tmpdir([
- "lib/mypkg.dist-info/METADATA",
- "lib/mypkg.dist-info/PKG-INFO",
- "lib/mypkg/would_be_removed.txt",
- "lib/mypkg/would_be_skipped.skip.txt",
- "lib/mypkg/__init__.py",
- "lib/mypkg/my_awesome_code.py",
- "lib/mypkg/__pycache__/my_awesome_code-magic.pyc",
- "lib/mypkg/support/support_file.py",
- "lib/mypkg/support/more_support.py",
- "lib/mypkg/support/would_be_skipped.skip.py",
- "lib/mypkg/support/__pycache__/support_file-magic.pyc",
- "lib/random_other_place/file_without_a_dot_pyc",
- "bin/mybin",
- ])
+ sample = in_tmpdir(
+ [
+ "lib/mypkg.dist-info/METADATA",
+ "lib/mypkg.dist-info/PKG-INFO",
+ "lib/mypkg/would_be_removed.txt",
+ "lib/mypkg/would_be_skipped.skip.txt",
+ "lib/mypkg/__init__.py",
+ "lib/mypkg/my_awesome_code.py",
+ "lib/mypkg/__pycache__/my_awesome_code-magic.pyc",
+ "lib/mypkg/support/support_file.py",
+ "lib/mypkg/support/more_support.py",
+ "lib/mypkg/support/would_be_skipped.skip.py",
+ "lib/mypkg/support/__pycache__/support_file-magic.pyc",
+ "lib/random_other_place/file_without_a_dot_pyc",
+ "bin/mybin",
+ ]
+ )
# Create the required files
for fname in sample:
@@ -84,30 +89,36 @@ def test_compressed_listing(tmpdir):
# Remove the files to be skipped from the paths
sample = [path for path in sample if ".skip." not in path]
- expected_remove = in_tmpdir([
- "bin/mybin",
- "lib/mypkg.dist-info/*",
- "lib/mypkg/*",
- "lib/random_other_place/file_without_a_dot_pyc",
- ])
-
- expected_skip = in_tmpdir([
- "lib/mypkg/would_be_skipped.skip.txt",
- "lib/mypkg/support/would_be_skipped.skip.py",
- ])
-
- expected_rename = in_tmpdir([
- "bin/",
- "lib/mypkg.dist-info/",
- "lib/mypkg/would_be_removed.txt",
- "lib/mypkg/__init__.py",
- "lib/mypkg/my_awesome_code.py",
- "lib/mypkg/__pycache__/",
- "lib/mypkg/support/support_file.py",
- "lib/mypkg/support/more_support.py",
- "lib/mypkg/support/__pycache__/",
- "lib/random_other_place/",
- ])
+ expected_remove = in_tmpdir(
+ [
+ "bin/mybin",
+ "lib/mypkg.dist-info/*",
+ "lib/mypkg/*",
+ "lib/random_other_place/file_without_a_dot_pyc",
+ ]
+ )
+
+ expected_skip = in_tmpdir(
+ [
+ "lib/mypkg/would_be_skipped.skip.txt",
+ "lib/mypkg/support/would_be_skipped.skip.py",
+ ]
+ )
+
+ expected_rename = in_tmpdir(
+ [
+ "bin/",
+ "lib/mypkg.dist-info/",
+ "lib/mypkg/would_be_removed.txt",
+ "lib/mypkg/__init__.py",
+ "lib/mypkg/my_awesome_code.py",
+ "lib/mypkg/__pycache__/",
+ "lib/mypkg/support/support_file.py",
+ "lib/mypkg/support/more_support.py",
+ "lib/mypkg/support/__pycache__/",
+ "lib/random_other_place/",
+ ]
+ )
will_remove, will_skip = compress_for_output_listing(sample)
will_rename = compress_for_rename(sample)
@@ -117,42 +128,37 @@ def test_compressed_listing(tmpdir):
class TestUninstallPathSet:
- def test_add(self, tmpdir, monkeypatch):
- monkeypatch.setattr(pip._internal.req.req_uninstall, 'is_local',
- mock_is_local)
+ def test_add(self, tmpdir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr(pip._internal.req.req_uninstall, "is_local", mock_is_local)
# Fix case for windows tests
- file_extant = os.path.normcase(os.path.join(tmpdir, 'foo'))
- file_nonexistent = os.path.normcase(
- os.path.join(tmpdir, 'nonexistent'))
- with open(file_extant, 'w'):
+ file_extant = os.path.normcase(os.path.join(tmpdir, "foo"))
+ file_nonexistent = os.path.normcase(os.path.join(tmpdir, "nonexistent"))
+ with open(file_extant, "w"):
pass
ups = UninstallPathSet(dist=Mock())
- assert ups.paths == set()
+ assert ups._paths == set()
ups.add(file_extant)
- assert ups.paths == {file_extant}
+ assert ups._paths == {file_extant}
ups.add(file_nonexistent)
- assert ups.paths == {file_extant}
+ assert ups._paths == {file_extant}
- def test_add_pth(self, tmpdir, monkeypatch):
- monkeypatch.setattr(pip._internal.req.req_uninstall, 'is_local',
- mock_is_local)
+ def test_add_pth(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr(pip._internal.req.req_uninstall, "is_local", mock_is_local)
# Fix case for windows tests
- tmpdir = os.path.normcase(tmpdir)
- on_windows = sys.platform == 'win32'
- pth_file = os.path.join(tmpdir, 'foo.pth')
- relative = '../../example'
+ tmpdir = os.path.normcase(tmp_path)
+ on_windows = sys.platform == "win32"
+ pth_file = os.path.join(tmpdir, "foo.pth")
+ relative = "../../example"
if on_windows:
- share = '\\\\example\\share\\'
- share_com = '\\\\example.com\\share\\'
+ share = "\\\\example\\share\\"
+ share_com = "\\\\example.com\\share\\"
# Create a .pth file for testing
- with open(pth_file, 'w') as f:
- f.writelines([tmpdir, '\n',
- relative, '\n'])
+ with open(pth_file, "w") as f:
+ f.writelines([tmpdir, "\n", relative, "\n"])
if on_windows:
- f.writelines([share, '\n',
- share_com, '\n'])
+ f.writelines([share, "\n", share_com, "\n"])
# Add paths to be removed
pth = UninstallPthEntries(pth_file)
pth.add(tmpdir)
@@ -168,55 +174,55 @@ class TestUninstallPathSet:
assert pth.entries == check
@pytest.mark.skipif("sys.platform == 'win32'")
- def test_add_symlink(self, tmpdir, monkeypatch):
- monkeypatch.setattr(pip._internal.req.req_uninstall, 'is_local',
- mock_is_local)
- f = os.path.join(tmpdir, 'foo')
- with open(f, 'w'):
+ def test_add_symlink(self, tmpdir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr(pip._internal.req.req_uninstall, "is_local", mock_is_local)
+ f = os.path.join(tmpdir, "foo")
+ with open(f, "w"):
pass
- foo_link = os.path.join(tmpdir, 'foo_link')
+ foo_link = os.path.join(tmpdir, "foo_link")
os.symlink(f, foo_link)
ups = UninstallPathSet(dist=Mock())
ups.add(foo_link)
- assert ups.paths == {foo_link}
+ assert ups._paths == {foo_link}
- def test_compact_shorter_path(self, monkeypatch):
- monkeypatch.setattr(pip._internal.req.req_uninstall, 'is_local',
- mock_is_local)
- monkeypatch.setattr('os.path.exists', lambda p: True)
+ def test_compact_shorter_path(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr(pip._internal.req.req_uninstall, "is_local", mock_is_local)
+ monkeypatch.setattr("os.path.exists", lambda p: True)
# This deals with nt/posix path differences
- short_path = os.path.normcase(os.path.abspath(
- os.path.join(os.path.sep, 'path')))
+ short_path = os.path.normcase(
+ os.path.abspath(os.path.join(os.path.sep, "path"))
+ )
ups = UninstallPathSet(dist=Mock())
ups.add(short_path)
- ups.add(os.path.join(short_path, 'longer'))
- assert compact(ups.paths) == {short_path}
+ ups.add(os.path.join(short_path, "longer"))
+ assert compact(ups._paths) == {short_path}
@pytest.mark.skipif("sys.platform == 'win32'")
- def test_detect_symlink_dirs(self, monkeypatch, tmpdir):
- monkeypatch.setattr(pip._internal.req.req_uninstall, 'is_local',
- mock_is_local)
+ def test_detect_symlink_dirs(
+ self, monkeypatch: pytest.MonkeyPatch, tmpdir: Path
+ ) -> None:
+ monkeypatch.setattr(pip._internal.req.req_uninstall, "is_local", mock_is_local)
# construct 2 paths:
# tmpdir/dir/file
# tmpdir/dirlink/file (where dirlink is a link to dir)
- d = tmpdir.joinpath('dir')
+ d = tmpdir.joinpath("dir")
d.mkdir()
- dlink = tmpdir.joinpath('dirlink')
+ dlink = tmpdir.joinpath("dirlink")
os.symlink(d, dlink)
- d.joinpath('file').touch()
- path1 = str(d.joinpath('file'))
- path2 = str(dlink.joinpath('file'))
+ d.joinpath("file").touch()
+ path1 = str(d.joinpath("file"))
+ path2 = str(dlink.joinpath("file"))
ups = UninstallPathSet(dist=Mock())
ups.add(path1)
ups.add(path2)
- assert ups.paths == {path1}
+ assert ups._paths == {path1}
class TestStashedUninstallPathSet:
- WALK_RESULT = [
+ WALK_RESULT: List[Tuple[str, List[str], List[str]]] = [
("A", ["B", "C"], ["a.py"]),
("A/B", ["D"], ["b.py"]),
("A/B/D", [], ["c.py"]),
@@ -228,35 +234,43 @@ class TestStashedUninstallPathSet:
]
@classmethod
- def mock_walk(cls, root):
+ def mock_walk(cls, root: str) -> Iterator[Tuple[str, List[str], List[str]]]:
for dirname, subdirs, files in cls.WALK_RESULT:
dirname = os.path.sep.join(dirname.split("/"))
if dirname.startswith(root):
- yield dirname[len(root) + 1:], subdirs, files
-
- def test_compress_for_rename(self, monkeypatch):
- paths = [os.path.sep.join(p.split("/")) for p in [
- "A/B/b.py",
- "A/B/D/c.py",
- "A/C/d.py",
- "A/E/f.py",
- "A/G/g.py",
- ]]
-
- expected_paths = [os.path.sep.join(p.split("/")) for p in [
- "A/B/", # selected everything below A/B
- "A/C/d.py", # did not select everything below A/C
- "A/E/", # only empty folders remain under A/E
- "A/G/g.py", # non-empty folder remains under A/G
- ]]
-
- monkeypatch.setattr('os.walk', self.mock_walk)
+ yield dirname[len(root) + 1 :], subdirs, files
+
+ def test_compress_for_rename(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ paths = [
+ os.path.sep.join(p.split("/"))
+ for p in [
+ "A/B/b.py",
+ "A/B/D/c.py",
+ "A/C/d.py",
+ "A/E/f.py",
+ "A/G/g.py",
+ ]
+ ]
+
+ expected_paths = [
+ os.path.sep.join(p.split("/"))
+ for p in [
+ "A/B/", # selected everything below A/B
+ "A/C/d.py", # did not select everything below A/C
+ "A/E/", # only empty folders remain under A/E
+ "A/G/g.py", # non-empty folder remains under A/G
+ ]
+ ]
+
+ monkeypatch.setattr("os.walk", self.mock_walk)
actual_paths = compress_for_rename(paths)
assert set(expected_paths) == set(actual_paths)
@classmethod
- def make_stash(cls, tmpdir, paths):
+ def make_stash(
+ cls, tmpdir: Path, paths: List[str]
+ ) -> Tuple[StashedUninstallPathSet, List[Tuple[str, str]]]:
for dirname, subdirs, files in cls.WALK_RESULT:
root = os.path.join(tmpdir, *dirname.split("/"))
if not os.path.exists(root):
@@ -269,15 +283,21 @@ class TestStashedUninstallPathSet:
pathset = StashedUninstallPathSet()
- paths = [os.path.join(tmpdir, *p.split('/')) for p in paths]
+ paths = [os.path.join(tmpdir, *p.split("/")) for p in paths]
stashed_paths = [(p, pathset.stash(p)) for p in paths]
return pathset, stashed_paths
- def test_stash(self, tmpdir):
- pathset, stashed_paths = self.make_stash(tmpdir, [
- "A/B/", "A/C/d.py", "A/E/", "A/G/g.py",
- ])
+ def test_stash(self, tmpdir: Path) -> None:
+ pathset, stashed_paths = self.make_stash(
+ tmpdir,
+ [
+ "A/B/",
+ "A/C/d.py",
+ "A/E/",
+ "A/G/g.py",
+ ],
+ )
for old_path, new_path in stashed_paths:
assert not os.path.exists(old_path)
@@ -285,10 +305,16 @@ class TestStashedUninstallPathSet:
assert stashed_paths == pathset._moves
- def test_commit(self, tmpdir):
- pathset, stashed_paths = self.make_stash(tmpdir, [
- "A/B/", "A/C/d.py", "A/E/", "A/G/g.py",
- ])
+ def test_commit(self, tmpdir: Path) -> None:
+ pathset, stashed_paths = self.make_stash(
+ tmpdir,
+ [
+ "A/B/",
+ "A/C/d.py",
+ "A/E/",
+ "A/G/g.py",
+ ],
+ )
pathset.commit()
@@ -296,10 +322,16 @@ class TestStashedUninstallPathSet:
assert not os.path.exists(old_path)
assert not os.path.exists(new_path)
- def test_rollback(self, tmpdir):
- pathset, stashed_paths = self.make_stash(tmpdir, [
- "A/B/", "A/C/d.py", "A/E/", "A/G/g.py",
- ])
+ def test_rollback(self, tmpdir: Path) -> None:
+ pathset, stashed_paths = self.make_stash(
+ tmpdir,
+ [
+ "A/B/",
+ "A/C/d.py",
+ "A/E/",
+ "A/G/g.py",
+ ],
+ )
pathset.rollback()
@@ -308,7 +340,7 @@ class TestStashedUninstallPathSet:
assert not os.path.exists(new_path)
@pytest.mark.skipif("sys.platform == 'win32'")
- def test_commit_symlinks(self, tmpdir):
+ def test_commit_symlinks(self, tmpdir: Path) -> None:
adir = tmpdir / "dir"
adir.mkdir()
dirlink = tmpdir / "dirlink"
@@ -320,8 +352,8 @@ class TestStashedUninstallPathSet:
pathset = StashedUninstallPathSet()
stashed_paths = []
- stashed_paths.append(pathset.stash(dirlink))
- stashed_paths.append(pathset.stash(filelink))
+ stashed_paths.append(pathset.stash(os.fspath(dirlink)))
+ stashed_paths.append(pathset.stash(os.fspath(filelink)))
for stashed_path in stashed_paths:
assert os.path.lexists(stashed_path)
assert not os.path.exists(dirlink)
@@ -340,7 +372,7 @@ class TestStashedUninstallPathSet:
assert os.path.isfile(afile)
@pytest.mark.skipif("sys.platform == 'win32'")
- def test_rollback_symlinks(self, tmpdir):
+ def test_rollback_symlinks(self, tmpdir: Path) -> None:
adir = tmpdir / "dir"
adir.mkdir()
dirlink = tmpdir / "dirlink"
@@ -352,8 +384,8 @@ class TestStashedUninstallPathSet:
pathset = StashedUninstallPathSet()
stashed_paths = []
- stashed_paths.append(pathset.stash(dirlink))
- stashed_paths.append(pathset.stash(filelink))
+ stashed_paths.append(pathset.stash(os.fspath(dirlink)))
+ stashed_paths.append(pathset.stash(os.fspath(filelink)))
for stashed_path in stashed_paths:
assert os.path.lexists(stashed_path)
assert not os.path.lexists(dirlink)
diff --git a/tests/unit/test_resolution_legacy_resolver.py b/tests/unit/test_resolution_legacy_resolver.py
index 236a4c624..8b9d1a58a 100644
--- a/tests/unit/test_resolution_legacy_resolver.py
+++ b/tests/unit/test_resolution_legacy_resolver.py
@@ -1,69 +1,159 @@
+import email.message
import logging
+import os
+from typing import List, Optional, Type, TypeVar, cast
from unittest import mock
import pytest
-from pip._vendor import pkg_resources
+from pip._vendor.packaging.specifiers import SpecifierSet
+from pip._vendor.packaging.utils import NormalizedName
-from pip._internal.exceptions import NoneMetadataError, UnsupportedPythonVersion
+from pip._internal.exceptions import (
+ InstallationError,
+ NoneMetadataError,
+ UnsupportedPythonVersion,
+)
+from pip._internal.metadata import BaseDistribution
+from pip._internal.models.candidate import InstallationCandidate
from pip._internal.req.constructors import install_req_from_line
+from pip._internal.req.req_set import RequirementSet
from pip._internal.resolution.legacy.resolver import (
Resolver,
_check_dist_requires_python,
)
-from pip._internal.utils.packaging import get_requires_python
-from tests.lib import make_test_finder
+from tests.lib import TestData, make_test_finder
from tests.lib.index import make_mock_candidate
+T = TypeVar("T")
-# We need to inherit from DistInfoDistribution for the `isinstance()`
-# check inside `packaging.get_metadata()` to work.
-class FakeDist(pkg_resources.DistInfoDistribution):
-
- def __init__(self, metadata, metadata_name=None):
- """
- :param metadata: The value that dist.get_metadata() should return
- for the `metadata_name` metadata.
- :param metadata_name: The name of the metadata to store
- (can be "METADATA" or "PKG-INFO"). Defaults to "METADATA".
- """
- if metadata_name is None:
- metadata_name = 'METADATA'
- self.project_name = 'my-project'
- self.metadata_name = metadata_name
- self.metadata = metadata
+class FakeDist(BaseDistribution):
+ def __init__(self, metadata: email.message.Message) -> None:
+ self._canonical_name = cast(NormalizedName, "my-project")
+ self._metadata = metadata
- def __str__(self):
- return f'<distribution {self.project_name!r}>'
+ def __str__(self) -> str:
+ return f"<distribution {self.canonical_name!r}>"
- def has_metadata(self, name):
- return (name == self.metadata_name)
+ @property
+ def canonical_name(self) -> NormalizedName:
+ return self._canonical_name
- def get_metadata(self, name):
- assert name == self.metadata_name
- return self.metadata
+ @property
+ def metadata(self) -> email.message.Message:
+ return self._metadata
-def make_fake_dist(requires_python=None, metadata_name=None):
- metadata = 'Name: test\n'
+def make_fake_dist(
+ *, klass: Type[BaseDistribution] = FakeDist, requires_python: Optional[str] = None
+) -> BaseDistribution:
+ metadata = email.message.Message()
+ metadata["Name"] = "my-project"
if requires_python is not None:
- metadata += f'Requires-Python:{requires_python}'
+ metadata["Requires-Python"] = requires_python
- return FakeDist(metadata, metadata_name=metadata_name)
+ # Too many arguments for "BaseDistribution"
+ return klass(metadata) # type: ignore[call-arg]
-class TestCheckDistRequiresPython:
+def make_test_resolver(
+ monkeypatch: pytest.MonkeyPatch,
+ mock_candidates: List[InstallationCandidate],
+) -> Resolver:
+ def _find_candidates(project_name: str) -> List[InstallationCandidate]:
+ return mock_candidates
+
+ finder = make_test_finder()
+ monkeypatch.setattr(finder, "find_all_candidates", _find_candidates)
+
+ return Resolver(
+ finder=finder,
+ preparer=mock.Mock(), # Not used.
+ make_install_req=install_req_from_line,
+ wheel_cache=None,
+ use_user_site=False,
+ force_reinstall=False,
+ ignore_dependencies=False,
+ ignore_installed=False,
+ ignore_requires_python=False,
+ upgrade_strategy="to-satisfy-only",
+ )
+
+
+class TestAddRequirement:
+ """
+ Test _add_requirement_to_set().
+ """
+
+ def test_unsupported_wheel_link_requirement_raises(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ # GIVEN
+ resolver = make_test_resolver(monkeypatch, [])
+ requirement_set = RequirementSet(check_supported_wheels=True)
+
+ install_req = install_req_from_line(
+ "https://whatever.com/peppercorn-0.4-py2.py3-bogus-any.whl",
+ )
+ assert install_req.link is not None
+ assert install_req.link.is_wheel
+ assert install_req.link.scheme == "https"
+
+ # WHEN / THEN
+ with pytest.raises(InstallationError):
+ resolver._add_requirement_to_set(requirement_set, install_req)
+
+ def test_unsupported_wheel_local_file_requirement_raises(
+ self, data: TestData, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ # GIVEN
+ resolver = make_test_resolver(monkeypatch, [])
+ requirement_set = RequirementSet(check_supported_wheels=True)
+
+ install_req = install_req_from_line(
+ os.fspath(data.packages.joinpath("simple.dist-0.1-py1-none-invalid.whl")),
+ )
+ assert install_req.link is not None
+ assert install_req.link.is_wheel
+ assert install_req.link.scheme == "file"
+
+ # WHEN / THEN
+ with pytest.raises(InstallationError):
+ resolver._add_requirement_to_set(requirement_set, install_req)
+
+ def test_exclusive_environment_markers(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Make sure excluding environment markers are handled correctly."""
+ # GIVEN
+ resolver = make_test_resolver(monkeypatch, [])
+ requirement_set = RequirementSet(check_supported_wheels=True)
+
+ eq36 = install_req_from_line("Django>=1.6.10,<1.7 ; python_version == '3.6'")
+ eq36.user_supplied = True
+ ne36 = install_req_from_line("Django>=1.6.10,<1.8 ; python_version != '3.6'")
+ ne36.user_supplied = True
+
+ # WHEN
+ resolver._add_requirement_to_set(requirement_set, eq36)
+ resolver._add_requirement_to_set(requirement_set, ne36)
+
+ # THEN
+ assert requirement_set.has_requirement("Django")
+ assert len(requirement_set.all_requirements) == 1
+
+class TestCheckDistRequiresPython:
"""
Test _check_dist_requires_python().
"""
- def test_compatible(self, caplog):
+ def test_compatible(self, caplog: pytest.LogCaptureFixture) -> None:
"""
Test a Python version compatible with the dist's Requires-Python.
"""
caplog.set_level(logging.DEBUG)
- dist = make_fake_dist('== 3.6.5')
+ dist = make_fake_dist(requires_python="== 3.6.5")
_check_dist_requires_python(
dist,
@@ -72,11 +162,11 @@ class TestCheckDistRequiresPython:
)
assert not len(caplog.records)
- def test_incompatible(self):
+ def test_incompatible(self) -> None:
"""
Test a Python version incompatible with the dist's Requires-Python.
"""
- dist = make_fake_dist('== 3.6.4')
+ dist = make_fake_dist(requires_python="== 3.6.4")
with pytest.raises(UnsupportedPythonVersion) as exc:
_check_dist_requires_python(
dist,
@@ -85,16 +175,18 @@ class TestCheckDistRequiresPython:
)
assert str(exc.value) == (
"Package 'my-project' requires a different Python: "
- "3.6.5 not in '== 3.6.4'"
+ "3.6.5 not in '==3.6.4'"
)
- def test_incompatible_with_ignore_requires(self, caplog):
+ def test_incompatible_with_ignore_requires(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
"""
Test a Python version incompatible with the dist's Requires-Python
while passing ignore_requires_python=True.
"""
caplog.set_level(logging.DEBUG)
- dist = make_fake_dist('== 3.6.4')
+ dist = make_fake_dist(requires_python="== 3.6.4")
_check_dist_requires_python(
dist,
version_info=(3, 6, 5),
@@ -102,20 +194,20 @@ class TestCheckDistRequiresPython:
)
assert len(caplog.records) == 1
record = caplog.records[0]
- assert record.levelname == 'DEBUG'
+ assert record.levelname == "DEBUG"
assert record.message == (
"Ignoring failed Requires-Python check for package 'my-project': "
- "3.6.5 not in '== 3.6.4'"
+ "3.6.5 not in '==3.6.4'"
)
- def test_none_requires_python(self, caplog):
+ def test_none_requires_python(self, caplog: pytest.LogCaptureFixture) -> None:
"""
Test a dist with Requires-Python None.
"""
caplog.set_level(logging.DEBUG)
dist = make_fake_dist()
# Make sure our test setup is correct.
- assert get_requires_python(dist) is None
+ assert dist.requires_python == SpecifierSet()
assert len(caplog.records) == 0
# Then there is no exception and no log message.
@@ -126,12 +218,12 @@ class TestCheckDistRequiresPython:
)
assert len(caplog.records) == 0
- def test_invalid_requires_python(self, caplog):
+ def test_invalid_requires_python(self, caplog: pytest.LogCaptureFixture) -> None:
"""
Test a dist with an invalid Requires-Python.
"""
caplog.set_level(logging.DEBUG)
- dist = make_fake_dist('invalid')
+ dist = make_fake_dist(requires_python="invalid")
_check_dist_requires_python(
dist,
version_info=(3, 6, 5),
@@ -139,27 +231,28 @@ class TestCheckDistRequiresPython:
)
assert len(caplog.records) == 1
record = caplog.records[0]
- assert record.levelname == 'WARNING'
+ assert record.levelname == "WARNING"
assert record.message == (
"Package 'my-project' has an invalid Requires-Python: "
"Invalid specifier: 'invalid'"
)
- @pytest.mark.parametrize('metadata_name', [
- 'METADATA',
- 'PKG-INFO',
- ])
- def test_empty_metadata_error(self, caplog, metadata_name):
- """
- Test dist.has_metadata() returning True and dist.get_metadata()
- returning None.
- """
- dist = make_fake_dist(metadata_name=metadata_name)
- dist.metadata = None
+ @pytest.mark.parametrize(
+ "metadata_name",
+ [
+ "METADATA",
+ "PKG-INFO",
+ ],
+ )
+ def test_empty_metadata_error(self, metadata_name: str) -> None:
+ """Test dist.metadata raises FileNotFoundError."""
- # Make sure our test setup is correct.
- assert dist.has_metadata(metadata_name)
- assert dist.get_metadata(metadata_name) is None
+ class NotWorkingFakeDist(FakeDist):
+ @property
+ def metadata(self) -> email.message.Message:
+ raise FileNotFoundError(metadata_name)
+
+ dist = make_fake_dist(klass=NotWorkingFakeDist)
with pytest.raises(NoneMetadataError) as exc:
_check_dist_requires_python(
@@ -177,27 +270,10 @@ class TestYankedWarning:
"""
Test _populate_link() emits warning if one or more candidates are yanked.
"""
- def _make_test_resolver(self, monkeypatch, mock_candidates):
- def _find_candidates(project_name):
- return mock_candidates
-
- finder = make_test_finder()
- monkeypatch.setattr(finder, "find_all_candidates", _find_candidates)
-
- return Resolver(
- finder=finder,
- preparer=mock.Mock(), # Not used.
- make_install_req=install_req_from_line,
- wheel_cache=None,
- use_user_site=False,
- force_reinstall=False,
- ignore_dependencies=False,
- ignore_installed=False,
- ignore_requires_python=False,
- upgrade_strategy="to-satisfy-only",
- )
- def test_sort_best_candidate__has_non_yanked(self, caplog, monkeypatch):
+ def test_sort_best_candidate__has_non_yanked(
+ self, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
"""
Test unyanked candidate preferred over yanked.
"""
@@ -206,18 +282,20 @@ class TestYankedWarning:
# tests are at fault here for being to dependent on exact output.
caplog.set_level(logging.WARNING)
candidates = [
- make_mock_candidate('1.0'),
- make_mock_candidate('2.0', yanked_reason='bad metadata #2'),
+ make_mock_candidate("1.0"),
+ make_mock_candidate("2.0", yanked_reason="bad metadata #2"),
]
ireq = install_req_from_line("pkg")
- resolver = self._make_test_resolver(monkeypatch, candidates)
+ resolver = make_test_resolver(monkeypatch, candidates)
resolver._populate_link(ireq)
assert ireq.link == candidates[0].link
assert len(caplog.records) == 0
- def test_sort_best_candidate__all_yanked(self, caplog, monkeypatch):
+ def test_sort_best_candidate__all_yanked(
+ self, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
"""
Test all candidates yanked.
"""
@@ -226,14 +304,14 @@ class TestYankedWarning:
# tests are at fault here for being to dependent on exact output.
caplog.set_level(logging.WARNING)
candidates = [
- make_mock_candidate('1.0', yanked_reason='bad metadata #1'),
+ make_mock_candidate("1.0", yanked_reason="bad metadata #1"),
# Put the best candidate in the middle, to test sorting.
- make_mock_candidate('3.0', yanked_reason='bad metadata #3'),
- make_mock_candidate('2.0', yanked_reason='bad metadata #2'),
+ make_mock_candidate("3.0", yanked_reason="bad metadata #3"),
+ make_mock_candidate("2.0", yanked_reason="bad metadata #2"),
]
ireq = install_req_from_line("pkg")
- resolver = self._make_test_resolver(monkeypatch, candidates)
+ resolver = make_test_resolver(monkeypatch, candidates)
resolver._populate_link(ireq)
assert ireq.link == candidates[1].link
@@ -241,23 +319,30 @@ class TestYankedWarning:
# Check the log messages.
assert len(caplog.records) == 1
record = caplog.records[0]
- assert record.levelname == 'WARNING'
+ assert record.levelname == "WARNING"
assert record.message == (
- 'The candidate selected for download or install is a yanked '
+ "The candidate selected for download or install is a yanked "
"version: 'mypackage' candidate "
- '(version 3.0 at https://example.com/pkg-3.0.tar.gz)\n'
- 'Reason for being yanked: bad metadata #3'
+ "(version 3.0 at https://example.com/pkg-3.0.tar.gz)\n"
+ "Reason for being yanked: bad metadata #3"
)
- @pytest.mark.parametrize('yanked_reason, expected_reason', [
- # Test no reason given.
- ('', '<none given>'),
- # Test a unicode string with a non-ascii character.
- ('curly quote: \u2018', 'curly quote: \u2018'),
- ])
+ @pytest.mark.parametrize(
+ "yanked_reason, expected_reason",
+ [
+ # Test no reason given.
+ ("", "<none given>"),
+ # Test a unicode string with a non-ascii character.
+ ("curly quote: \u2018", "curly quote: \u2018"),
+ ],
+ )
def test_sort_best_candidate__yanked_reason(
- self, caplog, monkeypatch, yanked_reason, expected_reason,
- ):
+ self,
+ caplog: pytest.LogCaptureFixture,
+ monkeypatch: pytest.MonkeyPatch,
+ yanked_reason: str,
+ expected_reason: str,
+ ) -> None:
"""
Test the log message with various reason strings.
"""
@@ -266,22 +351,22 @@ class TestYankedWarning:
# tests are at fault here for being to dependent on exact output.
caplog.set_level(logging.WARNING)
candidates = [
- make_mock_candidate('1.0', yanked_reason=yanked_reason),
+ make_mock_candidate("1.0", yanked_reason=yanked_reason),
]
ireq = install_req_from_line("pkg")
- resolver = self._make_test_resolver(monkeypatch, candidates)
+ resolver = make_test_resolver(monkeypatch, candidates)
resolver._populate_link(ireq)
assert ireq.link == candidates[0].link
assert len(caplog.records) == 1
record = caplog.records[0]
- assert record.levelname == 'WARNING'
+ assert record.levelname == "WARNING"
expected_message = (
- 'The candidate selected for download or install is a yanked '
+ "The candidate selected for download or install is a yanked "
"version: 'mypackage' candidate "
- '(version 1.0 at https://example.com/pkg-1.0.tar.gz)\n'
- 'Reason for being yanked: '
+ "(version 1.0 at https://example.com/pkg-1.0.tar.gz)\n"
+ "Reason for being yanked: "
) + expected_reason
assert record.message == expected_message
diff --git a/tests/unit/test_search_scope.py b/tests/unit/test_search_scope.py
index e7f4e3f16..d81283416 100644
--- a/tests/unit/test_search_scope.py
+++ b/tests/unit/test_search_scope.py
@@ -3,39 +3,39 @@ from pip._internal.req.constructors import install_req_from_line
class TestSearchScope:
-
- def test_get_formatted_locations_basic_auth(self):
+ def test_get_formatted_locations_basic_auth(self) -> None:
"""
Test that basic authentication credentials defined in URL
is not included in formatted output.
"""
index_urls = [
- 'https://pypi.org/simple',
- 'https://repo-user:repo-pass@repo.domain.com',
- ]
- find_links = [
- 'https://links-user:links-pass@page.domain.com'
+ "https://pypi.org/simple",
+ "https://repo-user:repo-pass@repo.domain.com",
]
+ find_links = ["https://links-user:links-pass@page.domain.com"]
search_scope = SearchScope(
- find_links=find_links, index_urls=index_urls,
+ find_links=find_links,
+ index_urls=index_urls,
+ no_index=False,
)
result = search_scope.get_formatted_locations()
- assert 'repo-user:****@repo.domain.com' in result
- assert 'repo-pass' not in result
- assert 'links-user:****@page.domain.com' in result
- assert 'links-pass' not in result
+ assert "repo-user:****@repo.domain.com" in result
+ assert "repo-pass" not in result
+ assert "links-user:****@page.domain.com" in result
+ assert "links-pass" not in result
- def test_get_index_urls_locations(self):
+ def test_get_index_urls_locations(self) -> None:
"""Check that the canonical name is on all indexes"""
search_scope = SearchScope(
find_links=[],
- index_urls=['file://index1/', 'file://index2'],
- )
- actual = search_scope.get_index_urls_locations(
- install_req_from_line('Complex_Name').name
+ index_urls=["file://index1/", "file://index2"],
+ no_index=False,
)
+ req = install_req_from_line("Complex_Name")
+ assert req.name is not None
+ actual = search_scope.get_index_urls_locations(req.name)
assert actual == [
- 'file://index1/complex-name/',
- 'file://index2/complex-name/',
+ "file://index1/complex-name/",
+ "file://index2/complex-name/",
]
diff --git a/tests/unit/test_self_check_outdated.py b/tests/unit/test_self_check_outdated.py
index 546e05d98..c025ff302 100644
--- a/tests/unit/test_self_check_outdated.py
+++ b/tests/unit/test_self_check_outdated.py
@@ -1,245 +1,180 @@
import datetime
-import functools
import json
+import logging
import os
import sys
+from optparse import Values
+from pathlib import Path
+from typing import Optional
+from unittest.mock import ANY, Mock, patch
-import freezegun
-import pretend
import pytest
-from pip._vendor.packaging.version import parse as parse_version
+from freezegun import freeze_time
+from pip._vendor.packaging.version import Version
from pip._internal import self_outdated_check
-from pip._internal.models.candidate import InstallationCandidate
-from pip._internal.self_outdated_check import (
- SelfCheckState,
- logger,
- pip_self_version_check,
-)
-from tests.lib.path import Path
-
-
-class MockBestCandidateResult:
- def __init__(self, best):
- self.best_candidate = best
-
-
-class MockPackageFinder:
-
- BASE_URL = 'https://pypi.org/simple/pip-{0}.tar.gz'
- PIP_PROJECT_NAME = 'pip'
- INSTALLATION_CANDIDATES = [
- InstallationCandidate(PIP_PROJECT_NAME, '6.9.0',
- BASE_URL.format('6.9.0')),
- InstallationCandidate(PIP_PROJECT_NAME, '3.3.1',
- BASE_URL.format('3.3.1')),
- InstallationCandidate(PIP_PROJECT_NAME, '1.0',
- BASE_URL.format('1.0')),
- ]
-
- @classmethod
- def create(cls, *args, **kwargs):
- return cls()
- def find_best_candidate(self, project_name):
- return MockBestCandidateResult(self.INSTALLATION_CANDIDATES[0])
-
-class MockDistribution:
- def __init__(self, installer, version):
- self.installer = installer
- self.version = parse_version(version)
-
-
-class MockEnvironment:
- def __init__(self, installer, installed_version):
- self.installer = installer
- self.installed_version = installed_version
-
- def get_distribution(self, name):
- if self.installed_version is None:
- return None
- return MockDistribution(self.installer, self.installed_version)
+@pytest.mark.parametrize(
+ ["key", "expected"],
+ [
+ (
+ "/hello/world/venv",
+ "fcd2d5175dd33d5df759ee7b045264230205ef837bf9f582f7c3ada7",
+ ),
+ (
+ "C:\\Users\\User\\Desktop\\venv",
+ "902cecc0745b8ecf2509ba473f3556f0ba222fedc6df433acda24aa5",
+ ),
+ ],
+)
+def test_get_statefile_name_known_values(key: str, expected: str) -> None:
+ assert expected == self_outdated_check._get_statefile_name(key)
-def _options():
- ''' Some default options that we pass to
- self_outdated_check.pip_self_version_check '''
- return pretend.stub(
- find_links=[], index_url='default_url', extra_index_urls=[],
- no_index=False, pre=False, cache_dir='',
+@freeze_time("1970-01-02T11:00:00Z")
+@patch("pip._internal.self_outdated_check._self_version_check_logic")
+@patch("pip._internal.self_outdated_check.SelfCheckState")
+def test_pip_self_version_check_calls_underlying_implementation(
+ mocked_state: Mock, mocked_function: Mock, tmpdir: Path
+) -> None:
+ # GIVEN
+ mock_session = Mock()
+ fake_options = Values(dict(cache_dir=str(tmpdir)))
+
+ # WHEN
+ self_outdated_check.pip_self_version_check(mock_session, fake_options)
+
+ # THEN
+ mocked_state.assert_called_once_with(cache_dir=str(tmpdir))
+ mocked_function.assert_called_once_with(
+ state=mocked_state(cache_dir=str(tmpdir)),
+ current_time=datetime.datetime(1970, 1, 2, 11, 0, 0),
+ local_version=ANY,
+ get_remote_version=ANY,
)
@pytest.mark.parametrize(
[
- 'stored_time',
- 'installed_ver',
- 'new_ver',
- 'installer',
- 'check_if_upgrade_required',
- 'check_warn_logs',
+ "installed_version",
+ "remote_version",
+ "stored_version",
+ "installed_by_pip",
+ "should_show_prompt",
],
[
- # Test we return None when installed version is None
- ('1970-01-01T10:00:00Z', None, '1.0', 'pip', False, False),
- # Need an upgrade - upgrade warning should print
- ('1970-01-01T10:00:00Z', '1.0', '6.9.0', 'pip', True, True),
- # Upgrade available, pip installed via rpm - warning should not print
- ('1970-01-01T10:00:00Z', '1.0', '6.9.0', 'rpm', True, False),
- # No upgrade - upgrade warning should not print
- ('1970-01-9T10:00:00Z', '6.9.0', '6.9.0', 'pip', False, False),
- ]
+ # A newer version available!
+ ("1.0", "2.0", None, True, True),
+ # A newer version available, and cached value is new too!
+ ("1.0", "2.0", "2.0", True, True),
+ # A newer version available, but was not installed by pip.
+ ("1.0", "2.0", None, False, False),
+ # On the latest version already.
+ ("2.0", "2.0", None, True, False),
+ # On the latest version already, and cached value matches.
+ ("2.0", "2.0", "2.0", True, False),
+ # A newer version available, but cached value is older.
+ ("1.0", "2.0", "1.0", True, False),
+ ],
)
-def test_pip_self_version_check(monkeypatch, stored_time, installed_ver,
- new_ver, installer,
- check_if_upgrade_required, check_warn_logs):
- monkeypatch.setattr(
- self_outdated_check,
- "get_default_environment",
- functools.partial(MockEnvironment, installer, installed_ver),
- )
- monkeypatch.setattr(
- self_outdated_check,
- "PackageFinder",
- MockPackageFinder,
- )
+def test_core_logic(
+ installed_version: str,
+ remote_version: str,
+ stored_version: Optional[str],
+ installed_by_pip: bool,
+ should_show_prompt: bool,
+ caplog: pytest.LogCaptureFixture,
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ # GIVEN
monkeypatch.setattr(
- logger,
- "warning",
- pretend.call_recorder(lambda *a, **kw: None),
- )
- monkeypatch.setattr(
- logger,
- "debug",
- pretend.call_recorder(lambda s, exc_info=None: None),
- )
-
- fake_state = pretend.stub(
- state={"last_check": stored_time, 'pypi_version': installed_ver},
- save=pretend.call_recorder(lambda v, t: None),
- )
- monkeypatch.setattr(
- self_outdated_check, 'SelfCheckState', lambda **kw: fake_state
+ self_outdated_check, "was_installed_by_pip", lambda _: installed_by_pip
)
+ mock_state = Mock()
+ mock_state.get.return_value = stored_version
+ fake_time = datetime.datetime(2000, 1, 1, 0, 0, 0)
+ version_that_should_be_checked = stored_version or remote_version
+
+ # WHEN
+ with caplog.at_level(logging.DEBUG):
+ return_value = self_outdated_check._self_version_check_logic(
+ state=mock_state,
+ current_time=fake_time,
+ local_version=Version(installed_version),
+ get_remote_version=lambda: remote_version,
+ )
+
+ # THEN
+ mock_state.get.assert_called_once_with(fake_time)
+ assert caplog.messages == [
+ f"Remote version of pip: {version_that_should_be_checked}",
+ f"Local version of pip: {installed_version}",
+ f"Was pip installed by pip? {installed_by_pip}",
+ ]
- with freezegun.freeze_time(
- "1970-01-09 10:00:00",
- ignore=[
- "six.moves",
- "pip._vendor.six.moves",
- "pip._vendor.requests.packages.urllib3.packages.six.moves",
- ]
- ):
- latest_pypi_version = pip_self_version_check(None, _options())
-
- # See we return None if not installed_version
- if not installed_ver:
- assert not latest_pypi_version
- # See that we saved the correct version
- elif check_if_upgrade_required:
- assert fake_state.save.calls == [
- pretend.call(new_ver, datetime.datetime(1970, 1, 9, 10, 00, 00)),
- ]
+ if stored_version:
+ mock_state.set.assert_not_called()
else:
- # Make sure no Exceptions
- assert not logger.debug.calls
- # See that save was not called
- assert fake_state.save.calls == []
-
- # Ensure we warn the user or not
- if check_warn_logs:
- assert len(logger.warning.calls) == 1
- else:
- assert len(logger.warning.calls) == 0
-
-
-statefile_name_case_1 = (
- "fcd2d5175dd33d5df759ee7b045264230205ef837bf9f582f7c3ada7"
-)
-
-statefile_name_case_2 = (
- "902cecc0745b8ecf2509ba473f3556f0ba222fedc6df433acda24aa5"
-)
-
-
-@pytest.mark.parametrize("key,expected", [
- ("/hello/world/venv", statefile_name_case_1),
- ("C:\\Users\\User\\Desktop\\venv", statefile_name_case_2),
-])
-def test_get_statefile_name_known_values(key, expected):
- assert expected == self_outdated_check._get_statefile_name(key)
-
-
-def _get_statefile_path(cache_dir, key):
- return os.path.join(
- cache_dir, "selfcheck", self_outdated_check._get_statefile_name(key)
- )
-
-
-def test_self_check_state_no_cache_dir():
- state = SelfCheckState(cache_dir=False)
- assert state.state == {}
- assert state.statefile_path is None
-
-
-def test_self_check_state_key_uses_sys_prefix(monkeypatch):
- key = "helloworld"
-
- monkeypatch.setattr(sys, "prefix", key)
- state = self_outdated_check.SelfCheckState("")
-
- assert state.key == key
-
-
-def test_self_check_state_reads_expected_statefile(monkeypatch, tmpdir):
- cache_dir = tmpdir / "cache_dir"
- cache_dir.mkdir()
- key = "helloworld"
- statefile_path = _get_statefile_path(str(cache_dir), key)
-
- last_check = "1970-01-02T11:00:00Z"
- pypi_version = "1.0"
- content = {
- "key": key,
- "last_check": last_check,
- "pypi_version": pypi_version,
- }
-
- Path(statefile_path).parent.mkdir()
-
- with open(statefile_path, "w") as f:
- json.dump(content, f)
-
- monkeypatch.setattr(sys, "prefix", key)
- state = self_outdated_check.SelfCheckState(str(cache_dir))
-
- assert state.state["last_check"] == last_check
- assert state.state["pypi_version"] == pypi_version
-
-
-def test_self_check_state_writes_expected_statefile(monkeypatch, tmpdir):
- cache_dir = tmpdir / "cache_dir"
- cache_dir.mkdir()
- key = "helloworld"
- statefile_path = _get_statefile_path(str(cache_dir), key)
-
- last_check = datetime.datetime.strptime(
- "1970-01-02T11:00:00Z", self_outdated_check.SELFCHECK_DATE_FMT
- )
- pypi_version = "1.0"
-
- monkeypatch.setattr(sys, "prefix", key)
- state = self_outdated_check.SelfCheckState(str(cache_dir))
-
- state.save(pypi_version, last_check)
- with open(statefile_path) as f:
- saved = json.load(f)
-
- expected = {
- "key": key,
- "last_check": last_check.strftime(
- self_outdated_check.SELFCHECK_DATE_FMT),
- "pypi_version": pypi_version,
- }
- assert expected == saved
+ mock_state.set.assert_called_once_with(
+ version_that_should_be_checked, fake_time
+ )
+
+ if not should_show_prompt:
+ assert return_value is None
+ return # the remaining assertions are for the other case.
+
+ assert return_value is not None
+ assert return_value.old == installed_version
+ assert return_value.new == remote_version
+
+
+class TestSelfCheckState:
+ def test_no_cache(self) -> None:
+ # GIVEN / WHEN
+ state = self_outdated_check.SelfCheckState(cache_dir="")
+ assert state._statefile_path is None
+
+ def test_reads_expected_statefile(self, tmpdir: Path) -> None:
+ # GIVEN
+ cache_dir = tmpdir / "cache_dir"
+ expected_path = (
+ cache_dir
+ / "selfcheck"
+ / self_outdated_check._get_statefile_name(sys.prefix)
+ )
+
+ cache_dir.mkdir()
+ (cache_dir / "selfcheck").mkdir()
+ expected_path.write_text('{"foo": "bar"}')
+
+ # WHEN
+ state = self_outdated_check.SelfCheckState(cache_dir=str(cache_dir))
+
+ # THEN
+ assert state._statefile_path == os.fspath(expected_path)
+ assert state._state == {"foo": "bar"}
+
+ def test_writes_expected_statefile(self, tmpdir: Path) -> None:
+ # GIVEN
+ cache_dir = tmpdir / "cache_dir"
+ cache_dir.mkdir()
+ expected_path = (
+ cache_dir
+ / "selfcheck"
+ / self_outdated_check._get_statefile_name(sys.prefix)
+ )
+
+ # WHEN
+ state = self_outdated_check.SelfCheckState(cache_dir=str(cache_dir))
+ state.set("1.0.0", datetime.datetime(2000, 1, 1, 0, 0, 0))
+
+ # THEN
+ assert state._statefile_path == os.fspath(expected_path)
+
+ contents = expected_path.read_text()
+ assert json.loads(contents) == {
+ "key": sys.prefix,
+ "last_check": "2000-01-01T00:00:00Z",
+ "pypi_version": "1.0.0",
+ }
diff --git a/tests/unit/test_target_python.py b/tests/unit/test_target_python.py
index c6af078a9..d3e27e39a 100644
--- a/tests/unit/test_target_python.py
+++ b/tests/unit/test_target_python.py
@@ -1,23 +1,31 @@
-from unittest.mock import patch
+from typing import Any, Dict, Optional, Tuple
+from unittest import mock
import pytest
+from pip._vendor.packaging.tags import Tag
from pip._internal.models.target_python import TargetPython
from tests.lib import CURRENT_PY_VERSION_INFO, pyversion
class TestTargetPython:
-
- @pytest.mark.parametrize('py_version_info, expected', [
- ((), ((0, 0, 0), '0.0')),
- ((2, ), ((2, 0, 0), '2.0')),
- ((3, ), ((3, 0, 0), '3.0')),
- ((3, 7), ((3, 7, 0), '3.7')),
- ((3, 7, 3), ((3, 7, 3), '3.7')),
- # Check a minor version with two digits.
- ((3, 10, 1), ((3, 10, 1), '3.10')),
- ])
- def test_init__py_version_info(self, py_version_info, expected):
+ @pytest.mark.parametrize(
+ "py_version_info, expected",
+ [
+ ((), ((0, 0, 0), "0.0")),
+ ((2,), ((2, 0, 0), "2.0")),
+ ((3,), ((3, 0, 0), "3.0")),
+ ((3, 7), ((3, 7, 0), "3.7")),
+ ((3, 7, 3), ((3, 7, 3), "3.7")),
+ # Check a minor version with two digits.
+ ((3, 10, 1), ((3, 10, 1), "3.10")),
+ ],
+ )
+ def test_init__py_version_info(
+ self,
+ py_version_info: Tuple[int, ...],
+ expected: Tuple[Tuple[int, int, int], str],
+ ) -> None:
"""
Test passing the py_version_info argument.
"""
@@ -31,7 +39,7 @@ class TestTargetPython:
assert target_python.py_version_info == expected_py_version_info
assert target_python.py_version == expected_py_version
- def test_init__py_version_info_none(self):
+ def test_init__py_version_info_none(self) -> None:
"""
Test passing py_version_info=None.
"""
@@ -42,61 +50,75 @@ class TestTargetPython:
assert target_python.py_version_info == CURRENT_PY_VERSION_INFO
assert target_python.py_version == pyversion
- @pytest.mark.parametrize('kwargs, expected', [
- ({}, ''),
- (dict(py_version_info=(3, 6)), "version_info='3.6'"),
- (
- dict(platforms=['darwin'], py_version_info=(3, 6)),
- "platforms=['darwin'] version_info='3.6'",
- ),
- (
- dict(
- platforms=['darwin'], py_version_info=(3, 6), abis=['cp36m'],
- implementation='cp'
+ @pytest.mark.parametrize(
+ "kwargs, expected",
+ [
+ ({}, ""),
+ (dict(py_version_info=(3, 6)), "version_info='3.6'"),
+ (
+ dict(platforms=["darwin"], py_version_info=(3, 6)),
+ "platforms=['darwin'] version_info='3.6'",
),
(
- "platforms=['darwin'] version_info='3.6' abis=['cp36m'] "
- "implementation='cp'"
+ dict(
+ platforms=["darwin"],
+ py_version_info=(3, 6),
+ abis=["cp36m"],
+ implementation="cp",
+ ),
+ (
+ "platforms=['darwin'] version_info='3.6' abis=['cp36m'] "
+ "implementation='cp'"
+ ),
),
- ),
- ])
- def test_format_given(self, kwargs, expected):
+ ],
+ )
+ def test_format_given(self, kwargs: Dict[str, Any], expected: str) -> None:
target_python = TargetPython(**kwargs)
actual = target_python.format_given()
assert actual == expected
- @pytest.mark.parametrize('py_version_info, expected_version', [
- ((), ''),
- ((2, ), '2'),
- ((3, ), '3'),
- ((3, 7), '37'),
- ((3, 7, 3), '37'),
- # Check a minor version with two digits.
- ((3, 10, 1), '310'),
- # Check that versions=None is passed to get_tags().
- (None, None),
- ])
- @patch('pip._internal.models.target_python.get_supported')
+ @pytest.mark.parametrize(
+ "py_version_info, expected_version",
+ [
+ ((), ""),
+ ((2,), "2"),
+ ((3,), "3"),
+ ((3, 7), "37"),
+ ((3, 7, 3), "37"),
+ # Check a minor version with two digits.
+ ((3, 10, 1), "310"),
+ # Check that versions=None is passed to get_tags().
+ (None, None),
+ ],
+ )
+ @mock.patch("pip._internal.models.target_python.get_supported")
def test_get_tags(
- self, mock_get_supported, py_version_info, expected_version,
- ):
- mock_get_supported.return_value = ['tag-1', 'tag-2']
+ self,
+ mock_get_supported: mock.Mock,
+ py_version_info: Optional[Tuple[int, ...]],
+ expected_version: Optional[str],
+ ) -> None:
+ mock_get_supported.return_value = ["tag-1", "tag-2"]
target_python = TargetPython(py_version_info=py_version_info)
actual = target_python.get_tags()
- assert actual == ['tag-1', 'tag-2']
+ assert actual == ["tag-1", "tag-2"]
- actual = mock_get_supported.call_args[1]['version']
+ actual = mock_get_supported.call_args[1]["version"]
assert actual == expected_version
# Check that the value was cached.
- assert target_python._valid_tags == ['tag-1', 'tag-2']
+ assert target_python._valid_tags == ["tag-1", "tag-2"]
- def test_get_tags__uses_cached_value(self):
+ def test_get_tags__uses_cached_value(self) -> None:
"""
Test that get_tags() uses the cached value.
"""
target_python = TargetPython(py_version_info=None)
- target_python._valid_tags = ['tag-1', 'tag-2']
+ target_python._valid_tags = [
+ Tag("py2", "none", "any"),
+ Tag("py3", "none", "any"),
+ ]
actual = target_python.get_tags()
- assert actual == ['tag-1', 'tag-2']
+ assert actual == [Tag("py2", "none", "any"), Tag("py3", "none", "any")]
diff --git a/tests/unit/test_urls.py b/tests/unit/test_urls.py
index 607023fd2..56ee80aa8 100644
--- a/tests/unit/test_urls.py
+++ b/tests/unit/test_urls.py
@@ -1,50 +1,57 @@
import os
import sys
import urllib.request
+from typing import Optional
import pytest
from pip._internal.utils.urls import get_url_scheme, path_to_url, url_to_path
-@pytest.mark.parametrize("url,expected", [
- ('http://localhost:8080/', 'http'),
- ('file:c:/path/to/file', 'file'),
- ('file:/dev/null', 'file'),
- ('', None),
-])
-def test_get_url_scheme(url, expected):
+@pytest.mark.parametrize(
+ "url,expected",
+ [
+ ("http://localhost:8080/", "http"),
+ ("file:c:/path/to/file", "file"),
+ ("file:/dev/null", "file"),
+ ("", None),
+ ],
+)
+def test_get_url_scheme(url: str, expected: Optional[str]) -> None:
assert get_url_scheme(url) == expected
@pytest.mark.skipif("sys.platform == 'win32'")
-def test_path_to_url_unix():
- assert path_to_url('/tmp/file') == 'file:///tmp/file'
- path = os.path.join(os.getcwd(), 'file')
- assert path_to_url('file') == 'file://' + urllib.request.pathname2url(path)
+def test_path_to_url_unix() -> None:
+ assert path_to_url("/tmp/file") == "file:///tmp/file"
+ path = os.path.join(os.getcwd(), "file")
+ assert path_to_url("file") == "file://" + urllib.request.pathname2url(path)
@pytest.mark.skipif("sys.platform != 'win32'")
-def test_path_to_url_win():
- assert path_to_url('c:/tmp/file') == 'file:///C:/tmp/file'
- assert path_to_url('c:\\tmp\\file') == 'file:///C:/tmp/file'
- assert path_to_url(r'\\unc\as\path') == 'file://unc/as/path'
- path = os.path.join(os.getcwd(), 'file')
- assert path_to_url('file') == 'file:' + urllib.request.pathname2url(path)
+def test_path_to_url_win() -> None:
+ assert path_to_url("c:/tmp/file") == "file:///C:/tmp/file"
+ assert path_to_url("c:\\tmp\\file") == "file:///C:/tmp/file"
+ assert path_to_url(r"\\unc\as\path") == "file://unc/as/path"
+ path = os.path.join(os.getcwd(), "file")
+ assert path_to_url("file") == "file:" + urllib.request.pathname2url(path)
-@pytest.mark.parametrize("url,win_expected,non_win_expected", [
- ('file:tmp', 'tmp', 'tmp'),
- ('file:c:/path/to/file', r'C:\path\to\file', 'c:/path/to/file'),
- ('file:/path/to/file', r'\path\to\file', '/path/to/file'),
- ('file://localhost/tmp/file', r'\tmp\file', '/tmp/file'),
- ('file://localhost/c:/tmp/file', r'C:\tmp\file', '/c:/tmp/file'),
- ('file://somehost/tmp/file', r'\\somehost\tmp\file', None),
- ('file:///tmp/file', r'\tmp\file', '/tmp/file'),
- ('file:///c:/tmp/file', r'C:\tmp\file', '/c:/tmp/file'),
-])
-def test_url_to_path(url, win_expected, non_win_expected):
- if sys.platform == 'win32':
+@pytest.mark.parametrize(
+ "url,win_expected,non_win_expected",
+ [
+ ("file:tmp", "tmp", "tmp"),
+ ("file:c:/path/to/file", r"C:\path\to\file", "c:/path/to/file"),
+ ("file:/path/to/file", r"\path\to\file", "/path/to/file"),
+ ("file://localhost/tmp/file", r"\tmp\file", "/tmp/file"),
+ ("file://localhost/c:/tmp/file", r"C:\tmp\file", "/c:/tmp/file"),
+ ("file://somehost/tmp/file", r"\\somehost\tmp\file", None),
+ ("file:///tmp/file", r"\tmp\file", "/tmp/file"),
+ ("file:///c:/tmp/file", r"C:\tmp\file", "/c:/tmp/file"),
+ ],
+)
+def test_url_to_path(url: str, win_expected: str, non_win_expected: str) -> None:
+ if sys.platform == "win32":
expected_path = win_expected
else:
expected_path = non_win_expected
@@ -57,9 +64,9 @@ def test_url_to_path(url, win_expected, non_win_expected):
@pytest.mark.skipif("sys.platform != 'win32'")
-def test_url_to_path_path_to_url_symmetry_win():
- path = r'C:\tmp\file'
+def test_url_to_path_path_to_url_symmetry_win() -> None:
+ path = r"C:\tmp\file"
assert url_to_path(path_to_url(path)) == path
- unc_path = r'\\unc\share\path'
+ unc_path = r"\\unc\share\path"
assert url_to_path(path_to_url(unc_path)) == unc_path
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 4e5bba2e1..0c9b07664 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -3,19 +3,21 @@ util tests
"""
import codecs
-import itertools
import os
import shutil
import stat
import sys
import time
from io import BytesIO
+from pathlib import Path
+from typing import Any, Callable, Iterator, List, NoReturn, Optional, Tuple, Type
from unittest.mock import Mock, patch
import pytest
from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError
from pip._internal.utils.deprecation import PipDeprecationWarning, deprecated
+from pip._internal.utils.egg_link import egg_link_path_from_location
from pip._internal.utils.encoding import BOMS, auto_decode
from pip._internal.utils.glibc import (
glibc_version_string,
@@ -27,10 +29,7 @@ from pip._internal.utils.misc import (
HiddenText,
build_netloc,
build_url_from_netloc,
- egg_link_path,
format_size,
- get_distribution,
- get_installed_distributions,
get_prog,
hide_url,
hide_value,
@@ -51,322 +50,203 @@ from pip._internal.utils.setuptools_build import make_setuptools_shim_args
class Tests_EgglinkPath:
- "util.egg_link_path() tests"
+ "util.egg_link_path_from_location() tests"
- def setup(self):
+ def setup(self) -> None:
- project = 'foo'
+ project = "foo"
self.mock_dist = Mock(project_name=project)
- self.site_packages = 'SITE_PACKAGES'
- self.user_site = 'USER_SITE'
- self.user_site_egglink = os.path.join(
- self.user_site,
- f'{project}.egg-link'
- )
+ self.site_packages = "SITE_PACKAGES"
+ self.user_site = "USER_SITE"
+ self.user_site_egglink = os.path.join(self.user_site, f"{project}.egg-link")
self.site_packages_egglink = os.path.join(
self.site_packages,
- f'{project}.egg-link',
+ f"{project}.egg-link",
)
# patches
- from pip._internal.utils import misc as utils
+ from pip._internal.utils import egg_link as utils
+
self.old_site_packages = utils.site_packages
- self.mock_site_packages = utils.site_packages = 'SITE_PACKAGES'
+ self.mock_site_packages = utils.site_packages = "SITE_PACKAGES"
self.old_running_under_virtualenv = utils.running_under_virtualenv
- self.mock_running_under_virtualenv = utils.running_under_virtualenv = \
- Mock()
+ self.mock_running_under_virtualenv = utils.running_under_virtualenv = Mock()
self.old_virtualenv_no_global = utils.virtualenv_no_global
self.mock_virtualenv_no_global = utils.virtualenv_no_global = Mock()
self.old_user_site = utils.user_site
self.mock_user_site = utils.user_site = self.user_site
from os import path
+
self.old_isfile = path.isfile
self.mock_isfile = path.isfile = Mock()
- def teardown(self):
- from pip._internal.utils import misc as utils
+ def teardown(self) -> None:
+ from pip._internal.utils import egg_link as utils
+
utils.site_packages = self.old_site_packages
utils.running_under_virtualenv = self.old_running_under_virtualenv
utils.virtualenv_no_global = self.old_virtualenv_no_global
utils.user_site = self.old_user_site
from os import path
+
path.isfile = self.old_isfile
- def eggLinkInUserSite(self, egglink):
+ def eggLinkInUserSite(self, egglink: str) -> bool:
return egglink == self.user_site_egglink
- def eggLinkInSitePackages(self, egglink):
+ def eggLinkInSitePackages(self, egglink: str) -> bool:
return egglink == self.site_packages_egglink
# ####################### #
# # egglink in usersite # #
# ####################### #
- def test_egglink_in_usersite_notvenv(self):
+ def test_egglink_in_usersite_notvenv(self) -> None:
self.mock_virtualenv_no_global.return_value = False
self.mock_running_under_virtualenv.return_value = False
self.mock_isfile.side_effect = self.eggLinkInUserSite
- assert egg_link_path(self.mock_dist) == self.user_site_egglink
+ assert (
+ egg_link_path_from_location(self.mock_dist.project_name)
+ == self.user_site_egglink
+ )
- def test_egglink_in_usersite_venv_noglobal(self):
+ def test_egglink_in_usersite_venv_noglobal(self) -> None:
self.mock_virtualenv_no_global.return_value = True
self.mock_running_under_virtualenv.return_value = True
self.mock_isfile.side_effect = self.eggLinkInUserSite
- assert egg_link_path(self.mock_dist) is None
+ assert egg_link_path_from_location(self.mock_dist.project_name) is None
- def test_egglink_in_usersite_venv_global(self):
+ def test_egglink_in_usersite_venv_global(self) -> None:
self.mock_virtualenv_no_global.return_value = False
self.mock_running_under_virtualenv.return_value = True
self.mock_isfile.side_effect = self.eggLinkInUserSite
- assert egg_link_path(self.mock_dist) == self.user_site_egglink
+ assert (
+ egg_link_path_from_location(self.mock_dist.project_name)
+ == self.user_site_egglink
+ )
# ####################### #
# # egglink in sitepkgs # #
# ####################### #
- def test_egglink_in_sitepkgs_notvenv(self):
+ def test_egglink_in_sitepkgs_notvenv(self) -> None:
self.mock_virtualenv_no_global.return_value = False
self.mock_running_under_virtualenv.return_value = False
self.mock_isfile.side_effect = self.eggLinkInSitePackages
- assert egg_link_path(self.mock_dist) == self.site_packages_egglink
+ assert (
+ egg_link_path_from_location(self.mock_dist.project_name)
+ == self.site_packages_egglink
+ )
- def test_egglink_in_sitepkgs_venv_noglobal(self):
+ def test_egglink_in_sitepkgs_venv_noglobal(self) -> None:
self.mock_virtualenv_no_global.return_value = True
self.mock_running_under_virtualenv.return_value = True
self.mock_isfile.side_effect = self.eggLinkInSitePackages
- assert egg_link_path(self.mock_dist) == self.site_packages_egglink
+ assert (
+ egg_link_path_from_location(self.mock_dist.project_name)
+ == self.site_packages_egglink
+ )
- def test_egglink_in_sitepkgs_venv_global(self):
+ def test_egglink_in_sitepkgs_venv_global(self) -> None:
self.mock_virtualenv_no_global.return_value = False
self.mock_running_under_virtualenv.return_value = True
self.mock_isfile.side_effect = self.eggLinkInSitePackages
- assert egg_link_path(self.mock_dist) == self.site_packages_egglink
+ assert (
+ egg_link_path_from_location(self.mock_dist.project_name)
+ == self.site_packages_egglink
+ )
# ################################## #
# # egglink in usersite & sitepkgs # #
# ################################## #
- def test_egglink_in_both_notvenv(self):
+ def test_egglink_in_both_notvenv(self) -> None:
self.mock_virtualenv_no_global.return_value = False
self.mock_running_under_virtualenv.return_value = False
self.mock_isfile.return_value = True
- assert egg_link_path(self.mock_dist) == self.user_site_egglink
+ assert (
+ egg_link_path_from_location(self.mock_dist.project_name)
+ == self.user_site_egglink
+ )
- def test_egglink_in_both_venv_noglobal(self):
+ def test_egglink_in_both_venv_noglobal(self) -> None:
self.mock_virtualenv_no_global.return_value = True
self.mock_running_under_virtualenv.return_value = True
self.mock_isfile.return_value = True
- assert egg_link_path(self.mock_dist) == self.site_packages_egglink
+ assert (
+ egg_link_path_from_location(self.mock_dist.project_name)
+ == self.site_packages_egglink
+ )
- def test_egglink_in_both_venv_global(self):
+ def test_egglink_in_both_venv_global(self) -> None:
self.mock_virtualenv_no_global.return_value = False
self.mock_running_under_virtualenv.return_value = True
self.mock_isfile.return_value = True
- assert egg_link_path(self.mock_dist) == self.site_packages_egglink
+ assert (
+ egg_link_path_from_location(self.mock_dist.project_name)
+ == self.site_packages_egglink
+ )
# ############## #
# # no egglink # #
# ############## #
- def test_noegglink_in_sitepkgs_notvenv(self):
+ def test_noegglink_in_sitepkgs_notvenv(self) -> None:
self.mock_virtualenv_no_global.return_value = False
self.mock_running_under_virtualenv.return_value = False
self.mock_isfile.return_value = False
- assert egg_link_path(self.mock_dist) is None
+ assert egg_link_path_from_location(self.mock_dist.project_name) is None
- def test_noegglink_in_sitepkgs_venv_noglobal(self):
+ def test_noegglink_in_sitepkgs_venv_noglobal(self) -> None:
self.mock_virtualenv_no_global.return_value = True
self.mock_running_under_virtualenv.return_value = True
self.mock_isfile.return_value = False
- assert egg_link_path(self.mock_dist) is None
+ assert egg_link_path_from_location(self.mock_dist.project_name) is None
- def test_noegglink_in_sitepkgs_venv_global(self):
+ def test_noegglink_in_sitepkgs_venv_global(self) -> None:
self.mock_virtualenv_no_global.return_value = False
self.mock_running_under_virtualenv.return_value = True
self.mock_isfile.return_value = False
- assert egg_link_path(self.mock_dist) is None
+ assert egg_link_path_from_location(self.mock_dist.project_name) is None
-@patch('pip._internal.utils.misc.dist_in_usersite')
-@patch('pip._internal.utils.misc.dist_is_local')
-@patch('pip._internal.utils.misc.dist_is_editable')
-class TestsGetDistributions:
- """Test get_installed_distributions() and get_distribution().
- """
- class MockWorkingSet(list):
- def require(self, name):
- pass
-
- workingset = MockWorkingSet((
- Mock(test_name="global", project_name="global"),
- Mock(test_name="editable", project_name="editable"),
- Mock(test_name="normal", project_name="normal"),
- Mock(test_name="user", project_name="user"),
- ))
-
- workingset_stdlib = MockWorkingSet((
- Mock(test_name='normal', project_name='argparse'),
- Mock(test_name='normal', project_name='wsgiref')
- ))
-
- workingset_freeze = MockWorkingSet((
- Mock(test_name='normal', project_name='pip'),
- Mock(test_name='normal', project_name='setuptools'),
- Mock(test_name='normal', project_name='distribute')
- ))
-
- def dist_is_editable(self, dist):
- return dist.test_name == "editable"
-
- def dist_is_local(self, dist):
- return dist.test_name != "global" and dist.test_name != 'user'
-
- def dist_in_usersite(self, dist):
- return dist.test_name == "user"
-
- @patch('pip._vendor.pkg_resources.working_set', workingset)
- def test_editables_only(self, mock_dist_is_editable,
- mock_dist_is_local,
- mock_dist_in_usersite):
- mock_dist_is_editable.side_effect = self.dist_is_editable
- mock_dist_is_local.side_effect = self.dist_is_local
- mock_dist_in_usersite.side_effect = self.dist_in_usersite
- dists = get_installed_distributions(editables_only=True)
- assert len(dists) == 1, dists
- assert dists[0].test_name == "editable"
-
- @patch('pip._vendor.pkg_resources.working_set', workingset)
- def test_exclude_editables(self, mock_dist_is_editable,
- mock_dist_is_local,
- mock_dist_in_usersite):
- mock_dist_is_editable.side_effect = self.dist_is_editable
- mock_dist_is_local.side_effect = self.dist_is_local
- mock_dist_in_usersite.side_effect = self.dist_in_usersite
- dists = get_installed_distributions(include_editables=False)
- assert len(dists) == 1
- assert dists[0].test_name == "normal"
-
- @patch('pip._vendor.pkg_resources.working_set', workingset)
- def test_include_globals(self, mock_dist_is_editable,
- mock_dist_is_local,
- mock_dist_in_usersite):
- mock_dist_is_editable.side_effect = self.dist_is_editable
- mock_dist_is_local.side_effect = self.dist_is_local
- mock_dist_in_usersite.side_effect = self.dist_in_usersite
- dists = get_installed_distributions(local_only=False)
- assert len(dists) == 4
-
- @patch('pip._vendor.pkg_resources.working_set', workingset)
- def test_user_only(self, mock_dist_is_editable,
- mock_dist_is_local,
- mock_dist_in_usersite):
- mock_dist_is_editable.side_effect = self.dist_is_editable
- mock_dist_is_local.side_effect = self.dist_is_local
- mock_dist_in_usersite.side_effect = self.dist_in_usersite
- dists = get_installed_distributions(local_only=False,
- user_only=True)
- assert len(dists) == 1
- assert dists[0].test_name == "user"
-
- @patch('pip._vendor.pkg_resources.working_set', workingset_stdlib)
- def test_gte_py27_excludes(self, mock_dist_is_editable,
- mock_dist_is_local,
- mock_dist_in_usersite):
- mock_dist_is_editable.side_effect = self.dist_is_editable
- mock_dist_is_local.side_effect = self.dist_is_local
- mock_dist_in_usersite.side_effect = self.dist_in_usersite
- dists = get_installed_distributions()
- assert len(dists) == 0
-
- @patch('pip._vendor.pkg_resources.working_set', workingset_freeze)
- def test_freeze_excludes(self, mock_dist_is_editable,
- mock_dist_is_local,
- mock_dist_in_usersite):
- mock_dist_is_editable.side_effect = self.dist_is_editable
- mock_dist_is_local.side_effect = self.dist_is_local
- mock_dist_in_usersite.side_effect = self.dist_in_usersite
- dists = get_installed_distributions(
- skip=('setuptools', 'pip', 'distribute'))
- assert len(dists) == 0
-
- @pytest.mark.parametrize(
- "working_set, req_name",
- itertools.chain(
- itertools.product(
- [workingset],
- (d.project_name for d in workingset),
- ),
- itertools.product(
- [workingset_stdlib],
- (d.project_name for d in workingset_stdlib),
- ),
- ),
- )
- def test_get_distribution(
- self,
- mock_dist_is_editable,
- mock_dist_is_local,
- mock_dist_in_usersite,
- working_set,
- req_name,
- ):
- """Ensure get_distribution() finds all kinds of distributions.
- """
- mock_dist_is_editable.side_effect = self.dist_is_editable
- mock_dist_is_local.side_effect = self.dist_is_local
- mock_dist_in_usersite.side_effect = self.dist_in_usersite
- with patch("pip._vendor.pkg_resources.working_set", working_set):
- dist = get_distribution(req_name)
- assert dist is not None
- assert dist.project_name == req_name
-
- @patch('pip._vendor.pkg_resources.working_set', workingset)
- def test_get_distribution_nonexist(
- self,
- mock_dist_is_editable,
- mock_dist_is_local,
- mock_dist_in_usersite,
- ):
- mock_dist_is_editable.side_effect = self.dist_is_editable
- mock_dist_is_local.side_effect = self.dist_is_local
- mock_dist_in_usersite.side_effect = self.dist_in_usersite
- dist = get_distribution("non-exist")
- assert dist is None
-
-
-def test_rmtree_errorhandler_nonexistent_directory(tmpdir):
+def test_rmtree_errorhandler_nonexistent_directory(tmpdir: Path) -> None:
"""
Test rmtree_errorhandler ignores the given non-existing directory.
"""
- nonexistent_path = str(tmpdir / 'foo')
+ nonexistent_path = str(tmpdir / "foo")
mock_func = Mock()
- rmtree_errorhandler(mock_func, nonexistent_path, None)
+ # Argument 3 to "rmtree_errorhandler" has incompatible type "None"; expected
+ # "Tuple[Type[BaseException], BaseException, TracebackType]"
+ rmtree_errorhandler(mock_func, nonexistent_path, None) # type: ignore[arg-type]
mock_func.assert_not_called()
-def test_rmtree_errorhandler_readonly_directory(tmpdir):
+def test_rmtree_errorhandler_readonly_directory(tmpdir: Path) -> None:
"""
Test rmtree_errorhandler makes the given read-only directory writable.
"""
# Create read only directory
- subdir_path = tmpdir / 'subdir'
+ subdir_path = tmpdir / "subdir"
subdir_path.mkdir()
path = str(subdir_path)
os.chmod(path, stat.S_IREAD)
# Make sure mock_func is called with the given path
mock_func = Mock()
- rmtree_errorhandler(mock_func, path, None)
+ # Argument 3 to "rmtree_errorhandler" has incompatible type "None"; expected
+ # "Tuple[Type[BaseException], BaseException, TracebackType]"
+ rmtree_errorhandler(mock_func, path, None) # type: ignore[arg-type]
mock_func.assert_called_with(path)
# Make sure the path is now writable
assert os.stat(path).st_mode & stat.S_IWRITE
-def test_rmtree_errorhandler_reraises_error(tmpdir):
+def test_rmtree_errorhandler_reraises_error(tmpdir: Path) -> None:
"""
Test rmtree_errorhandler reraises an exception
by the given unreadable directory.
"""
# Create directory without read permission
- subdir_path = tmpdir / 'subdir'
+ subdir_path = tmpdir / "subdir"
subdir_path.mkdir()
path = str(subdir_path)
os.chmod(path, stat.S_IWRITE)
@@ -374,59 +254,59 @@ def test_rmtree_errorhandler_reraises_error(tmpdir):
mock_func = Mock()
try:
- raise RuntimeError('test message')
+ raise RuntimeError("test message")
except RuntimeError:
# Make sure the handler reraises an exception
- with pytest.raises(RuntimeError, match='test message'):
- rmtree_errorhandler(mock_func, path, None)
+ with pytest.raises(RuntimeError, match="test message"):
+ # Argument 3 to "rmtree_errorhandler" has incompatible type "None"; expected
+ # "Tuple[Type[BaseException], BaseException, TracebackType]"
+ rmtree_errorhandler(mock_func, path, None) # type: ignore[arg-type]
mock_func.assert_not_called()
-def test_rmtree_skips_nonexistent_directory():
+def test_rmtree_skips_nonexistent_directory() -> None:
"""
Test wrapped rmtree doesn't raise an error
by the given nonexistent directory.
"""
- rmtree.__wrapped__('nonexistent-subdir')
+ rmtree.__wrapped__("nonexistent-subdir") # type: ignore[attr-defined]
class Failer:
- def __init__(self, duration=1):
+ def __init__(self, duration: int = 1) -> None:
self.succeed_after = time.time() + duration
- def call(self, *args, **kw):
+ def call(self, *args: Any, **kw: Any) -> None:
"""Fail with OSError self.max_fails times"""
if time.time() < self.succeed_after:
raise OSError("Failed")
-def test_rmtree_retries(tmpdir, monkeypatch):
+def test_rmtree_retries(monkeypatch: pytest.MonkeyPatch) -> None:
"""
Test pip._internal.utils.rmtree will retry failures
"""
- monkeypatch.setattr(shutil, 'rmtree', Failer(duration=1).call)
- rmtree('foo')
+ monkeypatch.setattr(shutil, "rmtree", Failer(duration=1).call)
+ rmtree("foo")
-def test_rmtree_retries_for_3sec(tmpdir, monkeypatch):
+def test_rmtree_retries_for_3sec(monkeypatch: pytest.MonkeyPatch) -> None:
"""
Test pip._internal.utils.rmtree will retry failures for no more than 3 sec
"""
- monkeypatch.setattr(shutil, 'rmtree', Failer(duration=5).call)
+ monkeypatch.setattr(shutil, "rmtree", Failer(duration=5).call)
with pytest.raises(OSError):
- rmtree('foo')
+ rmtree("foo")
if sys.byteorder == "little":
expected_byte_string = (
- "b'\\xff\\xfe/\\x00p\\x00a\\x00t\\x00h\\x00/"
- "\\x00d\\x00\\xe9\\x00f\\x00'"
+ "b'\\xff\\xfe/\\x00p\\x00a\\x00t\\x00h\\x00/\\x00d\\x00\\xe9\\x00f\\x00'"
)
elif sys.byteorder == "big":
expected_byte_string = (
- "b'\\xfe\\xff\\x00/\\x00p\\x00a\\x00t\\x00h\\"
- "x00/\\x00d\\x00\\xe9\\x00f'"
+ "b'\\xfe\\xff\\x00/\\x00p\\x00a\\x00t\\x00h\\x00/\\x00d\\x00\\xe9\\x00f'"
)
@@ -435,34 +315,34 @@ class Test_normalize_path:
# permission bit to create them, and Python 2 doesn't support it anyway, so
# it's easiest just to skip this test on Windows altogether.
@pytest.mark.skipif("sys.platform == 'win32'")
- def test_resolve_symlinks(self, tmpdir):
+ def test_resolve_symlinks(self, tmpdir: Path) -> None:
print(type(tmpdir))
print(dir(tmpdir))
orig_working_dir = os.getcwd()
os.chdir(tmpdir)
try:
- d = os.path.join('foo', 'bar')
- f = os.path.join(d, 'file1')
+ d = os.path.join("foo", "bar")
+ f = os.path.join(d, "file1")
os.makedirs(d)
- with open(f, 'w'): # Create the file
+ with open(f, "w"): # Create the file
pass
- os.symlink(d, 'dir_link')
- os.symlink(f, 'file_link')
-
- assert normalize_path(
- 'dir_link/file1', resolve_symlinks=True
- ) == os.path.join(tmpdir, f)
- assert normalize_path(
- 'dir_link/file1', resolve_symlinks=False
- ) == os.path.join(tmpdir, 'dir_link', 'file1')
+ os.symlink(d, "dir_link")
+ os.symlink(f, "file_link")
assert normalize_path(
- 'file_link', resolve_symlinks=True
+ "dir_link/file1", resolve_symlinks=True
) == os.path.join(tmpdir, f)
assert normalize_path(
- 'file_link', resolve_symlinks=False
- ) == os.path.join(tmpdir, 'file_link')
+ "dir_link/file1", resolve_symlinks=False
+ ) == os.path.join(tmpdir, "dir_link", "file1")
+
+ assert normalize_path("file_link", resolve_symlinks=True) == os.path.join(
+ tmpdir, f
+ )
+ assert normalize_path("file_link", resolve_symlinks=False) == os.path.join(
+ tmpdir, "file_link"
+ )
finally:
os.chdir(orig_working_dir)
@@ -470,352 +350,426 @@ class Test_normalize_path:
class TestHashes:
"""Tests for pip._internal.utils.hashes"""
- @pytest.mark.parametrize('hash_name, hex_digest, expected', [
- # Test a value that matches but with the wrong hash_name.
- ('sha384', 128 * 'a', False),
- # Test matching values, including values other than the first.
- ('sha512', 128 * 'a', True),
- ('sha512', 128 * 'b', True),
- # Test a matching hash_name with a value that doesn't match.
- ('sha512', 128 * 'c', False),
- ])
- def test_is_hash_allowed(self, hash_name, hex_digest, expected):
+ @pytest.mark.parametrize(
+ "hash_name, hex_digest, expected",
+ [
+ # Test a value that matches but with the wrong hash_name.
+ ("sha384", 128 * "a", False),
+ # Test matching values, including values other than the first.
+ ("sha512", 128 * "a", True),
+ ("sha512", 128 * "b", True),
+ # Test a matching hash_name with a value that doesn't match.
+ ("sha512", 128 * "c", False),
+ ],
+ )
+ def test_is_hash_allowed(
+ self, hash_name: str, hex_digest: str, expected: bool
+ ) -> None:
hashes_data = {
- 'sha512': [128 * 'a', 128 * 'b'],
+ "sha512": [128 * "a", 128 * "b"],
}
hashes = Hashes(hashes_data)
assert hashes.is_hash_allowed(hash_name, hex_digest) == expected
- def test_success(self, tmpdir):
+ def test_success(self, tmpdir: Path) -> None:
"""Make sure no error is raised when at least one hash matches.
Test check_against_path because it calls everything else.
"""
- file = tmpdir / 'to_hash'
- file.write_text('hello')
- hashes = Hashes({
- 'sha256': ['2cf24dba5fb0a30e26e83b2ac5b9e29e'
- '1b161e5c1fa7425e73043362938b9824'],
- 'sha224': ['wrongwrong'],
- 'md5': ['5d41402abc4b2a76b9719d911017c592']})
- hashes.check_against_path(file)
-
- def test_failure(self):
+ file = tmpdir / "to_hash"
+ file.write_text("hello")
+ hashes = Hashes(
+ {
+ "sha256": [
+ "2cf24dba5fb0a30e26e83b2ac5b9e29e"
+ "1b161e5c1fa7425e73043362938b9824"
+ ],
+ "sha224": ["wrongwrong"],
+ "md5": ["5d41402abc4b2a76b9719d911017c592"],
+ }
+ )
+ hashes.check_against_path(os.fspath(file))
+
+ def test_failure(self) -> None:
"""Hashes should raise HashMismatch when no hashes match."""
- hashes = Hashes({'sha256': ['wrongwrong']})
+ hashes = Hashes({"sha256": ["wrongwrong"]})
with pytest.raises(HashMismatch):
- hashes.check_against_file(BytesIO(b'hello'))
+ hashes.check_against_file(BytesIO(b"hello"))
- def test_missing_hashes(self):
+ def test_missing_hashes(self) -> None:
"""MissingHashes should raise HashMissing when any check is done."""
with pytest.raises(HashMissing):
- MissingHashes().check_against_file(BytesIO(b'hello'))
+ MissingHashes().check_against_file(BytesIO(b"hello"))
- def test_unknown_hash(self):
+ def test_unknown_hash(self) -> None:
"""Hashes should raise InstallationError when it encounters an unknown
hash."""
- hashes = Hashes({'badbad': ['dummy']})
+ hashes = Hashes({"badbad": ["dummy"]})
with pytest.raises(InstallationError):
- hashes.check_against_file(BytesIO(b'hello'))
+ hashes.check_against_file(BytesIO(b"hello"))
- def test_non_zero(self):
+ def test_non_zero(self) -> None:
"""Test that truthiness tests tell whether any known-good hashes
exist."""
- assert Hashes({'sha256': 'dummy'})
+ assert Hashes({"sha256": ["dummy"]})
assert not Hashes()
assert not Hashes({})
- def test_equality(self):
+ def test_equality(self) -> None:
assert Hashes() == Hashes()
- assert Hashes({'sha256': ['abcd']}) == Hashes({'sha256': ['abcd']})
- assert Hashes({'sha256': ['ab', 'cd']}) == Hashes({'sha256': ['cd', 'ab']})
+ assert Hashes({"sha256": ["abcd"]}) == Hashes({"sha256": ["abcd"]})
+ assert Hashes({"sha256": ["ab", "cd"]}) == Hashes({"sha256": ["cd", "ab"]})
- def test_hash(self):
+ def test_hash(self) -> None:
cache = {}
- cache[Hashes({'sha256': ['ab', 'cd']})] = 42
- assert cache[Hashes({'sha256': ['ab', 'cd']})] == 42
+ cache[Hashes({"sha256": ["ab", "cd"]})] = 42
+ assert cache[Hashes({"sha256": ["ab", "cd"]})] == 42
class TestEncoding:
"""Tests for pip._internal.utils.encoding"""
- def test_auto_decode_utf_16_le(self):
+ def test_auto_decode_utf_16_le(self) -> None:
data = (
- b'\xff\xfeD\x00j\x00a\x00n\x00g\x00o\x00=\x00'
- b'=\x001\x00.\x004\x00.\x002\x00'
+ b"\xff\xfeD\x00j\x00a\x00n\x00g\x00o\x00=\x00"
+ b"=\x001\x00.\x004\x00.\x002\x00"
)
assert data.startswith(codecs.BOM_UTF16_LE)
assert auto_decode(data) == "Django==1.4.2"
- def test_auto_decode_utf_16_be(self):
+ def test_auto_decode_utf_16_be(self) -> None:
data = (
- b'\xfe\xff\x00D\x00j\x00a\x00n\x00g\x00o\x00='
- b'\x00=\x001\x00.\x004\x00.\x002'
+ b"\xfe\xff\x00D\x00j\x00a\x00n\x00g\x00o\x00="
+ b"\x00=\x001\x00.\x004\x00.\x002"
)
assert data.startswith(codecs.BOM_UTF16_BE)
assert auto_decode(data) == "Django==1.4.2"
- def test_auto_decode_no_bom(self):
- assert auto_decode(b'foobar') == 'foobar'
+ def test_auto_decode_no_bom(self) -> None:
+ assert auto_decode(b"foobar") == "foobar"
- def test_auto_decode_pep263_headers(self):
- latin1_req = '# coding=latin1\n# Pas trop de café'
- assert auto_decode(latin1_req.encode('latin1')) == latin1_req
+ def test_auto_decode_pep263_headers(self) -> None:
+ latin1_req = "# coding=latin1\n# Pas trop de café"
+ assert auto_decode(latin1_req.encode("latin1")) == latin1_req
- def test_auto_decode_no_preferred_encoding(self):
+ def test_auto_decode_no_preferred_encoding(self) -> None:
om, em = Mock(), Mock()
- om.return_value = 'ascii'
+ om.return_value = "ascii"
em.return_value = None
- data = 'data'
- with patch('sys.getdefaultencoding', om):
- with patch('locale.getpreferredencoding', em):
+ data = "data"
+ with patch("sys.getdefaultencoding", om):
+ with patch("locale.getpreferredencoding", em):
ret = auto_decode(data.encode(sys.getdefaultencoding()))
assert ret == data
- @pytest.mark.parametrize('encoding', [encoding for bom, encoding in BOMS])
- def test_all_encodings_are_valid(self, encoding):
+ @pytest.mark.parametrize("encoding", [encoding for bom, encoding in BOMS])
+ def test_all_encodings_are_valid(self, encoding: str) -> None:
# we really only care that there is no LookupError
- assert ''.encode(encoding).decode(encoding) == ''
+ assert "".encode(encoding).decode(encoding) == ""
-def raises(error):
+def raises(error: Type[Exception]) -> NoReturn:
raise error
class TestGlibc:
@pytest.mark.skipif("sys.platform == 'win32'")
- def test_glibc_version_string(self, monkeypatch):
+ def test_glibc_version_string(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
- os, "confstr", lambda x: "glibc 2.20", raising=False,
+ os,
+ "confstr",
+ lambda x: "glibc 2.20",
+ raising=False,
)
assert glibc_version_string() == "2.20"
@pytest.mark.skipif("sys.platform == 'win32'")
- def test_glibc_version_string_confstr(self, monkeypatch):
+ def test_glibc_version_string_confstr(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
monkeypatch.setattr(
- os, "confstr", lambda x: "glibc 2.20", raising=False,
+ os,
+ "confstr",
+ lambda x: "glibc 2.20",
+ raising=False,
)
assert glibc_version_string_confstr() == "2.20"
- @pytest.mark.parametrize("failure", [
- lambda x: raises(ValueError),
- lambda x: raises(OSError),
- lambda x: "XXX",
- ])
- def test_glibc_version_string_confstr_fail(self, monkeypatch, failure):
+ @pytest.mark.parametrize(
+ "failure",
+ [
+ lambda x: raises(ValueError),
+ lambda x: raises(OSError),
+ lambda x: "XXX",
+ ],
+ )
+ def test_glibc_version_string_confstr_fail(
+ self, monkeypatch: pytest.MonkeyPatch, failure: Callable[[Any], Any]
+ ) -> None:
monkeypatch.setattr(os, "confstr", failure, raising=False)
assert glibc_version_string_confstr() is None
- def test_glibc_version_string_confstr_missing(self, monkeypatch):
+ def test_glibc_version_string_confstr_missing(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
monkeypatch.delattr(os, "confstr", raising=False)
assert glibc_version_string_confstr() is None
- def test_glibc_version_string_ctypes_missing(self, monkeypatch):
+ def test_glibc_version_string_ctypes_missing(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
monkeypatch.setitem(sys.modules, "ctypes", None)
assert glibc_version_string_ctypes() is None
-@pytest.mark.parametrize('version_info, expected', [
- ((), (0, 0, 0)),
- ((3, ), (3, 0, 0)),
- ((3, 6), (3, 6, 0)),
- ((3, 6, 2), (3, 6, 2)),
- ((3, 6, 2, 4), (3, 6, 2)),
-])
-def test_normalize_version_info(version_info, expected):
+@pytest.mark.parametrize(
+ "version_info, expected",
+ [
+ ((), (0, 0, 0)),
+ ((3,), (3, 0, 0)),
+ ((3, 6), (3, 6, 0)),
+ ((3, 6, 2), (3, 6, 2)),
+ ((3, 6, 2, 4), (3, 6, 2)),
+ ],
+)
+def test_normalize_version_info(
+ version_info: Tuple[int, ...], expected: Tuple[int, int, int]
+) -> None:
actual = normalize_version_info(version_info)
assert actual == expected
class TestGetProg:
-
@pytest.mark.parametrize(
("argv", "executable", "expected"),
[
- ('/usr/bin/pip', '', 'pip'),
- ('-c', '/usr/bin/python', '/usr/bin/python -m pip'),
- ('__main__.py', '/usr/bin/python', '/usr/bin/python -m pip'),
- ('/usr/bin/pip3', '', 'pip3'),
- ]
+ ("/usr/bin/pip", "", "pip"),
+ ("-c", "/usr/bin/python", "/usr/bin/python -m pip"),
+ ("__main__.py", "/usr/bin/python", "/usr/bin/python -m pip"),
+ ("/usr/bin/pip3", "", "pip3"),
+ ],
)
- def test_get_prog(self, monkeypatch, argv, executable, expected):
- monkeypatch.setattr('pip._internal.utils.misc.sys.argv', [argv])
- monkeypatch.setattr(
- 'pip._internal.utils.misc.sys.executable',
- executable
- )
+ def test_get_prog(
+ self, monkeypatch: pytest.MonkeyPatch, argv: str, executable: str, expected: str
+ ) -> None:
+ monkeypatch.setattr("pip._internal.utils.misc.sys.argv", [argv])
+ monkeypatch.setattr("pip._internal.utils.misc.sys.executable", executable)
assert get_prog() == expected
-@pytest.mark.parametrize('host_port, expected_netloc', [
- # Test domain name.
- (('example.com', None), 'example.com'),
- (('example.com', 5000), 'example.com:5000'),
- # Test IPv4 address.
- (('127.0.0.1', None), '127.0.0.1'),
- (('127.0.0.1', 5000), '127.0.0.1:5000'),
- # Test bare IPv6 address.
- (('2001:db6::1', None), '2001:db6::1'),
- # Test IPv6 with port.
- (('2001:db6::1', 5000), '[2001:db6::1]:5000'),
-])
-def test_build_netloc(host_port, expected_netloc):
+@pytest.mark.parametrize(
+ "host_port, expected_netloc",
+ [
+ # Test domain name.
+ (("example.com", None), "example.com"),
+ (("example.com", 5000), "example.com:5000"),
+ # Test IPv4 address.
+ (("127.0.0.1", None), "127.0.0.1"),
+ (("127.0.0.1", 5000), "127.0.0.1:5000"),
+ # Test bare IPv6 address.
+ (("2001:db6::1", None), "2001:db6::1"),
+ # Test IPv6 with port.
+ (("2001:db6::1", 5000), "[2001:db6::1]:5000"),
+ ],
+)
+def test_build_netloc(
+ host_port: Tuple[str, Optional[int]], expected_netloc: str
+) -> None:
assert build_netloc(*host_port) == expected_netloc
-@pytest.mark.parametrize('netloc, expected_url, expected_host_port', [
- # Test domain name.
- ('example.com', 'https://example.com', ('example.com', None)),
- ('example.com:5000', 'https://example.com:5000', ('example.com', 5000)),
- # Test IPv4 address.
- ('127.0.0.1', 'https://127.0.0.1', ('127.0.0.1', None)),
- ('127.0.0.1:5000', 'https://127.0.0.1:5000', ('127.0.0.1', 5000)),
- # Test bare IPv6 address.
- ('2001:db6::1', 'https://[2001:db6::1]', ('2001:db6::1', None)),
- # Test IPv6 with port.
- (
- '[2001:db6::1]:5000',
- 'https://[2001:db6::1]:5000',
- ('2001:db6::1', 5000)
- ),
- # Test netloc with auth.
- (
- 'user:password@localhost:5000',
- 'https://user:password@localhost:5000',
- ('localhost', 5000)
- )
-])
+@pytest.mark.parametrize(
+ "netloc, expected_url, expected_host_port",
+ [
+ # Test domain name.
+ ("example.com", "https://example.com", ("example.com", None)),
+ ("example.com:5000", "https://example.com:5000", ("example.com", 5000)),
+ # Test IPv4 address.
+ ("127.0.0.1", "https://127.0.0.1", ("127.0.0.1", None)),
+ ("127.0.0.1:5000", "https://127.0.0.1:5000", ("127.0.0.1", 5000)),
+ # Test bare IPv6 address.
+ ("2001:db6::1", "https://[2001:db6::1]", ("2001:db6::1", None)),
+ # Test IPv6 with port.
+ ("[2001:db6::1]:5000", "https://[2001:db6::1]:5000", ("2001:db6::1", 5000)),
+ # Test netloc with auth.
+ (
+ "user:password@localhost:5000",
+ "https://user:password@localhost:5000",
+ ("localhost", 5000),
+ ),
+ ],
+)
def test_build_url_from_netloc_and_parse_netloc(
- netloc, expected_url, expected_host_port,
-):
+ netloc: str,
+ expected_url: str,
+ expected_host_port: Tuple[str, Optional[int]],
+) -> None:
assert build_url_from_netloc(netloc) == expected_url
assert parse_netloc(netloc) == expected_host_port
-@pytest.mark.parametrize('netloc, expected', [
- # Test a basic case.
- ('example.com', ('example.com', (None, None))),
- # Test with username and no password.
- ('user@example.com', ('example.com', ('user', None))),
- # Test with username and password.
- ('user:pass@example.com', ('example.com', ('user', 'pass'))),
- # Test with username and empty password.
- ('user:@example.com', ('example.com', ('user', ''))),
- # Test the password containing an @ symbol.
- ('user:pass@word@example.com', ('example.com', ('user', 'pass@word'))),
- # Test the password containing a : symbol.
- ('user:pass:word@example.com', ('example.com', ('user', 'pass:word'))),
- # Test URL-encoded reserved characters.
- ('user%3Aname:%23%40%5E@example.com',
- ('example.com', ('user:name', '#@^'))),
-])
-def test_split_auth_from_netloc(netloc, expected):
+@pytest.mark.parametrize(
+ "netloc, expected",
+ [
+ # Test a basic case.
+ ("example.com", ("example.com", (None, None))),
+ # Test with username and no password.
+ ("user@example.com", ("example.com", ("user", None))),
+ # Test with username and password.
+ ("user:pass@example.com", ("example.com", ("user", "pass"))),
+ # Test with username and empty password.
+ ("user:@example.com", ("example.com", ("user", ""))),
+ # Test the password containing an @ symbol.
+ ("user:pass@word@example.com", ("example.com", ("user", "pass@word"))),
+ # Test the password containing a : symbol.
+ ("user:pass:word@example.com", ("example.com", ("user", "pass:word"))),
+ # Test URL-encoded reserved characters.
+ ("user%3Aname:%23%40%5E@example.com", ("example.com", ("user:name", "#@^"))),
+ ],
+)
+def test_split_auth_from_netloc(
+ netloc: str, expected: Tuple[str, Tuple[Optional[str], Optional[str]]]
+) -> None:
actual = split_auth_from_netloc(netloc)
assert actual == expected
-@pytest.mark.parametrize('url, expected', [
- # Test a basic case.
- ('http://example.com/path#anchor',
- ('http://example.com/path#anchor', 'example.com', (None, None))),
- # Test with username and no password.
- ('http://user@example.com/path#anchor',
- ('http://example.com/path#anchor', 'example.com', ('user', None))),
- # Test with username and password.
- ('http://user:pass@example.com/path#anchor',
- ('http://example.com/path#anchor', 'example.com', ('user', 'pass'))),
- # Test with username and empty password.
- ('http://user:@example.com/path#anchor',
- ('http://example.com/path#anchor', 'example.com', ('user', ''))),
- # Test the password containing an @ symbol.
- ('http://user:pass@word@example.com/path#anchor',
- ('http://example.com/path#anchor', 'example.com', ('user', 'pass@word'))),
- # Test the password containing a : symbol.
- ('http://user:pass:word@example.com/path#anchor',
- ('http://example.com/path#anchor', 'example.com', ('user', 'pass:word'))),
- # Test URL-encoded reserved characters.
- ('http://user%3Aname:%23%40%5E@example.com/path#anchor',
- ('http://example.com/path#anchor', 'example.com', ('user:name', '#@^'))),
-])
-def test_split_auth_netloc_from_url(url, expected):
+@pytest.mark.parametrize(
+ "url, expected",
+ [
+ # Test a basic case.
+ (
+ "http://example.com/path#anchor",
+ ("http://example.com/path#anchor", "example.com", (None, None)),
+ ),
+ # Test with username and no password.
+ (
+ "http://user@example.com/path#anchor",
+ ("http://example.com/path#anchor", "example.com", ("user", None)),
+ ),
+ # Test with username and password.
+ (
+ "http://user:pass@example.com/path#anchor",
+ ("http://example.com/path#anchor", "example.com", ("user", "pass")),
+ ),
+ # Test with username and empty password.
+ (
+ "http://user:@example.com/path#anchor",
+ ("http://example.com/path#anchor", "example.com", ("user", "")),
+ ),
+ # Test the password containing an @ symbol.
+ (
+ "http://user:pass@word@example.com/path#anchor",
+ ("http://example.com/path#anchor", "example.com", ("user", "pass@word")),
+ ),
+ # Test the password containing a : symbol.
+ (
+ "http://user:pass:word@example.com/path#anchor",
+ ("http://example.com/path#anchor", "example.com", ("user", "pass:word")),
+ ),
+ # Test URL-encoded reserved characters.
+ (
+ "http://user%3Aname:%23%40%5E@example.com/path#anchor",
+ ("http://example.com/path#anchor", "example.com", ("user:name", "#@^")),
+ ),
+ ],
+)
+def test_split_auth_netloc_from_url(
+ url: str, expected: Tuple[str, str, Tuple[Optional[str], Optional[str]]]
+) -> None:
actual = split_auth_netloc_from_url(url)
assert actual == expected
-@pytest.mark.parametrize('netloc, expected', [
- # Test a basic case.
- ('example.com', 'example.com'),
- # Test with username and no password.
- ('accesstoken@example.com', '****@example.com'),
- # Test with username and password.
- ('user:pass@example.com', 'user:****@example.com'),
- # Test with username and empty password.
- ('user:@example.com', 'user:****@example.com'),
- # Test the password containing an @ symbol.
- ('user:pass@word@example.com', 'user:****@example.com'),
- # Test the password containing a : symbol.
- ('user:pass:word@example.com', 'user:****@example.com'),
- # Test URL-encoded reserved characters.
- ('user%3Aname:%23%40%5E@example.com', 'user%3Aname:****@example.com'),
-])
-def test_redact_netloc(netloc, expected):
+@pytest.mark.parametrize(
+ "netloc, expected",
+ [
+ # Test a basic case.
+ ("example.com", "example.com"),
+ # Test with username and no password.
+ ("accesstoken@example.com", "****@example.com"),
+ # Test with username and password.
+ ("user:pass@example.com", "user:****@example.com"),
+ # Test with username and empty password.
+ ("user:@example.com", "user:****@example.com"),
+ # Test the password containing an @ symbol.
+ ("user:pass@word@example.com", "user:****@example.com"),
+ # Test the password containing a : symbol.
+ ("user:pass:word@example.com", "user:****@example.com"),
+ # Test URL-encoded reserved characters.
+ ("user%3Aname:%23%40%5E@example.com", "user%3Aname:****@example.com"),
+ ],
+)
+def test_redact_netloc(netloc: str, expected: str) -> None:
actual = redact_netloc(netloc)
assert actual == expected
-@pytest.mark.parametrize('auth_url, expected_url', [
- ('https://user:pass@domain.tld/project/tags/v0.2',
- 'https://domain.tld/project/tags/v0.2'),
- ('https://domain.tld/project/tags/v0.2',
- 'https://domain.tld/project/tags/v0.2',),
- ('https://user:pass@domain.tld/svn/project/trunk@8181',
- 'https://domain.tld/svn/project/trunk@8181'),
- ('https://domain.tld/project/trunk@8181',
- 'https://domain.tld/project/trunk@8181',),
- ('git+https://pypi.org/something',
- 'git+https://pypi.org/something'),
- ('git+https://user:pass@pypi.org/something',
- 'git+https://pypi.org/something'),
- ('git+ssh://git@pypi.org/something',
- 'git+ssh://pypi.org/something'),
-])
-def test_remove_auth_from_url(auth_url, expected_url):
+@pytest.mark.parametrize(
+ "auth_url, expected_url",
+ [
+ (
+ "https://user:pass@domain.tld/project/tags/v0.2",
+ "https://domain.tld/project/tags/v0.2",
+ ),
+ (
+ "https://domain.tld/project/tags/v0.2",
+ "https://domain.tld/project/tags/v0.2",
+ ),
+ (
+ "https://user:pass@domain.tld/svn/project/trunk@8181",
+ "https://domain.tld/svn/project/trunk@8181",
+ ),
+ (
+ "https://domain.tld/project/trunk@8181",
+ "https://domain.tld/project/trunk@8181",
+ ),
+ ("git+https://pypi.org/something", "git+https://pypi.org/something"),
+ ("git+https://user:pass@pypi.org/something", "git+https://pypi.org/something"),
+ ("git+ssh://git@pypi.org/something", "git+ssh://pypi.org/something"),
+ ],
+)
+def test_remove_auth_from_url(auth_url: str, expected_url: str) -> None:
url = remove_auth_from_url(auth_url)
assert url == expected_url
-@pytest.mark.parametrize('auth_url, expected_url', [
- ('https://accesstoken@example.com/abc', 'https://****@example.com/abc'),
- ('https://user:password@example.com', 'https://user:****@example.com'),
- ('https://user:@example.com', 'https://user:****@example.com'),
- ('https://example.com', 'https://example.com'),
- # Test URL-encoded reserved characters.
- ('https://user%3Aname:%23%40%5E@example.com',
- 'https://user%3Aname:****@example.com'),
-])
-def test_redact_auth_from_url(auth_url, expected_url):
+@pytest.mark.parametrize(
+ "auth_url, expected_url",
+ [
+ ("https://accesstoken@example.com/abc", "https://****@example.com/abc"),
+ ("https://user:password@example.com", "https://user:****@example.com"),
+ ("https://user:@example.com", "https://user:****@example.com"),
+ ("https://example.com", "https://example.com"),
+ # Test URL-encoded reserved characters.
+ (
+ "https://user%3Aname:%23%40%5E@example.com",
+ "https://user%3Aname:****@example.com",
+ ),
+ ],
+)
+def test_redact_auth_from_url(auth_url: str, expected_url: str) -> None:
url = redact_auth_from_url(auth_url)
assert url == expected_url
class TestHiddenText:
-
- def test_basic(self):
+ def test_basic(self) -> None:
"""
Test str(), repr(), and attribute access.
"""
- hidden = HiddenText('my-secret', redacted='######')
+ hidden = HiddenText("my-secret", redacted="######")
assert repr(hidden) == "<HiddenText '######'>"
- assert str(hidden) == '######'
- assert hidden.redacted == '######'
- assert hidden.secret == 'my-secret'
+ assert str(hidden) == "######"
+ assert hidden.redacted == "######"
+ assert hidden.secret == "my-secret"
- def test_equality_with_str(self):
+ def test_equality_with_str(self) -> None:
"""
Test equality (and inequality) with str objects.
"""
- hidden = HiddenText('secret', redacted='****')
+ hidden = HiddenText("secret", redacted="****")
# Test that the object doesn't compare equal to either its original
# or redacted forms.
@@ -825,50 +779,51 @@ class TestHiddenText:
assert hidden != hidden.redacted
assert hidden.redacted != hidden
- def test_equality_same_secret(self):
+ def test_equality_same_secret(self) -> None:
"""
Test equality with an object having the same secret.
"""
# Choose different redactions for the two objects.
- hidden1 = HiddenText('secret', redacted='****')
- hidden2 = HiddenText('secret', redacted='####')
+ hidden1 = HiddenText("secret", redacted="****")
+ hidden2 = HiddenText("secret", redacted="####")
assert hidden1 == hidden2
# Also test __ne__.
assert not hidden1 != hidden2
- def test_equality_different_secret(self):
+ def test_equality_different_secret(self) -> None:
"""
Test equality with an object having a different secret.
"""
- hidden1 = HiddenText('secret-1', redacted='****')
- hidden2 = HiddenText('secret-2', redacted='****')
+ hidden1 = HiddenText("secret-1", redacted="****")
+ hidden2 = HiddenText("secret-2", redacted="****")
assert hidden1 != hidden2
# Also test __eq__.
assert not hidden1 == hidden2
-def test_hide_value():
- hidden = hide_value('my-secret')
+def test_hide_value() -> None:
+ hidden = hide_value("my-secret")
assert repr(hidden) == "<HiddenText '****'>"
- assert str(hidden) == '****'
- assert hidden.redacted == '****'
- assert hidden.secret == 'my-secret'
+ assert str(hidden) == "****"
+ assert hidden.redacted == "****"
+ assert hidden.secret == "my-secret"
-def test_hide_url():
- hidden_url = hide_url('https://user:password@example.com')
+def test_hide_url() -> None:
+ hidden_url = hide_url("https://user:password@example.com")
assert repr(hidden_url) == "<HiddenText 'https://user:****@example.com'>"
- assert str(hidden_url) == 'https://user:****@example.com'
- assert hidden_url.redacted == 'https://user:****@example.com'
- assert hidden_url.secret == 'https://user:password@example.com'
+ assert str(hidden_url) == "https://user:****@example.com"
+ assert hidden_url.redacted == "https://user:****@example.com"
+ assert hidden_url.secret == "https://user:password@example.com"
@pytest.fixture()
-def patch_deprecation_check_version():
+def patch_deprecation_check_version() -> Iterator[None]:
# We do this, so that the deprecation tests are easier to write.
import pip._internal.utils.deprecation as d
+
old_version = d.current_version
d.current_version = "1.0"
yield
@@ -879,21 +834,29 @@ def patch_deprecation_check_version():
@pytest.mark.parametrize("replacement", [None, "a magic 8 ball"])
@pytest.mark.parametrize("gone_in", [None, "2.0"])
@pytest.mark.parametrize("issue", [None, 988])
-def test_deprecated_message_contains_information(gone_in, replacement, issue):
+@pytest.mark.parametrize("feature_flag", [None, "magic-8-ball"])
+def test_deprecated_message_contains_information(
+ gone_in: Optional[str],
+ replacement: Optional[str],
+ issue: Optional[int],
+ feature_flag: Optional[str],
+) -> None:
with pytest.warns(PipDeprecationWarning) as record:
deprecated(
- "Stop doing this!",
+ reason="Stop doing this!",
replacement=replacement,
gone_in=gone_in,
+ feature_flag=feature_flag,
issue=issue,
)
assert len(record) == 1
+ assert isinstance(record[0].message, PipDeprecationWarning)
message = record[0].message.args[0]
assert "DEPRECATION: Stop doing this!" in message
# Ensure non-None values are mentioned.
- for item in [gone_in, replacement, issue]:
+ for item in [gone_in, replacement, issue, feature_flag]:
if item is not None:
assert str(item) in message
@@ -901,12 +864,16 @@ def test_deprecated_message_contains_information(gone_in, replacement, issue):
@pytest.mark.usefixtures("patch_deprecation_check_version")
@pytest.mark.parametrize("replacement", [None, "a magic 8 ball"])
@pytest.mark.parametrize("issue", [None, 988])
-def test_deprecated_raises_error_if_too_old(replacement, issue):
+@pytest.mark.parametrize("feature_flag", [None, "magic-8-ball"])
+def test_deprecated_raises_error_if_too_old(
+ replacement: Optional[str], issue: Optional[int], feature_flag: Optional[str]
+) -> None:
with pytest.raises(PipDeprecationWarning) as exception:
deprecated(
- "Stop doing this!",
+ reason="Stop doing this!",
gone_in="1.0", # this matches the patched version.
replacement=replacement,
+ feature_flag=feature_flag,
issue=issue,
)
@@ -914,6 +881,7 @@ def test_deprecated_raises_error_if_too_old(replacement, issue):
assert "DEPRECATION: Stop doing this!" in message
assert "1.0" in message
+ assert str(feature_flag) not in message
# Ensure non-None values are mentioned.
for item in [replacement, issue]:
if item is not None:
@@ -921,50 +889,75 @@ def test_deprecated_raises_error_if_too_old(replacement, issue):
@pytest.mark.usefixtures("patch_deprecation_check_version")
-def test_deprecated_message_reads_well():
+def test_deprecated_message_reads_well_past() -> None:
with pytest.raises(PipDeprecationWarning) as exception:
deprecated(
- "Stop doing this!",
+ reason="Stop doing this!",
gone_in="1.0", # this matches the patched version.
replacement="to be nicer",
- issue="100000", # I hope we never reach this number.
+ feature_flag="magic-8-ball",
+ issue=100000,
)
message = exception.value.args[0]
assert message == (
"DEPRECATION: Stop doing this! "
- "This behavior change has been enforced since pip 1.0. "
+ "Since pip 1.0, this is no longer supported. "
+ "A possible replacement is to be nicer. "
+ "Discussion can be found at https://github.com/pypa/pip/issues/100000"
+ )
+
+
+@pytest.mark.usefixtures("patch_deprecation_check_version")
+def test_deprecated_message_reads_well_future() -> None:
+ with pytest.warns(PipDeprecationWarning) as record:
+ deprecated(
+ reason="Stop doing this!",
+ gone_in="2.0", # this is greater than the patched version.
+ replacement="to be nicer",
+ feature_flag="crisis",
+ issue=100000,
+ )
+
+ assert len(record) == 1
+ assert isinstance(record[0].message, PipDeprecationWarning)
+ message = record[0].message.args[0]
+
+ assert message == (
+ "DEPRECATION: Stop doing this! "
+ "pip 2.0 will enforce this behaviour change. "
"A possible replacement is to be nicer. "
- "Discussion can be found at "
- "https://github.com/pypa/pip/issues/100000."
+ "You can use the flag --use-feature=crisis to test the upcoming behaviour. "
+ "Discussion can be found at https://github.com/pypa/pip/issues/100000"
)
-def test_make_setuptools_shim_args():
+def test_make_setuptools_shim_args() -> None:
# Test all arguments at once, including the overall ordering.
args = make_setuptools_shim_args(
- '/dir/path/setup.py',
- global_options=['--some', '--option'],
+ "/dir/path/setup.py",
+ global_options=["--some", "--option"],
no_user_config=True,
unbuffered_output=True,
)
- assert args[1:3] == ['-u', '-c']
+ assert args[1:3] == ["-u", "-c"]
+ assert args[4:] == ["--some", "--option", "--no-user-cfg"]
+
+ shim = args[3]
# Spot-check key aspects of the command string.
- assert "sys.argv[0] = '/dir/path/setup.py'" in args[3]
- assert "__file__='/dir/path/setup.py'" in args[3]
- assert args[4:] == ['--some', '--option', '--no-user-cfg']
+ assert "import setuptools" in shim
+ assert "'/dir/path/setup.py'" in args[3]
+ assert "sys.argv[0] = __file__" in args[3]
-@pytest.mark.parametrize('global_options', [
- None,
- [],
- ['--some', '--option']
-])
-def test_make_setuptools_shim_args__global_options(global_options):
+@pytest.mark.parametrize("global_options", [None, [], ["--some", "--option"]])
+def test_make_setuptools_shim_args__global_options(
+ global_options: Optional[List[str]],
+) -> None:
args = make_setuptools_shim_args(
- '/dir/path/setup.py',
+ "/dir/path/setup.py",
global_options=global_options,
)
@@ -976,61 +969,79 @@ def test_make_setuptools_shim_args__global_options(global_options):
assert len(args) == 3
-@pytest.mark.parametrize('no_user_config', [False, True])
-def test_make_setuptools_shim_args__no_user_config(no_user_config):
+@pytest.mark.parametrize("no_user_config", [False, True])
+def test_make_setuptools_shim_args__no_user_config(no_user_config: bool) -> None:
args = make_setuptools_shim_args(
- '/dir/path/setup.py',
+ "/dir/path/setup.py",
no_user_config=no_user_config,
)
- assert ('--no-user-cfg' in args) == no_user_config
+ assert ("--no-user-cfg" in args) == no_user_config
-@pytest.mark.parametrize('unbuffered_output', [False, True])
-def test_make_setuptools_shim_args__unbuffered_output(unbuffered_output):
+@pytest.mark.parametrize("unbuffered_output", [False, True])
+def test_make_setuptools_shim_args__unbuffered_output(unbuffered_output: bool) -> None:
args = make_setuptools_shim_args(
- '/dir/path/setup.py',
- unbuffered_output=unbuffered_output
+ "/dir/path/setup.py", unbuffered_output=unbuffered_output
)
- assert ('-u' in args) == unbuffered_output
+ assert ("-u" in args) == unbuffered_output
-@pytest.mark.parametrize('isatty,no_stdin,expected', [
- (True, False, True),
- (False, False, False),
- (True, True, False),
- (False, True, False),
-])
-def test_is_console_interactive(monkeypatch, isatty, no_stdin, expected):
- monkeypatch.setattr(sys.stdin, 'isatty', Mock(return_value=isatty))
+@pytest.mark.parametrize(
+ "isatty,no_stdin,expected",
+ [
+ (True, False, True),
+ (False, False, False),
+ (True, True, False),
+ (False, True, False),
+ ],
+)
+def test_is_console_interactive(
+ monkeypatch: pytest.MonkeyPatch, isatty: bool, no_stdin: bool, expected: bool
+) -> None:
+ monkeypatch.setattr(sys.stdin, "isatty", Mock(return_value=isatty))
if no_stdin:
- monkeypatch.setattr(sys, 'stdin', None)
+ monkeypatch.setattr(sys, "stdin", None)
assert is_console_interactive() is expected
-@pytest.mark.parametrize('size,expected', [
- (123, "123 bytes"),
- (1234, "1.2 kB"),
- (123456, "123 kB"),
- (1234567890, "1234.6 MB"),
-])
-def test_format_size(size, expected):
+@pytest.mark.parametrize(
+ "size,expected",
+ [
+ (123, "123 bytes"),
+ (1234, "1.2 kB"),
+ (123456, "123 kB"),
+ (1234567890, "1234.6 MB"),
+ ],
+)
+def test_format_size(size: int, expected: str) -> None:
assert format_size(size) == expected
@pytest.mark.parametrize(
- ('rows', 'table', 'sizes'),
- [([], [], []),
- ([('I?', 'version', 'sdist', 'wheel'),
- ('', '1.18.2', 'zip', 'cp38-cp38m-win_amd64'),
- ('v', 1.18, 'zip')],
- ['I? version sdist wheel',
- ' 1.18.2 zip cp38-cp38m-win_amd64',
- 'v 1.18 zip'],
- [2, 7, 5, 20]),
- ([('I?', 'version', 'sdist', 'wheel'), (), ('v', '1.18.1', 'zip')],
- ['I? version sdist wheel', '', 'v 1.18.1 zip'],
- [2, 7, 5, 5])])
-def test_tabulate(rows, table, sizes):
+ ("rows", "table", "sizes"),
+ [
+ ([], [], []),
+ (
+ [
+ ("I?", "version", "sdist", "wheel"),
+ ("", "1.18.2", "zip", "cp38-cp38m-win_amd64"),
+ ("v", 1.18, "zip"),
+ ],
+ [
+ "I? version sdist wheel",
+ " 1.18.2 zip cp38-cp38m-win_amd64",
+ "v 1.18 zip",
+ ],
+ [2, 7, 5, 20],
+ ),
+ (
+ [("I?", "version", "sdist", "wheel"), (), ("v", "1.18.1", "zip")],
+ ["I? version sdist wheel", "", "v 1.18.1 zip"],
+ [2, 7, 5, 5],
+ ),
+ ],
+)
+def test_tabulate(rows: List[Tuple[str]], table: List[str], sizes: List[int]) -> None:
assert tabulate(rows) == (table, sizes)
diff --git a/tests/unit/test_utils_compatibility_tags.py b/tests/unit/test_utils_compatibility_tags.py
index ded7ecb06..f09c451b8 100644
--- a/tests/unit/test_utils_compatibility_tags.py
+++ b/tests/unit/test_utils_compatibility_tags.py
@@ -1,4 +1,5 @@
import sysconfig
+from typing import Any, Callable, Dict, List, Tuple
from unittest.mock import patch
import pytest
@@ -6,96 +7,102 @@ import pytest
from pip._internal.utils import compatibility_tags
-@pytest.mark.parametrize('version_info, expected', [
- ((2,), '2'),
- ((2, 8), '28'),
- ((3,), '3'),
- ((3, 6), '36'),
- # Test a tuple of length 3.
- ((3, 6, 5), '36'),
- # Test a 2-digit minor version.
- ((3, 10), '310'),
-])
-def test_version_info_to_nodot(version_info, expected):
+@pytest.mark.parametrize(
+ "version_info, expected",
+ [
+ ((2,), "2"),
+ ((2, 8), "28"),
+ ((3,), "3"),
+ ((3, 6), "36"),
+ # Test a tuple of length 3.
+ ((3, 6, 5), "36"),
+ # Test a 2-digit minor version.
+ ((3, 10), "310"),
+ ],
+)
+def test_version_info_to_nodot(version_info: Tuple[int], expected: str) -> None:
actual = compatibility_tags.version_info_to_nodot(version_info)
assert actual == expected
class Testcompatibility_tags:
-
- def mock_get_config_var(self, **kwd):
+ def mock_get_config_var(self, **kwd: str) -> Callable[[str], Any]:
"""
Patch sysconfig.get_config_var for arbitrary keys.
"""
get_config_var = sysconfig.get_config_var
- def _mock_get_config_var(var):
+ def _mock_get_config_var(var: str) -> Any:
if var in kwd:
return kwd[var]
return get_config_var(var)
+
return _mock_get_config_var
- def test_no_hyphen_tag(self):
+ def test_no_hyphen_tag(self) -> None:
"""
Test that no tag contains a hyphen.
"""
import pip._internal.utils.compatibility_tags
- mock_gcf = self.mock_get_config_var(SOABI='cpython-35m-darwin')
+ mock_gcf = self.mock_get_config_var(SOABI="cpython-35m-darwin")
- with patch('sysconfig.get_config_var', mock_gcf):
+ with patch("sysconfig.get_config_var", mock_gcf):
supported = pip._internal.utils.compatibility_tags.get_supported()
for tag in supported:
- assert '-' not in tag.interpreter
- assert '-' not in tag.abi
- assert '-' not in tag.platform
+ assert "-" not in tag.interpreter
+ assert "-" not in tag.abi
+ assert "-" not in tag.platform
class TestManylinux2010Tags:
-
- @pytest.mark.parametrize("manylinux2010,manylinux1", [
- ("manylinux2010_x86_64", "manylinux1_x86_64"),
- ("manylinux2010_i686", "manylinux1_i686"),
- ])
- def test_manylinux2010_implies_manylinux1(self, manylinux2010, manylinux1):
+ @pytest.mark.parametrize(
+ "manylinux2010,manylinux1",
+ [
+ ("manylinux2010_x86_64", "manylinux1_x86_64"),
+ ("manylinux2010_i686", "manylinux1_i686"),
+ ],
+ )
+ def test_manylinux2010_implies_manylinux1(
+ self, manylinux2010: str, manylinux1: str
+ ) -> None:
"""
Specifying manylinux2010 implies manylinux1.
"""
- groups = {}
+ groups: Dict[Tuple[str, str], List[str]] = {}
supported = compatibility_tags.get_supported(platforms=[manylinux2010])
for tag in supported:
- groups.setdefault(
- (tag.interpreter, tag.abi), []
- ).append(tag.platform)
+ groups.setdefault((tag.interpreter, tag.abi), []).append(tag.platform)
for arches in groups.values():
- if arches == ['any']:
+ if arches == ["any"]:
continue
assert arches[:2] == [manylinux2010, manylinux1]
class TestManylinux2014Tags:
-
- @pytest.mark.parametrize("manylinuxA,manylinuxB", [
- ("manylinux2014_x86_64", ["manylinux2010_x86_64",
- "manylinux1_x86_64"]),
- ("manylinux2014_i686", ["manylinux2010_i686", "manylinux1_i686"]),
- ])
- def test_manylinuxA_implies_manylinuxB(self, manylinuxA, manylinuxB):
+ @pytest.mark.parametrize(
+ "manylinuxA,manylinuxB",
+ [
+ ("manylinux2014_x86_64", ["manylinux2010_x86_64", "manylinux1_x86_64"]),
+ ("manylinux2014_i686", ["manylinux2010_i686", "manylinux1_i686"]),
+ ],
+ )
+ def test_manylinuxA_implies_manylinuxB(
+ self, manylinuxA: str, manylinuxB: List[str]
+ ) -> None:
"""
Specifying manylinux2014 implies manylinux2010/manylinux1.
"""
- groups = {}
+ groups: Dict[Tuple[str, str], List[str]] = {}
supported = compatibility_tags.get_supported(platforms=[manylinuxA])
for tag in supported:
- groups.setdefault(
- (tag.interpreter, tag.abi), []
- ).append(tag.platform)
+ groups.setdefault((tag.interpreter, tag.abi), []).append(tag.platform)
expected_arches = [manylinuxA]
expected_arches.extend(manylinuxB)
for arches in groups.values():
- if arches == ['any']:
+ if arches == ["any"]:
continue
assert arches[:3] == expected_arches
diff --git a/tests/unit/test_utils_distutils_args.py b/tests/unit/test_utils_distutils_args.py
index 96cdb1804..21f31e926 100644
--- a/tests/unit/test_utils_distutils_args.py
+++ b/tests/unit/test_utils_distutils_args.py
@@ -3,30 +3,30 @@ import pytest
from pip._internal.utils.distutils_args import parse_distutils_args
-def test_unknown_option_is_ok():
+def test_unknown_option_is_ok() -> None:
result = parse_distutils_args(["--foo"])
assert not result
-def test_option_is_returned():
+def test_option_is_returned() -> None:
result = parse_distutils_args(["--prefix=hello"])
assert result["prefix"] == "hello"
-def test_options_are_clobbered():
+def test_options_are_clobbered() -> None:
# Matches the current setuptools behavior that the last argument
# wins.
result = parse_distutils_args(["--prefix=hello", "--prefix=world"])
assert result["prefix"] == "world"
-def test_multiple_options_work():
+def test_multiple_options_work() -> None:
result = parse_distutils_args(["--prefix=hello", "--root=world"])
assert result["prefix"] == "hello"
assert result["root"] == "world"
-def test_multiple_invocations_do_not_keep_options():
+def test_multiple_invocations_do_not_keep_options() -> None:
result = parse_distutils_args(["--prefix=hello1"])
assert len(result) == 1
assert result["prefix"] == "hello1"
@@ -36,25 +36,28 @@ def test_multiple_invocations_do_not_keep_options():
assert result["root"] == "world1"
-@pytest.mark.parametrize("name,value", [
- ("exec-prefix", "1"),
- ("home", "2"),
- ("install-base", "3"),
- ("install-data", "4"),
- ("install-headers", "5"),
- ("install-lib", "6"),
- ("install-platlib", "7"),
- ("install-purelib", "8"),
- ("install-scripts", "9"),
- ("prefix", "10"),
- ("root", "11"),
-])
-def test_all_value_options_work(name, value):
+@pytest.mark.parametrize(
+ "name,value",
+ [
+ ("exec-prefix", "1"),
+ ("home", "2"),
+ ("install-base", "3"),
+ ("install-data", "4"),
+ ("install-headers", "5"),
+ ("install-lib", "6"),
+ ("install-platlib", "7"),
+ ("install-purelib", "8"),
+ ("install-scripts", "9"),
+ ("prefix", "10"),
+ ("root", "11"),
+ ],
+)
+def test_all_value_options_work(name: str, value: str) -> None:
result = parse_distutils_args([f"--{name}={value}"])
key_name = name.replace("-", "_")
assert result[key_name] == value
-def test_user_option_works():
+def test_user_option_works() -> None:
result = parse_distutils_args(["--user"])
- assert result["user"] == 1
+ assert result["user"]
diff --git a/tests/unit/test_utils_filesystem.py b/tests/unit/test_utils_filesystem.py
index 3ef814dce..7e3ecc2e0 100644
--- a/tests/unit/test_utils_filesystem.py
+++ b/tests/unit/test_utils_filesystem.py
@@ -1,61 +1,20 @@
import os
-import shutil
+from pathlib import Path
-import pytest
-from pip._internal.utils.filesystem import copy2_fixed, is_socket
-from tests.lib.filesystem import make_socket_file, make_unreadable_file
-from tests.lib.path import Path
-
-
-def make_file(path):
+def make_file(path: str) -> None:
Path(path).touch()
-def make_valid_symlink(path):
+def make_valid_symlink(path: str) -> None:
target = path + "1"
make_file(target)
os.symlink(target, path)
-def make_broken_symlink(path):
+def make_broken_symlink(path: str) -> None:
os.symlink("foo", path)
-def make_dir(path):
+def make_dir(path: str) -> None:
os.mkdir(path)
-
-
-skip_on_windows = pytest.mark.skipif("sys.platform == 'win32'")
-
-
-@skip_on_windows
-@pytest.mark.parametrize("create,result", [
- (make_socket_file, True),
- (make_file, False),
- (make_valid_symlink, False),
- (make_broken_symlink, False),
- (make_dir, False),
-])
-def test_is_socket(create, result, tmpdir):
- target = tmpdir.joinpath("target")
- create(target)
- assert os.path.lexists(target)
- assert is_socket(target) == result
-
-
-@pytest.mark.parametrize("create,error_type", [
- pytest.param(
- make_socket_file, shutil.SpecialFileError, marks=skip_on_windows
- ),
- (make_unreadable_file, OSError),
-])
-def test_copy2_fixed_raises_appropriate_errors(create, error_type, tmpdir):
- src = tmpdir.joinpath("src")
- create(src)
- dest = tmpdir.joinpath("dest")
-
- with pytest.raises(error_type):
- copy2_fixed(src, dest)
-
- assert not dest.exists()
diff --git a/tests/unit/test_utils_parallel.py b/tests/unit/test_utils_parallel.py
deleted file mode 100644
index 5a23f7d65..000000000
--- a/tests/unit/test_utils_parallel.py
+++ /dev/null
@@ -1,72 +0,0 @@
-"""Test multiprocessing/multithreading higher-order functions."""
-
-from contextlib import contextmanager
-from importlib import import_module
-from math import factorial
-from sys import modules
-
-from pytest import mark
-
-DUNDER_IMPORT = 'builtins.__import__'
-FUNC, ITERABLE = factorial, range(42)
-MAPS = 'map_multiprocess', 'map_multithread'
-_import = __import__
-
-
-def unload_parallel():
- try:
- del modules['pip._internal.utils.parallel']
- except KeyError:
- pass
-
-
-@contextmanager
-def tmp_import_parallel():
- unload_parallel()
- try:
- yield import_module('pip._internal.utils.parallel')
- finally:
- unload_parallel()
-
-
-def lack_sem_open(name, *args, **kwargs):
- """Raise ImportError on import of multiprocessing.synchronize."""
- if name.endswith('synchronize'):
- raise ImportError
- return _import(name, *args, **kwargs)
-
-
-def have_sem_open(name, *args, **kwargs):
- """Make sure multiprocessing.synchronize import is successful."""
- # We don't care about the return value
- # since we don't use the pool with this import.
- if name.endswith('synchronize'):
- return
- return _import(name, *args, **kwargs)
-
-
-@mark.parametrize('name', MAPS)
-def test_lack_sem_open(name, monkeypatch):
- """Test fallback when sem_open is not available.
-
- If so, multiprocessing[.dummy].Pool will fail to be created and
- map_async should fallback to map.
- """
- monkeypatch.setattr(DUNDER_IMPORT, lack_sem_open)
- with tmp_import_parallel() as parallel:
- assert getattr(parallel, name) is parallel._map_fallback
-
-
-@mark.parametrize('name', MAPS)
-def test_have_sem_open(name, monkeypatch):
- """Test fallback when sem_open is available."""
- monkeypatch.setattr(DUNDER_IMPORT, have_sem_open)
- with tmp_import_parallel() as parallel:
- assert getattr(parallel, name) is getattr(parallel, f'_{name}')
-
-
-@mark.parametrize('name', MAPS)
-def test_map(name):
- """Test correctness of result of asynchronous maps."""
- map_async = getattr(import_module('pip._internal.utils.parallel'), name)
- assert set(map_async(FUNC, ITERABLE)) == set(map(FUNC, ITERABLE))
diff --git a/tests/unit/test_utils_pkg_resources.py b/tests/unit/test_utils_pkg_resources.py
deleted file mode 100644
index 7cc95b91b..000000000
--- a/tests/unit/test_utils_pkg_resources.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from email.message import Message
-
-import pytest
-from pip._vendor.pkg_resources import DistInfoDistribution, Requirement
-
-from pip._internal.utils.packaging import get_metadata, get_requires_python
-from pip._internal.utils.pkg_resources import DictMetadata
-
-
-def test_dict_metadata_works():
- name = "simple"
- version = "0.1.0"
- require_a = "a==1.0"
- require_b = "b==1.1; extra == 'also_b'"
- requires = [require_a, require_b, "c==1.2; extra == 'also_c'"]
- extras = ["also_b", "also_c"]
- requires_python = ">=3"
-
- metadata = Message()
- metadata["Name"] = name
- metadata["Version"] = version
- for require in requires:
- metadata["Requires-Dist"] = require
- for extra in extras:
- metadata["Provides-Extra"] = extra
- metadata["Requires-Python"] = requires_python
-
- inner_metadata = DictMetadata({"METADATA": metadata.as_bytes()})
- dist = DistInfoDistribution(
- location="<in-memory>", metadata=inner_metadata, project_name=name
- )
-
- assert name == dist.project_name
- assert version == dist.version
- assert set(extras) == set(dist.extras)
- assert [Requirement.parse(require_a)] == dist.requires([])
- assert [
- Requirement.parse(require_a), Requirement.parse(require_b)
- ] == dist.requires(["also_b"])
- assert metadata.as_string() == get_metadata(dist).as_string()
- assert requires_python == get_requires_python(dist)
-
-
-def test_dict_metadata_throws_on_bad_unicode():
- metadata = DictMetadata({
- "METADATA": b"\xff"
- })
-
- with pytest.raises(UnicodeDecodeError) as e:
- metadata.get_metadata("METADATA")
- assert "METADATA" in str(e.value)
diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py
index 2c21603c7..a694b717f 100644
--- a/tests/unit/test_utils_subprocess.py
+++ b/tests/unit/test_utils_subprocess.py
@@ -1,7 +1,7 @@
import locale
import sys
from logging import DEBUG, ERROR, INFO, WARNING
-from textwrap import dedent
+from typing import List, Optional, Tuple, Type
import pytest
@@ -10,123 +10,47 @@ from pip._internal.exceptions import InstallationSubprocessError
from pip._internal.utils.logging import VERBOSE
from pip._internal.utils.misc import hide_value
from pip._internal.utils.subprocess import (
+ CommandArgs,
call_subprocess,
format_command_args,
make_command,
- make_subprocess_output_error,
subprocess_logger,
)
-@pytest.mark.parametrize('args, expected', [
- (['pip', 'list'], 'pip list'),
- (['foo', 'space space', 'new\nline', 'double"quote', "single'quote"],
- """foo 'space space' 'new\nline' 'double"quote' 'single'"'"'quote'"""),
- # Test HiddenText arguments.
- (make_command(hide_value('secret1'), 'foo', hide_value('secret2')),
- "'****' foo '****'"),
-])
-def test_format_command_args(args, expected):
+@pytest.mark.parametrize(
+ "args, expected",
+ [
+ (["pip", "list"], "pip list"),
+ (
+ ["foo", "space space", "new\nline", 'double"quote', "single'quote"],
+ """foo 'space space' 'new\nline' 'double"quote' 'single'"'"'quote'""",
+ ),
+ # Test HiddenText arguments.
+ (
+ make_command(hide_value("secret1"), "foo", hide_value("secret2")),
+ "'****' foo '****'",
+ ),
+ ],
+)
+def test_format_command_args(args: CommandArgs, expected: str) -> None:
actual = format_command_args(args)
assert actual == expected
-def test_make_subprocess_output_error():
- cmd_args = ['test', 'has space']
- cwd = '/path/to/cwd'
- lines = ['line1\n', 'line2\n', 'line3\n']
- actual = make_subprocess_output_error(
- cmd_args=cmd_args,
- cwd=cwd,
- lines=lines,
- exit_status=3,
- )
- expected = dedent("""\
- Command errored out with exit status 3:
- command: test 'has space'
- cwd: /path/to/cwd
- Complete output (3 lines):
- line1
- line2
- line3
- ----------------------------------------""")
- assert actual == expected, f'actual: {actual}'
-
-
-def test_make_subprocess_output_error__non_ascii_command_arg(monkeypatch):
- """
- Test a command argument with a non-ascii character.
- """
- cmd_args = ['foo', 'déf']
-
- # We need to monkeypatch so the encoding will be correct on Windows.
- monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8')
- actual = make_subprocess_output_error(
- cmd_args=cmd_args,
- cwd='/path/to/cwd',
- lines=[],
- exit_status=1,
- )
- expected = dedent("""\
- Command errored out with exit status 1:
- command: foo 'déf'
- cwd: /path/to/cwd
- Complete output (0 lines):
- ----------------------------------------""")
- assert actual == expected, f'actual: {actual}'
-
-
-def test_make_subprocess_output_error__non_ascii_cwd_python_3(monkeypatch):
- """
- Test a str (text) cwd with a non-ascii character in Python 3.
- """
- cmd_args = ['test']
- cwd = '/path/to/cwd/déf'
- actual = make_subprocess_output_error(
- cmd_args=cmd_args,
- cwd=cwd,
- lines=[],
- exit_status=1,
- )
- expected = dedent("""\
- Command errored out with exit status 1:
- command: test
- cwd: /path/to/cwd/déf
- Complete output (0 lines):
- ----------------------------------------""")
- assert actual == expected, f'actual: {actual}'
-
-
-# This test is mainly important for checking unicode in Python 2.
-def test_make_subprocess_output_error__non_ascii_line():
- """
- Test a line with a non-ascii character.
- """
- lines = ['curly-quote: \u2018\n']
- actual = make_subprocess_output_error(
- cmd_args=['test'],
- cwd='/path/to/cwd',
- lines=lines,
- exit_status=1,
- )
- expected = dedent("""\
- Command errored out with exit status 1:
- command: test
- cwd: /path/to/cwd
- Complete output (1 lines):
- curly-quote: \u2018
- ----------------------------------------""")
- assert actual == expected, f'actual: {actual}'
-
-
@pytest.mark.parametrize(
- ('stdout_only', 'expected'),
+ ("stdout_only", "expected"),
[
(True, ("out\n", "out\r\n")),
(False, ("out\nerr\n", "out\r\nerr\r\n", "err\nout\n", "err\r\nout\r\n")),
],
)
-def test_call_subprocess_stdout_only(capfd, monkeypatch, stdout_only, expected):
+def test_call_subprocess_stdout_only(
+ capfd: pytest.CaptureFixture[str],
+ monkeypatch: pytest.MonkeyPatch,
+ stdout_only: bool,
+ expected: Tuple[str, ...],
+) -> None:
log = []
monkeypatch.setattr(
subprocess_logger,
@@ -137,31 +61,30 @@ def test_call_subprocess_stdout_only(capfd, monkeypatch, stdout_only, expected):
[
sys.executable,
"-c",
- "import sys; "
- "sys.stdout.write('out\\n'); "
- "sys.stderr.write('err\\n')"
+ "import sys; sys.stdout.write('out\\n'); sys.stderr.write('err\\n')",
],
+ command_desc="test stdout_only",
stdout_only=stdout_only,
)
assert out in expected
captured = capfd.readouterr()
assert captured.err == ""
- assert (
- log == ["Running command %s", "out", "err"]
- or log == ["Running command %s", "err", "out"]
- )
+ assert log == ["Running command %s", "out", "err"] or log == [
+ "Running command %s",
+ "err",
+ "out",
+ ]
class FakeSpinner(SpinnerInterface):
-
- def __init__(self):
+ def __init__(self) -> None:
self.spin_count = 0
- self.final_status = None
+ self.final_status: Optional[str] = None
- def spin(self):
+ def spin(self) -> None:
self.spin_count += 1
- def finish(self, final_status):
+ def finish(self, final_status: str) -> None:
self.final_status = final_status
@@ -172,9 +95,15 @@ class TestCallSubprocess:
"""
def check_result(
- self, capfd, caplog, log_level, spinner, result, expected,
- expected_spinner,
- ):
+ self,
+ capfd: pytest.CaptureFixture[str],
+ caplog: pytest.LogCaptureFixture,
+ log_level: int,
+ spinner: FakeSpinner,
+ result: Optional[str],
+ expected: Tuple[Optional[List[str]], List[Tuple[str, int, str]]],
+ expected_spinner: Tuple[int, Optional[str]],
+ ) -> None:
"""
Check the result of calling call_subprocess().
@@ -196,15 +125,16 @@ class TestCallSubprocess:
if expected_proc is None:
assert result is None
else:
+ assert result is not None
assert result.splitlines() == expected_proc
# Confirm that stdout and stderr haven't been written to.
captured = capfd.readouterr()
- assert (captured.out, captured.err) == ('', '')
+ assert (captured.out, captured.err) == ("", "")
records = caplog.record_tuples
if len(records) != len(expected_records):
- raise RuntimeError(f'{records} != {expected_records}')
+ raise RuntimeError(f"{records} != {expected_records}")
for record, expected_record in zip(records, expected_records):
# Check the logger_name and log level parts exactly.
@@ -219,53 +149,88 @@ class TestCallSubprocess:
assert (spinner.spin_count, spinner.final_status) == expected_spinner
- def prepare_call(self, caplog, log_level, command=None):
+ def prepare_call(
+ self,
+ caplog: pytest.LogCaptureFixture,
+ log_level: int,
+ command: Optional[str] = None,
+ ) -> Tuple[List[str], FakeSpinner]:
if command is None:
command = 'print("Hello"); print("world")'
caplog.set_level(log_level)
spinner = FakeSpinner()
- args = [sys.executable, '-c', command]
+ args = [sys.executable, "-c", command]
return (args, spinner)
- def test_debug_logging(self, capfd, caplog):
+ def test_debug_logging(
+ self, capfd: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture
+ ) -> None:
"""
Test DEBUG logging (and without passing show_stdout=True).
"""
log_level = DEBUG
args, spinner = self.prepare_call(caplog, log_level)
- result = call_subprocess(args, spinner=spinner)
+ result = call_subprocess(
+ args,
+ command_desc="test debug logging",
+ spinner=spinner,
+ )
- expected = (['Hello', 'world'], [
- ('pip.subprocessor', VERBOSE, 'Running command '),
- ('pip.subprocessor', VERBOSE, 'Hello'),
- ('pip.subprocessor', VERBOSE, 'world'),
- ])
+ expected = (
+ ["Hello", "world"],
+ [
+ ("pip.subprocessor", VERBOSE, "Running "),
+ ("pip.subprocessor", VERBOSE, "Hello"),
+ ("pip.subprocessor", VERBOSE, "world"),
+ ],
+ )
# The spinner shouldn't spin in this case since the subprocess
# output is already being logged to the console.
self.check_result(
- capfd, caplog, log_level, spinner, result, expected,
+ capfd,
+ caplog,
+ log_level,
+ spinner,
+ result,
+ expected,
expected_spinner=(0, None),
)
- def test_info_logging(self, capfd, caplog):
+ def test_info_logging(
+ self, capfd: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture
+ ) -> None:
"""
Test INFO logging (and without passing show_stdout=True).
"""
log_level = INFO
args, spinner = self.prepare_call(caplog, log_level)
- result = call_subprocess(args, spinner=spinner)
+ result = call_subprocess(
+ args,
+ command_desc="test info logging",
+ spinner=spinner,
+ )
- expected = (['Hello', 'world'], [])
+ expected: Tuple[List[str], List[Tuple[str, int, str]]] = (
+ ["Hello", "world"],
+ [],
+ )
# The spinner should spin twice in this case since the subprocess
# output isn't being written to the console.
self.check_result(
- capfd, caplog, log_level, spinner, result, expected,
- expected_spinner=(2, 'done'),
+ capfd,
+ caplog,
+ log_level,
+ spinner,
+ result,
+ expected,
+ expected_spinner=(2, "done"),
)
- def test_info_logging__subprocess_error(self, capfd, caplog):
+ def test_info_logging__subprocess_error(
+ self, capfd: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture
+ ) -> None:
"""
Test INFO logging of a subprocess with an error (and without passing
show_stdout=True).
@@ -275,79 +240,85 @@ class TestCallSubprocess:
args, spinner = self.prepare_call(caplog, log_level, command=command)
with pytest.raises(InstallationSubprocessError) as exc:
- call_subprocess(args, spinner=spinner)
+ call_subprocess(
+ args,
+ command_desc="test info logging with subprocess error",
+ spinner=spinner,
+ )
result = None
- exc_message = str(exc.value)
- assert exc_message.startswith(
- 'Command errored out with exit status 1: '
+ exception = exc.value
+ assert exception.reference == "subprocess-exited-with-error"
+ assert "exit code: 1" in exception.message
+ assert exception.note_stmt
+ assert "not a problem with pip" in exception.note_stmt
+ # Check that the process output is captured, and would be shown.
+ assert exception.context
+ assert "Hello\n" in exception.context
+ assert "fail\n" in exception.context
+ assert "world\n" in exception.context
+
+ expected = (
+ None,
+ [
+ # pytest's caplog overrides th formatter, which means that we
+ # won't see the message formatted through our formatters.
+ ("pip.subprocessor", ERROR, "[present-rich]"),
+ ],
)
- assert exc_message.endswith('Check the logs for full command output.')
-
- expected = (None, [
- ('pip.subprocessor', ERROR, 'Complete output (3 lines):\n'),
- ])
# The spinner should spin three times in this case since the
# subprocess output isn't being written to the console.
self.check_result(
- capfd, caplog, log_level, spinner, result, expected,
- expected_spinner=(3, 'error'),
+ capfd,
+ caplog,
+ log_level,
+ spinner,
+ result,
+ expected,
+ expected_spinner=(3, "error"),
)
- # Do some further checking on the captured log records to confirm
- # that the subprocess output was logged.
- last_record = caplog.record_tuples[-1]
- last_message = last_record[2]
- lines = last_message.splitlines()
-
- # We have to sort before comparing the lines because we can't
- # guarantee the order in which stdout and stderr will appear.
- # For example, we observed the stderr lines coming before stdout
- # in CI for PyPy 2.7 even though stdout happens first chronologically.
- actual = sorted(lines)
- # Test the "command" line separately because we can't test an
- # exact match.
- command_line = actual.pop(1)
- assert actual == [
- ' cwd: None',
- '----------------------------------------',
- 'Command errored out with exit status 1:',
- 'Complete output (3 lines):',
- 'Hello',
- 'fail',
- 'world',
- ], f'lines: {actual}' # Show the full output on failure.
-
- assert command_line.startswith(' command: ')
- assert command_line.endswith('print("world"); exit("fail")\'')
-
- def test_info_logging_with_show_stdout_true(self, capfd, caplog):
+ def test_info_logging_with_show_stdout_true(
+ self, capfd: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture
+ ) -> None:
"""
Test INFO logging with show_stdout=True.
"""
log_level = INFO
args, spinner = self.prepare_call(caplog, log_level)
- result = call_subprocess(args, spinner=spinner, show_stdout=True)
+ result = call_subprocess(
+ args,
+ command_desc="test info logging with show_stdout",
+ spinner=spinner,
+ show_stdout=True,
+ )
- expected = (['Hello', 'world'], [
- ('pip.subprocessor', INFO, 'Running command '),
- ('pip.subprocessor', INFO, 'Hello'),
- ('pip.subprocessor', INFO, 'world'),
- ])
+ expected = (
+ ["Hello", "world"],
+ [
+ ("pip.subprocessor", INFO, "Running "),
+ ("pip.subprocessor", INFO, "Hello"),
+ ("pip.subprocessor", INFO, "world"),
+ ],
+ )
# The spinner shouldn't spin in this case since the subprocess
# output is already being written to the console.
self.check_result(
- capfd, caplog, log_level, spinner, result, expected,
+ capfd,
+ caplog,
+ log_level,
+ spinner,
+ result,
+ expected,
expected_spinner=(0, None),
)
- @pytest.mark.parametrize((
- 'exit_status', 'show_stdout', 'extra_ok_returncodes', 'log_level',
- 'expected'),
+ @pytest.mark.parametrize(
+ ("exit_status", "show_stdout", "extra_ok_returncodes", "log_level", "expected"),
[
# The spinner should show here because show_stdout=False means
# the subprocess should get logged at DEBUG level, but the passed
# log level is only INFO.
- (0, False, None, INFO, (None, 'done', 2)),
+ (0, False, None, INFO, (None, "done", 2)),
# Test some cases where the spinner should not be shown.
(0, False, None, DEBUG, (None, None, 0)),
# Test show_stdout=True.
@@ -356,16 +327,22 @@ class TestCallSubprocess:
# The spinner should show here because show_stdout=True means
# the subprocess should get logged at INFO level, but the passed
# log level is only WARNING.
- (0, True, None, WARNING, (None, 'done', 2)),
+ (0, True, None, WARNING, (None, "done", 2)),
# Test a non-zero exit status.
- (3, False, None, INFO, (InstallationSubprocessError, 'error', 2)),
+ (3, False, None, INFO, (InstallationSubprocessError, "error", 2)),
# Test a non-zero exit status also in extra_ok_returncodes.
- (3, False, (3, ), INFO, (None, 'done', 2)),
- ])
+ (3, False, (3,), INFO, (None, "done", 2)),
+ ],
+ )
def test_spinner_finish(
- self, exit_status, show_stdout, extra_ok_returncodes, log_level,
- caplog, expected,
- ):
+ self,
+ exit_status: int,
+ show_stdout: bool,
+ extra_ok_returncodes: Optional[Tuple[int, ...]],
+ log_level: int,
+ caplog: pytest.LogCaptureFixture,
+ expected: Tuple[Optional[Type[Exception]], Optional[str], int],
+ ) -> None:
"""
Test that the spinner finishes correctly.
"""
@@ -373,13 +350,13 @@ class TestCallSubprocess:
expected_final_status = expected[1]
expected_spin_count = expected[2]
- command = (
- f'print("Hello"); print("world"); exit({exit_status})'
- )
+ command = f'print("Hello"); print("world"); exit({exit_status})'
args, spinner = self.prepare_call(caplog, log_level, command=command)
+ exc_type: Optional[Type[Exception]]
try:
call_subprocess(
args,
+ command_desc="spinner go spinny",
show_stdout=show_stdout,
extra_ok_returncodes=extra_ok_returncodes,
spinner=spinner,
@@ -393,15 +370,16 @@ class TestCallSubprocess:
assert spinner.final_status == expected_final_status
assert spinner.spin_count == expected_spin_count
- def test_closes_stdin(self):
+ def test_closes_stdin(self) -> None:
with pytest.raises(InstallationSubprocessError):
call_subprocess(
- [sys.executable, '-c', 'input()'],
+ [sys.executable, "-c", "input()"],
show_stdout=True,
+ command_desc="stdin reader",
)
-def test_unicode_decode_error(caplog):
+def test_unicode_decode_error(caplog: pytest.LogCaptureFixture) -> None:
if locale.getpreferredencoding() != "UTF-8":
pytest.skip("locale.getpreferredencoding() is not UTF-8")
caplog.set_level(INFO)
@@ -411,9 +389,10 @@ def test_unicode_decode_error(caplog):
"-c",
"import sys; sys.stdout.buffer.write(b'\\xff')",
],
- show_stdout=True
+ command_desc="invalid decode output",
+ show_stdout=True,
)
assert len(caplog.records) == 2
- # First log record is "Running command ..."
+ # First log record is "Running ..."
assert caplog.record_tuples[1] == ("pip.subprocessor", INFO, "\\xff")
diff --git a/tests/unit/test_utils_temp_dir.py b/tests/unit/test_utils_temp_dir.py
index 0d1b0a5ea..4a656d23a 100644
--- a/tests/unit/test_utils_temp_dir.py
+++ b/tests/unit/test_utils_temp_dir.py
@@ -2,6 +2,8 @@ import itertools
import os
import stat
import tempfile
+from pathlib import Path
+from typing import Any, Iterator, Optional, Union
import pytest
@@ -10,6 +12,7 @@ from pip._internal.utils.misc import ensure_dir
from pip._internal.utils.temp_dir import (
AdjacentTempDirectory,
TempDirectory,
+ _Default,
_default,
global_tempdir_manager,
tempdir_registry,
@@ -18,38 +21,31 @@ from pip._internal.utils.temp_dir import (
# No need to test symlinked directories on Windows
@pytest.mark.skipif("sys.platform == 'win32'")
-def test_symlinked_path():
+def test_symlinked_path() -> None:
with TempDirectory() as tmp_dir:
assert os.path.exists(tmp_dir.path)
alt_tmp_dir = tempfile.mkdtemp(prefix="pip-test-")
- assert (
- os.path.dirname(tmp_dir.path) ==
- os.path.dirname(os.path.realpath(alt_tmp_dir))
+ assert os.path.dirname(tmp_dir.path) == os.path.dirname(
+ os.path.realpath(alt_tmp_dir)
)
# are we on a system where /tmp is a symlink
if os.path.realpath(alt_tmp_dir) != os.path.abspath(alt_tmp_dir):
- assert (
- os.path.dirname(tmp_dir.path) !=
- os.path.dirname(alt_tmp_dir)
- )
+ assert os.path.dirname(tmp_dir.path) != os.path.dirname(alt_tmp_dir)
else:
- assert (
- os.path.dirname(tmp_dir.path) ==
- os.path.dirname(alt_tmp_dir)
- )
+ assert os.path.dirname(tmp_dir.path) == os.path.dirname(alt_tmp_dir)
os.rmdir(tmp_dir.path)
assert not os.path.exists(tmp_dir.path)
-def test_deletes_readonly_files():
- def create_file(*args):
+def test_deletes_readonly_files() -> None:
+ def create_file(*args: str) -> None:
fpath = os.path.join(*args)
ensure_dir(os.path.dirname(fpath))
with open(fpath, "w") as f:
f.write("Holla!")
- def readonly_file(*args):
+ def readonly_file(*args: str) -> None:
fpath = os.path.join(*args)
os.chmod(fpath, stat.S_IREAD)
@@ -63,7 +59,7 @@ def test_deletes_readonly_files():
readonly_file(tmp_dir.path, "subfolder", "readonly-file")
-def test_path_access_after_context_raises():
+def test_path_access_after_context_raises() -> None:
with TempDirectory() as tmp_dir:
path = tmp_dir.path
@@ -73,7 +69,7 @@ def test_path_access_after_context_raises():
assert path in str(e.value)
-def test_path_access_after_clean_raises():
+def test_path_access_after_clean_raises() -> None:
tmp_dir = TempDirectory()
path = tmp_dir.path
tmp_dir.cleanup()
@@ -84,7 +80,7 @@ def test_path_access_after_clean_raises():
assert path in str(e.value)
-def test_create_and_cleanup_work():
+def test_create_and_cleanup_work() -> None:
tmp_dir = TempDirectory()
created_path = tmp_dir.path
@@ -95,18 +91,21 @@ def test_create_and_cleanup_work():
assert not os.path.exists(created_path)
-@pytest.mark.parametrize("name", [
- "ABC",
- "ABC.dist-info",
- "_+-",
- "_package",
- "A......B",
- "AB",
- "A",
- "2",
-])
-def test_adjacent_directory_names(name):
- def names():
+@pytest.mark.parametrize(
+ "name",
+ [
+ "ABC",
+ "ABC.dist-info",
+ "_+-",
+ "_package",
+ "A......B",
+ "AB",
+ "A",
+ "2",
+ ],
+)
+def test_adjacent_directory_names(name: str) -> None:
+ def names() -> Iterator[str]:
return AdjacentTempDirectory._generate_names(name)
chars = AdjacentTempDirectory.LEADING_CHARS
@@ -132,15 +131,12 @@ def test_adjacent_directory_names(name):
assert len(some_names) > 0.9 * len(set(some_names))
# Ensure the first few names are the same length as the original
- same_len = list(itertools.takewhile(
- lambda x: len(x) == len(name),
- some_names
- ))
+ same_len = list(itertools.takewhile(lambda x: len(x) == len(name), some_names))
assert len(same_len) > 10
# Check the first group are correct
- expected_names = ['~' + name[1:]]
- expected_names.extend('~' + c + name[2:] for c in chars)
+ expected_names = ["~" + name[1:]]
+ expected_names.extend("~" + c + name[2:] for c in chars)
for x, y in zip(some_names, expected_names):
assert x == y
@@ -159,16 +155,20 @@ def test_adjacent_directory_names(name):
assert all(x.endswith(name) for x in some_names)
-@pytest.mark.parametrize("name", [
- "A",
- "ABC",
- "ABC.dist-info",
- "_+-",
- "_package",
-])
-def test_adjacent_directory_exists(name, tmpdir):
+@pytest.mark.parametrize(
+ "name",
+ [
+ "A",
+ "ABC",
+ "ABC.dist-info",
+ "_+-",
+ "_package",
+ ],
+)
+def test_adjacent_directory_exists(name: str, tmpdir: Path) -> None:
block_name, expect_name = itertools.islice(
- AdjacentTempDirectory._generate_names(name), 2)
+ AdjacentTempDirectory._generate_names(name), 2
+ )
original = os.path.join(tmpdir, name)
blocker = os.path.join(tmpdir, block_name)
@@ -180,10 +180,10 @@ def test_adjacent_directory_exists(name, tmpdir):
assert expect_name == os.path.split(atmp_dir.path)[1]
-def test_adjacent_directory_permission_error(monkeypatch):
+def test_adjacent_directory_permission_error(monkeypatch: pytest.MonkeyPatch) -> None:
name = "ABC"
- def raising_mkdir(*args, **kwargs):
+ def raising_mkdir(*args: Any, **kwargs: Any) -> None:
raise OSError("Unknown OSError")
with TempDirectory() as tmp_dir:
@@ -197,7 +197,7 @@ def test_adjacent_directory_permission_error(monkeypatch):
pass
-def test_global_tempdir_manager():
+def test_global_tempdir_manager() -> None:
with global_tempdir_manager():
d = TempDirectory(globally_managed=True)
path = d.path
@@ -205,7 +205,7 @@ def test_global_tempdir_manager():
assert not os.path.exists(path)
-def test_tempdirectory_asserts_global_tempdir(monkeypatch):
+def test_tempdirectory_asserts_global_tempdir(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(temp_dir, "_tempdir_manager", None)
with pytest.raises(AssertionError):
TempDirectory(globally_managed=True)
@@ -215,21 +215,26 @@ deleted_kind = "deleted"
not_deleted_kind = "not-deleted"
-@pytest.mark.parametrize("delete,kind,exists", [
- (None, deleted_kind, False),
- (_default, deleted_kind, False),
- (True, deleted_kind, False),
- (False, deleted_kind, True),
- (None, not_deleted_kind, True),
- (_default, not_deleted_kind, True),
- (True, not_deleted_kind, False),
- (False, not_deleted_kind, True),
- (None, "unspecified", False),
- (_default, "unspecified", False),
- (True, "unspecified", False),
- (False, "unspecified", True),
-])
-def test_tempdir_registry(kind, delete, exists):
+@pytest.mark.parametrize(
+ "delete,kind,exists",
+ [
+ (None, deleted_kind, False),
+ (_default, deleted_kind, False),
+ (True, deleted_kind, False),
+ (False, deleted_kind, True),
+ (None, not_deleted_kind, True),
+ (_default, not_deleted_kind, True),
+ (True, not_deleted_kind, False),
+ (False, not_deleted_kind, True),
+ (None, "unspecified", False),
+ (_default, "unspecified", False),
+ (True, "unspecified", False),
+ (False, "unspecified", True),
+ ],
+)
+def test_tempdir_registry(
+ delete: Union[bool, _Default], kind: str, exists: bool
+) -> None:
with tempdir_registry() as registry:
registry.set_delete(deleted_kind, True)
registry.set_delete(not_deleted_kind, False)
@@ -240,14 +245,13 @@ def test_tempdir_registry(kind, delete, exists):
assert os.path.exists(path) == exists
-@pytest.mark.parametrize("delete,exists", [
- (_default, True), (None, False)
-])
+@pytest.mark.parametrize("delete,exists", [(_default, True), (None, False)])
def test_temp_dir_does_not_delete_explicit_paths_by_default(
- tmpdir, delete, exists
-):
- path = tmpdir / "example"
- path.mkdir()
+ tmpdir: Path, delete: Optional[_Default], exists: bool
+) -> None:
+ p = tmpdir / "example"
+ p.mkdir()
+ path = os.fspath(p)
with tempdir_registry() as registry:
registry.set_delete(deleted_kind, True)
@@ -259,7 +263,7 @@ def test_temp_dir_does_not_delete_explicit_paths_by_default(
@pytest.mark.parametrize("should_delete", [True, False])
-def test_tempdir_registry_lazy(should_delete):
+def test_tempdir_registry_lazy(should_delete: bool) -> None:
"""
Test the registry entry can be updated after a temp dir is created,
to change whether a kind should be deleted or not.
diff --git a/tests/unit/test_utils_unpacking.py b/tests/unit/test_utils_unpacking.py
index 760b09cf1..382142ac1 100644
--- a/tests/unit/test_utils_unpacking.py
+++ b/tests/unit/test_utils_unpacking.py
@@ -1,3 +1,4 @@
+import io
import os
import shutil
import stat
@@ -6,11 +7,14 @@ import tarfile
import tempfile
import time
import zipfile
+from pathlib import Path
+from typing import List, Tuple
import pytest
from pip._internal.exceptions import InstallationError
from pip._internal.utils.unpacking import is_within_directory, untar_file, unzip_file
+from tests.lib import TestData
class TestUnpackArchives:
@@ -33,172 +37,173 @@ class TestUnpackArchives:
"""
- def setup(self):
+ def setup(self) -> None:
self.tempdir = tempfile.mkdtemp()
self.old_mask = os.umask(0o022)
self.symlink_expected_mode = None
- def teardown(self):
+ def teardown(self) -> None:
os.umask(self.old_mask)
shutil.rmtree(self.tempdir, ignore_errors=True)
- def mode(self, path):
+ def mode(self, path: str) -> int:
return stat.S_IMODE(os.stat(path).st_mode)
- def confirm_files(self):
+ def confirm_files(self) -> None:
# expectations based on 022 umask set above and the unpack logic that
# sets execute permissions, not preservation
for fname, expected_mode, test, expected_contents in [
- ('file.txt', 0o644, os.path.isfile, b'file\n'),
+ ("file.txt", 0o644, os.path.isfile, b"file\n"),
# We don't test the "symlink.txt" contents for now.
- ('symlink.txt', 0o644, os.path.isfile, None),
- ('script_owner.sh', 0o755, os.path.isfile, b'file\n'),
- ('script_group.sh', 0o755, os.path.isfile, b'file\n'),
- ('script_world.sh', 0o755, os.path.isfile, b'file\n'),
- ('dir', 0o755, os.path.isdir, None),
- (os.path.join('dir', 'dirfile'), 0o644, os.path.isfile, b''),
+ ("symlink.txt", 0o644, os.path.isfile, None),
+ ("script_owner.sh", 0o755, os.path.isfile, b"file\n"),
+ ("script_group.sh", 0o755, os.path.isfile, b"file\n"),
+ ("script_world.sh", 0o755, os.path.isfile, b"file\n"),
+ ("dir", 0o755, os.path.isdir, None),
+ (os.path.join("dir", "dirfile"), 0o644, os.path.isfile, b""),
]:
path = os.path.join(self.tempdir, fname)
- if path.endswith('symlink.txt') and sys.platform == 'win32':
+ if path.endswith("symlink.txt") and sys.platform == "win32":
# no symlinks created on windows
continue
assert test(path), path
if expected_contents is not None:
- with open(path, mode='rb') as f:
+ with open(path, mode="rb") as f:
contents = f.read()
- assert contents == expected_contents, f'fname: {fname}'
- if sys.platform == 'win32':
+ assert contents == expected_contents, f"fname: {fname}"
+ if sys.platform == "win32":
# the permissions tests below don't apply in windows
# due to os.chmod being a noop
continue
mode = self.mode(path)
- assert mode == expected_mode, (
- f"mode: {mode}, expected mode: {expected_mode}"
- )
+ assert (
+ mode == expected_mode
+ ), f"mode: {mode}, expected mode: {expected_mode}"
- def make_zip_file(self, filename, file_list):
+ def make_zip_file(self, filename: str, file_list: List[str]) -> str:
"""
Create a zip file for test case
"""
test_zip = os.path.join(self.tempdir, filename)
- with zipfile.ZipFile(test_zip, 'w') as myzip:
+ with zipfile.ZipFile(test_zip, "w") as myzip:
for item in file_list:
- myzip.writestr(item, 'file content')
+ myzip.writestr(item, "file content")
return test_zip
- def make_tar_file(self, filename, file_list):
+ def make_tar_file(self, filename: str, file_list: List[str]) -> str:
"""
Create a tar file for test case
"""
test_tar = os.path.join(self.tempdir, filename)
- with tarfile.open(test_tar, 'w') as mytar:
+ with tarfile.open(test_tar, "w") as mytar:
for item in file_list:
file_tarinfo = tarfile.TarInfo(item)
- mytar.addfile(file_tarinfo, 'file content')
+ mytar.addfile(file_tarinfo, io.BytesIO(b"file content"))
return test_tar
- def test_unpack_tgz(self, data):
+ def test_unpack_tgz(self, data: TestData) -> None:
"""
Test unpacking a *.tgz, and setting execute permissions
"""
test_file = data.packages.joinpath("test_tar.tgz")
- untar_file(test_file, self.tempdir)
+ untar_file(os.fspath(test_file), self.tempdir)
self.confirm_files()
# Check the timestamp of an extracted file
- file_txt_path = os.path.join(self.tempdir, 'file.txt')
+ file_txt_path = os.path.join(self.tempdir, "file.txt")
mtime = time.gmtime(os.stat(file_txt_path).st_mtime)
assert mtime[0:6] == (2013, 8, 16, 5, 13, 37), mtime
- def test_unpack_zip(self, data):
+ def test_unpack_zip(self, data: TestData) -> None:
"""
Test unpacking a *.zip, and setting execute permissions
"""
test_file = data.packages.joinpath("test_zip.zip")
- unzip_file(test_file, self.tempdir)
+ unzip_file(os.fspath(test_file), self.tempdir)
self.confirm_files()
- def test_unpack_zip_failure(self):
+ def test_unpack_zip_failure(self) -> None:
"""
Test unpacking a *.zip with file containing .. path
and expect exception
"""
- files = ['regular_file.txt', os.path.join('..', 'outside_file.txt')]
- test_zip = self.make_zip_file('test_zip.zip', files)
+ files = ["regular_file.txt", os.path.join("..", "outside_file.txt")]
+ test_zip = self.make_zip_file("test_zip.zip", files)
with pytest.raises(InstallationError) as e:
unzip_file(test_zip, self.tempdir)
- assert 'trying to install outside target directory' in str(e.value)
+ assert "trying to install outside target directory" in str(e.value)
- def test_unpack_zip_success(self):
+ def test_unpack_zip_success(self) -> None:
"""
Test unpacking a *.zip with regular files,
no file will be installed outside target directory after unpack
so no exception raised
"""
files = [
- 'regular_file1.txt',
- os.path.join('dir', 'dir_file1.txt'),
- os.path.join('dir', '..', 'dir_file2.txt'),
+ "regular_file1.txt",
+ os.path.join("dir", "dir_file1.txt"),
+ os.path.join("dir", "..", "dir_file2.txt"),
]
- test_zip = self.make_zip_file('test_zip.zip', files)
+ test_zip = self.make_zip_file("test_zip.zip", files)
unzip_file(test_zip, self.tempdir)
- def test_unpack_tar_failure(self):
+ def test_unpack_tar_failure(self) -> None:
"""
Test unpacking a *.tar with file containing .. path
and expect exception
"""
- files = ['regular_file.txt', os.path.join('..', 'outside_file.txt')]
- test_tar = self.make_tar_file('test_tar.tar', files)
+ files = ["regular_file.txt", os.path.join("..", "outside_file.txt")]
+ test_tar = self.make_tar_file("test_tar.tar", files)
with pytest.raises(InstallationError) as e:
untar_file(test_tar, self.tempdir)
- assert 'trying to install outside target directory' in str(e.value)
+ assert "trying to install outside target directory" in str(e.value)
- def test_unpack_tar_success(self):
+ def test_unpack_tar_success(self) -> None:
"""
Test unpacking a *.tar with regular files,
no file will be installed outside target directory after unpack
so no exception raised
"""
files = [
- 'regular_file1.txt',
- os.path.join('dir', 'dir_file1.txt'),
- os.path.join('dir', '..', 'dir_file2.txt'),
+ "regular_file1.txt",
+ os.path.join("dir", "dir_file1.txt"),
+ os.path.join("dir", "..", "dir_file2.txt"),
]
- test_tar = self.make_tar_file('test_tar.tar', files)
+ test_tar = self.make_tar_file("test_tar.tar", files)
untar_file(test_tar, self.tempdir)
-def test_unpack_tar_unicode(tmpdir):
+def test_unpack_tar_unicode(tmpdir: Path) -> None:
test_tar = tmpdir / "test.tar"
# tarfile tries to decode incoming
- with tarfile.open(
- test_tar, "w", format=tarfile.PAX_FORMAT, encoding="utf-8"
- ) as f:
+ with tarfile.open(test_tar, "w", format=tarfile.PAX_FORMAT, encoding="utf-8") as f:
metadata = tarfile.TarInfo("dir/åäö_日本語.py")
- f.addfile(metadata, "hello world")
+ f.addfile(metadata, io.BytesIO(b"hello world"))
output_dir = tmpdir / "output"
output_dir.mkdir()
- untar_file(test_tar, str(output_dir))
+ untar_file(os.fspath(test_tar), str(output_dir))
output_dir_name = str(output_dir)
contents = os.listdir(output_dir_name)
- assert u"åäö_日本語.py" in contents
-
-
-@pytest.mark.parametrize('args, expected', [
- # Test the second containing the first.
- (('parent/sub', 'parent/'), False),
- # Test the first not ending in a trailing slash.
- (('parent', 'parent/foo'), True),
- # Test target containing `..` but still inside the parent.
- (('parent/', 'parent/foo/../bar'), True),
- # Test target within the parent
- (('parent/', 'parent/sub'), True),
- # Test target outside parent
- (('parent/', 'parent/../sub'), False),
-])
-def test_is_within_directory(args, expected):
+ assert "åäö_日本語.py" in contents
+
+
+@pytest.mark.parametrize(
+ "args, expected",
+ [
+ # Test the second containing the first.
+ (("parent/sub", "parent/"), False),
+ # Test the first not ending in a trailing slash.
+ (("parent", "parent/foo"), True),
+ # Test target containing `..` but still inside the parent.
+ (("parent/", "parent/foo/../bar"), True),
+ # Test target within the parent
+ (("parent/", "parent/sub"), True),
+ # Test target outside parent
+ (("parent/", "parent/../sub"), False),
+ ],
+)
+def test_is_within_directory(args: Tuple[str, str], expected: bool) -> None:
result = is_within_directory(*args)
assert result == expected
diff --git a/tests/unit/test_utils_virtualenv.py b/tests/unit/test_utils_virtualenv.py
index 625539d76..38d5383ce 100644
--- a/tests/unit/test_utils_virtualenv.py
+++ b/tests/unit/test_utils_virtualenv.py
@@ -1,25 +1,35 @@
import logging
+import os
import site
import sys
+from pathlib import Path
+from typing import List, Optional
import pytest
from pip._internal.utils import virtualenv
-@pytest.mark.parametrize("real_prefix, base_prefix, expected", [
- (None, None, False), # Python 2 base interpreter
- (None, sys.prefix, False), # Python 3 base interpreter
- (None, "not_sys_prefix", True), # PEP405 venv
- (sys.prefix, None, True), # Unknown case
- (sys.prefix, sys.prefix, True), # Unknown case
- (sys.prefix, "not_sys_prefix", True), # Unknown case
- ("not_sys_prefix", None, True), # Python 2 virtualenv
- ("not_sys_prefix", sys.prefix, True), # Python 3 virtualenv
- ("not_sys_prefix", "not_sys_prefix", True), # Unknown case
-])
+@pytest.mark.parametrize(
+ "real_prefix, base_prefix, expected",
+ [
+ (None, None, False), # Python 2 base interpreter
+ (None, sys.prefix, False), # Python 3 base interpreter
+ (None, "not_sys_prefix", True), # PEP405 venv
+ (sys.prefix, None, True), # Unknown case
+ (sys.prefix, sys.prefix, True), # Unknown case
+ (sys.prefix, "not_sys_prefix", True), # Unknown case
+ ("not_sys_prefix", None, True), # Python 2 virtualenv
+ ("not_sys_prefix", sys.prefix, True), # Python 3 virtualenv
+ ("not_sys_prefix", "not_sys_prefix", True), # Unknown case
+ ],
+)
def test_running_under_virtualenv(
- monkeypatch, real_prefix, base_prefix, expected):
+ monkeypatch: pytest.MonkeyPatch,
+ real_prefix: Optional[str],
+ base_prefix: Optional[str],
+ expected: bool,
+) -> None:
# Use raising=False to prevent AttributeError on missing attribute
if real_prefix is None:
monkeypatch.delattr(sys, "real_prefix", raising=False)
@@ -33,7 +43,8 @@ def test_running_under_virtualenv(
@pytest.mark.parametrize(
- "under_virtualenv, no_global_file, expected", [
+ "under_virtualenv, no_global_file, expected",
+ [
(False, False, False),
(False, True, False),
(True, False, False),
@@ -41,27 +52,29 @@ def test_running_under_virtualenv(
],
)
def test_virtualenv_no_global_with_regular_virtualenv(
- monkeypatch,
- tmpdir,
- under_virtualenv,
- no_global_file,
- expected,
-):
- monkeypatch.setattr(virtualenv, '_running_under_venv', lambda: False)
-
- monkeypatch.setattr(site, '__file__', tmpdir / 'site.py')
+ monkeypatch: pytest.MonkeyPatch,
+ tmpdir: Path,
+ under_virtualenv: bool,
+ no_global_file: bool,
+ expected: bool,
+) -> None:
+ monkeypatch.setattr(virtualenv, "_running_under_venv", lambda: False)
+
+ monkeypatch.setattr(site, "__file__", os.fspath(tmpdir / "site.py"))
monkeypatch.setattr(
- virtualenv, '_running_under_regular_virtualenv',
+ virtualenv,
+ "_running_under_regular_virtualenv",
lambda: under_virtualenv,
)
if no_global_file:
- (tmpdir / 'no-global-site-packages.txt').touch()
+ (tmpdir / "no-global-site-packages.txt").touch()
assert virtualenv.virtualenv_no_global() == expected
@pytest.mark.parametrize(
- "pyvenv_cfg_lines, under_venv, expected, expect_warning", [
+ "pyvenv_cfg_lines, under_venv, expected, expect_warning",
+ [
(None, False, False, False),
(None, True, True, True), # this has a warning.
(
@@ -87,20 +100,16 @@ def test_virtualenv_no_global_with_regular_virtualenv(
],
)
def test_virtualenv_no_global_with_pep_405_virtual_environment(
- monkeypatch,
- caplog,
- pyvenv_cfg_lines,
- under_venv,
- expected,
- expect_warning,
-):
- monkeypatch.setattr(
- virtualenv, '_running_under_regular_virtualenv', lambda: False
- )
- monkeypatch.setattr(
- virtualenv, '_get_pyvenv_cfg_lines', lambda: pyvenv_cfg_lines
- )
- monkeypatch.setattr(virtualenv, '_running_under_venv', lambda: under_venv)
+ monkeypatch: pytest.MonkeyPatch,
+ caplog: pytest.LogCaptureFixture,
+ pyvenv_cfg_lines: Optional[List[str]],
+ under_venv: bool,
+ expected: bool,
+ expect_warning: bool,
+) -> None:
+ monkeypatch.setattr(virtualenv, "_running_under_regular_virtualenv", lambda: False)
+ monkeypatch.setattr(virtualenv, "_get_pyvenv_cfg_lines", lambda: pyvenv_cfg_lines)
+ monkeypatch.setattr(virtualenv, "_running_under_venv", lambda: under_venv)
with caplog.at_level(logging.WARNING):
assert virtualenv.virtualenv_no_global() == expected
@@ -115,21 +124,22 @@ def test_virtualenv_no_global_with_pep_405_virtual_environment(
@pytest.mark.parametrize(
- "contents, expected", [
+ "contents, expected",
+ [
(None, None),
("", []),
("a = b\nc = d\n", ["a = b", "c = d"]),
("a = b\nc = d", ["a = b", "c = d"]), # no trailing newlines
- ]
+ ],
)
def test_get_pyvenv_cfg_lines_for_pep_405_virtual_environment(
- monkeypatch,
- tmpdir,
- contents,
- expected,
-):
- monkeypatch.setattr(sys, 'prefix', str(tmpdir))
+ monkeypatch: pytest.MonkeyPatch,
+ tmpdir: Path,
+ contents: Optional[str],
+ expected: Optional[List[str]],
+) -> None:
+ monkeypatch.setattr(sys, "prefix", str(tmpdir))
if contents is not None:
- tmpdir.joinpath('pyvenv.cfg').write_text(contents)
+ tmpdir.joinpath("pyvenv.cfg").write_text(contents)
assert virtualenv._get_pyvenv_cfg_lines() == expected
diff --git a/tests/unit/test_utils_wheel.py b/tests/unit/test_utils_wheel.py
index 878d8d777..4e8e72be6 100644
--- a/tests/unit/test_utils_wheel.py
+++ b/tests/unit/test_utils_wheel.py
@@ -2,19 +2,22 @@ import os
from contextlib import ExitStack
from email import message_from_string
from io import BytesIO
+from pathlib import Path
+from typing import Callable, Iterator
from zipfile import ZipFile
import pytest
from pip._internal.exceptions import UnsupportedWheel
from pip._internal.utils import wheel
-from tests.lib.path import Path
+from tests.lib import TestData
+
+_ZipDir = Callable[[Path], ZipFile]
@pytest.fixture
-def zip_dir():
- def make_zip(path):
- # type: (Path) -> ZipFile
+def zip_dir() -> Iterator[_ZipDir]:
+ def make_zip(path: Path) -> ZipFile:
buf = BytesIO()
with ZipFile(buf, "w", allowZip64=True) as z:
for dirpath, _, filenames in os.walk(path):
@@ -33,7 +36,7 @@ def zip_dir():
yield make_zip
-def test_wheel_dist_info_dir_found(tmpdir, zip_dir):
+def test_wheel_dist_info_dir_found(tmpdir: Path, zip_dir: _ZipDir) -> None:
expected = "simple-0.1.dist-info"
dist_info_dir = tmpdir / expected
dist_info_dir.mkdir()
@@ -41,7 +44,7 @@ def test_wheel_dist_info_dir_found(tmpdir, zip_dir):
assert wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") == expected
-def test_wheel_dist_info_dir_multiple(tmpdir, zip_dir):
+def test_wheel_dist_info_dir_multiple(tmpdir: Path, zip_dir: _ZipDir) -> None:
dist_info_dir_1 = tmpdir / "simple-0.1.dist-info"
dist_info_dir_1.mkdir()
dist_info_dir_1.joinpath("WHEEL").touch()
@@ -53,13 +56,13 @@ def test_wheel_dist_info_dir_multiple(tmpdir, zip_dir):
assert "multiple .dist-info directories found" in str(e.value)
-def test_wheel_dist_info_dir_none(tmpdir, zip_dir):
+def test_wheel_dist_info_dir_none(tmpdir: Path, zip_dir: _ZipDir) -> None:
with pytest.raises(UnsupportedWheel) as e:
wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple")
assert "directory not found" in str(e.value)
-def test_wheel_dist_info_dir_wrong_name(tmpdir, zip_dir):
+def test_wheel_dist_info_dir_wrong_name(tmpdir: Path, zip_dir: _ZipDir) -> None:
dist_info_dir = tmpdir / "unrelated-0.1.dist-info"
dist_info_dir.mkdir()
dist_info_dir.joinpath("WHEEL").touch()
@@ -68,13 +71,11 @@ def test_wheel_dist_info_dir_wrong_name(tmpdir, zip_dir):
assert "does not start with 'simple'" in str(e.value)
-def test_wheel_version_ok(tmpdir, data):
- assert wheel.wheel_version(
- message_from_string("Wheel-Version: 1.9")
- ) == (1, 9)
+def test_wheel_version_ok(data: TestData) -> None:
+ assert wheel.wheel_version(message_from_string("Wheel-Version: 1.9")) == (1, 9)
-def test_wheel_metadata_fails_missing_wheel(tmpdir, zip_dir):
+def test_wheel_metadata_fails_missing_wheel(tmpdir: Path, zip_dir: _ZipDir) -> None:
dist_info_dir = tmpdir / "simple-0.1.0.dist-info"
dist_info_dir.mkdir()
dist_info_dir.joinpath("METADATA").touch()
@@ -84,7 +85,7 @@ def test_wheel_metadata_fails_missing_wheel(tmpdir, zip_dir):
assert "could not read" in str(e.value)
-def test_wheel_metadata_fails_on_bad_encoding(tmpdir, zip_dir):
+def test_wheel_metadata_fails_on_bad_encoding(tmpdir: Path, zip_dir: _ZipDir) -> None:
dist_info_dir = tmpdir / "simple-0.1.0.dist-info"
dist_info_dir.mkdir()
dist_info_dir.joinpath("METADATA").touch()
@@ -95,27 +96,28 @@ def test_wheel_metadata_fails_on_bad_encoding(tmpdir, zip_dir):
assert "error decoding" in str(e.value)
-def test_wheel_version_fails_on_no_wheel_version():
+def test_wheel_version_fails_on_no_wheel_version() -> None:
with pytest.raises(UnsupportedWheel) as e:
wheel.wheel_version(message_from_string(""))
assert "missing Wheel-Version" in str(e.value)
-@pytest.mark.parametrize("version", [
- ("",),
- ("1.b",),
- ("1.",),
-])
-def test_wheel_version_fails_on_bad_wheel_version(version):
+@pytest.mark.parametrize(
+ "version",
+ [
+ ("",),
+ ("1.b",),
+ ("1.",),
+ ],
+)
+def test_wheel_version_fails_on_bad_wheel_version(version: str) -> None:
with pytest.raises(UnsupportedWheel) as e:
- wheel.wheel_version(
- message_from_string(f"Wheel-Version: {version}")
- )
+ wheel.wheel_version(message_from_string(f"Wheel-Version: {version}"))
assert "invalid Wheel-Version" in str(e.value)
-def test_check_compatibility():
- name = 'test'
+def test_check_compatibility() -> None:
+ name = "test"
vc = wheel.VERSION_COMPATIBLE
# Major version is higher - should be incompatible
@@ -124,7 +126,7 @@ def test_check_compatibility():
# test raises with correct error
with pytest.raises(UnsupportedWheel) as e:
wheel.check_compatibility(higher_v, name)
- assert 'is not compatible' in str(e)
+ assert "is not compatible" in str(e)
# Should only log.warning - minor version is greater
higher_v = (vc[0], vc[1] + 1)
diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py
index 403f39466..566c88cf0 100644
--- a/tests/unit/test_vcs.py
+++ b/tests/unit/test_vcs.py
@@ -1,12 +1,13 @@
import os
import pathlib
-from unittest import TestCase
-from unittest.mock import patch
+from typing import Any, Dict, List, Optional, Tuple, Type
+from unittest import TestCase, mock
import pytest
from pip._internal.exceptions import BadCommand, InstallationError
-from pip._internal.utils.misc import hide_url, hide_value
+from pip._internal.utils.misc import HiddenText, hide_url, hide_value
+from pip._internal.utils.subprocess import CommandArgs
from pip._internal.vcs import make_vcs_requirement_url
from pip._internal.vcs.bazaar import Bazaar
from pip._internal.vcs.git import Git, RemoteNotValidError, looks_like_hash
@@ -17,143 +18,181 @@ from tests.lib import is_svn_installed, need_svn
@pytest.mark.skipif(
- 'TRAVIS' not in os.environ,
- reason='Subversion is only required under Travis')
-def test_ensure_svn_available():
- """Make sure that svn is available when running in Travis."""
+ "CI" not in os.environ, reason="Subversion is only required under CI"
+)
+def test_ensure_svn_available() -> None:
+ """Make sure that svn is available when running in CI."""
assert is_svn_installed()
-@pytest.mark.parametrize('args, expected', [
- # Test without subdir.
- (('git+https://example.com/pkg', 'dev', 'myproj'),
- 'git+https://example.com/pkg@dev#egg=myproj'),
- # Test with subdir.
- (('git+https://example.com/pkg', 'dev', 'myproj', 'sub/dir'),
- 'git+https://example.com/pkg@dev#egg=myproj&subdirectory=sub/dir'),
- # Test with None subdir.
- (('git+https://example.com/pkg', 'dev', 'myproj', None),
- 'git+https://example.com/pkg@dev#egg=myproj'),
- # Test an unescaped project name.
- (('git+https://example.com/pkg', 'dev', 'zope-interface'),
- 'git+https://example.com/pkg@dev#egg=zope_interface'),
-])
-def test_make_vcs_requirement_url(args, expected):
+@pytest.mark.parametrize(
+ "args, expected",
+ [
+ # Test without subdir.
+ (
+ ("git+https://example.com/pkg", "dev", "myproj"),
+ "git+https://example.com/pkg@dev#egg=myproj",
+ ),
+ # Test with subdir.
+ (
+ ("git+https://example.com/pkg", "dev", "myproj", "sub/dir"),
+ "git+https://example.com/pkg@dev#egg=myproj&subdirectory=sub/dir",
+ ),
+ # Test with None subdir.
+ (
+ ("git+https://example.com/pkg", "dev", "myproj", None),
+ "git+https://example.com/pkg@dev#egg=myproj",
+ ),
+ # Test an unescaped project name.
+ (
+ ("git+https://example.com/pkg", "dev", "zope-interface"),
+ "git+https://example.com/pkg@dev#egg=zope_interface",
+ ),
+ ],
+)
+def test_make_vcs_requirement_url(args: Tuple[Any, ...], expected: str) -> None:
actual = make_vcs_requirement_url(*args)
assert actual == expected
-def test_rev_options_repr():
- rev_options = RevOptions(Git, 'develop')
+def test_rev_options_repr() -> None:
+ rev_options = RevOptions(Git, "develop")
assert repr(rev_options) == "<RevOptions git: rev='develop'>"
-@pytest.mark.parametrize(('vc_class', 'expected1', 'expected2', 'kwargs'), [
- # First check VCS-specific RevOptions behavior.
- (Bazaar, [], ['-r', '123'], {}),
- (Git, ['HEAD'], ['123'], {}),
- (Mercurial, [], ['123'], {}),
- (Subversion, [], ['-r', '123'], {}),
- # Test extra_args. For this, test using a single VersionControl class.
- (Git, ['HEAD', 'opt1', 'opt2'], ['123', 'opt1', 'opt2'],
- dict(extra_args=['opt1', 'opt2'])),
-])
-def test_rev_options_to_args(vc_class, expected1, expected2, kwargs):
+@pytest.mark.parametrize(
+ ("vc_class", "expected1", "expected2", "kwargs"),
+ [
+ # First check VCS-specific RevOptions behavior.
+ (Bazaar, [], ["-r", "123"], {}),
+ (Git, ["HEAD"], ["123"], {}),
+ (Mercurial, [], ["123"], {}),
+ (Subversion, [], ["-r", "123"], {}),
+ # Test extra_args. For this, test using a single VersionControl class.
+ (
+ Git,
+ ["HEAD", "opt1", "opt2"],
+ ["123", "opt1", "opt2"],
+ dict(extra_args=["opt1", "opt2"]),
+ ),
+ ],
+)
+def test_rev_options_to_args(
+ vc_class: Type[VersionControl],
+ expected1: List[str],
+ expected2: List[str],
+ kwargs: Dict[str, Any],
+) -> None:
"""
Test RevOptions.to_args().
"""
assert RevOptions(vc_class, **kwargs).to_args() == expected1
- assert RevOptions(vc_class, '123', **kwargs).to_args() == expected2
+ assert RevOptions(vc_class, "123", **kwargs).to_args() == expected2
-def test_rev_options_to_display():
+def test_rev_options_to_display() -> None:
"""
Test RevOptions.to_display().
"""
# The choice of VersionControl class doesn't matter here since
# the implementation is the same for all of them.
rev_options = RevOptions(Git)
- assert rev_options.to_display() == ''
+ assert rev_options.to_display() == ""
- rev_options = RevOptions(Git, 'master')
- assert rev_options.to_display() == ' (to revision master)'
+ rev_options = RevOptions(Git, "master")
+ assert rev_options.to_display() == " (to revision master)"
-def test_rev_options_make_new():
+def test_rev_options_make_new() -> None:
"""
Test RevOptions.make_new().
"""
# The choice of VersionControl class doesn't matter here since
# the implementation is the same for all of them.
- rev_options = RevOptions(Git, 'master', extra_args=['foo', 'bar'])
- new_options = rev_options.make_new('develop')
+ rev_options = RevOptions(Git, "master", extra_args=["foo", "bar"])
+ new_options = rev_options.make_new("develop")
assert new_options is not rev_options
- assert new_options.extra_args == ['foo', 'bar']
- assert new_options.rev == 'develop'
+ assert new_options.extra_args == ["foo", "bar"]
+ assert new_options.rev == "develop"
assert new_options.vc_class is Git
-@pytest.mark.parametrize('sha, expected', [
- ((40 * 'a'), True),
- ((40 * 'A'), True),
- # Test a string containing all valid characters.
- ((18 * 'a' + '0123456789abcdefABCDEF'), True),
- ((40 * 'g'), False),
- ((39 * 'a'), False),
- ((41 * 'a'), False)
-])
-def test_looks_like_hash(sha, expected):
+@pytest.mark.parametrize(
+ "sha, expected",
+ [
+ ((40 * "a"), True),
+ ((40 * "A"), True),
+ # Test a string containing all valid characters.
+ ((18 * "a" + "0123456789abcdefABCDEF"), True),
+ ((40 * "g"), False),
+ ((39 * "a"), False),
+ ((41 * "a"), False),
+ ],
+)
+def test_looks_like_hash(sha: str, expected: bool) -> None:
assert looks_like_hash(sha) == expected
-@pytest.mark.parametrize('vcs_cls, remote_url, expected', [
- # Mercurial is one of the subclasses using the base class implementation.
- # `hg://` isn't a real prefix but it tests the default behaviour.
- (Mercurial, 'hg://user@example.com/MyProject', False),
- (Mercurial, 'http://example.com/MyProject', True),
- # The Git subclasses should return true in all cases.
- (Git, 'git://example.com/MyProject', True),
- (Git, 'http://example.com/MyProject', True),
- # Subversion also overrides the base class implementation.
- (Subversion, 'svn://example.com/MyProject', True),
-])
-def test_should_add_vcs_url_prefix(vcs_cls, remote_url, expected):
+@pytest.mark.parametrize(
+ "vcs_cls, remote_url, expected",
+ [
+ # Mercurial is one of the subclasses using the base class implementation.
+ # `hg://` isn't a real prefix but it tests the default behaviour.
+ (Mercurial, "hg://user@example.com/MyProject", False),
+ (Mercurial, "http://example.com/MyProject", True),
+ # The Git subclasses should return true in all cases.
+ (Git, "git://example.com/MyProject", True),
+ (Git, "http://example.com/MyProject", True),
+ # Subversion also overrides the base class implementation.
+ (Subversion, "svn://example.com/MyProject", True),
+ ],
+)
+def test_should_add_vcs_url_prefix(
+ vcs_cls: Type[VersionControl], remote_url: str, expected: bool
+) -> None:
actual = vcs_cls.should_add_vcs_url_prefix(remote_url)
assert actual == expected
-@pytest.mark.parametrize("url, target", [
- # A fully qualified remote url. No changes needed.
- ("ssh://bob@server/foo/bar.git", "ssh://bob@server/foo/bar.git"),
- ("git://bob@server/foo/bar.git", "git://bob@server/foo/bar.git"),
- # User is optional and does not need a default.
- ("ssh://server/foo/bar.git", "ssh://server/foo/bar.git"),
- # The common scp shorthand for ssh remotes. Pip won't recognise these as
- # git remotes until they have a 'ssh://' prefix and the ':' in the middle
- # is gone.
- ("git@example.com:foo/bar.git", "ssh://git@example.com/foo/bar.git"),
- ("example.com:foo.git", "ssh://example.com/foo.git"),
- # Http(s) remote names are already complete and should remain unchanged.
- ("https://example.com/foo", "https://example.com/foo"),
- ("http://example.com/foo/bar.git", "http://example.com/foo/bar.git"),
- ("https://bob@example.com/foo", "https://bob@example.com/foo"),
- ])
-def test_git_remote_url_to_pip(url, target):
+@pytest.mark.parametrize(
+ "url, target",
+ [
+ # A fully qualified remote url. No changes needed.
+ ("ssh://bob@server/foo/bar.git", "ssh://bob@server/foo/bar.git"),
+ ("git://bob@server/foo/bar.git", "git://bob@server/foo/bar.git"),
+ # User is optional and does not need a default.
+ ("ssh://server/foo/bar.git", "ssh://server/foo/bar.git"),
+ # The common scp shorthand for ssh remotes. Pip won't recognise these as
+ # git remotes until they have a 'ssh://' prefix and the ':' in the middle
+ # is gone.
+ ("git@example.com:foo/bar.git", "ssh://git@example.com/foo/bar.git"),
+ ("example.com:foo.git", "ssh://example.com/foo.git"),
+ # Http(s) remote names are already complete and should remain unchanged.
+ ("https://example.com/foo", "https://example.com/foo"),
+ ("http://example.com/foo/bar.git", "http://example.com/foo/bar.git"),
+ ("https://bob@example.com/foo", "https://bob@example.com/foo"),
+ ],
+)
+def test_git_remote_url_to_pip(url: str, target: str) -> None:
assert Git._git_remote_to_pip_url(url) == target
-@pytest.mark.parametrize("url, platform", [
- # Windows paths with the ':' drive prefix look dangerously close to SCP.
- ("c:/piffle/wiffle/waffle/poffle.git", "nt"),
- (r"c:\faffle\waffle\woffle\piffle.git", "nt"),
- # Unix paths less so but test them anyway.
- ("/muffle/fuffle/pufffle/fluffle.git", "posix"),
-])
-def test_paths_are_not_mistaken_for_scp_shorthand(url, platform):
+@pytest.mark.parametrize(
+ "url, platform",
+ [
+ # Windows paths with the ':' drive prefix look dangerously close to SCP.
+ ("c:/piffle/wiffle/waffle/poffle.git", "nt"),
+ (r"c:\faffle\waffle\woffle\piffle.git", "nt"),
+ # Unix paths less so but test them anyway.
+ ("/muffle/fuffle/pufffle/fluffle.git", "posix"),
+ ],
+)
+def test_paths_are_not_mistaken_for_scp_shorthand(url: str, platform: str) -> None:
# File paths should not be mistaken for SCP shorthand. If they do then
# 'c:/piffle/wiffle' would end up as 'ssh://c/piffle/wiffle'.
from pip._internal.vcs.git import SCP_REGEX
+
assert not SCP_REGEX.match(url)
if platform == os.name:
@@ -161,16 +200,16 @@ def test_paths_are_not_mistaken_for_scp_shorthand(url, platform):
Git._git_remote_to_pip_url(url)
-def test_git_remote_local_path(tmpdir):
+def test_git_remote_local_path(tmpdir: pathlib.Path) -> None:
path = pathlib.Path(tmpdir, "project.git")
path.mkdir()
# Path must exist to be recognised as a local git remote.
assert Git._git_remote_to_pip_url(str(path)) == path.as_uri()
-@patch('pip._internal.vcs.git.Git.get_remote_url')
-@patch('pip._internal.vcs.git.Git.get_revision')
-@patch('pip._internal.vcs.git.Git.get_subdirectory')
+@mock.patch("pip._internal.vcs.git.Git.get_remote_url")
+@mock.patch("pip._internal.vcs.git.Git.get_revision")
+@mock.patch("pip._internal.vcs.git.Git.get_subdirectory")
@pytest.mark.parametrize(
"git_url, target_url_prefix",
[
@@ -187,46 +226,51 @@ def test_git_remote_local_path(tmpdir):
)
@pytest.mark.network
def test_git_get_src_requirements(
- mock_get_subdirectory, mock_get_revision, mock_get_remote_url,
- git_url, target_url_prefix,
-):
- sha = '5547fa909e83df8bd743d3978d6667497983a4b7'
+ mock_get_subdirectory: mock.Mock,
+ mock_get_revision: mock.Mock,
+ mock_get_remote_url: mock.Mock,
+ git_url: str,
+ target_url_prefix: str,
+) -> None:
+ sha = "5547fa909e83df8bd743d3978d6667497983a4b7"
mock_get_remote_url.return_value = Git._git_remote_to_pip_url(git_url)
mock_get_revision.return_value = sha
mock_get_subdirectory.return_value = None
- ret = Git.get_src_requirement('.', 'pip-test-package')
+ ret = Git.get_src_requirement(".", "pip-test-package")
target = f"{target_url_prefix}@{sha}#egg=pip_test_package"
assert ret == target
-@patch('pip._internal.vcs.git.Git.get_revision_sha')
-def test_git_resolve_revision_rev_exists(get_sha_mock):
- get_sha_mock.return_value = ('123456', False)
- url = 'git+https://git.example.com'
- rev_options = Git.make_rev_options('develop')
+@mock.patch("pip._internal.vcs.git.Git.get_revision_sha")
+def test_git_resolve_revision_rev_exists(get_sha_mock: mock.Mock) -> None:
+ get_sha_mock.return_value = ("123456", False)
+ url = HiddenText("git+https://git.example.com", redacted="*")
+ rev_options = Git.make_rev_options("develop")
- new_options = Git.resolve_revision('.', url, rev_options)
- assert new_options.rev == '123456'
+ new_options = Git.resolve_revision(".", url, rev_options)
+ assert new_options.rev == "123456"
-@patch('pip._internal.vcs.git.Git.get_revision_sha')
-def test_git_resolve_revision_rev_not_found(get_sha_mock):
+@mock.patch("pip._internal.vcs.git.Git.get_revision_sha")
+def test_git_resolve_revision_rev_not_found(get_sha_mock: mock.Mock) -> None:
get_sha_mock.return_value = (None, False)
- url = 'git+https://git.example.com'
- rev_options = Git.make_rev_options('develop')
+ url = HiddenText("git+https://git.example.com", redacted="*")
+ rev_options = Git.make_rev_options("develop")
- new_options = Git.resolve_revision('.', url, rev_options)
- assert new_options.rev == 'develop'
+ new_options = Git.resolve_revision(".", url, rev_options)
+ assert new_options.rev == "develop"
-@patch('pip._internal.vcs.git.Git.get_revision_sha')
-def test_git_resolve_revision_not_found_warning(get_sha_mock, caplog):
+@mock.patch("pip._internal.vcs.git.Git.get_revision_sha")
+def test_git_resolve_revision_not_found_warning(
+ get_sha_mock: mock.Mock, caplog: pytest.LogCaptureFixture
+) -> None:
get_sha_mock.return_value = (None, False)
- url = 'git+https://git.example.com'
- sha = 40 * 'a'
+ url = HiddenText("git+https://git.example.com", redacted="*")
+ sha = 40 * "a"
rev_options = Git.make_rev_options(sha)
# resolve_revision with a full sha would fail here because
@@ -234,44 +278,53 @@ def test_git_resolve_revision_not_found_warning(get_sha_mock, caplog):
# test_resolve_commit_not_on_branch.
rev_options = Git.make_rev_options(sha[:6])
- new_options = Git.resolve_revision('.', url, rev_options)
- assert new_options.rev == 'aaaaaa'
+ new_options = Git.resolve_revision(".", url, rev_options)
+ assert new_options.rev == "aaaaaa"
# Check that a warning got logged only for the abbreviated hash.
messages = [r.getMessage() for r in caplog.records]
- messages = [msg for msg in messages if msg.startswith('Did not find ')]
+ messages = [msg for msg in messages if msg.startswith("Did not find ")]
assert messages == [
"Did not find branch or tag 'aaaaaa', assuming revision or ref."
]
-@pytest.mark.parametrize('rev_name,result', (
- ('5547fa909e83df8bd743d3978d6667497983a4b7', True),
- ('5547fa909', False),
- ('5678', False),
- ('abc123', False),
- ('foo', False),
- (None, False),
-))
-@patch('pip._internal.vcs.git.Git.get_revision')
-def test_git_is_commit_id_equal(mock_get_revision, rev_name, result):
+@pytest.mark.parametrize(
+ "rev_name,result",
+ (
+ ("5547fa909e83df8bd743d3978d6667497983a4b7", True),
+ ("5547fa909", False),
+ ("5678", False),
+ ("abc123", False),
+ ("foo", False),
+ (None, False),
+ ),
+)
+@mock.patch("pip._internal.vcs.git.Git.get_revision")
+def test_git_is_commit_id_equal(
+ mock_get_revision: mock.Mock, rev_name: Optional[str], result: bool
+) -> None:
"""
Test Git.is_commit_id_equal().
"""
- mock_get_revision.return_value = '5547fa909e83df8bd743d3978d6667497983a4b7'
- assert Git.is_commit_id_equal('/path', rev_name) is result
+ mock_get_revision.return_value = "5547fa909e83df8bd743d3978d6667497983a4b7"
+ assert Git.is_commit_id_equal("/path", rev_name) is result
# The non-SVN backends all use the same get_netloc_and_auth(), so only test
# Git as a representative.
-@pytest.mark.parametrize('args, expected', [
- # Test a basic case.
- (('example.com', 'https'), ('example.com', (None, None))),
- # Test with username and password.
- (('user:pass@example.com', 'https'),
- ('user:pass@example.com', (None, None))),
-])
-def test_git__get_netloc_and_auth(args, expected):
+@pytest.mark.parametrize(
+ "args, expected",
+ [
+ # Test a basic case.
+ (("example.com", "https"), ("example.com", (None, None))),
+ # Test with username and password.
+ (("user:pass@example.com", "https"), ("user:pass@example.com", (None, None))),
+ ],
+)
+def test_git__get_netloc_and_auth(
+ args: Tuple[str, str], expected: Tuple[str, Tuple[None, None]]
+) -> None:
"""
Test VersionControl.get_netloc_and_auth().
"""
@@ -280,21 +333,27 @@ def test_git__get_netloc_and_auth(args, expected):
assert actual == expected
-@pytest.mark.parametrize('args, expected', [
- # Test https.
- (('example.com', 'https'), ('example.com', (None, None))),
- # Test https with username and no password.
- (('user@example.com', 'https'), ('example.com', ('user', None))),
- # Test https with username and password.
- (('user:pass@example.com', 'https'), ('example.com', ('user', 'pass'))),
- # Test https with URL-encoded reserved characters.
- (('user%3Aname:%23%40%5E@example.com', 'https'),
- ('example.com', ('user:name', '#@^'))),
- # Test ssh with username and password.
- (('user:pass@example.com', 'ssh'),
- ('user:pass@example.com', (None, None))),
-])
-def test_subversion__get_netloc_and_auth(args, expected):
+@pytest.mark.parametrize(
+ "args, expected",
+ [
+ # Test https.
+ (("example.com", "https"), ("example.com", (None, None))),
+ # Test https with username and no password.
+ (("user@example.com", "https"), ("example.com", ("user", None))),
+ # Test https with username and password.
+ (("user:pass@example.com", "https"), ("example.com", ("user", "pass"))),
+ # Test https with URL-encoded reserved characters.
+ (
+ ("user%3Aname:%23%40%5E@example.com", "https"),
+ ("example.com", ("user:name", "#@^")),
+ ),
+ # Test ssh with username and password.
+ (("user:pass@example.com", "ssh"), ("user:pass@example.com", (None, None))),
+ ],
+)
+def test_subversion__get_netloc_and_auth(
+ args: Tuple[str, str], expected: Tuple[str, Tuple[Optional[str], Optional[str]]]
+) -> None:
"""
Test Subversion.get_netloc_and_auth().
"""
@@ -303,29 +362,38 @@ def test_subversion__get_netloc_and_auth(args, expected):
assert actual == expected
-def test_git__get_url_rev__idempotent():
+def test_git__get_url_rev__idempotent() -> None:
"""
Check that Git.get_url_rev_and_auth() is idempotent for what the code calls
"stub URLs" (i.e. URLs that don't contain "://").
Also check that it doesn't change self.url.
"""
- url = 'git+git@git.example.com:MyProject#egg=MyProject'
+ url = "git+git@git.example.com:MyProject#egg=MyProject"
result1 = Git.get_url_rev_and_auth(url)
result2 = Git.get_url_rev_and_auth(url)
- expected = ('git@git.example.com:MyProject', None, (None, None))
+ expected = ("git@git.example.com:MyProject", None, (None, None))
assert result1 == expected
assert result2 == expected
-@pytest.mark.parametrize('url, expected', [
- ('svn+https://svn.example.com/MyProject',
- ('https://svn.example.com/MyProject', None, (None, None))),
- # Test a "+" in the path portion.
- ('svn+https://svn.example.com/My+Project',
- ('https://svn.example.com/My+Project', None, (None, None))),
-])
-def test_version_control__get_url_rev_and_auth(url, expected):
+@pytest.mark.parametrize(
+ "url, expected",
+ [
+ (
+ "svn+https://svn.example.com/MyProject",
+ ("https://svn.example.com/MyProject", None, (None, None)),
+ ),
+ # Test a "+" in the path portion.
+ (
+ "svn+https://svn.example.com/My+Project",
+ ("https://svn.example.com/My+Project", None, (None, None)),
+ ),
+ ],
+)
+def test_version_control__get_url_rev_and_auth(
+ url: str, expected: Tuple[str, None, Tuple[None, None]]
+) -> None:
"""
Test the basic case of VersionControl.get_url_rev_and_auth().
"""
@@ -333,12 +401,15 @@ def test_version_control__get_url_rev_and_auth(url, expected):
assert actual == expected
-@pytest.mark.parametrize('url', [
- 'https://svn.example.com/MyProject',
- # Test a URL containing a "+" (but not in the scheme).
- 'https://svn.example.com/My+Project',
-])
-def test_version_control__get_url_rev_and_auth__missing_plus(url):
+@pytest.mark.parametrize(
+ "url",
+ [
+ "https://svn.example.com/MyProject",
+ # Test a URL containing a "+" (but not in the scheme).
+ "https://svn.example.com/My+Project",
+ ],
+)
+def test_version_control__get_url_rev_and_auth__missing_plus(url: str) -> None:
"""
Test passing a URL to VersionControl.get_url_rev_and_auth() with a "+"
missing from the scheme.
@@ -346,14 +417,17 @@ def test_version_control__get_url_rev_and_auth__missing_plus(url):
with pytest.raises(ValueError) as excinfo:
VersionControl.get_url_rev_and_auth(url)
- assert 'malformed VCS url' in str(excinfo.value)
+ assert "malformed VCS url" in str(excinfo.value)
-@pytest.mark.parametrize('url', [
- # Test a URL with revision part as empty.
- 'git+https://github.com/MyUser/myProject.git@#egg=py_pkg',
-])
-def test_version_control__get_url_rev_and_auth__no_revision(url):
+@pytest.mark.parametrize(
+ "url",
+ [
+ # Test a URL with revision part as empty.
+ "git+https://github.com/MyUser/myProject.git@#egg=py_pkg",
+ ],
+)
+def test_version_control__get_url_rev_and_auth__no_revision(url: str) -> None:
"""
Test passing a URL to VersionControl.get_url_rev_and_auth() with
empty revision
@@ -361,7 +435,7 @@ def test_version_control__get_url_rev_and_auth__no_revision(url):
with pytest.raises(InstallationError) as excinfo:
VersionControl.get_url_rev_and_auth(url)
- assert 'an empty revision (after @)' in str(excinfo.value)
+ assert "an empty revision (after @)" in str(excinfo.value)
@pytest.mark.parametrize("vcs_cls", [Bazaar, Git, Mercurial, Subversion])
@@ -373,39 +447,54 @@ def test_version_control__get_url_rev_and_auth__no_revision(url):
],
ids=["FileNotFoundError", "PermissionError"],
)
-def test_version_control__run_command__fails(vcs_cls, exc_cls, msg_re):
+def test_version_control__run_command__fails(
+ vcs_cls: Type[VersionControl], exc_cls: Type[Exception], msg_re: str
+) -> None:
"""
Test that ``VersionControl.run_command()`` raises ``BadCommand``
when the command is not found or when the user have no permission
to execute it. The error message must contains the command name.
"""
- with patch("pip._internal.vcs.versioncontrol.call_subprocess") as call:
+ with mock.patch("pip._internal.vcs.versioncontrol.call_subprocess") as call:
call.side_effect = exc_cls
with pytest.raises(BadCommand, match=msg_re.format(name=vcs_cls.name)):
- vcs_cls.run_command([])
-
-
-@pytest.mark.parametrize('url, expected', [
- # Test http.
- ('bzr+http://bzr.myproject.org/MyProject/trunk/#egg=MyProject',
- 'http://bzr.myproject.org/MyProject/trunk/'),
- # Test https.
- ('bzr+https://bzr.myproject.org/MyProject/trunk/#egg=MyProject',
- 'https://bzr.myproject.org/MyProject/trunk/'),
- # Test ftp.
- ('bzr+ftp://bzr.myproject.org/MyProject/trunk/#egg=MyProject',
- 'ftp://bzr.myproject.org/MyProject/trunk/'),
- # Test sftp.
- ('bzr+sftp://bzr.myproject.org/MyProject/trunk/#egg=MyProject',
- 'sftp://bzr.myproject.org/MyProject/trunk/'),
- # Test launchpad.
- ('bzr+lp:MyLaunchpadProject#egg=MyLaunchpadProject',
- 'lp:MyLaunchpadProject'),
- # Test ssh (special handling).
- ('bzr+ssh://bzr.myproject.org/MyProject/trunk/#egg=MyProject',
- 'bzr+ssh://bzr.myproject.org/MyProject/trunk/'),
-])
-def test_bazaar__get_url_rev_and_auth(url, expected):
+ # https://github.com/python/mypy/issues/3283
+ vcs_cls.run_command([]) # type: ignore[arg-type]
+
+
+@pytest.mark.parametrize(
+ "url, expected",
+ [
+ # Test http.
+ (
+ "bzr+http://bzr.myproject.org/MyProject/trunk/#egg=MyProject",
+ "http://bzr.myproject.org/MyProject/trunk/",
+ ),
+ # Test https.
+ (
+ "bzr+https://bzr.myproject.org/MyProject/trunk/#egg=MyProject",
+ "https://bzr.myproject.org/MyProject/trunk/",
+ ),
+ # Test ftp.
+ (
+ "bzr+ftp://bzr.myproject.org/MyProject/trunk/#egg=MyProject",
+ "ftp://bzr.myproject.org/MyProject/trunk/",
+ ),
+ # Test sftp.
+ (
+ "bzr+sftp://bzr.myproject.org/MyProject/trunk/#egg=MyProject",
+ "sftp://bzr.myproject.org/MyProject/trunk/",
+ ),
+ # Test launchpad.
+ ("bzr+lp:MyLaunchpadProject#egg=MyLaunchpadProject", "lp:MyLaunchpadProject"),
+ # Test ssh (special handling).
+ (
+ "bzr+ssh://bzr.myproject.org/MyProject/trunk/#egg=MyProject",
+ "bzr+ssh://bzr.myproject.org/MyProject/trunk/",
+ ),
+ ],
+)
+def test_bazaar__get_url_rev_and_auth(url: str, expected: str) -> None:
"""
Test Bazaar.get_url_rev_and_auth().
"""
@@ -413,21 +502,34 @@ def test_bazaar__get_url_rev_and_auth(url, expected):
assert actual == (expected, None, (None, None))
-@pytest.mark.parametrize('url, expected', [
- # Test an https URL.
- ('svn+https://svn.example.com/MyProject#egg=MyProject',
- ('https://svn.example.com/MyProject', None, (None, None))),
- # Test an https URL with a username and password.
- ('svn+https://user:pass@svn.example.com/MyProject#egg=MyProject',
- ('https://svn.example.com/MyProject', None, ('user', 'pass'))),
- # Test an ssh URL.
- ('svn+ssh://svn.example.com/MyProject#egg=MyProject',
- ('svn+ssh://svn.example.com/MyProject', None, (None, None))),
- # Test an ssh URL with a username.
- ('svn+ssh://user@svn.example.com/MyProject#egg=MyProject',
- ('svn+ssh://user@svn.example.com/MyProject', None, (None, None))),
-])
-def test_subversion__get_url_rev_and_auth(url, expected):
+@pytest.mark.parametrize(
+ "url, expected",
+ [
+ # Test an https URL.
+ (
+ "svn+https://svn.example.com/MyProject#egg=MyProject",
+ ("https://svn.example.com/MyProject", None, (None, None)),
+ ),
+ # Test an https URL with a username and password.
+ (
+ "svn+https://user:pass@svn.example.com/MyProject#egg=MyProject",
+ ("https://svn.example.com/MyProject", None, ("user", "pass")),
+ ),
+ # Test an ssh URL.
+ (
+ "svn+ssh://svn.example.com/MyProject#egg=MyProject",
+ ("svn+ssh://svn.example.com/MyProject", None, (None, None)),
+ ),
+ # Test an ssh URL with a username.
+ (
+ "svn+ssh://user@svn.example.com/MyProject#egg=MyProject",
+ ("svn+ssh://user@svn.example.com/MyProject", None, (None, None)),
+ ),
+ ],
+)
+def test_subversion__get_url_rev_and_auth(
+ url: str, expected: Tuple[str, None, Tuple[Optional[str], Optional[str]]]
+) -> None:
"""
Test Subversion.get_url_rev_and_auth().
"""
@@ -437,12 +539,17 @@ def test_subversion__get_url_rev_and_auth(url, expected):
# The non-SVN backends all use the same make_rev_args(), so only test
# Git as a representative.
-@pytest.mark.parametrize('username, password, expected', [
- (None, None, []),
- ('user', None, []),
- ('user', hide_value('pass'), []),
-])
-def test_git__make_rev_args(username, password, expected):
+@pytest.mark.parametrize(
+ "username, password, expected",
+ [
+ (None, None, []),
+ ("user", None, []),
+ ("user", hide_value("pass"), []),
+ ],
+)
+def test_git__make_rev_args(
+ username: Optional[str], password: Optional[HiddenText], expected: CommandArgs
+) -> None:
"""
Test VersionControl.make_rev_args().
"""
@@ -450,13 +557,21 @@ def test_git__make_rev_args(username, password, expected):
assert actual == expected
-@pytest.mark.parametrize('username, password, expected', [
- (None, None, []),
- ('user', None, ['--username', 'user']),
- ('user', hide_value('pass'),
- ['--username', 'user', '--password', hide_value('pass')]),
-])
-def test_subversion__make_rev_args(username, password, expected):
+@pytest.mark.parametrize(
+ "username, password, expected",
+ [
+ (None, None, []),
+ ("user", None, ["--username", "user"]),
+ (
+ "user",
+ hide_value("pass"),
+ ["--username", "user", "--password", hide_value("pass")],
+ ),
+ ],
+)
+def test_subversion__make_rev_args(
+ username: Optional[str], password: Optional[HiddenText], expected: CommandArgs
+) -> None:
"""
Test Subversion.make_rev_args().
"""
@@ -464,38 +579,40 @@ def test_subversion__make_rev_args(username, password, expected):
assert actual == expected
-def test_subversion__get_url_rev_options():
+def test_subversion__get_url_rev_options() -> None:
"""
Test Subversion.get_url_rev_options().
"""
- secret_url = (
- 'svn+https://user:pass@svn.example.com/MyProject@v1.0#egg=MyProject'
- )
+ secret_url = "svn+https://user:pass@svn.example.com/MyProject@v1.0#egg=MyProject"
hidden_url = hide_url(secret_url)
url, rev_options = Subversion().get_url_rev_options(hidden_url)
- assert url == hide_url('https://svn.example.com/MyProject')
- assert rev_options.rev == 'v1.0'
+ assert url == hide_url("https://svn.example.com/MyProject")
+ assert rev_options.rev == "v1.0"
assert rev_options.extra_args == (
- ['--username', 'user', '--password', hide_value('pass')]
+ ["--username", "user", "--password", hide_value("pass")]
)
-def test_get_git_version():
+def test_get_git_version() -> None:
git_version = Git().get_git_version()
assert git_version >= (1, 0, 0)
-@pytest.mark.parametrize('use_interactive,is_atty,expected', [
- (None, False, False),
- (None, True, True),
- (False, False, False),
- (False, True, False),
- (True, False, True),
- (True, True, True),
-])
-@patch('sys.stdin.isatty')
+@pytest.mark.parametrize(
+ "use_interactive,is_atty,expected",
+ [
+ (None, False, False),
+ (None, True, True),
+ (False, False, False),
+ (False, True, False),
+ (True, False, True),
+ (True, True, True),
+ ],
+)
+@mock.patch("sys.stdin.isatty")
def test_subversion__init_use_interactive(
- mock_isatty, use_interactive, is_atty, expected):
+ mock_isatty: mock.Mock, use_interactive: bool, is_atty: bool, expected: bool
+) -> None:
"""
Test Subversion.__init__() with mocked sys.stdin.isatty() output.
"""
@@ -505,7 +622,7 @@ def test_subversion__init_use_interactive(
@need_svn
-def test_subversion__call_vcs_version():
+def test_subversion__call_vcs_version() -> None:
"""
Test Subversion.call_vcs_version() against local ``svn``.
"""
@@ -517,25 +634,33 @@ def test_subversion__call_vcs_version():
assert version[0] >= 1
-@pytest.mark.parametrize('svn_output, expected_version', [
- ('svn, version 1.10.3 (r1842928)\n'
- ' compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0',
- (1, 10, 3)),
- ('svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0)\n'
- ' compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2',
- (1, 12, 0)),
- ('svn, version 1.9.7 (r1800392)', (1, 9, 7)),
- ('svn, version 1.9.7a1 (r1800392)', ()),
- ('svn, version 1.9 (r1800392)', (1, 9)),
- ('svn, version .9.7 (r1800392)', ()),
- ('svn version 1.9.7 (r1800392)', ()),
- ('svn 1.9.7', ()),
- ('svn, version . .', ()),
- ('', ()),
-])
-@patch('pip._internal.vcs.subversion.Subversion.run_command')
+@pytest.mark.parametrize(
+ "svn_output, expected_version",
+ [
+ (
+ "svn, version 1.10.3 (r1842928)\n"
+ " compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0",
+ (1, 10, 3),
+ ),
+ (
+ "svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0)\n"
+ " compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2",
+ (1, 12, 0),
+ ),
+ ("svn, version 1.9.7 (r1800392)", (1, 9, 7)),
+ ("svn, version 1.9.7a1 (r1800392)", ()),
+ ("svn, version 1.9 (r1800392)", (1, 9)),
+ ("svn, version .9.7 (r1800392)", ()),
+ ("svn version 1.9.7 (r1800392)", ()),
+ ("svn 1.9.7", ()),
+ ("svn, version . .", ()),
+ ("", ()),
+ ],
+)
+@mock.patch("pip._internal.vcs.subversion.Subversion.run_command")
def test_subversion__call_vcs_version_patched(
- mock_run_command, svn_output, expected_version):
+ mock_run_command: mock.Mock, svn_output: str, expected_version: Tuple[int, ...]
+) -> None:
"""
Test Subversion.call_vcs_version() against patched output.
"""
@@ -544,8 +669,10 @@ def test_subversion__call_vcs_version_patched(
assert version == expected_version
-@patch('pip._internal.vcs.subversion.Subversion.run_command')
-def test_subversion__call_vcs_version_svn_not_installed(mock_run_command):
+@mock.patch("pip._internal.vcs.subversion.Subversion.run_command")
+def test_subversion__call_vcs_version_svn_not_installed(
+ mock_run_command: mock.Mock,
+) -> None:
"""
Test Subversion.call_vcs_version() when svn is not installed.
"""
@@ -554,13 +681,16 @@ def test_subversion__call_vcs_version_svn_not_installed(mock_run_command):
Subversion().call_vcs_version()
-@pytest.mark.parametrize('version', [
- (),
- (1,),
- (1, 8),
- (1, 8, 0),
-])
-def test_subversion__get_vcs_version_cached(version):
+@pytest.mark.parametrize(
+ "version",
+ [
+ (),
+ (1,),
+ (1, 8),
+ (1, 8, 0),
+ ],
+)
+def test_subversion__get_vcs_version_cached(version: Tuple[int, ...]) -> None:
"""
Test Subversion.get_vcs_version() with previously cached result.
"""
@@ -569,13 +699,18 @@ def test_subversion__get_vcs_version_cached(version):
assert svn.get_vcs_version() == version
-@pytest.mark.parametrize('vcs_version', [
- (),
- (1, 7),
- (1, 8, 0),
-])
-@patch('pip._internal.vcs.subversion.Subversion.call_vcs_version')
-def test_subversion__get_vcs_version_call_vcs(mock_call_vcs, vcs_version):
+@pytest.mark.parametrize(
+ "vcs_version",
+ [
+ (),
+ (1, 7),
+ (1, 8, 0),
+ ],
+)
+@mock.patch("pip._internal.vcs.subversion.Subversion.call_vcs_version")
+def test_subversion__get_vcs_version_call_vcs(
+ mock_call_vcs: mock.Mock, vcs_version: Tuple[int, ...]
+) -> None:
"""
Test Subversion.get_vcs_version() with mocked output from
call_vcs_version().
@@ -588,16 +723,20 @@ def test_subversion__get_vcs_version_call_vcs(mock_call_vcs, vcs_version):
assert svn._vcs_version == vcs_version
-@pytest.mark.parametrize('use_interactive,vcs_version,expected_options', [
- (False, (), ['--non-interactive']),
- (False, (1, 7, 0), ['--non-interactive']),
- (False, (1, 8, 0), ['--non-interactive']),
- (True, (), []),
- (True, (1, 7, 0), []),
- (True, (1, 8, 0), ['--force-interactive']),
-])
+@pytest.mark.parametrize(
+ "use_interactive,vcs_version,expected_options",
+ [
+ (False, (), ["--non-interactive"]),
+ (False, (1, 7, 0), ["--non-interactive"]),
+ (False, (1, 8, 0), ["--non-interactive"]),
+ (True, (), []),
+ (True, (1, 7, 0), []),
+ (True, (1, 8, 0), ["--force-interactive"]),
+ ],
+)
def test_subversion__get_remote_call_options(
- use_interactive, vcs_version, expected_options):
+ use_interactive: bool, vcs_version: Tuple[int, ...], expected_options: List[str]
+) -> None:
"""
Test Subversion.get_remote_call_options().
"""
@@ -607,57 +746,87 @@ def test_subversion__get_remote_call_options(
class TestSubversionArgs(TestCase):
- def setUp(self):
- patcher = patch('pip._internal.vcs.versioncontrol.call_subprocess')
+ def setUp(self) -> None:
+ patcher = mock.patch("pip._internal.vcs.versioncontrol.call_subprocess")
self.addCleanup(patcher.stop)
self.call_subprocess_mock = patcher.start()
# Test Data.
- self.url = 'svn+http://username:password@svn.example.com/'
+ self.url = "svn+http://username:password@svn.example.com/"
# use_interactive is set to False to test that remote call options are
# properly added.
self.svn = Subversion(use_interactive=False)
self.rev_options = RevOptions(Subversion)
- self.dest = '/tmp/test'
+ self.dest = "/tmp/test"
- def assert_call_args(self, args):
+ def assert_call_args(self, args: CommandArgs) -> None:
assert self.call_subprocess_mock.call_args[0][0] == args
- def test_obtain(self):
- self.svn.obtain(self.dest, hide_url(self.url))
- self.assert_call_args([
- 'svn', 'checkout', '-q', '--non-interactive', '--username',
- 'username', '--password', hide_value('password'),
- hide_url('http://svn.example.com/'), '/tmp/test',
- ])
-
- def test_fetch_new(self):
- self.svn.fetch_new(self.dest, hide_url(self.url), self.rev_options)
- self.assert_call_args([
- 'svn', 'checkout', '-q', '--non-interactive',
- hide_url('svn+http://username:password@svn.example.com/'),
- '/tmp/test',
- ])
-
- def test_fetch_new_revision(self):
- rev_options = RevOptions(Subversion, '123')
- self.svn.fetch_new(self.dest, hide_url(self.url), rev_options)
- self.assert_call_args([
- 'svn', 'checkout', '-q', '--non-interactive', '-r', '123',
- hide_url('svn+http://username:password@svn.example.com/'),
- '/tmp/test',
- ])
-
- def test_switch(self):
+ def test_obtain(self) -> None:
+ self.svn.obtain(self.dest, hide_url(self.url), verbosity=0)
+ self.assert_call_args(
+ [
+ "svn",
+ "checkout",
+ "--quiet",
+ "--non-interactive",
+ "--username",
+ "username",
+ "--password",
+ hide_value("password"),
+ hide_url("http://svn.example.com/"),
+ "/tmp/test",
+ ]
+ )
+
+ def test_fetch_new(self) -> None:
+ self.svn.fetch_new(self.dest, hide_url(self.url), self.rev_options, verbosity=0)
+ self.assert_call_args(
+ [
+ "svn",
+ "checkout",
+ "--quiet",
+ "--non-interactive",
+ hide_url("svn+http://username:password@svn.example.com/"),
+ "/tmp/test",
+ ]
+ )
+
+ def test_fetch_new_revision(self) -> None:
+ rev_options = RevOptions(Subversion, "123")
+ self.svn.fetch_new(self.dest, hide_url(self.url), rev_options, verbosity=0)
+ self.assert_call_args(
+ [
+ "svn",
+ "checkout",
+ "--quiet",
+ "--non-interactive",
+ "-r",
+ "123",
+ hide_url("svn+http://username:password@svn.example.com/"),
+ "/tmp/test",
+ ]
+ )
+
+ def test_switch(self) -> None:
self.svn.switch(self.dest, hide_url(self.url), self.rev_options)
- self.assert_call_args([
- 'svn', 'switch', '--non-interactive',
- hide_url('svn+http://username:password@svn.example.com/'),
- '/tmp/test',
- ])
-
- def test_update(self):
+ self.assert_call_args(
+ [
+ "svn",
+ "switch",
+ "--non-interactive",
+ hide_url("svn+http://username:password@svn.example.com/"),
+ "/tmp/test",
+ ]
+ )
+
+ def test_update(self) -> None:
self.svn.update(self.dest, hide_url(self.url), self.rev_options)
- self.assert_call_args([
- 'svn', 'update', '--non-interactive', '/tmp/test',
- ])
+ self.assert_call_args(
+ [
+ "svn",
+ "update",
+ "--non-interactive",
+ "/tmp/test",
+ ]
+ )
diff --git a/tests/unit/test_vcs_mercurial.py b/tests/unit/test_vcs_mercurial.py
index 07224c0a4..d8e8f6cad 100644
--- a/tests/unit/test_vcs_mercurial.py
+++ b/tests/unit/test_vcs_mercurial.py
@@ -4,6 +4,7 @@ Contains functional tests of the Mercurial class.
import configparser
import os
+from pathlib import Path
from pip._internal.utils.misc import hide_url
from pip._internal.vcs.mercurial import Mercurial
@@ -11,22 +12,22 @@ from tests.lib import need_mercurial
@need_mercurial
-def test_mercurial_switch_updates_config_file_when_found(tmpdir):
+def test_mercurial_switch_updates_config_file_when_found(tmpdir: Path) -> None:
hg = Mercurial()
options = hg.make_rev_options()
- hg_dir = os.path.join(tmpdir, '.hg')
+ hg_dir = os.path.join(tmpdir, ".hg")
os.mkdir(hg_dir)
config = configparser.RawConfigParser()
- config.add_section('paths')
- config.set('paths', 'default', 'old_url')
+ config.add_section("paths")
+ config.set("paths", "default", "old_url")
- hgrc_path = os.path.join(hg_dir, 'hgrc')
- with open(hgrc_path, 'w') as f:
+ hgrc_path = os.path.join(hg_dir, "hgrc")
+ with open(hgrc_path, "w") as f:
config.write(f)
- hg.switch(tmpdir, hide_url('new_url'), options)
+ hg.switch(os.fspath(tmpdir), hide_url("new_url"), options)
config.read(hgrc_path)
- default_path = config.get('paths', 'default')
- assert default_path == 'new_url'
+ default_path = config.get("paths", "default")
+ assert default_path == "new_url"
diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py
index 3b39f91b9..6aec64702 100644
--- a/tests/unit/test_wheel.py
+++ b/tests/unit/test_wheel.py
@@ -2,8 +2,11 @@
import csv
import logging
import os
+import pathlib
import textwrap
from email import message_from_string
+from pathlib import Path
+from typing import Dict, List, Optional, Tuple, cast
from unittest.mock import patch
import pytest
@@ -11,7 +14,6 @@ from pip._vendor.packaging.requirements import Requirement
from pip._internal.exceptions import InstallationError
from pip._internal.locations import get_scheme
-from pip._internal.metadata import get_wheel_distribution
from pip._internal.models.direct_url import (
DIRECT_URL_METADATA_NAME,
ArchiveInfo,
@@ -20,59 +22,69 @@ from pip._internal.models.direct_url import (
from pip._internal.models.scheme import Scheme
from pip._internal.operations.build.wheel_legacy import get_legacy_build_wheel_path
from pip._internal.operations.install import wheel
+from pip._internal.operations.install.wheel import InstalledCSVRow, RecordPath
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.misc import hash_file
from pip._internal.utils.unpacking import unpack_file
-from tests.lib import DATA_DIR, assert_paths_equal
+from tests.lib import DATA_DIR, TestData, assert_paths_equal
from tests.lib.wheel import make_wheel
-def call_get_legacy_build_wheel_path(caplog, names):
+def call_get_legacy_build_wheel_path(
+ caplog: pytest.LogCaptureFixture, names: List[str]
+) -> Optional[str]:
wheel_path = get_legacy_build_wheel_path(
names=names,
- temp_dir='/tmp/abcd',
- name='pendulum',
- command_args=['arg1', 'arg2'],
- command_output='output line 1\noutput line 2\n',
+ temp_dir="/tmp/abcd",
+ name="pendulum",
+ command_args=["arg1", "arg2"],
+ command_output="output line 1\noutput line 2\n",
)
return wheel_path
-def test_get_legacy_build_wheel_path(caplog):
- actual = call_get_legacy_build_wheel_path(caplog, names=['name'])
- assert_paths_equal(actual, '/tmp/abcd/name')
+def test_get_legacy_build_wheel_path(caplog: pytest.LogCaptureFixture) -> None:
+ actual = call_get_legacy_build_wheel_path(caplog, names=["name"])
+ assert actual is not None
+ assert_paths_equal(actual, "/tmp/abcd/name")
assert not caplog.records
-def test_get_legacy_build_wheel_path__no_names(caplog):
+def test_get_legacy_build_wheel_path__no_names(
+ caplog: pytest.LogCaptureFixture,
+) -> None:
caplog.set_level(logging.INFO)
actual = call_get_legacy_build_wheel_path(caplog, names=[])
assert actual is None
assert len(caplog.records) == 1
record = caplog.records[0]
- assert record.levelname == 'WARNING'
+ assert record.levelname == "WARNING"
assert record.message.splitlines() == [
"Legacy build of wheel for 'pendulum' created no files.",
"Command arguments: arg1 arg2",
- 'Command output: [use --verbose to show]',
+ "Command output: [use --verbose to show]",
]
-def test_get_legacy_build_wheel_path__multiple_names(caplog):
+def test_get_legacy_build_wheel_path__multiple_names(
+ caplog: pytest.LogCaptureFixture,
+) -> None:
caplog.set_level(logging.INFO)
# Deliberately pass the names in non-sorted order.
actual = call_get_legacy_build_wheel_path(
- caplog, names=['name2', 'name1'],
+ caplog,
+ names=["name2", "name1"],
)
- assert_paths_equal(actual, '/tmp/abcd/name1')
+ assert actual is not None
+ assert_paths_equal(actual, "/tmp/abcd/name1")
assert len(caplog.records) == 1
record = caplog.records[0]
- assert record.levelname == 'WARNING'
+ assert record.levelname == "WARNING"
assert record.message.splitlines() == [
"Legacy build of wheel for 'pendulum' created more than one file.",
"Filenames (choosing first): ['name1', 'name2']",
"Command arguments: arg1 arg2",
- 'Command output: [use --verbose to show]',
+ "Command output: [use --verbose to show]",
]
@@ -84,148 +96,176 @@ def test_get_legacy_build_wheel_path__multiple_names(caplog):
"進入點 = 套件.模組:函å¼",
],
)
-def test_get_entrypoints(tmp_path, console_scripts):
+def test_get_entrypoints(tmp_path: pathlib.Path, console_scripts: str) -> None:
entry_points_text = """
[console_scripts]
{}
[section]
common:one = module:func
common:two = module:other_func
- """.format(console_scripts)
+ """.format(
+ console_scripts
+ )
- wheel_zip = make_wheel(
+ distribution = make_wheel(
"simple",
"0.1.0",
extra_metadata_files={
"entry_points.txt": entry_points_text,
},
- ).save_to_dir(tmp_path)
- distribution = get_wheel_distribution(wheel_zip, "simple")
+ ).as_distribution("simple")
- assert wheel.get_entrypoints(distribution) == (
- dict([console_scripts.split(' = ')]),
- {},
- )
+ entry_point, entry_point_value = console_scripts.split(" = ")
+ assert wheel.get_entrypoints(distribution) == ({entry_point: entry_point_value}, {})
-def test_get_entrypoints_no_entrypoints(tmp_path):
- wheel_zip = make_wheel("simple", "0.1.0").save_to_dir(tmp_path)
- distribution = get_wheel_distribution(wheel_zip, "simple")
+def test_get_entrypoints_no_entrypoints(tmp_path: pathlib.Path) -> None:
+ distribution = make_wheel("simple", "0.1.0").as_distribution("simple")
console, gui = wheel.get_entrypoints(distribution)
assert console == {}
assert gui == {}
-@pytest.mark.parametrize("outrows, expected", [
- ([
- ('', '', 'a'),
- ('', '', ''),
- ], [
- ('', '', ''),
- ('', '', 'a'),
- ]),
- ([
- # Include an int to check avoiding the following error:
- # > TypeError: '<' not supported between instances of 'str' and 'int'
- ('', '', 1),
- ('', '', ''),
- ], [
- ('', '', ''),
- ('', '', '1'),
- ]),
- ([
- # Test the normalization correctly encode everything for csv.writer().
- ('😉', '', 1),
- ('', '', ''),
- ], [
- ('', '', ''),
- ('😉', '', '1'),
- ]),
-])
-def test_normalized_outrows(outrows, expected):
+@pytest.mark.parametrize(
+ "outrows, expected",
+ [
+ (
+ [
+ ("", "", "a"),
+ ("", "", ""),
+ ],
+ [
+ ("", "", ""),
+ ("", "", "a"),
+ ],
+ ),
+ (
+ [
+ # Include an int to check avoiding the following error:
+ # > TypeError: '<' not supported between instances of 'str' and 'int'
+ ("", "", 1),
+ ("", "", ""),
+ ],
+ [
+ ("", "", ""),
+ ("", "", "1"),
+ ],
+ ),
+ (
+ [
+ # Test the normalization correctly encode everything for csv.writer().
+ ("😉", "", 1),
+ ("", "", ""),
+ ],
+ [
+ ("", "", ""),
+ ("😉", "", "1"),
+ ],
+ ),
+ ],
+)
+def test_normalized_outrows(
+ outrows: List[Tuple[RecordPath, str, str]], expected: List[Tuple[str, str, str]]
+) -> None:
actual = wheel._normalized_outrows(outrows)
assert actual == expected
-def call_get_csv_rows_for_installed(tmpdir, text):
- path = tmpdir.joinpath('temp.txt')
+def call_get_csv_rows_for_installed(tmpdir: Path, text: str) -> List[InstalledCSVRow]:
+ path = tmpdir.joinpath("temp.txt")
path.write_text(text)
# Test that an installed file appearing in RECORD has its filename
# updated in the new RECORD file.
- installed = {'a': 'z'}
- changed = set()
- generated = []
- lib_dir = '/lib/dir'
+ installed = cast(Dict[RecordPath, RecordPath], {"a": "z"})
+ lib_dir = "/lib/dir"
- with open(path, **wheel.csv_io_kwargs('r')) as f:
+ with open(path, **wheel.csv_io_kwargs("r")) as f:
record_rows = list(csv.reader(f))
outrows = wheel.get_csv_rows_for_installed(
- record_rows, installed=installed, changed=changed,
- generated=generated, lib_dir=lib_dir,
+ record_rows,
+ installed=installed,
+ changed=set(),
+ generated=[],
+ lib_dir=lib_dir,
)
return outrows
-def test_get_csv_rows_for_installed(tmpdir, caplog):
- text = textwrap.dedent("""\
+def test_get_csv_rows_for_installed(
+ tmpdir: Path, caplog: pytest.LogCaptureFixture
+) -> None:
+ text = textwrap.dedent(
+ """\
a,b,c
d,e,f
- """)
+ """
+ )
outrows = call_get_csv_rows_for_installed(tmpdir, text)
expected = [
- ('z', 'b', 'c'),
- ('d', 'e', 'f'),
+ ("z", "b", "c"),
+ ("d", "e", "f"),
]
assert outrows == expected
# Check there were no warnings.
assert len(caplog.records) == 0
-def test_get_csv_rows_for_installed__long_lines(tmpdir, caplog):
- text = textwrap.dedent("""\
+def test_get_csv_rows_for_installed__long_lines(
+ tmpdir: Path, caplog: pytest.LogCaptureFixture
+) -> None:
+ text = textwrap.dedent(
+ """\
a,b,c,d
e,f,g
h,i,j,k
- """)
+ """
+ )
outrows = call_get_csv_rows_for_installed(tmpdir, text)
-
- expected = [
- ('z', 'b', 'c'),
- ('e', 'f', 'g'),
- ('h', 'i', 'j'),
+ assert outrows == [
+ ("z", "b", "c"),
+ ("e", "f", "g"),
+ ("h", "i", "j"),
]
- assert outrows == expected
messages = [rec.message for rec in caplog.records]
- expected = [
+ assert messages == [
"RECORD line has more than three elements: ['a', 'b', 'c', 'd']",
- "RECORD line has more than three elements: ['h', 'i', 'j', 'k']"
+ "RECORD line has more than three elements: ['h', 'i', 'j', 'k']",
]
- assert messages == expected
-
-
-@pytest.mark.parametrize("text,expected", [
- ("Root-Is-Purelib: true", True),
- ("Root-Is-Purelib: false", False),
- ("Root-Is-Purelib: hello", False),
- ("", False),
- ("root-is-purelib: true", True),
- ("root-is-purelib: True", True),
-])
-def test_wheel_root_is_purelib(text, expected):
+
+
+@pytest.mark.parametrize(
+ "text,expected",
+ [
+ ("Root-Is-Purelib: true", True),
+ ("Root-Is-Purelib: false", False),
+ ("Root-Is-Purelib: hello", False),
+ ("", False),
+ ("root-is-purelib: true", True),
+ ("root-is-purelib: True", True),
+ ],
+)
+def test_wheel_root_is_purelib(text: str, expected: bool) -> None:
assert wheel.wheel_root_is_purelib(message_from_string(text)) == expected
-class TestWheelFile:
+def test_dist_from_broken_wheel_fails(data: TestData) -> None:
+ from pip._internal.exceptions import InvalidWheel
+ from pip._internal.metadata import FilesystemWheel, get_wheel_distribution
- def test_unpack_wheel_no_flatten(self, tmpdir):
- filepath = os.path.join(DATA_DIR, 'packages',
- 'meta-1.0-py2.py3-none-any.whl')
- unpack_file(filepath, tmpdir)
- assert os.path.isdir(os.path.join(tmpdir, 'meta-1.0.dist-info'))
+ package = data.packages.joinpath("corruptwheel-1.0-py2.py3-none-any.whl")
+ with pytest.raises(InvalidWheel):
+ get_wheel_distribution(FilesystemWheel(os.fspath(package)), "brokenwheel")
+
+
+class TestWheelFile:
+ def test_unpack_wheel_no_flatten(self, tmpdir: Path) -> None:
+ filepath = os.path.join(DATA_DIR, "packages", "meta-1.0-py2.py3-none-any.whl")
+ unpack_file(filepath, os.fspath(tmpdir))
+ assert os.path.isdir(os.path.join(tmpdir, "meta-1.0.dist-info"))
class TestInstallUnpackedWheel:
@@ -233,13 +273,13 @@ class TestInstallUnpackedWheel:
Tests for moving files from wheel src to scheme paths
"""
- def prep(self, data, tmpdir):
+ def prep(self, data: TestData, tmp_path: Path) -> None:
# Since Path implements __add__, os.path.join returns a Path object.
# Passing Path objects to interfaces expecting str (like
# `compileall.compile_file`) can cause failures, so we normalize it
# to a string here.
- tmpdir = str(tmpdir)
- self.name = 'sample'
+ tmpdir = str(tmp_path)
+ self.name = "sample"
self.wheelpath = make_wheel(
"sample",
"1.2.0",
@@ -286,43 +326,41 @@ class TestInstallUnpackedWheel:
"gui_scripts": ["sample2 = sample:main"],
},
).save_to_dir(tmpdir)
- self.req = Requirement('sample')
- self.src = os.path.join(tmpdir, 'src')
- self.dest = os.path.join(tmpdir, 'dest')
+ self.req = Requirement("sample")
+ self.src = os.path.join(tmpdir, "src")
+ self.dest = os.path.join(tmpdir, "dest")
self.scheme = Scheme(
- purelib=os.path.join(self.dest, 'lib'),
- platlib=os.path.join(self.dest, 'lib'),
- headers=os.path.join(self.dest, 'headers'),
- scripts=os.path.join(self.dest, 'bin'),
- data=os.path.join(self.dest, 'data'),
+ purelib=os.path.join(self.dest, "lib"),
+ platlib=os.path.join(self.dest, "lib"),
+ headers=os.path.join(self.dest, "headers"),
+ scripts=os.path.join(self.dest, "bin"),
+ data=os.path.join(self.dest, "data"),
)
- self.src_dist_info = os.path.join(
- self.src, 'sample-1.2.0.dist-info')
+ self.src_dist_info = os.path.join(self.src, "sample-1.2.0.dist-info")
self.dest_dist_info = os.path.join(
- self.scheme.purelib, 'sample-1.2.0.dist-info')
+ self.scheme.purelib, "sample-1.2.0.dist-info"
+ )
- def assert_permission(self, path, mode):
+ def assert_permission(self, path: str, mode: int) -> None:
target_mode = os.stat(path).st_mode & 0o777
assert (target_mode & mode) == mode, oct(target_mode)
- def assert_installed(self, expected_permission):
+ def assert_installed(self, expected_permission: int) -> None:
# lib
- assert os.path.isdir(
- os.path.join(self.scheme.purelib, 'sample'))
+ assert os.path.isdir(os.path.join(self.scheme.purelib, "sample"))
# dist-info
- metadata = os.path.join(self.dest_dist_info, 'METADATA')
+ metadata = os.path.join(self.dest_dist_info, "METADATA")
self.assert_permission(metadata, expected_permission)
- record = os.path.join(self.dest_dist_info, 'RECORD')
+ record = os.path.join(self.dest_dist_info, "RECORD")
self.assert_permission(record, expected_permission)
# data files
- data_file = os.path.join(self.scheme.data, 'my_data', 'data_file')
+ data_file = os.path.join(self.scheme.data, "my_data", "data_file")
assert os.path.isfile(data_file)
# package data
- pkg_data = os.path.join(
- self.scheme.purelib, 'sample', 'package_data.dat')
+ pkg_data = os.path.join(self.scheme.purelib, "sample", "package_data.dat")
assert os.path.isfile(pkg_data)
- def test_std_install(self, data, tmpdir):
+ def test_std_install(self, data: TestData, tmpdir: Path) -> None:
self.prep(data, tmpdir)
wheel.install_wheel(
self.name,
@@ -332,11 +370,10 @@ class TestInstallUnpackedWheel:
)
self.assert_installed(0o644)
- @pytest.mark.parametrize("user_mask, expected_permission", [
- (0o27, 0o640)
- ])
- def test_std_install_with_custom_umask(self, data, tmpdir,
- user_mask, expected_permission):
+ @pytest.mark.parametrize("user_mask, expected_permission", [(0o27, 0o640)])
+ def test_std_install_with_custom_umask(
+ self, data: TestData, tmpdir: Path, user_mask: int, expected_permission: int
+ ) -> None:
"""Test that the files created after install honor the permissions
set when the user sets a custom umask"""
@@ -353,7 +390,7 @@ class TestInstallUnpackedWheel:
finally:
os.umask(prev_umask)
- def test_std_install_requested(self, data, tmpdir):
+ def test_std_install_requested(self, data: TestData, tmpdir: Path) -> None:
self.prep(data, tmpdir)
wheel.install_wheel(
self.name,
@@ -363,10 +400,10 @@ class TestInstallUnpackedWheel:
requested=True,
)
self.assert_installed(0o644)
- requested_path = os.path.join(self.dest_dist_info, 'REQUESTED')
+ requested_path = os.path.join(self.dest_dist_info, "REQUESTED")
assert os.path.isfile(requested_path)
- def test_std_install_with_direct_url(self, data, tmpdir):
+ def test_std_install_with_direct_url(self, data: TestData, tmpdir: Path) -> None:
"""Test that install_wheel creates direct_url.json metadata when
provided with a direct_url argument. Also test that the RECORDS
file contains an entry for direct_url.json in that case.
@@ -385,26 +422,24 @@ class TestInstallUnpackedWheel:
req_description=str(self.req),
direct_url=direct_url,
)
- direct_url_path = os.path.join(
- self.dest_dist_info, DIRECT_URL_METADATA_NAME
- )
+ direct_url_path = os.path.join(self.dest_dist_info, DIRECT_URL_METADATA_NAME)
self.assert_permission(direct_url_path, 0o644)
- with open(direct_url_path, 'rb') as f:
+ with open(direct_url_path, "rb") as f1:
expected_direct_url_json = direct_url.to_json()
- direct_url_json = f.read().decode("utf-8")
+ direct_url_json = f1.read().decode("utf-8")
assert direct_url_json == expected_direct_url_json
# check that the direc_url file is part of RECORDS
- with open(os.path.join(self.dest_dist_info, "RECORD")) as f:
- assert DIRECT_URL_METADATA_NAME in f.read()
+ with open(os.path.join(self.dest_dist_info, "RECORD")) as f2:
+ assert DIRECT_URL_METADATA_NAME in f2.read()
- def test_install_prefix(self, data, tmpdir):
- prefix = os.path.join(os.path.sep, 'some', 'path')
+ def test_install_prefix(self, data: TestData, tmpdir: Path) -> None:
+ prefix = os.path.join(os.path.sep, "some", "path")
self.prep(data, tmpdir)
scheme = get_scheme(
self.name,
user=False,
home=None,
- root=tmpdir,
+ root=str(tmpdir), # Casting needed for CPython 3.10+. See GH-10358.
isolated=False,
prefix=prefix,
)
@@ -415,11 +450,11 @@ class TestInstallUnpackedWheel:
req_description=str(self.req),
)
- bin_dir = 'Scripts' if WINDOWS else 'bin'
- assert os.path.exists(os.path.join(tmpdir, 'some', 'path', bin_dir))
- assert os.path.exists(os.path.join(tmpdir, 'some', 'path', 'my_data'))
+ bin_dir = "Scripts" if WINDOWS else "bin"
+ assert os.path.exists(os.path.join(tmpdir, "some", "path", bin_dir))
+ assert os.path.exists(os.path.join(tmpdir, "some", "path", "my_data"))
- def test_dist_info_contains_empty_dir(self, data, tmpdir):
+ def test_dist_info_contains_empty_dir(self, data: TestData, tmpdir: Path) -> None:
"""
Test that empty dirs are not installed
"""
@@ -432,14 +467,12 @@ class TestInstallUnpackedWheel:
req_description=str(self.req),
)
self.assert_installed(0o644)
- assert not os.path.isdir(
- os.path.join(self.dest_dist_info, 'empty_dir'))
+ assert not os.path.isdir(os.path.join(self.dest_dist_info, "empty_dir"))
- @pytest.mark.parametrize(
- "path",
- ["/tmp/example", "../example", "./../example"]
- )
- def test_wheel_install_rejects_bad_paths(self, data, tmpdir, path):
+ @pytest.mark.parametrize("path", ["/tmp/example", "../example", "./../example"])
+ def test_wheel_install_rejects_bad_paths(
+ self, data: TestData, tmpdir: Path, path: str
+ ) -> None:
self.prep(data, tmpdir)
wheel_path = make_wheel(
"simple", "0.1.0", extra_files={path: "example contents\n"}
@@ -457,15 +490,11 @@ class TestInstallUnpackedWheel:
assert "example" in exc_text
@pytest.mark.xfail(strict=True)
- @pytest.mark.parametrize(
- "entrypoint", ["hello = hello", "hello = hello:"]
- )
- @pytest.mark.parametrize(
- "entrypoint_type", ["console_scripts", "gui_scripts"]
- )
+ @pytest.mark.parametrize("entrypoint", ["hello = hello", "hello = hello:"])
+ @pytest.mark.parametrize("entrypoint_type", ["console_scripts", "gui_scripts"])
def test_invalid_entrypoints_fail(
- self, data, tmpdir, entrypoint, entrypoint_type
- ):
+ self, data: TestData, tmpdir: Path, entrypoint: str, entrypoint_type: str
+ ) -> None:
self.prep(data, tmpdir)
wheel_path = make_wheel(
"simple", "0.1.0", entry_points={entrypoint_type: [entrypoint]}
@@ -490,41 +519,34 @@ class TestMessageAboutScriptsNotOnPATH:
"which may not be expanded by all applications."
)
- def _template(self, paths, scripts):
- with patch.dict('os.environ', {'PATH': os.pathsep.join(paths)}):
+ def _template(self, paths: List[str], scripts: List[str]) -> Optional[str]:
+ with patch.dict("os.environ", {"PATH": os.pathsep.join(paths)}):
return wheel.message_about_scripts_not_on_PATH(scripts)
- def test_no_script(self):
- retval = self._template(
- paths=['/a/b', '/c/d/bin'],
- scripts=[]
- )
+ def test_no_script(self) -> None:
+ retval = self._template(paths=["/a/b", "/c/d/bin"], scripts=[])
assert retval is None
- def test_single_script__single_dir_not_on_PATH(self):
- retval = self._template(
- paths=['/a/b', '/c/d/bin'],
- scripts=['/c/d/foo']
- )
+ def test_single_script__single_dir_not_on_PATH(self) -> None:
+ retval = self._template(paths=["/a/b", "/c/d/bin"], scripts=["/c/d/foo"])
assert retval is not None
assert "--no-warn-script-location" in retval
assert "foo is installed in '/c/d'" in retval
assert self.tilde_warning_msg not in retval
- def test_two_script__single_dir_not_on_PATH(self):
+ def test_two_script__single_dir_not_on_PATH(self) -> None:
retval = self._template(
- paths=['/a/b', '/c/d/bin'],
- scripts=['/c/d/foo', '/c/d/baz']
+ paths=["/a/b", "/c/d/bin"], scripts=["/c/d/foo", "/c/d/baz"]
)
assert retval is not None
assert "--no-warn-script-location" in retval
assert "baz and foo are installed in '/c/d'" in retval
assert self.tilde_warning_msg not in retval
- def test_multi_script__multi_dir_not_on_PATH(self):
+ def test_multi_script__multi_dir_not_on_PATH(self) -> None:
retval = self._template(
- paths=['/a/b', '/c/d/bin'],
- scripts=['/c/d/foo', '/c/d/bar', '/c/d/baz', '/a/b/c/spam']
+ paths=["/a/b", "/c/d/bin"],
+ scripts=["/c/d/foo", "/c/d/bar", "/c/d/baz", "/a/b/c/spam"],
)
assert retval is not None
assert "--no-warn-script-location" in retval
@@ -532,13 +554,10 @@ class TestMessageAboutScriptsNotOnPATH:
assert "spam is installed in '/a/b/c'" in retval
assert self.tilde_warning_msg not in retval
- def test_multi_script_all__multi_dir_not_on_PATH(self):
+ def test_multi_script_all__multi_dir_not_on_PATH(self) -> None:
retval = self._template(
- paths=['/a/b', '/c/d/bin'],
- scripts=[
- '/c/d/foo', '/c/d/bar', '/c/d/baz',
- '/a/b/c/spam', '/a/b/c/eggs'
- ]
+ paths=["/a/b", "/c/d/bin"],
+ scripts=["/c/d/foo", "/c/d/bar", "/c/d/baz", "/a/b/c/spam", "/a/b/c/eggs"],
)
assert retval is not None
assert "--no-warn-script-location" in retval
@@ -546,77 +565,71 @@ class TestMessageAboutScriptsNotOnPATH:
assert "eggs and spam are installed in '/a/b/c'" in retval
assert self.tilde_warning_msg not in retval
- def test_two_script__single_dir_on_PATH(self):
+ def test_two_script__single_dir_on_PATH(self) -> None:
retval = self._template(
- paths=['/a/b', '/c/d/bin'],
- scripts=['/a/b/foo', '/a/b/baz']
+ paths=["/a/b", "/c/d/bin"], scripts=["/a/b/foo", "/a/b/baz"]
)
assert retval is None
- def test_multi_script__multi_dir_on_PATH(self):
+ def test_multi_script__multi_dir_on_PATH(self) -> None:
retval = self._template(
- paths=['/a/b', '/c/d/bin'],
- scripts=['/a/b/foo', '/a/b/bar', '/a/b/baz', '/c/d/bin/spam']
+ paths=["/a/b", "/c/d/bin"],
+ scripts=["/a/b/foo", "/a/b/bar", "/a/b/baz", "/c/d/bin/spam"],
)
assert retval is None
- def test_multi_script__single_dir_on_PATH(self):
+ def test_multi_script__single_dir_on_PATH(self) -> None:
retval = self._template(
- paths=['/a/b', '/c/d/bin'],
- scripts=['/a/b/foo', '/a/b/bar', '/a/b/baz']
+ paths=["/a/b", "/c/d/bin"], scripts=["/a/b/foo", "/a/b/bar", "/a/b/baz"]
)
assert retval is None
- def test_single_script__single_dir_on_PATH(self):
- retval = self._template(
- paths=['/a/b', '/c/d/bin'],
- scripts=['/a/b/foo']
- )
+ def test_single_script__single_dir_on_PATH(self) -> None:
+ retval = self._template(paths=["/a/b", "/c/d/bin"], scripts=["/a/b/foo"])
assert retval is None
- def test_PATH_check_case_insensitive_on_windows(self):
- retval = self._template(
- paths=['C:\\A\\b'],
- scripts=['c:\\a\\b\\c', 'C:/A/b/d']
- )
+ def test_PATH_check_case_insensitive_on_windows(self) -> None:
+ retval = self._template(paths=["C:\\A\\b"], scripts=["c:\\a\\b\\c", "C:/A/b/d"])
if WINDOWS:
assert retval is None
else:
assert retval is not None
assert self.tilde_warning_msg not in retval
- def test_trailing_ossep_removal(self):
+ def test_trailing_ossep_removal(self) -> None:
retval = self._template(
- paths=[os.path.join('a', 'b', '')],
- scripts=[os.path.join('a', 'b', 'c')]
+ paths=[os.path.join("a", "b", "")], scripts=[os.path.join("a", "b", "c")]
)
assert retval is None
- def test_missing_PATH_env_treated_as_empty_PATH_env(self, monkeypatch):
- scripts = ['a/b/foo']
+ def test_missing_PATH_env_treated_as_empty_PATH_env(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ scripts = ["a/b/foo"]
- monkeypatch.delenv('PATH')
+ monkeypatch.delenv("PATH")
retval_missing = wheel.message_about_scripts_not_on_PATH(scripts)
- monkeypatch.setenv('PATH', '')
+ monkeypatch.setenv("PATH", "")
retval_empty = wheel.message_about_scripts_not_on_PATH(scripts)
assert retval_missing == retval_empty
- def test_no_script_tilde_in_path(self):
- retval = self._template(
- paths=['/a/b', '/c/d/bin', '~/e', '/f/g~g'],
- scripts=[]
- )
+ def test_no_script_tilde_in_path(self) -> None:
+ retval = self._template(paths=["/a/b", "/c/d/bin", "~/e", "/f/g~g"], scripts=[])
assert retval is None
- def test_multi_script_all_tilde__multi_dir_not_on_PATH(self):
+ def test_multi_script_all_tilde__multi_dir_not_on_PATH(self) -> None:
retval = self._template(
- paths=['/a/b', '/c/d/bin', '~e/f'],
+ paths=["/a/b", "/c/d/bin", "~e/f"],
scripts=[
- '/c/d/foo', '/c/d/bar', '/c/d/baz',
- '/a/b/c/spam', '/a/b/c/eggs', '/e/f/tilde'
- ]
+ "/c/d/foo",
+ "/c/d/bar",
+ "/c/d/baz",
+ "/a/b/c/spam",
+ "/a/b/c/eggs",
+ "/e/f/tilde",
+ ],
)
assert retval is not None
assert "--no-warn-script-location" in retval
@@ -625,13 +638,16 @@ class TestMessageAboutScriptsNotOnPATH:
assert "tilde is installed in '/e/f'" in retval
assert self.tilde_warning_msg in retval
- def test_multi_script_all_tilde_not_at_start__multi_dir_not_on_PATH(self):
+ def test_multi_script_all_tilde_not_at_start__multi_dir_not_on_PATH(self) -> None:
retval = self._template(
- paths=['/e/f~f', '/c/d/bin'],
+ paths=["/e/f~f", "/c/d/bin"],
scripts=[
- '/c/d/foo', '/c/d/bar', '/c/d/baz',
- '/e/f~f/c/spam', '/e/f~f/c/eggs'
- ]
+ "/c/d/foo",
+ "/c/d/bar",
+ "/c/d/baz",
+ "/e/f~f/c/spam",
+ "/e/f~f/c/eggs",
+ ],
)
assert retval is not None
assert "--no-warn-script-location" in retval
@@ -641,26 +657,27 @@ class TestMessageAboutScriptsNotOnPATH:
class TestWheelHashCalculators:
-
- def prep(self, tmpdir):
+ def prep(self, tmpdir: Path) -> None:
self.test_file = tmpdir.joinpath("hash.file")
# Want this big enough to trigger the internal read loops.
self.test_file_len = 2 * 1024 * 1024
with open(str(self.test_file), "w") as fp:
fp.truncate(self.test_file_len)
- self.test_file_hash = \
- '5647f05ec18958947d32874eeb788fa396a05d0bab7c1b71f112ceb7e9b31eee'
- self.test_file_hash_encoded = \
- 'sha256=VkfwXsGJWJR9ModO63iPo5agXQurfBtx8RLOt-mzHu4'
+ self.test_file_hash = (
+ "5647f05ec18958947d32874eeb788fa396a05d0bab7c1b71f112ceb7e9b31eee"
+ )
+ self.test_file_hash_encoded = (
+ "sha256=VkfwXsGJWJR9ModO63iPo5agXQurfBtx8RLOt-mzHu4"
+ )
- def test_hash_file(self, tmpdir):
+ def test_hash_file(self, tmpdir: Path) -> None:
self.prep(tmpdir)
- h, length = hash_file(self.test_file)
+ h, length = hash_file(os.fspath(self.test_file))
assert length == self.test_file_len
assert h.hexdigest() == self.test_file_hash
- def test_rehash(self, tmpdir):
+ def test_rehash(self, tmpdir: Path) -> None:
self.prep(tmpdir)
- h, length = wheel.rehash(self.test_file)
+ h, length = wheel.rehash(os.fspath(self.test_file))
assert length == str(self.test_file_len)
assert h == self.test_file_hash_encoded
diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py
index 2f180723c..5444056e7 100644
--- a/tests/unit/test_wheel_builder.py
+++ b/tests/unit/test_wheel_builder.py
@@ -1,11 +1,16 @@
import logging
-from unittest.mock import patch
+import os
+from pathlib import Path
+from typing import Optional, cast
+from unittest import mock
import pytest
from pip._internal import wheel_builder
from pip._internal.models.link import Link
from pip._internal.operations.build.wheel_legacy import format_command_result
+from pip._internal.req.req_install import InstallRequirement
+from pip._internal.vcs.git import Git
from tests.lib import _create_test_package
@@ -14,33 +19,31 @@ from tests.lib import _create_test_package
[
# Trivial.
("pip-18.0", True),
-
# Ambiguous.
("foo-2-2", True),
("im-valid", True),
-
# Invalid.
("invalid", False),
("im_invalid", False),
],
)
-def test_contains_egg_info(s, expected):
+def test_contains_egg_info(s: str, expected: bool) -> None:
result = wheel_builder._contains_egg_info(s)
assert result == expected
class ReqMock:
-
def __init__(
self,
- name="pendulum",
- is_wheel=False,
- editable=False,
- link=None,
- constraint=False,
- source_dir="/tmp/pip-install-123/pendulum",
- use_pep517=True,
- ):
+ name: str = "pendulum",
+ is_wheel: bool = False,
+ editable: bool = False,
+ link: Optional[Link] = None,
+ constraint: bool = False,
+ source_dir: Optional[str] = "/tmp/pip-install-123/pendulum",
+ use_pep517: bool = True,
+ supports_pyproject_editable: bool = False,
+ ) -> None:
self.name = name
self.is_wheel = is_wheel
self.editable = editable
@@ -48,10 +51,14 @@ class ReqMock:
self.constraint = constraint
self.source_dir = source_dir
self.use_pep517 = use_pep517
+ self._supports_pyproject_editable = supports_pyproject_editable
+
+ def supports_pyproject_editable(self) -> bool:
+ return self._supports_pyproject_editable
@pytest.mark.parametrize(
- "req, disallow_binaries, expected",
+ "req, disallow_bdist_wheel, expected",
[
# When binaries are allowed, we build.
(ReqMock(use_pep517=True), False, True),
@@ -64,8 +71,17 @@ class ReqMock:
(ReqMock(constraint=True), False, False),
# We don't build reqs that are already wheels.
(ReqMock(is_wheel=True), False, False),
- # We don't build editables.
- (ReqMock(editable=True), False, False),
+ (ReqMock(editable=True, use_pep517=False), False, False),
+ (
+ ReqMock(editable=True, use_pep517=True, supports_pyproject_editable=True),
+ False,
+ True,
+ ),
+ (
+ ReqMock(editable=True, use_pep517=True, supports_pyproject_editable=False),
+ False,
+ False,
+ ),
(ReqMock(source_dir=None), False, False),
# By default (i.e. when binaries are allowed), VCS requirements
# should be built in install mode.
@@ -93,10 +109,12 @@ class ReqMock:
),
],
)
-def test_should_build_for_install_command(req, disallow_binaries, expected):
+def test_should_build_for_install_command(
+ req: ReqMock, disallow_bdist_wheel: bool, expected: bool
+) -> None:
should_build = wheel_builder.should_build_for_install_command(
- req,
- check_binary_allowed=lambda req: not disallow_binaries,
+ cast(InstallRequirement, req),
+ check_bdist_wheel_allowed=lambda req: not disallow_bdist_wheel,
)
assert should_build is expected
@@ -107,34 +125,37 @@ def test_should_build_for_install_command(req, disallow_binaries, expected):
(ReqMock(), True),
(ReqMock(constraint=True), False),
(ReqMock(is_wheel=True), False),
- (ReqMock(editable=True), True),
+ (ReqMock(editable=True, use_pep517=False), True),
+ (ReqMock(editable=True, use_pep517=True), True),
(ReqMock(source_dir=None), True),
(ReqMock(link=Link("git+https://g.c/org/repo")), True),
],
)
-def test_should_build_for_wheel_command(req, expected):
- should_build = wheel_builder.should_build_for_wheel_command(req)
+def test_should_build_for_wheel_command(req: ReqMock, expected: bool) -> None:
+ should_build = wheel_builder.should_build_for_wheel_command(
+ cast(InstallRequirement, req)
+ )
assert should_build is expected
-@patch("pip._internal.wheel_builder.is_wheel_installed")
-def test_should_build_legacy_wheel_not_installed(is_wheel_installed):
+@mock.patch("pip._internal.wheel_builder.is_wheel_installed")
+def test_should_build_legacy_wheel_not_installed(is_wheel_installed: mock.Mock) -> None:
is_wheel_installed.return_value = False
legacy_req = ReqMock(use_pep517=False)
should_build = wheel_builder.should_build_for_install_command(
- legacy_req,
- check_binary_allowed=lambda req: True,
+ cast(InstallRequirement, legacy_req),
+ check_bdist_wheel_allowed=lambda req: True,
)
assert not should_build
-@patch("pip._internal.wheel_builder.is_wheel_installed")
-def test_should_build_legacy_wheel_installed(is_wheel_installed):
+@mock.patch("pip._internal.wheel_builder.is_wheel_installed")
+def test_should_build_legacy_wheel_installed(is_wheel_installed: mock.Mock) -> None:
is_wheel_installed.return_value = True
legacy_req = ReqMock(use_pep517=False)
should_build = wheel_builder.should_build_for_install_command(
- legacy_req,
- check_binary_allowed=lambda req: True,
+ cast(InstallRequirement, legacy_req),
+ check_bdist_wheel_allowed=lambda req: True,
)
assert should_build
@@ -142,76 +163,81 @@ def test_should_build_legacy_wheel_installed(is_wheel_installed):
@pytest.mark.parametrize(
"req, expected",
[
- (ReqMock(editable=True), False),
+ (ReqMock(editable=True, use_pep517=False), False),
+ (ReqMock(editable=True, use_pep517=True), False),
(ReqMock(source_dir=None), False),
(ReqMock(link=Link("git+https://g.c/org/repo")), False),
(ReqMock(link=Link("https://g.c/dist.tgz")), False),
(ReqMock(link=Link("https://g.c/dist-2.0.4.tgz")), True),
],
)
-def test_should_cache(req, expected):
- assert wheel_builder._should_cache(req) is expected
+def test_should_cache(req: ReqMock, expected: bool) -> None:
+ assert wheel_builder._should_cache(cast(InstallRequirement, req)) is expected
-def test_should_cache_git_sha(script, tmpdir):
- repo_path = _create_test_package(script, name="mypkg")
- commit = script.run(
- "git", "rev-parse", "HEAD", cwd=repo_path
- ).stdout.strip()
+def test_should_cache_git_sha(tmpdir: Path) -> None:
+ repo_path = os.fspath(_create_test_package(tmpdir, name="mypkg"))
+ commit = Git.get_revision(repo_path)
# a link referencing a sha should be cached
url = "git+https://g.c/o/r@" + commit + "#egg=mypkg"
req = ReqMock(link=Link(url), source_dir=repo_path)
- assert wheel_builder._should_cache(req)
+ assert wheel_builder._should_cache(cast(InstallRequirement, req))
# a link not referencing a sha should not be cached
url = "git+https://g.c/o/r@master#egg=mypkg"
req = ReqMock(link=Link(url), source_dir=repo_path)
- assert not wheel_builder._should_cache(req)
+ assert not wheel_builder._should_cache(cast(InstallRequirement, req))
-def test_format_command_result__INFO(caplog):
+def test_format_command_result__INFO(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
actual = format_command_result(
# Include an argument with a space to test argument quoting.
- command_args=['arg1', 'second arg'],
- command_output='output line 1\noutput line 2\n',
+ command_args=["arg1", "second arg"],
+ command_output="output line 1\noutput line 2\n",
)
assert actual.splitlines() == [
"Command arguments: arg1 'second arg'",
- 'Command output: [use --verbose to show]',
+ "Command output: [use --verbose to show]",
]
-@pytest.mark.parametrize('command_output', [
- # Test trailing newline.
- 'output line 1\noutput line 2\n',
- # Test no trailing newline.
- 'output line 1\noutput line 2',
-])
-def test_format_command_result__DEBUG(caplog, command_output):
+@pytest.mark.parametrize(
+ "command_output",
+ [
+ # Test trailing newline.
+ "output line 1\noutput line 2\n",
+ # Test no trailing newline.
+ "output line 1\noutput line 2",
+ ],
+)
+def test_format_command_result__DEBUG(
+ caplog: pytest.LogCaptureFixture, command_output: str
+) -> None:
caplog.set_level(logging.DEBUG)
actual = format_command_result(
- command_args=['arg1', 'arg2'],
+ command_args=["arg1", "arg2"],
command_output=command_output,
)
assert actual.splitlines() == [
"Command arguments: arg1 arg2",
- 'Command output:',
- 'output line 1',
- 'output line 2',
- '----------------------------------------',
+ "Command output:",
+ "output line 1",
+ "output line 2",
]
-@pytest.mark.parametrize('log_level', ['DEBUG', 'INFO'])
-def test_format_command_result__empty_output(caplog, log_level):
+@pytest.mark.parametrize("log_level", ["DEBUG", "INFO"])
+def test_format_command_result__empty_output(
+ caplog: pytest.LogCaptureFixture, log_level: str
+) -> None:
caplog.set_level(log_level)
actual = format_command_result(
- command_args=['arg1', 'arg2'],
- command_output='',
+ command_args=["arg1", "arg2"],
+ command_output="",
)
assert actual.splitlines() == [
"Command arguments: arg1 arg2",
- 'Command output: None',
+ "Command output: None",
]
diff --git a/tools/news/template.rst b/tools/news/template.rst
index 91003f202..8d0ceb89f 100644
--- a/tools/news/template.rst
+++ b/tools/news/template.rst
@@ -1,41 +1,45 @@
-{% set underline = "=" %}
-
-{{ underline * ((top_line)|length) }}
-{% for section in sections %}
-{% set underline = "-" %}
-{% if section %}
-{{ section }}
-{{ underline * section|length }}{% set underline = "~" %}
-
-{% endif %}
-{% if sections[section] %}
-{% for category, val in definitions.items() if category in sections[section] and category != 'trivial' %}
-
-{{ definitions[category]['name'] }}
-{{ underline * definitions[category]['name']|length }}
-
-{% if definitions[category]['showcontent'] %}
-{% for text, values in sections[section][category]|dictsort(by='value') %}
-- {{ text }}{% if category != 'vendor' and category != 'process' %} ({{ values|sort|join(', ') }}){% endif %}
-
+{# This is a heavily customised version of towncrier's default template. #}
+
+{#-
+ Only render if there's any changes to show.
+
+ This serves as a compatibility "hack" since we render unreleased news entries
+ in our changelog with ``sphinxcontrib.towncrier``; which triggers a render even
+ when there's no entries to be rendered.
+#}
+{% if sections[''] %}
+
+{#- Heading for individual version #}
+{{ versiondata.version }} ({{ versiondata.date }})
+{{ top_underline * ((versiondata.version + versiondata.date)|length + 3) }}
+{#
+
+ The following loop will run exactly once, with ``section_name == ""``.
+
+ This is due to the undocumented "sections" feature in towncrier.
+ See https://github.com/twisted/towncrier/issues/61.
+
+ We don't use this feature, and this template doesn't render the section
+ heading for that reason.
+#}
+{% for section_name, entries_by_type in sections.items() -%}
+{# Only show types with entries and ``showcontent = true``, using the order from pyproject.toml #}
+{% for type_ in definitions if (sections[section_name][type_] and definitions[type_]['showcontent']) %}
+
+{# Heading for individual types #}
+{{ definitions[type_]['name'] }}
+{{ underlines[0] * definitions[type_]['name']|length }}
+{# This is the loop that generates individual entries #}
+{% for message, issue_reference in sections[section_name][type_]|dictsort(by='value') %}
+
+- {{ message }}
+ {%- if type_ not in ["vendor", "process"] %} ({{ issue_reference|sort|join(', ') }}){% endif %}
{% endfor %}
-{% else %}
-- {{ sections[section][category]['']|sort|join(', ') }}
-
-
-{% endif %}
-{% if sections[section][category]|length == 0 %}
-No significant changes.
-
-
-{% else %}
-{% endif %}
-{% endfor %}
{% else %}
-
+{# We only have entries where the type has ``showcontent = true``. #}
No significant changes.
-
-{% endif %}
-{% endfor %}
+{% endfor -%}
+{% endfor -%}
+{% endif -%}
diff --git a/tools/tox_pip.py b/tools/protected_pip.py
index 671518029..48230719e 100644
--- a/tools/tox_pip.py
+++ b/tools/protected_pip.py
@@ -1,15 +1,16 @@
import os
+import pathlib
import shutil
import subprocess
import sys
from glob import glob
-from typing import List
+from typing import Iterable, Union
VIRTUAL_ENV = os.environ["VIRTUAL_ENV"]
TOX_PIP_DIR = os.path.join(VIRTUAL_ENV, "pip")
-def pip(args: List[str]) -> None:
+def pip(args: Iterable[Union[str, pathlib.Path]]) -> None:
# First things first, get a recent (stable) version of pip.
if not os.path.exists(TOX_PIP_DIR):
subprocess.check_call(
@@ -30,7 +31,7 @@ def pip(args: List[str]) -> None:
pypath = pypath_env.split(os.pathsep) if pypath_env is not None else []
pypath.insert(0, TOX_PIP_DIR)
os.environ["PYTHONPATH"] = os.pathsep.join(pypath)
- subprocess.check_call([sys.executable, "-m", "pip"] + args)
+ subprocess.check_call([sys.executable, "-m", "pip", *(os.fspath(a) for a in args)])
if __name__ == "__main__":
diff --git a/tools/release/__init__.py b/tools/release/__init__.py
index cc60f442f..ebd1b9014 100644
--- a/tools/release/__init__.py
+++ b/tools/release/__init__.py
@@ -26,8 +26,7 @@ def get_version_from_arguments(session: Session) -> Optional[str]:
# We delegate to a script here, so that it can depend on packaging.
session.install("packaging")
cmd = [
- # https://github.com/theacodes/nox/pull/378
- os.path.join(session.bin, "python"), # type: ignore
+ os.path.join(session.bin, "python"),
"tools/release/check_version.py",
version,
]
@@ -86,7 +85,7 @@ def commit_file(session: Session, filename: str, *, message: str) -> None:
def generate_news(session: Session, version: str) -> None:
session.install("towncrier")
- session.run("towncrier", "--yes", "--version", version, silent=True)
+ session.run("towncrier", "build", "--yes", "--version", version, silent=True)
def update_version_file(version: str, filepath: str) -> None:
@@ -156,12 +155,11 @@ def workdir(
"""Temporarily chdir when entering CM and chdir back on exit."""
orig_dir = pathlib.Path.cwd()
- # https://github.com/theacodes/nox/pull/376
- nox_session.chdir(dir_path) # type: ignore
+ nox_session.chdir(dir_path)
try:
yield dir_path
finally:
- nox_session.chdir(orig_dir) # type: ignore
+ nox_session.chdir(orig_dir)
@contextlib.contextmanager
@@ -175,23 +173,18 @@ def isolated_temporary_checkout(
git_checkout_dir = tmp_dir / f"pip-build-{target_ref}"
nox_session.run(
# fmt: off
- "git", "worktree", "add", "--force", "--checkout",
- str(git_checkout_dir), str(target_ref),
+ "git", "clone",
+ "--depth", "1",
+ "--config", "core.autocrlf=false",
+ "--branch", str(target_ref),
+ "--",
+ ".", str(git_checkout_dir),
# fmt: on
external=True,
silent=True,
)
- try:
- yield git_checkout_dir
- finally:
- nox_session.run(
- # fmt: off
- "git", "worktree", "remove", "--force", str(git_checkout_dir),
- # fmt: on
- external=True,
- silent=True,
- )
+ yield git_checkout_dir
def get_git_untracked_files() -> Iterator[str]:
diff --git a/tools/vendoring/patches/appdirs.patch b/tools/vendoring/patches/appdirs.patch
deleted file mode 100644
index 69afd3e86..000000000
--- a/tools/vendoring/patches/appdirs.patch
+++ /dev/null
@@ -1,115 +0,0 @@
-diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py
-index ae67001a..3a52b758 100644
---- a/src/pip/_vendor/appdirs.py
-+++ b/src/pip/_vendor/appdirs.py
-@@ -37,6 +37,10 @@ if sys.platform.startswith('java'):
- # are actually checked for and the rest of the module expects
- # *sys.platform* style strings.
- system = 'linux2'
-+elif sys.platform == 'cli' and os.name == 'nt':
-+ # Detect Windows in IronPython to match pip._internal.utils.compat.WINDOWS
-+ # Discussion: <https://github.com/pypa/pip/pull/7501>
-+ system = 'win32'
- else:
- system = sys.platform
-
-@@ -64,7 +68,7 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
- for a discussion of issues.
-
- Typical user data directories are:
-- Mac OS X: ~/Library/Application Support/<AppName>
-+ Mac OS X: ~/Library/Application Support/<AppName> # or ~/.config/<AppName>, if the other does not exist
- Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined
- Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName>
- Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>
-@@ -150,7 +154,7 @@ def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
- if appname:
- if version:
- appname = os.path.join(appname, version)
-- pathlist = [os.sep.join([x, appname]) for x in pathlist]
-+ pathlist = [os.path.join(x, appname) for x in pathlist]
-
- if multipath:
- path = os.pathsep.join(pathlist)
-@@ -203,6 +203,8 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
- return path
-
-
-+# for the discussion regarding site_config_dir locations
-+# see <https://github.com/pypa/pip/issues/1733>
- def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
- r"""Return full path to the user-shared data dir for this application.
-
-@@ -238,14 +244,15 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False)
- if appname and version:
- path = os.path.join(path, version)
- else:
-- # XDG default for $XDG_CONFIG_DIRS
-+ # XDG default for $XDG_CONFIG_DIRS (missing or empty)
-+ # see <https://github.com/pypa/pip/pull/7501#discussion_r360624829>
- # only first, if multipath is False
-- path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
-- pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
-+ path = os.getenv('XDG_CONFIG_DIRS') or '/etc/xdg'
-+ pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) if x]
- if appname:
- if version:
- appname = os.path.join(appname, version)
-- pathlist = [os.sep.join([x, appname]) for x in pathlist]
-+ pathlist = [os.path.join(x, appname) for x in pathlist]
-
- if multipath:
- path = os.pathsep.join(pathlist)
-@@ -291,6 +300,10 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
- if appauthor is None:
- appauthor = appname
- path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
-+ # When using Python 2, return paths as bytes on Windows like we do on
-+ # other operating systems. See helper function docs for more details.
-+ if not PY3 and isinstance(path, unicode):
-+ path = _win_path_to_bytes(path)
- if appname:
- if appauthor is not False:
- path = os.path.join(path, appauthor, appname)
-@@ -557,18 +570,32 @@ def _get_win_folder_with_jna(csidl_name):
-
- if system == "win32":
- try:
-- import win32com.shell
-- _get_win_folder = _get_win_folder_with_pywin32
-+ from ctypes import windll
-+ _get_win_folder = _get_win_folder_with_ctypes
- except ImportError:
- try:
-- from ctypes import windll
-- _get_win_folder = _get_win_folder_with_ctypes
-+ import com.sun.jna
-+ _get_win_folder = _get_win_folder_with_jna
- except ImportError:
-- try:
-- import com.sun.jna
-- _get_win_folder = _get_win_folder_with_jna
-- except ImportError:
-- _get_win_folder = _get_win_folder_from_registry
-+ _get_win_folder = _get_win_folder_from_registry
-+
-+
-+def _win_path_to_bytes(path):
-+ """Encode Windows paths to bytes. Only used on Python 2.
-+
-+ Motivation is to be consistent with other operating systems where paths
-+ are also returned as bytes. This avoids problems mixing bytes and Unicode
-+ elsewhere in the codebase. For more details and discussion see
-+ <https://github.com/pypa/pip/issues/3463>.
-+
-+ If encoding using ASCII and MBCS fails, return the original Unicode path.
-+ """
-+ for encoding in ('ASCII', 'MBCS'):
-+ try:
-+ return path.encode(encoding)
-+ except (UnicodeEncodeError, LookupError):
-+ pass
-+ return path
-
-
- #---- self test code
diff --git a/tools/vendoring/patches/certifi.patch b/tools/vendoring/patches/certifi.patch
index a36a0020f..31554505c 100644
--- a/tools/vendoring/patches/certifi.patch
+++ b/tools/vendoring/patches/certifi.patch
@@ -1,10 +1,10 @@
diff --git a/src/pip/_vendor/certifi/core.py b/src/pip/_vendor/certifi/core.py
-index 5d2b8cd32..b8140cf1a 100644
+index 497d938..f34045b 100644
--- a/src/pip/_vendor/certifi/core.py
+++ b/src/pip/_vendor/certifi/core.py
-@@ -8,7 +8,21 @@ This module returns the installation location of cacert.pem or its contents.
- """
- import os
+@@ -8,7 +8,21 @@ import os
+ import types
+ from typing import Union
+
+class _PipPatchedCertificate(Exception):
@@ -38,4 +38,4 @@ index 5d2b8cd32..b8140cf1a 100644
+ pass
except ImportError:
- # This fallback will work for Python versions prior to 3.7 that lack the
+ Package = Union[types.ModuleType, str]
diff --git a/tools/vendoring/patches/pkg_resources.patch b/tools/vendoring/patches/pkg_resources.patch
new file mode 100644
index 000000000..6556a8608
--- /dev/null
+++ b/tools/vendoring/patches/pkg_resources.patch
@@ -0,0 +1,22 @@
+diff --git a/src/pip/_vendor/pkg_resources/__init__.py b/src/pip/_vendor/pkg_resources/__init__.py
+index a457ff27e..4cd562cf9 100644
+--- a/src/pip/_vendor/pkg_resources/__init__.py
++++ b/src/pip/_vendor/pkg_resources/__init__.py
+@@ -77,7 +77,7 @@ except ImportError:
+ importlib_machinery = None
+
+ from . import py31compat
+-from pkg_resources.extern import appdirs
++from pkg_resources.extern import platformdirs
+ from pkg_resources.extern import packaging
+ __import__('pkg_resources.extern.packaging.version')
+ __import__('pkg_resources.extern.packaging.specifiers')
+@@ -1310,7 +1310,7 @@ def get_default_cache():
+ """
+ return (
+ os.environ.get('PYTHON_EGG_CACHE')
+- or appdirs.user_cache_dir(appname='Python-Eggs')
++ or platformdirs.user_cache_dir(appname='Python-Eggs')
+ )
+
+
diff --git a/tools/vendoring/patches/pygments.patch b/tools/vendoring/patches/pygments.patch
new file mode 100644
index 000000000..3cabf9d6d
--- /dev/null
+++ b/tools/vendoring/patches/pygments.patch
@@ -0,0 +1,37 @@
+This patch mainly handles tweaking imports into a form that can be transformed
+to import from the vendored namespace.
+
+diff --git a/src/pip/_vendor/pygments/cmdline.py b/src/pip/_vendor/pygments/cmdline.py
+index d9a0fdc8b..db6de0cd3 100644
+--- a/src/pip/_vendor/pygments/cmdline.py
++++ b/src/pip/_vendor/pygments/cmdline.py
+@@ -410,11 +410,11 @@ def is_only_option(opt):
+ outfile = UnclosingTextIOWrapper(outfile, encoding=fmter.encoding)
+ fmter.encoding = None
+ try:
+- import colorama.initialise
++ import colorama.initialise as colorama_initialise
+ except ImportError:
+ pass
+ else:
+- outfile = colorama.initialise.wrap_stream(
++ outfile = colorama_initialise.wrap_stream(
+ outfile, convert=None, strip=None, autoreset=False, wrap=True)
+
+ # When using the LaTeX formatter and the option `escapeinside` is
+diff --git a/src/pip/_vendor/pygments/__main__.py b/src/pip/_vendor/pygments/__main__.py
+index c6e2517df..76255b525 100644
+--- a/src/pip/_vendor/pygments/__main__.py
++++ b/src/pip/_vendor/pygments/__main__.py
+@@ -9,9 +9,9 @@
+ """
+
+ import sys
+-import pygments.cmdline
++from pygments.cmdline import main
+
+ try:
+- sys.exit(pygments.cmdline.main(sys.argv))
++ sys.exit(main(sys.argv))
+ except KeyboardInterrupt:
+ sys.exit(1)
diff --git a/tools/vendoring/patches/requests.patch b/tools/vendoring/patches/requests.patch
index d1bb4309b..f9c722dfe 100644
--- a/tools/vendoring/patches/requests.patch
+++ b/tools/vendoring/patches/requests.patch
@@ -1,22 +1,23 @@
diff --git a/src/pip/_vendor/requests/packages.py b/src/pip/_vendor/requests/packages.py
-index 0f8ae0d38..9582fa730 100644
+index 77c45c9e..9582fa73 100644
--- a/src/pip/_vendor/requests/packages.py
+++ b/src/pip/_vendor/requests/packages.py
-@@ -1,26 +1,16 @@
+@@ -1,28 +1,16 @@
import sys
-
+
-try:
-- from pip._vendor import chardet
+- import chardet
-except ImportError:
-- import charset_normalizer as chardet
- import warnings
-
-- warnings.filterwarnings('ignore', 'Trying to detect', module='charset_normalizer')
+- import charset_normalizer as chardet
+-
+- warnings.filterwarnings("ignore", "Trying to detect", module="charset_normalizer")
-
# This code exists for backwards compatibility reasons.
# I don't like it either. Just look the other way. :)
-
--for package in ('urllib3', 'idna'):
+
+-for package in ("urllib3", "idna"):
- locals()[package] = __import__(package)
+for package in ('urllib3', 'idna', 'chardet'):
+ vendored_package = "pip._vendor." + package
@@ -24,35 +25,36 @@ index 0f8ae0d38..9582fa730 100644
# This traversal is apparently necessary such that the identities are
# preserved (requests.packages.urllib3.* is urllib3.*)
for mod in list(sys.modules):
-- if mod == package or mod.startswith(package + '.'):
-- sys.modules['requests.packages.' + mod] = sys.modules[mod]
+- if mod == package or mod.startswith(f"{package}."):
+- sys.modules[f"requests.packages.{mod}"] = sys.modules[mod]
+ if mod == vendored_package or mod.startswith(vendored_package + '.'):
+ unprefixed_mod = mod[len("pip._vendor."):]
+ sys.modules['pip._vendor.requests.packages.' + unprefixed_mod] = sys.modules[mod]
-
+
-target = chardet.__name__
-for mod in list(sys.modules):
-- if mod == target or mod.startswith(target + '.'):
-- sys.modules['requests.packages.' + target.replace(target, 'chardet')] = sys.modules[mod]
+- if mod == target or mod.startswith(f"{target}."):
+- target = target.replace(target, "chardet")
+- sys.modules[f"requests.packages.{target}"] = sys.modules[mod]
# Kinda cool, though, right?
diff --git a/src/pip/_vendor/requests/__init__.py b/src/pip/_vendor/requests/__init__.py
-index 973497f5e..4f80e28fc 100644
+index 7ac8e297..1e21e7e4 100644
--- a/src/pip/_vendor/requests/__init__.py
+++ b/src/pip/_vendor/requests/__init__.py
-@@ -44,10 +44,7 @@ from pip._vendor import urllib3
- import warnings
+@@ -44,10 +44,7 @@ import urllib3
+
from .exceptions import RequestsDependencyWarning
-
+
-try:
- from charset_normalizer import __version__ as charset_normalizer_version
-except ImportError:
- charset_normalizer_version = None
+charset_normalizer_version = None
-
+
try:
- from pip._vendor.chardet import __version__ as chardet_version
-@@ -107,6 +104,11 @@ except (AssertionError, ValueError):
+ from chardet import __version__ as chardet_version
+@@ -118,6 +115,11 @@ except (AssertionError, ValueError):
# if the standard library doesn't support SNI or the
# 'ssl' library isn't available.
try:
@@ -66,54 +68,58 @@ index 973497f5e..4f80e28fc 100644
except ImportError:
diff --git a/src/pip/_vendor/requests/compat.py b/src/pip/_vendor/requests/compat.py
-index 409b7b028..9e2937167 100644
+index 6776163c..8ff49e46 100644
--- a/src/pip/_vendor/requests/compat.py
+++ b/src/pip/_vendor/requests/compat.py
-@@ -8,10 +8,7 @@ This module handles import compatibility issues between Python 2 and
- Python 3.
+@@ -7,10 +7,7 @@ between Python 2 and Python 3. It remains for backwards
+ compatibility until the next major version.
"""
-
+
-try:
-- from pip._vendor import chardet
+- import chardet
-except ImportError:
- import charset_normalizer as chardet
-+from pip._vendor import chardet
-
++import chardet
+
import sys
-
-@@ -28,10 +25,14 @@ is_py2 = (_ver[0] == 2)
+
+@@ -27,19 +24,10 @@ is_py2 = _ver[0] == 2
#: Python 3.x?
- is_py3 = (_ver[0] == 3)
-
+ is_py3 = _ver[0] == 3
+
+-# json/simplejson module import resolution
+-has_simplejson = False
-try:
- import simplejson as json
+-
+- has_simplejson = True
-except ImportError:
- import json
+-
+-if has_simplejson:
+- from simplejson import JSONDecodeError
+-else:
+- from json import JSONDecodeError
+# Note: We've patched out simplejson support in pip because it prevents
+# upgrading simplejson on Windows.
-+# try:
-+# import simplejson as json
-+# except (ImportError, SyntaxError):
-+# # simplejson does not support Python 3.2, it throws a SyntaxError
-+# # because of u'...' Unicode literals.
+import json
-
- # ---------
- # Specifics
-
++from json import JSONDecodeError
+
+ # Keep OrderedDict for backwards compatibility.
+ from collections import OrderedDict
diff --git a/src/pip/_vendor/requests/help.py b/src/pip/_vendor/requests/help.py
-index 3a843404c..745f0d7b3 100644
+index 8fbcd656..c5e9c19e 100644
--- a/src/pip/_vendor/requests/help.py
+++ b/src/pip/_vendor/requests/help.py
-@@ -11,10 +11,7 @@ from pip._vendor import urllib3
-
+@@ -10,10 +10,7 @@ import urllib3
+
from . import __version__ as requests_version
-
+
-try:
- import charset_normalizer
-except ImportError:
- charset_normalizer = None
+charset_normalizer = None
-
+
try:
- from pip._vendor import chardet
+ import chardet
diff --git a/tools/vendoring/patches/urllib3-disable-brotli.patch b/tools/vendoring/patches/urllib3-disable-brotli.patch
new file mode 100644
index 000000000..1058ac479
--- /dev/null
+++ b/tools/vendoring/patches/urllib3-disable-brotli.patch
@@ -0,0 +1,39 @@
+diff --git a/src/pip/_vendor/urllib3/response.py b/src/pip/_vendor/urllib3/response.py
+index fdb50ddb2..db259d6ce 100644
+--- a/src/pip/_vendor/urllib3/response.py
++++ b/src/pip/_vendor/urllib3/response.py
+@@ -7,13 +7,7 @@
+ from socket import error as SocketError
+ from socket import timeout as SocketTimeout
+
+-try:
+- try:
+- import brotlicffi as brotli
+- except ImportError:
+- import brotli
+-except ImportError:
+- brotli = None
++brotli = None
+
+ from ._collections import HTTPHeaderDict
+ from .connection import BaseSSLError, HTTPException
+diff --git a/src/pip/_vendor/urllib3/util/request.py b/src/pip/_vendor/urllib3/util/request.py
+index b574b081e..330766ef4 100644
+--- a/src/pip/_vendor/urllib3/util/request.py
++++ b/src/pip/_vendor/urllib3/util/request.py
+@@ -13,15 +13,6 @@
+ SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"])
+
+ ACCEPT_ENCODING = "gzip,deflate"
+-try:
+- try:
+- import brotlicffi as _unused_module_brotli # noqa: F401
+- except ImportError:
+- import brotli as _unused_module_brotli # noqa: F401
+-except ImportError:
+- pass
+-else:
+- ACCEPT_ENCODING += ",br"
+
+ _FAILEDTELL = object()
+
diff --git a/tools/vendoring/patches/urllib3.patch b/tools/vendoring/patches/urllib3.patch
new file mode 100644
index 000000000..3ca7226fa
--- /dev/null
+++ b/tools/vendoring/patches/urllib3.patch
@@ -0,0 +1,29 @@
+diff --git a/src/pip/_vendor/urllib3/contrib/securetransport.py b/src/pip/_vendor/urllib3/contrib/securetransport.py
+index b97555454..189132baa 100644
+--- a/src/pip/_vendor/urllib3/contrib/securetransport.py
++++ b/src/pip/_vendor/urllib3/contrib/securetransport.py
+@@ -19,8 +19,8 @@
+
+ To use this module, simply import and inject it::
+
+- import urllib3.contrib.securetransport
+- urllib3.contrib.securetransport.inject_into_urllib3()
++ import urllib3.contrib.securetransport as securetransport
++ securetransport.inject_into_urllib3()
+
+ Happy TLSing!
+
+diff --git a/src/pip/_vendor/urllib3/contrib/pyopenssl.py b/src/pip/_vendor/urllib3/contrib/pyopenssl.py
+index c43146279..4cded53f6 100644
+--- a/src/pip/_vendor/urllib3/contrib/pyopenssl.py
++++ b/src/pip/_vendor/urllib3/contrib/pyopenssl.py
+@@ -28,7 +28,7 @@
+ .. code-block:: python
+
+ try:
+- import urllib3.contrib.pyopenssl
+- urllib3.contrib.pyopenssl.inject_into_urllib3()
++ import urllib3.contrib.pyopenssl as pyopenssl
++ pyopenssl.inject_into_urllib3()
+ except ImportError:
+ pass
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 5000f115e..000000000
--- a/tox.ini
+++ /dev/null
@@ -1,81 +0,0 @@
-[tox]
-minversion = 3.4.0
-envlist =
- docs, packaging, lint, vendoring,
- py36, py37, py38, py39, pypy3
-
-[helpers]
-# Wrapper for calls to pip that make sure the version being used is the
-# original virtualenv (stable) version, and not the code being tested.
-pip = python {toxinidir}/tools/tox_pip.py
-mkdirp = python -c 'import os, sys; os.path.exists(sys.argv[1]) or os.mkdir(sys.argv[1])'
-
-[testenv]
-# Remove USERNAME once we drop PY2.
-passenv =
- CI
- GIT_SSL_CAINFO
- USERNAME
- HTTP_PROXY
- HTTPS_PROXY
- NO_PROXY
-setenv =
- # This is required in order to get UTF-8 output inside of the subprocesses
- # that our tests use.
- LC_CTYPE = en_US.UTF-8
-deps = -r{toxinidir}/tests/requirements.txt
-commands_pre =
- python -c 'import shutil, sys; shutil.rmtree(sys.argv[1], ignore_errors=True)' {toxinidir}/tests/data/common_wheels
- {[helpers]pip} wheel -w {toxinidir}/tests/data/common_wheels -r {toxinidir}/tests/requirements-common_wheels.txt
-commands = pytest []
-install_command = {[helpers]pip} install {opts} {packages}
-list_dependencies_command = {[helpers]pip} freeze --all
-
-[testenv:coverage]
-basepython = python3
-commands =
- {[helpers]mkdirp} {toxinidir}/.coverage-output
- pytest --cov=pip --cov-config={toxinidir}/setup.cfg []
-
-setenv =
- # Used in coverage configuration in setup.cfg.
- COVERAGE_OUTPUT_DIR = {toxinidir}/.coverage-output
- # Ensure coverage is enabled in child processes in virtual environments
- # since they won't already have been enabled by pytest-cov.
- COVERAGE_PROCESS_START = {toxinidir}/setup.cfg
- # Used in coverage configuration in setup.cfg.
- PIP_CI_COVERAGE_EXCLUDES = if PY2
-
-[testenv:docs]
-# Don't skip install here since pip_sphinxext uses pip's internals.
-deps = -r{toxinidir}/docs/requirements.txt
-basepython = python3
-commands =
- sphinx-build -W -j auto -d {envtmpdir}/doctrees/html -b html docs/html docs/build/html
- # Having the conf.py in the docs/html is weird but needed because we
- # can not use a different configuration directory vs source directory on RTD
- # currently -- https://github.com/rtfd/readthedocs.org/issues/1543.
- # That is why we have a "-c docs/html" in the next line.
- sphinx-build -W -j auto -d {envtmpdir}/doctrees/man -b man docs/man docs/build/man -c docs/html
-
-[testenv:lint]
-skip_install = True
-commands_pre =
-deps = pre-commit
-commands =
- pre-commit run [] --all-files --show-diff-on-failure --hook-stage=manual
-
-[testenv:vendoring]
-basepython = python3
-skip_install = True
-commands_pre =
-deps =
- vendoring~=0.3.3
- # Required, otherwise we interpret --no-binary :all: as
- # "do not build wheels", which fails for PEP 517 requirements
- pip>=19.3.1
-whitelist_externals = git
-commands =
- # Check that the vendoring is up-to-date
- vendoring sync . -v
- git diff --exit-code