././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743903941.5992763 yarl-1.19.0/0000755000175100001660000000000014774356306012173 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/.coveragerc0000644000175100001660000000125114774356277014322 0ustar00runnerdocker[html] show_contexts = true skip_covered = false [paths] _site-packages-to-src-mapping = . */lib/pypy*/site-packages */lib/python*/site-packages *\Lib\site-packages [report] fail_under = 98.95 skip_covered = true skip_empty = true show_missing = true exclude_also = ^\s*@pytest\.mark\.xfail if TYPE_CHECKING assert False : \.\.\.(\s*#.*)?$ ^ +\.\.\.$ [run] branch = true cover_pylib = false # https://coverage.rtfd.io/en/latest/contexts.html#dynamic-contexts # dynamic_context = test_function # conflicts with `pytest-cov` if set here parallel = true plugins = covdefaults Cython.Coverage relative_files = true source = . source_pkgs = yarl ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743903941.5842762 yarl-1.19.0/CHANGES/0000755000175100001660000000000014774356306013243 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/CHANGES/.TEMPLATE.rst0000644000175100001660000000467514774356277015311 0ustar00runnerdocker{# TOWNCRIER TEMPLATE #} *({{ versiondata.date }})* {% for section, _ in sections.items() %} {% set underline = underlines[0] %}{% if section %}{{section}} {{ underline * section|length }}{% set underline = underlines[1] %} {% endif %} {% if sections[section] %} {% for category, val in definitions.items() if category in sections[section]%} {{ definitions[category]['name'] }} {{ underline * definitions[category]['name']|length }} {% if definitions[category]['showcontent'] %} {% for text, change_note_refs in sections[section][category].items() %} - {{ text }} {{- '\n' * 2 -}} {#- NOTE: Replacing 'e' with 'f' is a hack that prevents Jinja's `int` NOTE: filter internal implementation from treating the input as an NOTE: infinite float when it looks like a scientific notation (with a NOTE: single 'e' char in between digits), raising an `OverflowError`, NOTE: subsequently. 'f' is still a hex letter so it won't affect the NOTE: check for whether it's a (short or long) commit hash or not. Ref: https://github.com/pallets/jinja/issues/1921 -#} {%- set pr_issue_numbers = change_note_refs | map('lower') | map('replace', 'e', 'f') | map('int', default=None) | select('integer') | map('string') | list -%} {%- set arbitrary_refs = [] -%} {%- set commit_refs = [] -%} {%- with -%} {%- set commit_ref_candidates = change_note_refs | reject('in', pr_issue_numbers) -%} {%- for cf in commit_ref_candidates -%} {%- if cf | length in (7, 8, 40) and cf | int(default=None, base=16) is not none -%} {%- set _ = commit_refs.append(cf) -%} {%- else -%} {%- set _ = arbitrary_refs.append(cf) -%} {%- endif -%} {%- endfor -%} {%- endwith -%} {% if pr_issue_numbers %} *Related issues and pull requests on GitHub:* :issue:`{{ pr_issue_numbers | join('`, :issue:`') }}`. {{- '\n' * 2 -}} {%- endif -%} {% if commit_refs %} *Related commits on GitHub:* :commit:`{{ commit_refs | join('`, :commit:`') }}`. {{- '\n' * 2 -}} {%- endif -%} {% if arbitrary_refs %} *Unlinked references:* {{ arbitrary_refs | join(', ') }}. {{- '\n' * 2 -}} {%- endif -%} {% endfor %} {% else %} - {{ sections[section][category]['']|join(', ') }} {% endif %} {% if sections[section][category]|length == 0 %} No significant changes. {% else %} {% endif %} {% endfor %} {% else %} No significant changes. {% endif %} {% endfor %} ---- {{ '\n' * 2 }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/CHANGES/.gitignore0000644000175100001660000000057614774356277015252 0ustar00runnerdocker* !.TEMPLATE.rst !.gitignore !README.rst !*.bugfix !*.bugfix.rst !*.bugfix.*.rst !*.breaking !*.breaking.rst !*.breaking.*.rst !*.contrib !*.contrib.rst !*.contrib.*.rst !*.deprecation !*.deprecation.rst !*.deprecation.*.rst !*.doc !*.doc.rst !*.doc.*.rst !*.feature !*.feature.rst !*.feature.*.rst !*.misc !*.misc.rst !*.misc.*.rst !*.packaging !*.packaging.rst !*.packaging.*.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/CHANGES/README.rst0000644000175100001660000001077214774356277014750 0ustar00runnerdocker.. _Adding change notes with your PRs: Adding change notes with your PRs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ It is very important to maintain a log for news of how updating to the new version of the software will affect end-users. This is why we enforce collection of the change fragment files in pull requests as per `Towncrier philosophy`_. The idea is that when somebody makes a change, they must record the bits that would affect end-users only including information that would be useful to them. Then, when the maintainers publish a new release, they'll automatically use these records to compose a change log for the respective version. It is important to understand that including unnecessary low-level implementation related details generates noise that is not particularly useful to the end-users most of the time. And so such details should be recorded in the Git history rather than a changelog. Alright! So how to add a news fragment? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``yarl`` uses `towncrier `_ for changelog management. To submit a change note about your PR, add a text file into the ``CHANGES/`` folder. It should contain an explanation of what applying this PR will change in the way end-users interact with the project. One sentence is usually enough but feel free to add as many details as you feel necessary for the users to understand what it means. **Use the past tense** for the text in your fragment because, combined with others, it will be a part of the "news digest" telling the readers **what changed** in a specific version of the library *since the previous version*. You should also use *reStructuredText* syntax for highlighting code (inline or block), linking parts of the docs or external sites. However, you do not need to reference the issue or PR numbers here as *towncrier* will automatically add a reference to all of the affected issues when rendering the news file. If you wish to sign your change, feel free to add ``-- by :user:`github-username``` at the end (replace ``github-username`` with your own!). Finally, name your file following the convention that Towncrier understands: it should start with the number of an issue or a PR followed by a dot, then add a patch type, like ``feature``, ``doc``, ``contrib`` etc., and add ``.rst`` as a suffix. If you need to add more than one fragment, you may add an optional sequence number (delimited with another period) between the type and the suffix. In general the name will follow ``..rst`` pattern, where the categories are: - ``bugfix``: A bug fix for something we deemed an improper undesired behavior that got corrected in the release to match pre-agreed expectations. - ``feature``: A new behavior, public APIs. That sort of stuff. - ``deprecation``: A declaration of future API removals and breaking changes in behavior. - ``breaking``: When something public gets removed in a breaking way. Could be deprecated in an earlier release. - ``doc``: Notable updates to the documentation structure or build process. - ``packaging``: Notes for downstreams about unobvious side effects and tooling. Changes in the test invocation considerations and runtime assumptions. - ``contrib``: Stuff that affects the contributor experience. e.g. Running tests, building the docs, setting up the development environment. - ``misc``: Changes that are hard to assign to any of the above categories. A pull request may have more than one of these components, for example a code change may introduce a new feature that deprecates an old feature, in which case two fragments should be added. It is not necessary to make a separate documentation fragment for documentation changes accompanying the relevant code changes. Examples for adding changelog entries to your Pull Requests ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File :file:`CHANGES/603.removal.1.rst`: .. code-block:: rst Dropped Python 3.5 support; Python 3.6 is the minimal supported Python version -- by :user:`webknjaz`. File :file:`CHANGES/550.bugfix.rst`: .. code-block:: rst Started shipping Windows wheels for the x86 architecture -- by :user:`Dreamsorcerer`. File :file:`CHANGES/553.feature.rst`: .. code-block:: rst Added support for ``GenericAliases`` (``MultiDict[str]``) under Python 3.9 and higher -- by :user:`mjpieters`. .. tip:: See :file:`towncrier.toml` for all available categories (``tool.towncrier.type``). .. _Towncrier philosophy: https://towncrier.readthedocs.io/en/stable/#philosophy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/CHANGES.rst0000644000175100001660000014650414774356277014016 0ustar00runnerdocker========= Changelog ========= .. 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/#adding-a-news-entry we named the news folder "changes". WARNING: Don't drop the next directive! .. towncrier release notes start 1.19.0 ====== *(2025-04-05)* Bug fixes --------- - Fixed entire name being re-encoded when using :py:meth:`yarl.URL.with_suffix` -- by :user:`NTFSvolume`. *Related issues and pull requests on GitHub:* :issue:`1468`. Features -------- - Started building armv7l wheels for manylinux -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1495`. Contributor-facing changes -------------------------- - GitHub Actions CI/CD is now configured to manage caching pip-ecosystem dependencies using `re-actors/cache-python-deps`_ -- an action by :user:`webknjaz` that takes into account ABI stability and the exact version of Python runtime. .. _`re-actors/cache-python-deps`: https://github.com/marketplace/actions/cache-python-deps *Related issues and pull requests on GitHub:* :issue:`1471`. - Increased minimum `propcache`_ version to 0.2.1 to fix failing tests -- by :user:`bdraco`. .. _`propcache`: https://github.com/aio-libs/propcache *Related issues and pull requests on GitHub:* :issue:`1479`. - Added all hidden folders to pytest's ``norecursedirs`` to prevent it from trying to collect tests there -- by :user:`lysnikolaou`. *Related issues and pull requests on GitHub:* :issue:`1480`. Miscellaneous internal changes ------------------------------ - Improved accuracy of type annotations -- by :user:`Dreamsorcerer`. *Related issues and pull requests on GitHub:* :issue:`1484`. - Improved performance of parsing query strings -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1493`, :issue:`1497`. - Improved performance of the C unquoter -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1496`, :issue:`1498`. ---- 1.18.3 ====== *(2024-12-01)* Bug fixes --------- - Fixed uppercase ASCII hosts being rejected by :meth:`URL.build() ` and :py:meth:`~yarl.URL.with_host` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`954`, :issue:`1442`. Miscellaneous internal changes ------------------------------ - Improved performances of multiple path properties on cache miss -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1443`. ---- 1.18.2 ====== *(2024-11-29)* No significant changes. ---- 1.18.1 ====== *(2024-11-29)* Miscellaneous internal changes ------------------------------ - Improved cache performance when :class:`~yarl.URL` objects are constructed from :py:meth:`~yarl.URL.build` with ``encoded=True`` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1432`. - Improved cache performance for operations that produce a new :class:`~yarl.URL` object -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1434`, :issue:`1436`. ---- 1.18.0 ====== *(2024-11-21)* Features -------- - Added ``keep_query`` and ``keep_fragment`` flags in the :py:meth:`yarl.URL.with_path`, :py:meth:`yarl.URL.with_name` and :py:meth:`yarl.URL.with_suffix` methods, allowing users to optionally retain the query string and fragment in the resulting URL when replacing the path -- by :user:`paul-nameless`. *Related issues and pull requests on GitHub:* :issue:`111`, :issue:`1421`. Contributor-facing changes -------------------------- - Started running downstream ``aiohttp`` tests in CI -- by :user:`Cycloctane`. *Related issues and pull requests on GitHub:* :issue:`1415`. Miscellaneous internal changes ------------------------------ - Improved performance of converting :class:`~yarl.URL` to a string -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1422`. ---- 1.17.2 ====== *(2024-11-17)* Bug fixes --------- - Stopped implicitly allowing the use of Cython pre-release versions when building the distribution package -- by :user:`ajsanchezsanz` and :user:`markgreene74`. *Related issues and pull requests on GitHub:* :issue:`1411`, :issue:`1412`. - Fixed a bug causing :attr:`~yarl.URL.port` to return the default port when the given port was zero -- by :user:`gmacon`. *Related issues and pull requests on GitHub:* :issue:`1413`. Features -------- - Make error messages include details of incorrect type when ``port`` is not int in :py:meth:`~yarl.URL.build`. -- by :user:`Cycloctane`. *Related issues and pull requests on GitHub:* :issue:`1414`. Packaging updates and notes for downstreams ------------------------------------------- - Stopped implicitly allowing the use of Cython pre-release versions when building the distribution package -- by :user:`ajsanchezsanz` and :user:`markgreene74`. *Related issues and pull requests on GitHub:* :issue:`1411`, :issue:`1412`. Miscellaneous internal changes ------------------------------ - Improved performance of the :py:meth:`~yarl.URL.joinpath` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1418`. ---- 1.17.1 ====== *(2024-10-30)* Miscellaneous internal changes ------------------------------ - Improved performance of many :class:`~yarl.URL` methods -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1396`, :issue:`1397`, :issue:`1398`. - Improved performance of passing a `dict` or `str` to :py:meth:`~yarl.URL.extend_query` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1401`. ---- 1.17.0 ====== *(2024-10-28)* Features -------- - Added :attr:`~yarl.URL.host_port_subcomponent` which returns the :rfc:`3986#section-3.2.2` host and :rfc:`3986#section-3.2.3` port subcomponent -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1375`. ---- 1.16.0 ====== *(2024-10-21)* Bug fixes --------- - Fixed blocking I/O to load Python code when creating a new :class:`~yarl.URL` with non-ascii characters in the network location part -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1342`. Removals and backward incompatible breaking changes --------------------------------------------------- - Migrated to using a single cache for encoding hosts -- by :user:`bdraco`. Passing ``ip_address_size`` and ``host_validate_size`` to :py:meth:`~yarl.cache_configure` is deprecated in favor of the new ``encode_host_size`` parameter and will be removed in a future release. For backwards compatibility, the old parameters affect the ``encode_host`` cache size. *Related issues and pull requests on GitHub:* :issue:`1348`, :issue:`1357`, :issue:`1363`. Miscellaneous internal changes ------------------------------ - Improved performance of constructing :class:`~yarl.URL` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1336`. - Improved performance of calling :py:meth:`~yarl.URL.build` and constructing unencoded :class:`~yarl.URL` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1345`. - Reworked the internal encoding cache to improve performance on cache hit -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1369`. ---- 1.15.5 ====== *(2024-10-18)* Miscellaneous internal changes ------------------------------ - Improved performance of the :py:meth:`~yarl.URL.joinpath` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1304`. - Improved performance of the :py:meth:`~yarl.URL.extend_query` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1305`. - Improved performance of the :py:meth:`~yarl.URL.origin` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1306`. - Improved performance of the :py:meth:`~yarl.URL.with_path` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1307`. - Improved performance of the :py:meth:`~yarl.URL.with_query` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1308`, :issue:`1328`. - Improved performance of the :py:meth:`~yarl.URL.update_query` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1309`, :issue:`1327`. - Improved performance of the :py:meth:`~yarl.URL.join` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1313`. - Improved performance of :class:`~yarl.URL` equality checks -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1315`. - Improved performance of :class:`~yarl.URL` methods that modify the network location -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1316`. - Improved performance of the :py:meth:`~yarl.URL.with_fragment` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1317`. - Improved performance of calculating the hash of :class:`~yarl.URL` objects -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1318`. - Improved performance of the :py:meth:`~yarl.URL.relative` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1319`. - Improved performance of the :py:meth:`~yarl.URL.with_name` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1320`. - Improved performance of :attr:`~yarl.URL.parent` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1321`. - Improved performance of the :py:meth:`~yarl.URL.with_scheme` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1322`. ---- 1.15.4 ====== *(2024-10-16)* Miscellaneous internal changes ------------------------------ - Improved performance of the quoter when all characters are safe -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1288`. - Improved performance of unquoting strings -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1292`, :issue:`1293`. - Improved performance of calling :py:meth:`~yarl.URL.build` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1297`. ---- 1.15.3 ====== *(2024-10-15)* Bug fixes --------- - Fixed :py:meth:`~yarl.URL.build` failing to validate paths must start with a ``/`` when passing ``authority`` -- by :user:`bdraco`. The validation only worked correctly when passing ``host``. *Related issues and pull requests on GitHub:* :issue:`1265`. Removals and backward incompatible breaking changes --------------------------------------------------- - Removed support for Python 3.8 as it has reached end of life -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1203`. Miscellaneous internal changes ------------------------------ - Improved performance of constructing :class:`~yarl.URL` when the net location is only the host -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1271`. ---- 1.15.2 ====== *(2024-10-13)* Miscellaneous internal changes ------------------------------ - Improved performance of converting :class:`~yarl.URL` to a string -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1234`. - Improved performance of :py:meth:`~yarl.URL.joinpath` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1248`, :issue:`1250`. - Improved performance of constructing query strings from :class:`~multidict.MultiDict` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1256`. - Improved performance of constructing query strings with ``int`` values -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1259`. ---- 1.15.1 ====== *(2024-10-12)* Miscellaneous internal changes ------------------------------ - Improved performance of calling :py:meth:`~yarl.URL.build` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1222`. - Improved performance of all :class:`~yarl.URL` methods that create new :class:`~yarl.URL` objects -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1226`. - Improved performance of :class:`~yarl.URL` methods that modify the network location -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1229`. ---- 1.15.0 ====== *(2024-10-11)* Bug fixes --------- - Fixed validation with :py:meth:`~yarl.URL.with_scheme` when passed scheme is not lowercase -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1189`. Features -------- - Started building ``armv7l`` wheels -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1204`. Miscellaneous internal changes ------------------------------ - Improved performance of constructing unencoded :class:`~yarl.URL` objects -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1188`. - Added a cache for parsing hosts to reduce overhead of encoding :class:`~yarl.URL` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1190`. - Improved performance of constructing query strings from :class:`~collections.abc.Mapping` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1193`. - Improved performance of converting :class:`~yarl.URL` objects to strings -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1198`. ---- 1.14.0 ====== *(2024-10-08)* Packaging updates and notes for downstreams ------------------------------------------- - Switched to using the :mod:`propcache ` package for property caching -- by :user:`bdraco`. The :mod:`propcache ` package is derived from the property caching code in :mod:`yarl` and has been broken out to avoid maintaining it for multiple projects. *Related issues and pull requests on GitHub:* :issue:`1169`. Contributor-facing changes -------------------------- - Started testing with Hypothesis -- by :user:`webknjaz` and :user:`bdraco`. Special thanks to :user:`Zac-HD` for helping us get started with this framework. *Related issues and pull requests on GitHub:* :issue:`860`. Miscellaneous internal changes ------------------------------ - Improved performance of :py:meth:`~yarl.URL.is_default_port` when no explicit port is set -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1168`. - Improved performance of converting :class:`~yarl.URL` to a string when no explicit port is set -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1170`. - Improved performance of the :py:meth:`~yarl.URL.origin` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1175`. - Improved performance of encoding hosts -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1176`. ---- 1.13.1 ====== *(2024-09-27)* Miscellaneous internal changes ------------------------------ - Improved performance of calling :py:meth:`~yarl.URL.build` with ``authority`` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1163`. ---- 1.13.0 ====== *(2024-09-26)* Bug fixes --------- - Started rejecting ASCII hostnames with invalid characters. For host strings that look like authority strings, the exception message includes advice on what to do instead -- by :user:`mjpieters`. *Related issues and pull requests on GitHub:* :issue:`880`, :issue:`954`. - Fixed IPv6 addresses missing brackets when the :class:`~yarl.URL` was converted to a string -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1157`, :issue:`1158`. Features -------- - Added :attr:`~yarl.URL.host_subcomponent` which returns the :rfc:`3986#section-3.2.2` host subcomponent -- by :user:`bdraco`. The only current practical difference between :attr:`~yarl.URL.raw_host` and :attr:`~yarl.URL.host_subcomponent` is that IPv6 addresses are returned bracketed. *Related issues and pull requests on GitHub:* :issue:`1159`. ---- 1.12.1 ====== *(2024-09-23)* No significant changes. ---- 1.12.0 ====== *(2024-09-23)* Features -------- - Added :attr:`~yarl.URL.path_safe` to be able to fetch the path without ``%2F`` and ``%25`` decoded -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1150`. Removals and backward incompatible breaking changes --------------------------------------------------- - Restore decoding ``%2F`` (``/``) in ``URL.path`` -- by :user:`bdraco`. This change restored the behavior before :issue:`1057`. *Related issues and pull requests on GitHub:* :issue:`1151`. Miscellaneous internal changes ------------------------------ - Improved performance of processing paths -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1143`. ---- 1.11.1 ====== *(2024-09-09)* Bug fixes --------- - Allowed scheme replacement for relative URLs if the scheme does not require a host -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`280`, :issue:`1138`. - Allowed empty host for URL schemes other than the special schemes listed in the WHATWG URL spec -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1136`. Features -------- - Loosened restriction on integers as query string values to allow classes that implement ``__int__`` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1139`. Miscellaneous internal changes ------------------------------ - Improved performance of normalizing paths -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1137`. ---- 1.11.0 ====== *(2024-09-08)* Features -------- - Added :meth:`URL.extend_query() ` method, which can be used to extend parameters without replacing same named keys -- by :user:`bdraco`. This method was primarily added to replace the inefficient hand rolled method currently used in ``aiohttp``. *Related issues and pull requests on GitHub:* :issue:`1128`. Miscellaneous internal changes ------------------------------ - Improved performance of the Cython ``cached_property`` implementation -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1122`. - Simplified computing ports by removing unnecessary code -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1123`. - Improved performance of encoding non IPv6 hosts -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1125`. - Improved performance of :meth:`URL.build() ` when the path, query string, or fragment is an empty string -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1126`. - Improved performance of the :meth:`URL.update_query() ` method -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1130`. - Improved performance of processing query string changes when arguments are :class:`str` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1131`. ---- 1.10.0 ====== *(2024-09-06)* Bug fixes --------- - Fixed joining a path when the existing path was empty -- by :user:`bdraco`. A regression in :meth:`URL.join() ` was introduced in :issue:`1082`. *Related issues and pull requests on GitHub:* :issue:`1118`. Features -------- - Added :meth:`URL.without_query_params() ` method, to drop some parameters from query string -- by :user:`hongquan`. *Related issues and pull requests on GitHub:* :issue:`774`, :issue:`898`, :issue:`1010`. - The previously protected types ``_SimpleQuery``, ``_QueryVariable``, and ``_Query`` are now available for use externally as ``SimpleQuery``, ``QueryVariable``, and ``Query`` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1050`, :issue:`1113`. Contributor-facing changes -------------------------- - Replaced all :class:`~typing.Optional` with :class:`~typing.Union` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1095`. Miscellaneous internal changes ------------------------------ - Significantly improved performance of parsing the network location -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1112`. - Added internal types to the cache to prevent future refactoring errors -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1117`. ---- 1.9.11 ====== *(2024-09-04)* Bug fixes --------- - Fixed a :exc:`TypeError` with ``MultiDictProxy`` and Python 3.8 -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1084`, :issue:`1105`, :issue:`1107`. Miscellaneous internal changes ------------------------------ - Improved performance of encoding hosts -- by :user:`bdraco`. Previously, the library would unconditionally try to parse a host as an IP Address. The library now avoids trying to parse a host as an IP Address if the string is not in one of the formats described in :rfc:`3986#section-3.2.2`. *Related issues and pull requests on GitHub:* :issue:`1104`. ---- 1.9.10 ====== *(2024-09-04)* Bug fixes --------- - :meth:`URL.join() ` has been changed to match :rfc:`3986` and align with :meth:`/ operation ` and :meth:`URL.joinpath() ` when joining URLs with empty segments. Previously :py:func:`urllib.parse.urljoin` was used, which has known issues with empty segments (`python/cpython#84774 `_). Due to the semantics of :meth:`URL.join() `, joining an URL with scheme requires making it relative, prefixing with ``./``. .. code-block:: pycon >>> URL("https://web.archive.org/web/").join(URL("./https://github.com/aio-libs/yarl")) URL('https://web.archive.org/web/https://github.com/aio-libs/yarl') Empty segments are honored in the base as well as the joined part. .. code-block:: pycon >>> URL("https://web.archive.org/web/https://").join(URL("github.com/aio-libs/yarl")) URL('https://web.archive.org/web/https://github.com/aio-libs/yarl') -- by :user:`commonism` This change initially appeared in 1.9.5 but was reverted in 1.9.6 to resolve a problem with query string handling. *Related issues and pull requests on GitHub:* :issue:`1039`, :issue:`1082`. Features -------- - Added :attr:`~yarl.URL.absolute` which is now preferred over ``URL.is_absolute()`` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1100`. ---- 1.9.9 ===== *(2024-09-04)* Bug fixes --------- - Added missing type on :attr:`~yarl.URL.port` -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1097`. ---- 1.9.8 ===== *(2024-09-03)* Features -------- - Covered the :class:`~yarl.URL` object with types -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1084`. - Cache parsing of IP Addresses when encoding hosts -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1086`. Contributor-facing changes -------------------------- - Covered the :class:`~yarl.URL` object with types -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1084`. Miscellaneous internal changes ------------------------------ - Improved performance of handling ports -- by :user:`bdraco`. *Related issues and pull requests on GitHub:* :issue:`1081`. ---- 1.9.7 ===== *(2024-09-01)* Removals and backward incompatible breaking changes --------------------------------------------------- - Removed support :rfc:`3986#section-3.2.3` port normalization when the scheme is not one of ``http``, ``https``, ``wss``, or ``ws`` -- by :user:`bdraco`. Support for port normalization was recently added in :issue:`1033` and contained code that would do blocking I/O if the scheme was not one of the four listed above. The code has been removed because this library is intended to be safe for usage with :mod:`asyncio`. *Related issues and pull requests on GitHub:* :issue:`1076`. Miscellaneous internal changes ------------------------------ - Improved performance of property caching -- by :user:`bdraco`. The ``reify`` implementation from ``aiohttp`` was adapted to replace the internal ``cached_property`` implementation. *Related issues and pull requests on GitHub:* :issue:`1070`. ---- 1.9.6 ===== *(2024-08-30)* Bug fixes --------- - Reverted :rfc:`3986` compatible :meth:`URL.join() ` honoring empty segments which was introduced in :issue:`1039`. This change introduced a regression handling query string parameters with joined URLs. The change was reverted to maintain compatibility with the previous behavior. *Related issues and pull requests on GitHub:* :issue:`1067`. ---- 1.9.5 ===== *(2024-08-30)* Bug fixes --------- - Joining URLs with empty segments has been changed to match :rfc:`3986`. Previously empty segments would be removed from path, breaking use-cases such as .. code-block:: python URL("https://web.archive.org/web/") / "https://github.com/" Now :meth:`/ operation ` and :meth:`URL.joinpath() ` keep empty segments, but do not introduce new empty segments. e.g. .. code-block:: python URL("https://example.org/") / "" does not introduce an empty segment. -- by :user:`commonism` and :user:`youtux` *Related issues and pull requests on GitHub:* :issue:`1026`. - The default protocol ports of well-known URI schemes are now taken into account during the normalization of the URL string representation in accordance with :rfc:`3986#section-3.2.3`. Specified ports are removed from the :class:`str` representation of a :class:`~yarl.URL` if the port matches the scheme's default port -- by :user:`commonism`. *Related issues and pull requests on GitHub:* :issue:`1033`. - :meth:`URL.join() ` has been changed to match :rfc:`3986` and align with :meth:`/ operation ` and :meth:`URL.joinpath() ` when joining URLs with empty segments. Previously :py:func:`urllib.parse.urljoin` was used, which has known issues with empty segments (`python/cpython#84774 `_). Due to the semantics of :meth:`URL.join() `, joining an URL with scheme requires making it relative, prefixing with ``./``. .. code-block:: pycon >>> URL("https://web.archive.org/web/").join(URL("./https://github.com/aio-libs/yarl")) URL('https://web.archive.org/web/https://github.com/aio-libs/yarl') Empty segments are honored in the base as well as the joined part. .. code-block:: pycon >>> URL("https://web.archive.org/web/https://").join(URL("github.com/aio-libs/yarl")) URL('https://web.archive.org/web/https://github.com/aio-libs/yarl') -- by :user:`commonism` *Related issues and pull requests on GitHub:* :issue:`1039`. Removals and backward incompatible breaking changes --------------------------------------------------- - Stopped decoding ``%2F`` (``/``) in ``URL.path``, as this could lead to code incorrectly treating it as a path separator -- by :user:`Dreamsorcerer`. *Related issues and pull requests on GitHub:* :issue:`1057`. - Dropped support for Python 3.7 -- by :user:`Dreamsorcerer`. *Related issues and pull requests on GitHub:* :issue:`1016`. Improved documentation ---------------------- - On the :doc:`Contributing docs ` page, a link to the ``Towncrier philosophy`` has been fixed. *Related issues and pull requests on GitHub:* :issue:`981`. - The pre-existing :meth:`/ magic method ` has been documented in the API reference -- by :user:`commonism`. *Related issues and pull requests on GitHub:* :issue:`1026`. Packaging updates and notes for downstreams ------------------------------------------- - A flaw in the logic for copying the project directory into a temporary folder that led to infinite recursion when :envvar:`TMPDIR` was set to a project subdirectory path. This was happening in Fedora and its downstream due to the use of `pyproject-rpm-macros `__. It was only reproducible with ``pip wheel`` and was not affecting the ``pyproject-build`` users. -- by :user:`hroncok` and :user:`webknjaz` *Related issues and pull requests on GitHub:* :issue:`992`, :issue:`1014`. - Support Python 3.13 and publish non-free-threaded wheels *Related issues and pull requests on GitHub:* :issue:`1054`. Contributor-facing changes -------------------------- - The CI/CD setup has been updated to test ``arm64`` wheels under macOS 14, except for Python 3.7 that is unsupported in that environment -- by :user:`webknjaz`. *Related issues and pull requests on GitHub:* :issue:`1015`. - Removed unused type ignores and casts -- by :user:`hauntsaninja`. *Related issues and pull requests on GitHub:* :issue:`1031`. Miscellaneous internal changes ------------------------------ - ``port``, ``scheme``, and ``raw_host`` are now ``cached_property`` -- by :user:`bdraco`. ``aiohttp`` accesses these properties quite often, which cause :mod:`urllib` to build the ``_hostinfo`` property every time. ``port``, ``scheme``, and ``raw_host`` are now cached properties, which will improve performance. *Related issues and pull requests on GitHub:* :issue:`1044`, :issue:`1058`. ---- 1.9.4 (2023-12-06) ================== Bug fixes --------- - Started raising :py:exc:`TypeError` when a string value is passed into :py:meth:`~yarl.URL.build` as the ``port`` argument -- by :user:`commonism`. Previously the empty string as port would create malformed URLs when rendered as string representations. (:issue:`883`) Packaging updates and notes for downstreams ------------------------------------------- - The leading ``--`` has been dropped from the :pep:`517` in-tree build backend config setting names. ``--pure-python`` is now just ``pure-python`` -- by :user:`webknjaz`. The usage now looks as follows: .. code-block:: console $ python -m build \ --config-setting=pure-python=true \ --config-setting=with-cython-tracing=true (:issue:`963`) Contributor-facing changes -------------------------- - A step-by-step :doc:`Release Guide ` guide has been added, describing how to release *yarl* -- by :user:`webknjaz`. This is primarily targeting maintainers. (:issue:`960`) - Coverage collection has been implemented for the Cython modules -- by :user:`webknjaz`. It will also be reported to Codecov from any non-release CI jobs. To measure coverage in a development environment, *yarl* can be installed in editable mode: .. code-block:: console $ python -Im pip install -e . Editable install produces C-files required for the Cython coverage plugin to map the measurements back to the PYX-files. :issue:`961` - It is now possible to request line tracing in Cython builds using the ``with-cython-tracing`` :pep:`517` config setting -- :user:`webknjaz`. This can be used in CI and development environment to measure coverage on Cython modules, but is not normally useful to the end-users or downstream packagers. Here's a usage example: .. code-block:: console $ python -Im pip install . --config-settings=with-cython-tracing=true For editable installs, this setting is on by default. Otherwise, it's off unless requested explicitly. The following produces C-files required for the Cython coverage plugin to map the measurements back to the PYX-files: .. code-block:: console $ python -Im pip install -e . Alternatively, the ``YARL_CYTHON_TRACING=1`` environment variable can be set to do the same as the :pep:`517` config setting. :issue:`962` 1.9.3 (2023-11-20) ================== Bug fixes --------- - Stopped dropping trailing slashes in :py:meth:`~yarl.URL.joinpath` -- by :user:`gmacon`. (:issue:`862`, :issue:`866`) - Started accepting string subclasses in :meth:`~yarl.URL.__truediv__` operations (``URL / segment``) -- by :user:`mjpieters`. (:issue:`871`, :issue:`884`) - Fixed the human representation of URLs with square brackets in usernames and passwords -- by :user:`mjpieters`. (:issue:`876`, :issue:`882`) - Updated type hints to include ``URL.missing_port()``, ``URL.__bytes__()`` and the ``encoding`` argument to :py:meth:`~yarl.URL.joinpath` -- by :user:`mjpieters`. (:issue:`891`) Packaging updates and notes for downstreams ------------------------------------------- - Integrated Cython 3 to enable building *yarl* under Python 3.12 -- by :user:`mjpieters`. (:issue:`829`, :issue:`881`) - Declared modern ``setuptools.build_meta`` as the :pep:`517` build backend in :file:`pyproject.toml` explicitly -- by :user:`webknjaz`. (:issue:`886`) - Converted most of the packaging setup into a declarative :file:`setup.cfg` config -- by :user:`webknjaz`. (:issue:`890`) - The packaging is replaced from an old-fashioned :file:`setup.py` to an in-tree :pep:`517` build backend -- by :user:`webknjaz`. Whenever the end-users or downstream packagers need to build ``yarl`` from source (a Git checkout or an sdist), they may pass a ``config_settings`` flag ``--pure-python``. If this flag is not set, a C-extension will be built and included into the distribution. Here is how this can be done with ``pip``: .. code-block:: console $ python -m pip install . --config-settings=--pure-python=false This will also work with ``-e | --editable``. The same can be achieved via ``pypa/build``: .. code-block:: console $ python -m build --config-setting=--pure-python=false Adding ``-w | --wheel`` can force ``pypa/build`` produce a wheel from source directly, as opposed to building an ``sdist`` and then building from it. (:issue:`893`) .. attention:: v1.9.3 was the only version using the ``--pure-python`` setting name. Later versions dropped the ``--`` prefix, making it just ``pure-python``. - Declared Python 3.12 supported officially in the distribution package metadata -- by :user:`edgarrmondragon`. (:issue:`942`) Contributor-facing changes -------------------------- - A regression test for no-host URLs was added per :issue:`821` and :rfc:`3986` -- by :user:`kenballus`. (:issue:`821`, :issue:`822`) - Started testing *yarl* against Python 3.12 in CI -- by :user:`mjpieters`. (:issue:`881`) - All Python 3.12 jobs are now marked as required to pass in CI -- by :user:`edgarrmondragon`. (:issue:`942`) - MyST is now integrated in Sphinx -- by :user:`webknjaz`. This allows the contributors to author new documents in Markdown when they have difficulties with going straight RST. (:issue:`953`) 1.9.2 (2023-04-25) ================== Bugfixes -------- - Fix regression with :meth:`~yarl.URL.__truediv__` and absolute URLs with empty paths causing the raw path to lack the leading ``/``. (`#854 `_) 1.9.1 (2023-04-21) ================== Bugfixes -------- - Marked tests that fail on older Python patch releases (< 3.7.10, < 3.8.8 and < 3.9.2) as expected to fail due to missing a security fix for CVE-2021-23336. (`#850 `_) 1.9.0 (2023-04-19) ================== This release was never published to PyPI, due to issues with the build process. Features -------- - Added ``URL.joinpath(*elements)``, to create a new URL appending multiple path elements. (`#704 `_) - Made :meth:`URL.__truediv__() ` return ``NotImplemented`` if called with an unsupported type — by :user:`michaeljpeters`. (`#832 `_) Bugfixes -------- - Path normalization for absolute URLs no longer raises a ValueError exception when ``..`` segments would otherwise go beyond the URL path root. (`#536 `_) - Fixed an issue with update_query() not getting rid of the query when argument is None. (`#792 `_) - Added some input restrictions on with_port() function to prevent invalid boolean inputs or out of valid port inputs; handled incorrect 0 port representation. (`#793 `_) - Made :py:meth:`~yarl.URL.build` raise a :py:exc:`TypeError` if the ``host`` argument is :py:data:`None` — by :user:`paulpapacz`. (`#808 `_) - Fixed an issue with ``update_query()`` getting rid of the query when the argument is empty but not ``None``. (`#845 `_) Misc ---- - `#220 `_ 1.8.2 (2022-12-03) ================== This is the first release that started shipping wheels for Python 3.11. 1.8.1 (2022-08-01) ================== Misc ---- - `#694 `_, `#699 `_, `#700 `_, `#701 `_, `#702 `_, `#703 `_, `#739 `_ 1.8.0 (2022-08-01) ================== Features -------- - Added ``URL.raw_suffix``, ``URL.suffix``, ``URL.raw_suffixes``, ``URL.suffixes``, ``URL.with_suffix``. (`#613 `_) Improved Documentation ---------------------- - Fixed broken internal references to :meth:`~yarl.URL.human_repr`. (`#665 `_) - Fixed broken external references to :doc:`multidict:index` docs. (`#665 `_) Deprecations and Removals ------------------------- - Dropped Python 3.6 support. (`#672 `_) Misc ---- - `#646 `_, `#699 `_, `#701 `_ 1.7.2 (2021-11-01) ================== Bugfixes -------- - Changed call in ``with_port()`` to stop reencoding parts of the URL that were already encoded. (`#623 `_) 1.7.1 (2021-10-07) ================== Bugfixes -------- - Fix 1.7.0 build error 1.7.0 (2021-10-06) ================== Features -------- - Add ``__bytes__()`` magic method so that ``bytes(url)`` will work and use optimal ASCII encoding. (`#582 `_) - Started shipping platform-specific arm64 wheels for Apple Silicon. (`#622 `_) - Started shipping platform-specific wheels with the ``musl`` tag targeting typical Alpine Linux runtimes. (`#622 `_) - Added support for Python 3.10. (`#622 `_) 1.6.3 (2020-11-14) ================== Bugfixes -------- - No longer loose characters when decoding incorrect percent-sequences (like ``%e2%82%f8``). All non-decodable percent-sequences are now preserved. `#517 `_ - Provide x86 Windows wheels. `#535 `_ ---- 1.6.2 (2020-10-12) ================== Bugfixes -------- - Provide generated ``.c`` files in TarBall distribution. `#530 `_ 1.6.1 (2020-10-12) ================== Features -------- - Provide wheels for ``aarch64``, ``i686``, ``ppc64le``, ``s390x`` architectures on Linux as well as ``x86_64``. `#507 `_ - Provide wheels for Python 3.9. `#526 `_ Bugfixes -------- - ``human_repr()`` now always produces valid representation equivalent to the original URL (if the original URL is valid). `#511 `_ - Fixed requoting a single percent followed by a percent-encoded character in the Cython implementation. `#514 `_ - Fix ValueError when decoding ``%`` which is not followed by two hexadecimal digits. `#516 `_ - Fix decoding ``%`` followed by a space and hexadecimal digit. `#520 `_ - Fix annotation of ``with_query()``/``update_query()`` methods for ``key=[val1, val2]`` case. `#528 `_ Removal ------- - Drop Python 3.5 support; Python 3.6 is the minimal supported Python version. ---- 1.6.0 (2020-09-23) ================== Features -------- - Allow for int and float subclasses in query, while still denying bool. `#492 `_ Bugfixes -------- - Do not requote arguments in ``URL.build()``, ``with_xxx()`` and in ``/`` operator. `#502 `_ - Keep IPv6 brackets in ``origin()``. `#504 `_ ---- 1.5.1 (2020-08-01) ================== Bugfixes -------- - Fix including relocated internal ``yarl._quoting_c`` C-extension into published PyPI dists. `#485 `_ Misc ---- - `#484 `_ ---- 1.5.0 (2020-07-26) ================== Features -------- - Convert host to lowercase on URL building. `#386 `_ - Allow using ``mod`` operator (``%``) for updating query string (an alias for ``update_query()`` method). `#435 `_ - Allow use of sequences such as ``list`` and ``tuple`` in the values of a mapping such as ``dict`` to represent that a key has many values:: url = URL("http://example.com") assert url.with_query({"a": [1, 2]}) == URL("http://example.com/?a=1&a=2") `#443 `_ - Support ``URL.build()`` with scheme and path (creates a relative URL). `#464 `_ - Cache slow IDNA encode/decode calls. `#476 `_ - Add ``@final`` / ``Final`` type hints `#477 `_ - Support URL authority/raw_authority properties and authority argument of ``URL.build()`` method. `#478 `_ - Hide the library implementation details, make the exposed public list very clean. `#483 `_ Bugfixes -------- - Fix tests with newer Python (3.7.6, 3.8.1 and 3.9.0+). `#409 `_ - Fix a bug where query component, passed in a form of mapping or sequence, is unquoted in unexpected way. `#426 `_ - Hide ``Query`` and ``QueryVariable`` type aliases in ``__init__.pyi``, now they are prefixed with underscore. `#431 `_ - Keep IPv6 brackets after updating port/user/password. `#451 `_ ---- 1.4.2 (2019-12-05) ================== Features -------- - Workaround for missing ``str.isascii()`` in Python 3.6 `#389 `_ ---- 1.4.1 (2019-11-29) ================== * Fix regression, make the library work on Python 3.5 and 3.6 again. 1.4.0 (2019-11-29) ================== * Distinguish an empty password in URL from a password not provided at all (#262) * Fixed annotations for optional parameters of ``URL.build`` (#309) * Use None as default value of ``user`` parameter of ``URL.build`` (#309) * Enforce building C Accelerated modules when installing from source tarball, use ``YARL_NO_EXTENSIONS`` environment variable for falling back to (slower) Pure Python implementation (#329) * Drop Python 3.5 support * Fix quoting of plus in path by pure python version (#339) * Don't create a new URL if fragment is unchanged (#292) * Included in error message the path that produces starting slash forbidden error (#376) * Skip slow IDNA encoding for ASCII-only strings (#387) 1.3.0 (2018-12-11) ================== * Fix annotations for ``query`` parameter (#207) * An incoming query sequence can have int variables (the same as for Mapping type) (#208) * Add ``URL.explicit_port`` property (#218) * Give a friendlier error when port can't be converted to int (#168) * ``bool(URL())`` now returns ``False`` (#272) 1.2.6 (2018-06-14) ================== * Drop Python 3.4 trove classifier (#205) 1.2.5 (2018-05-23) ================== * Fix annotations for ``build`` (#199) 1.2.4 (2018-05-08) ================== * Fix annotations for ``cached_property`` (#195) 1.2.3 (2018-05-03) ================== * Accept ``str`` subclasses in ``URL`` constructor (#190) 1.2.2 (2018-05-01) ================== * Fix build 1.2.1 (2018-04-30) ================== * Pin minimal required Python to 3.5.3 (#189) 1.2.0 (2018-04-30) ================== * Forbid inheritance, replace ``__init__`` with ``__new__`` (#171) * Support PEP-561 (provide type hinting marker) (#182) 1.1.1 (2018-02-17) ================== * Fix performance regression: don't encode empty ``netloc`` (#170) 1.1.0 (2018-01-21) ================== * Make pure Python quoter consistent with Cython version (#162) 1.0.0 (2018-01-15) ================== * Use fast path if quoted string does not need requoting (#154) * Speed up quoting/unquoting by ``_Quoter`` and ``_Unquoter`` classes (#155) * Drop ``yarl.quote`` and ``yarl.unquote`` public functions (#155) * Add custom string writer, reuse static buffer if available (#157) Code is 50-80 times faster than Pure Python version (was 4-5 times faster) * Don't recode IP zone (#144) * Support ``encoded=True`` in ``yarl.URL.build()`` (#158) * Fix updating query with multiple keys (#160) 0.18.0 (2018-01-10) =================== * Fallback to IDNA 2003 if domain name is not IDNA 2008 compatible (#152) 0.17.0 (2017-12-30) =================== * Use IDNA 2008 for domain name processing (#149) 0.16.0 (2017-12-07) =================== * Fix raising ``TypeError`` by ``url.query_string()`` after ``url.with_query({})`` (empty mapping) (#141) 0.15.0 (2017-11-23) =================== * Add ``raw_path_qs`` attribute (#137) 0.14.2 (2017-11-14) =================== * Restore ``strict`` parameter as no-op in ``quote`` / ``unquote`` 0.14.1 (2017-11-13) =================== * Restore ``strict`` parameter as no-op for sake of compatibility with aiohttp 2.2 0.14.0 (2017-11-11) =================== * Drop strict mode (#123) * Fix ``"ValueError: Unallowed PCT %"`` when there's a ``"%"`` in the URL (#124) 0.13.0 (2017-10-01) =================== * Document ``encoded`` parameter (#102) * Support relative URLs like ``'?key=value'`` (#100) * Unsafe encoding for QS fixed. Encode ``;`` character in value parameter (#104) * Process passwords without user names (#95) 0.12.0 (2017-06-26) =================== * Properly support paths without leading slash in ``URL.with_path()`` (#90) * Enable type annotation checks 0.11.0 (2017-06-26) =================== * Normalize path (#86) * Clear query and fragment parts in ``.with_path()`` (#85) 0.10.3 (2017-06-13) =================== * Prevent double URL arguments unquoting (#83) 0.10.2 (2017-05-05) =================== * Unexpected hash behavior (#75) 0.10.1 (2017-05-03) =================== * Unexpected compare behavior (#73) * Do not quote or unquote + if not a query string. (#74) 0.10.0 (2017-03-14) =================== * Added ``URL.build`` class method (#58) * Added ``path_qs`` attribute (#42) 0.9.8 (2017-02-16) ================== * Do not quote ``:`` in path 0.9.7 (2017-02-16) ================== * Load from pickle without _cache (#56) * Percent-encoded pluses in path variables become spaces (#59) 0.9.6 (2017-02-15) ================== * Revert backward incompatible change (BaseURL) 0.9.5 (2017-02-14) ================== * Fix BaseURL rich comparison support 0.9.4 (2017-02-14) ================== * Use BaseURL 0.9.3 (2017-02-14) ================== * Added BaseURL 0.9.2 (2017-02-08) ================== * Remove debug print 0.9.1 (2017-02-07) ================== * Do not lose tail chars (#45) 0.9.0 (2017-02-07) ================== * Allow to quote ``%`` in non strict mode (#21) * Incorrect parsing of query parameters with %3B (;) inside (#34) * Fix core dumps (#41) * ``tmpbuf`` - compiling error (#43) * Added ``URL.update_path()`` method * Added ``URL.update_query()`` method (#47) 0.8.1 (2016-12-03) ================== * Fix broken aiohttp: revert back ``quote`` / ``unquote``. 0.8.0 (2016-12-03) ================== * Support more verbose error messages in ``.with_query()`` (#24) * Don't percent-encode ``@`` and ``:`` in path (#32) * Don't expose ``yarl.quote`` and ``yarl.unquote``, these functions are part of private API 0.7.1 (2016-11-18) ================== * Accept not only ``str`` but all classes inherited from ``str`` also (#25) 0.7.0 (2016-11-07) ================== * Accept ``int`` as value for ``.with_query()`` 0.6.0 (2016-11-07) ================== * Explicitly use UTF8 encoding in :file:`setup.py` (#20) * Properly unquote non-UTF8 strings (#19) 0.5.3 (2016-11-02) ================== * Don't use :py:class:`typing.NamedTuple` fields but indexes on URL construction 0.5.2 (2016-11-02) ================== * Inline ``_encode`` class method 0.5.1 (2016-11-02) ================== * Make URL construction faster by removing extra classmethod calls 0.5.0 (2016-11-02) ================== * Add Cython optimization for quoting/unquoting * Provide binary wheels 0.4.3 (2016-09-29) ================== * Fix typing stubs 0.4.2 (2016-09-29) ================== * Expose ``quote()`` and ``unquote()`` as public API 0.4.1 (2016-09-28) ================== * Support empty values in query (``'/path?arg'``) 0.4.0 (2016-09-27) ================== * Introduce ``relative()`` (#16) 0.3.2 (2016-09-27) ================== * Typo fixes #15 0.3.1 (2016-09-26) ================== * Support sequence of pairs as ``with_query()`` parameter 0.3.0 (2016-09-26) ================== * Introduce ``is_default_port()`` 0.2.1 (2016-09-26) ================== * Raise ValueError for URLs like 'http://:8080/' 0.2.0 (2016-09-18) ================== * Avoid doubling slashes when joining paths (#13) * Appending path starting from slash is forbidden (#12) 0.1.4 (2016-09-09) ================== * Add ``kwargs`` support for ``with_query()`` (#10) 0.1.3 (2016-09-07) ================== * Document ``with_query()``, ``with_fragment()`` and ``origin()`` * Allow ``None`` for ``with_query()`` and ``with_fragment()`` 0.1.2 (2016-09-07) ================== * Fix links, tune docs theme. 0.1.1 (2016-09-06) ================== * Update README, old version used obsolete API 0.1.0 (2016-09-06) ================== * The library was deeply refactored, bytes are gone away but all accepted strings are encoded if needed. 0.0.1 (2016-08-30) ================== * The first release. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/LICENSE0000644000175100001660000002613614774356277013217 0ustar00runnerdocker Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 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. See the License for the specific language governing permissions and limitations under the License. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/MANIFEST.in0000644000175100001660000000056614774356277013747 0ustar00runnerdockerinclude .coveragerc include pyproject.toml include pytest.ini include towncrier.toml include LICENSE include NOTICE include CHANGES.rst include README.rst graft yarl graft packaging graft docs graft CHANGES graft requirements graft tests global-exclude *.pyc global-exclude *.cache exclude yarl/*.c exclude yarl/*.html exclude yarl/*.so exclude yarl/*.pyd prune docs/_build ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/NOTICE0000644000175100001660000000114114774356277013103 0ustar00runnerdocker Copyright 2016-2021, Andrew Svetlov and aio-libs team Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 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. See the License for the specific language governing permissions and limitations under the License. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743903941.5992763 yarl-1.19.0/PKG-INFO0000644000175100001660000021411314774356306013272 0ustar00runnerdockerMetadata-Version: 2.4 Name: yarl Version: 1.19.0 Summary: Yet another URL library Home-page: https://github.com/aio-libs/yarl Author: Andrew Svetlov Author-email: andrew.svetlov@gmail.com Maintainer: aiohttp team Maintainer-email: team@aiohttp.org License: Apache-2.0 Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org Project-URL: CI: GitHub Workflows, https://github.com/aio-libs/yarl/actions?query=branch:master Project-URL: Code of Conduct, https://github.com/aio-libs/.github/blob/master/CODE_OF_CONDUCT.md Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/yarl Project-URL: Docs: Changelog, https://yarl.aio-libs.org/en/latest/changes/ Project-URL: Docs: RTD, https://yarl.aio-libs.org Project-URL: GitHub: issues, https://github.com/aio-libs/yarl/issues Project-URL: GitHub: repo, https://github.com/aio-libs/yarl Keywords: cython,cext,yarl Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Cython Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.9 Description-Content-Type: text/x-rst License-File: LICENSE License-File: NOTICE Requires-Dist: idna>=2.0 Requires-Dist: multidict>=4.0 Requires-Dist: propcache>=0.2.1 Dynamic: license-file yarl ==== The module provides handy URL class for URL parsing and changing. .. image:: https://github.com/aio-libs/yarl/workflows/CI/badge.svg :target: https://github.com/aio-libs/yarl/actions?query=workflow%3ACI :align: right .. image:: https://codecov.io/gh/aio-libs/yarl/graph/badge.svg?flag=pytest :target: https://app.codecov.io/gh/aio-libs/yarl?flags[]=pytest :alt: Codecov coverage for the pytest-driven measurements .. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json :target: https://codspeed.io/aio-libs/yarl .. image:: https://badge.fury.io/py/yarl.svg :target: https://badge.fury.io/py/yarl .. image:: https://readthedocs.org/projects/yarl/badge/?version=latest :target: https://yarl.aio-libs.org .. image:: https://img.shields.io/pypi/pyversions/yarl.svg :target: https://pypi.python.org/pypi/yarl .. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat :target: https://matrix.to/#/%23aio-libs:matrix.org :alt: Matrix Room — #aio-libs:matrix.org .. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat :target: https://matrix.to/#/%23aio-libs-space:matrix.org :alt: Matrix Space — #aio-libs-space:matrix.org Introduction ------------ Url is constructed from ``str``: .. code-block:: pycon >>> from yarl import URL >>> url = URL('https://www.python.org/~guido?arg=1#frag') >>> url URL('https://www.python.org/~guido?arg=1#frag') All url parts: *scheme*, *user*, *password*, *host*, *port*, *path*, *query* and *fragment* are accessible by properties: .. code-block:: pycon >>> url.scheme 'https' >>> url.host 'www.python.org' >>> url.path '/~guido' >>> url.query_string 'arg=1' >>> url.query >>> url.fragment 'frag' All url manipulations produce a new url object: .. code-block:: pycon >>> url = URL('https://www.python.org') >>> url / 'foo' / 'bar' URL('https://www.python.org/foo/bar') >>> url / 'foo' % {'bar': 'baz'} URL('https://www.python.org/foo?bar=baz') Strings passed to constructor and modification methods are automatically encoded giving canonical representation as result: .. code-block:: pycon >>> url = URL('https://www.python.org/шлях') >>> url URL('https://www.python.org/%D1%88%D0%BB%D1%8F%D1%85') Regular properties are *percent-decoded*, use ``raw_`` versions for getting *encoded* strings: .. code-block:: pycon >>> url.path '/шлях' >>> url.raw_path '/%D1%88%D0%BB%D1%8F%D1%85' Human readable representation of URL is available as ``.human_repr()``: .. code-block:: pycon >>> url.human_repr() 'https://www.python.org/шлях' For full documentation please read https://yarl.aio-libs.org. Installation ------------ :: $ pip install yarl The library is Python 3 only! PyPI contains binary wheels for Linux, Windows and MacOS. If you want to install ``yarl`` on another operating system where wheels are not provided, the tarball will be used to compile the library from the source code. It requires a C compiler and and Python headers installed. To skip the compilation you must explicitly opt-in by using a PEP 517 configuration setting ``pure-python``, or setting the ``YARL_NO_EXTENSIONS`` environment variable to a non-empty value, e.g.: .. code-block:: console $ pip install yarl --config-settings=pure-python=false Please note that the pure-Python (uncompiled) version is much slower. However, PyPy always uses a pure-Python implementation, and, as such, it is unaffected by this variable. Dependencies ------------ YARL requires multidict_ and propcache_ libraries. API documentation ------------------ The documentation is located at https://yarl.aio-libs.org. Why isn't boolean supported by the URL query API? ------------------------------------------------- There is no standard for boolean representation of boolean values. Some systems prefer ``true``/``false``, others like ``yes``/``no``, ``on``/``off``, ``Y``/``N``, ``1``/``0``, etc. ``yarl`` cannot make an unambiguous decision on how to serialize ``bool`` values because it is specific to how the end-user's application is built and would be different for different apps. The library doesn't accept booleans in the API; a user should convert bools into strings using own preferred translation protocol. Comparison with other URL libraries ------------------------------------ * furl (https://pypi.python.org/pypi/furl) The library has rich functionality but the ``furl`` object is mutable. I'm afraid to pass this object into foreign code: who knows if the code will modify my url in a terrible way while I just want to send URL with handy helpers for accessing URL properties. ``furl`` has other non-obvious tricky things but the main objection is mutability. * URLObject (https://pypi.python.org/pypi/URLObject) URLObject is immutable, that's pretty good. Every URL change generates a new URL object. But the library doesn't do any decode/encode transformations leaving the end user to cope with these gory details. Source code ----------- The project is hosted on GitHub_ Please file an issue on the `bug tracker `_ if you have found a bug or have some suggestion in order to improve the library. Discussion list --------------- *aio-libs* google group: https://groups.google.com/forum/#!forum/aio-libs Feel free to post your questions and ideas here. Authors and License ------------------- The ``yarl`` package is written by Andrew Svetlov. It's *Apache 2* licensed and freely available. .. _GitHub: https://github.com/aio-libs/yarl .. _multidict: https://github.com/aio-libs/multidict .. _propcache: https://github.com/aio-libs/propcache ========= Changelog ========= .. 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/#adding-a-news-entry we named the news folder "changes". WARNING: Don't drop the next directive! .. towncrier release notes start 1.19.0 ====== *(2025-04-05)* Bug fixes --------- - Fixed entire name being re-encoded when using ``yarl.URL.with_suffix()`` -- by `@NTFSvolume `__. *Related issues and pull requests on GitHub:* `#1468 `__. Features -------- - Started building armv7l wheels for manylinux -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1495 `__. Contributor-facing changes -------------------------- - GitHub Actions CI/CD is now configured to manage caching pip-ecosystem dependencies using `re-actors/cache-python-deps`_ -- an action by `@webknjaz `__ that takes into account ABI stability and the exact version of Python runtime. .. _`re-actors/cache-python-deps`: https://github.com/marketplace/actions/cache-python-deps *Related issues and pull requests on GitHub:* `#1471 `__. - Increased minimum `propcache`_ version to 0.2.1 to fix failing tests -- by `@bdraco `__. .. _`propcache`: https://github.com/aio-libs/propcache *Related issues and pull requests on GitHub:* `#1479 `__. - Added all hidden folders to pytest's ``norecursedirs`` to prevent it from trying to collect tests there -- by `@lysnikolaou `__. *Related issues and pull requests on GitHub:* `#1480 `__. Miscellaneous internal changes ------------------------------ - Improved accuracy of type annotations -- by `@Dreamsorcerer `__. *Related issues and pull requests on GitHub:* `#1484 `__. - Improved performance of parsing query strings -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1493 `__, `#1497 `__. - Improved performance of the C unquoter -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1496 `__, `#1498 `__. ---- 1.18.3 ====== *(2024-12-01)* Bug fixes --------- - Fixed uppercase ASCII hosts being rejected by ``URL.build()()`` and ``yarl.URL.with_host()`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#954 `__, `#1442 `__. Miscellaneous internal changes ------------------------------ - Improved performances of multiple path properties on cache miss -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1443 `__. ---- 1.18.2 ====== *(2024-11-29)* No significant changes. ---- 1.18.1 ====== *(2024-11-29)* Miscellaneous internal changes ------------------------------ - Improved cache performance when ``~yarl.URL`` objects are constructed from ``yarl.URL.build()`` with ``encoded=True`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1432 `__. - Improved cache performance for operations that produce a new ``~yarl.URL`` object -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1434 `__, `#1436 `__. ---- 1.18.0 ====== *(2024-11-21)* Features -------- - Added ``keep_query`` and ``keep_fragment`` flags in the ``yarl.URL.with_path()``, ``yarl.URL.with_name()`` and ``yarl.URL.with_suffix()`` methods, allowing users to optionally retain the query string and fragment in the resulting URL when replacing the path -- by `@paul-nameless `__. *Related issues and pull requests on GitHub:* `#111 `__, `#1421 `__. Contributor-facing changes -------------------------- - Started running downstream ``aiohttp`` tests in CI -- by `@Cycloctane `__. *Related issues and pull requests on GitHub:* `#1415 `__. Miscellaneous internal changes ------------------------------ - Improved performance of converting ``~yarl.URL`` to a string -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1422 `__. ---- 1.17.2 ====== *(2024-11-17)* Bug fixes --------- - Stopped implicitly allowing the use of Cython pre-release versions when building the distribution package -- by `@ajsanchezsanz `__ and `@markgreene74 `__. *Related issues and pull requests on GitHub:* `#1411 `__, `#1412 `__. - Fixed a bug causing ``~yarl.URL.port`` to return the default port when the given port was zero -- by `@gmacon `__. *Related issues and pull requests on GitHub:* `#1413 `__. Features -------- - Make error messages include details of incorrect type when ``port`` is not int in ``yarl.URL.build()``. -- by `@Cycloctane `__. *Related issues and pull requests on GitHub:* `#1414 `__. Packaging updates and notes for downstreams ------------------------------------------- - Stopped implicitly allowing the use of Cython pre-release versions when building the distribution package -- by `@ajsanchezsanz `__ and `@markgreene74 `__. *Related issues and pull requests on GitHub:* `#1411 `__, `#1412 `__. Miscellaneous internal changes ------------------------------ - Improved performance of the ``yarl.URL.joinpath()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1418 `__. ---- 1.17.1 ====== *(2024-10-30)* Miscellaneous internal changes ------------------------------ - Improved performance of many ``~yarl.URL`` methods -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1396 `__, `#1397 `__, `#1398 `__. - Improved performance of passing a `dict` or `str` to ``yarl.URL.extend_query()`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1401 `__. ---- 1.17.0 ====== *(2024-10-28)* Features -------- - Added ``~yarl.URL.host_port_subcomponent`` which returns the ``3986#section-3.2.2`` host and ``3986#section-3.2.3`` port subcomponent -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1375 `__. ---- 1.16.0 ====== *(2024-10-21)* Bug fixes --------- - Fixed blocking I/O to load Python code when creating a new ``~yarl.URL`` with non-ascii characters in the network location part -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1342 `__. Removals and backward incompatible breaking changes --------------------------------------------------- - Migrated to using a single cache for encoding hosts -- by `@bdraco `__. Passing ``ip_address_size`` and ``host_validate_size`` to ``yarl.cache_configure()`` is deprecated in favor of the new ``encode_host_size`` parameter and will be removed in a future release. For backwards compatibility, the old parameters affect the ``encode_host`` cache size. *Related issues and pull requests on GitHub:* `#1348 `__, `#1357 `__, `#1363 `__. Miscellaneous internal changes ------------------------------ - Improved performance of constructing ``~yarl.URL`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1336 `__. - Improved performance of calling ``yarl.URL.build()`` and constructing unencoded ``~yarl.URL`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1345 `__. - Reworked the internal encoding cache to improve performance on cache hit -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1369 `__. ---- 1.15.5 ====== *(2024-10-18)* Miscellaneous internal changes ------------------------------ - Improved performance of the ``yarl.URL.joinpath()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1304 `__. - Improved performance of the ``yarl.URL.extend_query()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1305 `__. - Improved performance of the ``yarl.URL.origin()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1306 `__. - Improved performance of the ``yarl.URL.with_path()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1307 `__. - Improved performance of the ``yarl.URL.with_query()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1308 `__, `#1328 `__. - Improved performance of the ``yarl.URL.update_query()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1309 `__, `#1327 `__. - Improved performance of the ``yarl.URL.join()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1313 `__. - Improved performance of ``~yarl.URL`` equality checks -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1315 `__. - Improved performance of ``~yarl.URL`` methods that modify the network location -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1316 `__. - Improved performance of the ``yarl.URL.with_fragment()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1317 `__. - Improved performance of calculating the hash of ``~yarl.URL`` objects -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1318 `__. - Improved performance of the ``yarl.URL.relative()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1319 `__. - Improved performance of the ``yarl.URL.with_name()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1320 `__. - Improved performance of ``~yarl.URL.parent`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1321 `__. - Improved performance of the ``yarl.URL.with_scheme()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1322 `__. ---- 1.15.4 ====== *(2024-10-16)* Miscellaneous internal changes ------------------------------ - Improved performance of the quoter when all characters are safe -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1288 `__. - Improved performance of unquoting strings -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1292 `__, `#1293 `__. - Improved performance of calling ``yarl.URL.build()`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1297 `__. ---- 1.15.3 ====== *(2024-10-15)* Bug fixes --------- - Fixed ``yarl.URL.build()`` failing to validate paths must start with a ``/`` when passing ``authority`` -- by `@bdraco `__. The validation only worked correctly when passing ``host``. *Related issues and pull requests on GitHub:* `#1265 `__. Removals and backward incompatible breaking changes --------------------------------------------------- - Removed support for Python 3.8 as it has reached end of life -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1203 `__. Miscellaneous internal changes ------------------------------ - Improved performance of constructing ``~yarl.URL`` when the net location is only the host -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1271 `__. ---- 1.15.2 ====== *(2024-10-13)* Miscellaneous internal changes ------------------------------ - Improved performance of converting ``~yarl.URL`` to a string -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1234 `__. - Improved performance of ``yarl.URL.joinpath()`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1248 `__, `#1250 `__. - Improved performance of constructing query strings from ``~multidict.MultiDict`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1256 `__. - Improved performance of constructing query strings with ``int`` values -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1259 `__. ---- 1.15.1 ====== *(2024-10-12)* Miscellaneous internal changes ------------------------------ - Improved performance of calling ``yarl.URL.build()`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1222 `__. - Improved performance of all ``~yarl.URL`` methods that create new ``~yarl.URL`` objects -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1226 `__. - Improved performance of ``~yarl.URL`` methods that modify the network location -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1229 `__. ---- 1.15.0 ====== *(2024-10-11)* Bug fixes --------- - Fixed validation with ``yarl.URL.with_scheme()`` when passed scheme is not lowercase -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1189 `__. Features -------- - Started building ``armv7l`` wheels -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1204 `__. Miscellaneous internal changes ------------------------------ - Improved performance of constructing unencoded ``~yarl.URL`` objects -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1188 `__. - Added a cache for parsing hosts to reduce overhead of encoding ``~yarl.URL`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1190 `__. - Improved performance of constructing query strings from ``~collections.abc.Mapping`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1193 `__. - Improved performance of converting ``~yarl.URL`` objects to strings -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1198 `__. ---- 1.14.0 ====== *(2024-10-08)* Packaging updates and notes for downstreams ------------------------------------------- - Switched to using the ``propcache`` package for property caching -- by `@bdraco `__. The ``propcache`` package is derived from the property caching code in ``yarl`` and has been broken out to avoid maintaining it for multiple projects. *Related issues and pull requests on GitHub:* `#1169 `__. Contributor-facing changes -------------------------- - Started testing with Hypothesis -- by `@webknjaz `__ and `@bdraco `__. Special thanks to `@Zac-HD `__ for helping us get started with this framework. *Related issues and pull requests on GitHub:* `#860 `__. Miscellaneous internal changes ------------------------------ - Improved performance of ``yarl.URL.is_default_port()`` when no explicit port is set -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1168 `__. - Improved performance of converting ``~yarl.URL`` to a string when no explicit port is set -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1170 `__. - Improved performance of the ``yarl.URL.origin()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1175 `__. - Improved performance of encoding hosts -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1176 `__. ---- 1.13.1 ====== *(2024-09-27)* Miscellaneous internal changes ------------------------------ - Improved performance of calling ``yarl.URL.build()`` with ``authority`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1163 `__. ---- 1.13.0 ====== *(2024-09-26)* Bug fixes --------- - Started rejecting ASCII hostnames with invalid characters. For host strings that look like authority strings, the exception message includes advice on what to do instead -- by `@mjpieters `__. *Related issues and pull requests on GitHub:* `#880 `__, `#954 `__. - Fixed IPv6 addresses missing brackets when the ``~yarl.URL`` was converted to a string -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1157 `__, `#1158 `__. Features -------- - Added ``~yarl.URL.host_subcomponent`` which returns the ``3986#section-3.2.2`` host subcomponent -- by `@bdraco `__. The only current practical difference between ``~yarl.URL.raw_host`` and ``~yarl.URL.host_subcomponent`` is that IPv6 addresses are returned bracketed. *Related issues and pull requests on GitHub:* `#1159 `__. ---- 1.12.1 ====== *(2024-09-23)* No significant changes. ---- 1.12.0 ====== *(2024-09-23)* Features -------- - Added ``~yarl.URL.path_safe`` to be able to fetch the path without ``%2F`` and ``%25`` decoded -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1150 `__. Removals and backward incompatible breaking changes --------------------------------------------------- - Restore decoding ``%2F`` (``/``) in ``URL.path`` -- by `@bdraco `__. This change restored the behavior before `#1057 `__. *Related issues and pull requests on GitHub:* `#1151 `__. Miscellaneous internal changes ------------------------------ - Improved performance of processing paths -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1143 `__. ---- 1.11.1 ====== *(2024-09-09)* Bug fixes --------- - Allowed scheme replacement for relative URLs if the scheme does not require a host -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#280 `__, `#1138 `__. - Allowed empty host for URL schemes other than the special schemes listed in the WHATWG URL spec -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1136 `__. Features -------- - Loosened restriction on integers as query string values to allow classes that implement ``__int__`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1139 `__. Miscellaneous internal changes ------------------------------ - Improved performance of normalizing paths -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1137 `__. ---- 1.11.0 ====== *(2024-09-08)* Features -------- - Added ``URL.extend_query()()`` method, which can be used to extend parameters without replacing same named keys -- by `@bdraco `__. This method was primarily added to replace the inefficient hand rolled method currently used in ``aiohttp``. *Related issues and pull requests on GitHub:* `#1128 `__. Miscellaneous internal changes ------------------------------ - Improved performance of the Cython ``cached_property`` implementation -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1122 `__. - Simplified computing ports by removing unnecessary code -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1123 `__. - Improved performance of encoding non IPv6 hosts -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1125 `__. - Improved performance of ``URL.build()()`` when the path, query string, or fragment is an empty string -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1126 `__. - Improved performance of the ``URL.update_query()()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1130 `__. - Improved performance of processing query string changes when arguments are ``str`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1131 `__. ---- 1.10.0 ====== *(2024-09-06)* Bug fixes --------- - Fixed joining a path when the existing path was empty -- by `@bdraco `__. A regression in ``URL.join()()`` was introduced in `#1082 `__. *Related issues and pull requests on GitHub:* `#1118 `__. Features -------- - Added ``URL.without_query_params()()`` method, to drop some parameters from query string -- by `@hongquan `__. *Related issues and pull requests on GitHub:* `#774 `__, `#898 `__, `#1010 `__. - The previously protected types ``_SimpleQuery``, ``_QueryVariable``, and ``_Query`` are now available for use externally as ``SimpleQuery``, ``QueryVariable``, and ``Query`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1050 `__, `#1113 `__. Contributor-facing changes -------------------------- - Replaced all ``~typing.Optional`` with ``~typing.Union`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1095 `__. Miscellaneous internal changes ------------------------------ - Significantly improved performance of parsing the network location -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1112 `__. - Added internal types to the cache to prevent future refactoring errors -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1117 `__. ---- 1.9.11 ====== *(2024-09-04)* Bug fixes --------- - Fixed a ``TypeError`` with ``MultiDictProxy`` and Python 3.8 -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1084 `__, `#1105 `__, `#1107 `__. Miscellaneous internal changes ------------------------------ - Improved performance of encoding hosts -- by `@bdraco `__. Previously, the library would unconditionally try to parse a host as an IP Address. The library now avoids trying to parse a host as an IP Address if the string is not in one of the formats described in ``3986#section-3.2.2``. *Related issues and pull requests on GitHub:* `#1104 `__. ---- 1.9.10 ====== *(2024-09-04)* Bug fixes --------- - ``URL.join()()`` has been changed to match ``3986`` and align with ``/ operation()`` and ``URL.joinpath()()`` when joining URLs with empty segments. Previously ``urllib.parse.urljoin`` was used, which has known issues with empty segments (`python/cpython#84774 `_). Due to the semantics of ``URL.join()()``, joining an URL with scheme requires making it relative, prefixing with ``./``. .. code-block:: pycon >>> URL("https://web.archive.org/web/").join(URL("./https://github.com/aio-libs/yarl")) URL('https://web.archive.org/web/https://github.com/aio-libs/yarl') Empty segments are honored in the base as well as the joined part. .. code-block:: pycon >>> URL("https://web.archive.org/web/https://").join(URL("github.com/aio-libs/yarl")) URL('https://web.archive.org/web/https://github.com/aio-libs/yarl') -- by `@commonism `__ This change initially appeared in 1.9.5 but was reverted in 1.9.6 to resolve a problem with query string handling. *Related issues and pull requests on GitHub:* `#1039 `__, `#1082 `__. Features -------- - Added ``~yarl.URL.absolute`` which is now preferred over ``URL.is_absolute()`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1100 `__. ---- 1.9.9 ===== *(2024-09-04)* Bug fixes --------- - Added missing type on ``~yarl.URL.port`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1097 `__. ---- 1.9.8 ===== *(2024-09-03)* Features -------- - Covered the ``~yarl.URL`` object with types -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1084 `__. - Cache parsing of IP Addresses when encoding hosts -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1086 `__. Contributor-facing changes -------------------------- - Covered the ``~yarl.URL`` object with types -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1084 `__. Miscellaneous internal changes ------------------------------ - Improved performance of handling ports -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1081 `__. ---- 1.9.7 ===== *(2024-09-01)* Removals and backward incompatible breaking changes --------------------------------------------------- - Removed support ``3986#section-3.2.3`` port normalization when the scheme is not one of ``http``, ``https``, ``wss``, or ``ws`` -- by `@bdraco `__. Support for port normalization was recently added in `#1033 `__ and contained code that would do blocking I/O if the scheme was not one of the four listed above. The code has been removed because this library is intended to be safe for usage with ``asyncio``. *Related issues and pull requests on GitHub:* `#1076 `__. Miscellaneous internal changes ------------------------------ - Improved performance of property caching -- by `@bdraco `__. The ``reify`` implementation from ``aiohttp`` was adapted to replace the internal ``cached_property`` implementation. *Related issues and pull requests on GitHub:* `#1070 `__. ---- 1.9.6 ===== *(2024-08-30)* Bug fixes --------- - Reverted ``3986`` compatible ``URL.join()()`` honoring empty segments which was introduced in `#1039 `__. This change introduced a regression handling query string parameters with joined URLs. The change was reverted to maintain compatibility with the previous behavior. *Related issues and pull requests on GitHub:* `#1067 `__. ---- 1.9.5 ===== *(2024-08-30)* Bug fixes --------- - Joining URLs with empty segments has been changed to match ``3986``. Previously empty segments would be removed from path, breaking use-cases such as .. code-block:: python URL("https://web.archive.org/web/") / "https://github.com/" Now ``/ operation()`` and ``URL.joinpath()()`` keep empty segments, but do not introduce new empty segments. e.g. .. code-block:: python URL("https://example.org/") / "" does not introduce an empty segment. -- by `@commonism `__ and `@youtux `__ *Related issues and pull requests on GitHub:* `#1026 `__. - The default protocol ports of well-known URI schemes are now taken into account during the normalization of the URL string representation in accordance with ``3986#section-3.2.3``. Specified ports are removed from the ``str`` representation of a ``~yarl.URL`` if the port matches the scheme's default port -- by `@commonism `__. *Related issues and pull requests on GitHub:* `#1033 `__. - ``URL.join()()`` has been changed to match ``3986`` and align with ``/ operation()`` and ``URL.joinpath()()`` when joining URLs with empty segments. Previously ``urllib.parse.urljoin`` was used, which has known issues with empty segments (`python/cpython#84774 `_). Due to the semantics of ``URL.join()()``, joining an URL with scheme requires making it relative, prefixing with ``./``. .. code-block:: pycon >>> URL("https://web.archive.org/web/").join(URL("./https://github.com/aio-libs/yarl")) URL('https://web.archive.org/web/https://github.com/aio-libs/yarl') Empty segments are honored in the base as well as the joined part. .. code-block:: pycon >>> URL("https://web.archive.org/web/https://").join(URL("github.com/aio-libs/yarl")) URL('https://web.archive.org/web/https://github.com/aio-libs/yarl') -- by `@commonism `__ *Related issues and pull requests on GitHub:* `#1039 `__. Removals and backward incompatible breaking changes --------------------------------------------------- - Stopped decoding ``%2F`` (``/``) in ``URL.path``, as this could lead to code incorrectly treating it as a path separator -- by `@Dreamsorcerer `__. *Related issues and pull requests on GitHub:* `#1057 `__. - Dropped support for Python 3.7 -- by `@Dreamsorcerer `__. *Related issues and pull requests on GitHub:* `#1016 `__. Improved documentation ---------------------- - On the ``Contributing docs`` page, a link to the ``Towncrier philosophy`` has been fixed. *Related issues and pull requests on GitHub:* `#981 `__. - The pre-existing ``/ magic method()`` has been documented in the API reference -- by `@commonism `__. *Related issues and pull requests on GitHub:* `#1026 `__. Packaging updates and notes for downstreams ------------------------------------------- - A flaw in the logic for copying the project directory into a temporary folder that led to infinite recursion when ``TMPDIR`` was set to a project subdirectory path. This was happening in Fedora and its downstream due to the use of `pyproject-rpm-macros `__. It was only reproducible with ``pip wheel`` and was not affecting the ``pyproject-build`` users. -- by `@hroncok `__ and `@webknjaz `__ *Related issues and pull requests on GitHub:* `#992 `__, `#1014 `__. - Support Python 3.13 and publish non-free-threaded wheels *Related issues and pull requests on GitHub:* `#1054 `__. Contributor-facing changes -------------------------- - The CI/CD setup has been updated to test ``arm64`` wheels under macOS 14, except for Python 3.7 that is unsupported in that environment -- by `@webknjaz `__. *Related issues and pull requests on GitHub:* `#1015 `__. - Removed unused type ignores and casts -- by `@hauntsaninja `__. *Related issues and pull requests on GitHub:* `#1031 `__. Miscellaneous internal changes ------------------------------ - ``port``, ``scheme``, and ``raw_host`` are now ``cached_property`` -- by `@bdraco `__. ``aiohttp`` accesses these properties quite often, which cause ``urllib`` to build the ``_hostinfo`` property every time. ``port``, ``scheme``, and ``raw_host`` are now cached properties, which will improve performance. *Related issues and pull requests on GitHub:* `#1044 `__, `#1058 `__. ---- 1.9.4 (2023-12-06) ================== Bug fixes --------- - Started raising ``TypeError`` when a string value is passed into ``yarl.URL.build()`` as the ``port`` argument -- by `@commonism `__. Previously the empty string as port would create malformed URLs when rendered as string representations. (`#883 `__) Packaging updates and notes for downstreams ------------------------------------------- - The leading ``--`` has been dropped from the `PEP 517 `__ in-tree build backend config setting names. ``--pure-python`` is now just ``pure-python`` -- by `@webknjaz `__. The usage now looks as follows: .. code-block:: console $ python -m build \ --config-setting=pure-python=true \ --config-setting=with-cython-tracing=true (`#963 `__) Contributor-facing changes -------------------------- - A step-by-step ``Release Guide`` guide has been added, describing how to release *yarl* -- by `@webknjaz `__. This is primarily targeting maintainers. (`#960 `__) - Coverage collection has been implemented for the Cython modules -- by `@webknjaz `__. It will also be reported to Codecov from any non-release CI jobs. To measure coverage in a development environment, *yarl* can be installed in editable mode: .. code-block:: console $ python -Im pip install -e . Editable install produces C-files required for the Cython coverage plugin to map the measurements back to the PYX-files. `#961 `__ - It is now possible to request line tracing in Cython builds using the ``with-cython-tracing`` `PEP 517 `__ config setting -- `@webknjaz `__. This can be used in CI and development environment to measure coverage on Cython modules, but is not normally useful to the end-users or downstream packagers. Here's a usage example: .. code-block:: console $ python -Im pip install . --config-settings=with-cython-tracing=true For editable installs, this setting is on by default. Otherwise, it's off unless requested explicitly. The following produces C-files required for the Cython coverage plugin to map the measurements back to the PYX-files: .. code-block:: console $ python -Im pip install -e . Alternatively, the ``YARL_CYTHON_TRACING=1`` environment variable can be set to do the same as the `PEP 517 `__ config setting. `#962 `__ 1.9.3 (2023-11-20) ================== Bug fixes --------- - Stopped dropping trailing slashes in ``yarl.URL.joinpath()`` -- by `@gmacon `__. (`#862 `__, `#866 `__) - Started accepting string subclasses in ``yarl.URL.__truediv__()`` operations (``URL / segment``) -- by `@mjpieters `__. (`#871 `__, `#884 `__) - Fixed the human representation of URLs with square brackets in usernames and passwords -- by `@mjpieters `__. (`#876 `__, `#882 `__) - Updated type hints to include ``URL.missing_port()``, ``URL.__bytes__()`` and the ``encoding`` argument to ``yarl.URL.joinpath()`` -- by `@mjpieters `__. (`#891 `__) Packaging updates and notes for downstreams ------------------------------------------- - Integrated Cython 3 to enable building *yarl* under Python 3.12 -- by `@mjpieters `__. (`#829 `__, `#881 `__) - Declared modern ``setuptools.build_meta`` as the `PEP 517 `__ build backend in ``pyproject.toml`` explicitly -- by `@webknjaz `__. (`#886 `__) - Converted most of the packaging setup into a declarative ``setup.cfg`` config -- by `@webknjaz `__. (`#890 `__) - The packaging is replaced from an old-fashioned ``setup.py`` to an in-tree `PEP 517 `__ build backend -- by `@webknjaz `__. Whenever the end-users or downstream packagers need to build ``yarl`` from source (a Git checkout or an sdist), they may pass a ``config_settings`` flag ``--pure-python``. If this flag is not set, a C-extension will be built and included into the distribution. Here is how this can be done with ``pip``: .. code-block:: console $ python -m pip install . --config-settings=--pure-python=false This will also work with ``-e | --editable``. The same can be achieved via ``pypa/build``: .. code-block:: console $ python -m build --config-setting=--pure-python=false Adding ``-w | --wheel`` can force ``pypa/build`` produce a wheel from source directly, as opposed to building an ``sdist`` and then building from it. (`#893 `__) .. attention:: v1.9.3 was the only version using the ``--pure-python`` setting name. Later versions dropped the ``--`` prefix, making it just ``pure-python``. - Declared Python 3.12 supported officially in the distribution package metadata -- by `@edgarrmondragon `__. (`#942 `__) Contributor-facing changes -------------------------- - A regression test for no-host URLs was added per `#821 `__ and ``3986`` -- by `@kenballus `__. (`#821 `__, `#822 `__) - Started testing *yarl* against Python 3.12 in CI -- by `@mjpieters `__. (`#881 `__) - All Python 3.12 jobs are now marked as required to pass in CI -- by `@edgarrmondragon `__. (`#942 `__) - MyST is now integrated in Sphinx -- by `@webknjaz `__. This allows the contributors to author new documents in Markdown when they have difficulties with going straight RST. (`#953 `__) 1.9.2 (2023-04-25) ================== Bugfixes -------- - Fix regression with ``yarl.URL.__truediv__()`` and absolute URLs with empty paths causing the raw path to lack the leading ``/``. (`#854 `_) 1.9.1 (2023-04-21) ================== Bugfixes -------- - Marked tests that fail on older Python patch releases (< 3.7.10, < 3.8.8 and < 3.9.2) as expected to fail due to missing a security fix for CVE-2021-23336. (`#850 `_) 1.9.0 (2023-04-19) ================== This release was never published to PyPI, due to issues with the build process. Features -------- - Added ``URL.joinpath(*elements)``, to create a new URL appending multiple path elements. (`#704 `_) - Made ``URL.__truediv__()()`` return ``NotImplemented`` if called with an unsupported type — by `@michaeljpeters `__. (`#832 `_) Bugfixes -------- - Path normalization for absolute URLs no longer raises a ValueError exception when ``..`` segments would otherwise go beyond the URL path root. (`#536 `_) - Fixed an issue with update_query() not getting rid of the query when argument is None. (`#792 `_) - Added some input restrictions on with_port() function to prevent invalid boolean inputs or out of valid port inputs; handled incorrect 0 port representation. (`#793 `_) - Made ``yarl.URL.build()`` raise a ``TypeError`` if the ``host`` argument is ``None`` — by `@paulpapacz `__. (`#808 `_) - Fixed an issue with ``update_query()`` getting rid of the query when the argument is empty but not ``None``. (`#845 `_) Misc ---- - `#220 `_ 1.8.2 (2022-12-03) ================== This is the first release that started shipping wheels for Python 3.11. 1.8.1 (2022-08-01) ================== Misc ---- - `#694 `_, `#699 `_, `#700 `_, `#701 `_, `#702 `_, `#703 `_, `#739 `_ 1.8.0 (2022-08-01) ================== Features -------- - Added ``URL.raw_suffix``, ``URL.suffix``, ``URL.raw_suffixes``, ``URL.suffixes``, ``URL.with_suffix``. (`#613 `_) Improved Documentation ---------------------- - Fixed broken internal references to ``yarl.URL.human_repr()``. (`#665 `_) - Fixed broken external references to ``multidict:index`` docs. (`#665 `_) Deprecations and Removals ------------------------- - Dropped Python 3.6 support. (`#672 `_) Misc ---- - `#646 `_, `#699 `_, `#701 `_ 1.7.2 (2021-11-01) ================== Bugfixes -------- - Changed call in ``with_port()`` to stop reencoding parts of the URL that were already encoded. (`#623 `_) 1.7.1 (2021-10-07) ================== Bugfixes -------- - Fix 1.7.0 build error 1.7.0 (2021-10-06) ================== Features -------- - Add ``__bytes__()`` magic method so that ``bytes(url)`` will work and use optimal ASCII encoding. (`#582 `_) - Started shipping platform-specific arm64 wheels for Apple Silicon. (`#622 `_) - Started shipping platform-specific wheels with the ``musl`` tag targeting typical Alpine Linux runtimes. (`#622 `_) - Added support for Python 3.10. (`#622 `_) 1.6.3 (2020-11-14) ================== Bugfixes -------- - No longer loose characters when decoding incorrect percent-sequences (like ``%e2%82%f8``). All non-decodable percent-sequences are now preserved. `#517 `_ - Provide x86 Windows wheels. `#535 `_ ---- 1.6.2 (2020-10-12) ================== Bugfixes -------- - Provide generated ``.c`` files in TarBall distribution. `#530 `_ 1.6.1 (2020-10-12) ================== Features -------- - Provide wheels for ``aarch64``, ``i686``, ``ppc64le``, ``s390x`` architectures on Linux as well as ``x86_64``. `#507 `_ - Provide wheels for Python 3.9. `#526 `_ Bugfixes -------- - ``human_repr()`` now always produces valid representation equivalent to the original URL (if the original URL is valid). `#511 `_ - Fixed requoting a single percent followed by a percent-encoded character in the Cython implementation. `#514 `_ - Fix ValueError when decoding ``%`` which is not followed by two hexadecimal digits. `#516 `_ - Fix decoding ``%`` followed by a space and hexadecimal digit. `#520 `_ - Fix annotation of ``with_query()``/``update_query()`` methods for ``key=[val1, val2]`` case. `#528 `_ Removal ------- - Drop Python 3.5 support; Python 3.6 is the minimal supported Python version. ---- 1.6.0 (2020-09-23) ================== Features -------- - Allow for int and float subclasses in query, while still denying bool. `#492 `_ Bugfixes -------- - Do not requote arguments in ``URL.build()``, ``with_xxx()`` and in ``/`` operator. `#502 `_ - Keep IPv6 brackets in ``origin()``. `#504 `_ ---- 1.5.1 (2020-08-01) ================== Bugfixes -------- - Fix including relocated internal ``yarl._quoting_c`` C-extension into published PyPI dists. `#485 `_ Misc ---- - `#484 `_ ---- 1.5.0 (2020-07-26) ================== Features -------- - Convert host to lowercase on URL building. `#386 `_ - Allow using ``mod`` operator (``%``) for updating query string (an alias for ``update_query()`` method). `#435 `_ - Allow use of sequences such as ``list`` and ``tuple`` in the values of a mapping such as ``dict`` to represent that a key has many values:: url = URL("http://example.com") assert url.with_query({"a": [1, 2]}) == URL("http://example.com/?a=1&a=2") `#443 `_ - Support ``URL.build()`` with scheme and path (creates a relative URL). `#464 `_ - Cache slow IDNA encode/decode calls. `#476 `_ - Add ``@final`` / ``Final`` type hints `#477 `_ - Support URL authority/raw_authority properties and authority argument of ``URL.build()`` method. `#478 `_ - Hide the library implementation details, make the exposed public list very clean. `#483 `_ Bugfixes -------- - Fix tests with newer Python (3.7.6, 3.8.1 and 3.9.0+). `#409 `_ - Fix a bug where query component, passed in a form of mapping or sequence, is unquoted in unexpected way. `#426 `_ - Hide ``Query`` and ``QueryVariable`` type aliases in ``__init__.pyi``, now they are prefixed with underscore. `#431 `_ - Keep IPv6 brackets after updating port/user/password. `#451 `_ ---- 1.4.2 (2019-12-05) ================== Features -------- - Workaround for missing ``str.isascii()`` in Python 3.6 `#389 `_ ---- 1.4.1 (2019-11-29) ================== * Fix regression, make the library work on Python 3.5 and 3.6 again. 1.4.0 (2019-11-29) ================== * Distinguish an empty password in URL from a password not provided at all (#262) * Fixed annotations for optional parameters of ``URL.build`` (#309) * Use None as default value of ``user`` parameter of ``URL.build`` (#309) * Enforce building C Accelerated modules when installing from source tarball, use ``YARL_NO_EXTENSIONS`` environment variable for falling back to (slower) Pure Python implementation (#329) * Drop Python 3.5 support * Fix quoting of plus in path by pure python version (#339) * Don't create a new URL if fragment is unchanged (#292) * Included in error message the path that produces starting slash forbidden error (#376) * Skip slow IDNA encoding for ASCII-only strings (#387) 1.3.0 (2018-12-11) ================== * Fix annotations for ``query`` parameter (#207) * An incoming query sequence can have int variables (the same as for Mapping type) (#208) * Add ``URL.explicit_port`` property (#218) * Give a friendlier error when port can't be converted to int (#168) * ``bool(URL())`` now returns ``False`` (#272) 1.2.6 (2018-06-14) ================== * Drop Python 3.4 trove classifier (#205) 1.2.5 (2018-05-23) ================== * Fix annotations for ``build`` (#199) 1.2.4 (2018-05-08) ================== * Fix annotations for ``cached_property`` (#195) 1.2.3 (2018-05-03) ================== * Accept ``str`` subclasses in ``URL`` constructor (#190) 1.2.2 (2018-05-01) ================== * Fix build 1.2.1 (2018-04-30) ================== * Pin minimal required Python to 3.5.3 (#189) 1.2.0 (2018-04-30) ================== * Forbid inheritance, replace ``__init__`` with ``__new__`` (#171) * Support PEP-561 (provide type hinting marker) (#182) 1.1.1 (2018-02-17) ================== * Fix performance regression: don't encode empty ``netloc`` (#170) 1.1.0 (2018-01-21) ================== * Make pure Python quoter consistent with Cython version (#162) 1.0.0 (2018-01-15) ================== * Use fast path if quoted string does not need requoting (#154) * Speed up quoting/unquoting by ``_Quoter`` and ``_Unquoter`` classes (#155) * Drop ``yarl.quote`` and ``yarl.unquote`` public functions (#155) * Add custom string writer, reuse static buffer if available (#157) Code is 50-80 times faster than Pure Python version (was 4-5 times faster) * Don't recode IP zone (#144) * Support ``encoded=True`` in ``yarl.URL.build()`` (#158) * Fix updating query with multiple keys (#160) 0.18.0 (2018-01-10) =================== * Fallback to IDNA 2003 if domain name is not IDNA 2008 compatible (#152) 0.17.0 (2017-12-30) =================== * Use IDNA 2008 for domain name processing (#149) 0.16.0 (2017-12-07) =================== * Fix raising ``TypeError`` by ``url.query_string()`` after ``url.with_query({})`` (empty mapping) (#141) 0.15.0 (2017-11-23) =================== * Add ``raw_path_qs`` attribute (#137) 0.14.2 (2017-11-14) =================== * Restore ``strict`` parameter as no-op in ``quote`` / ``unquote`` 0.14.1 (2017-11-13) =================== * Restore ``strict`` parameter as no-op for sake of compatibility with aiohttp 2.2 0.14.0 (2017-11-11) =================== * Drop strict mode (#123) * Fix ``"ValueError: Unallowed PCT %"`` when there's a ``"%"`` in the URL (#124) 0.13.0 (2017-10-01) =================== * Document ``encoded`` parameter (#102) * Support relative URLs like ``'?key=value'`` (#100) * Unsafe encoding for QS fixed. Encode ``;`` character in value parameter (#104) * Process passwords without user names (#95) 0.12.0 (2017-06-26) =================== * Properly support paths without leading slash in ``URL.with_path()`` (#90) * Enable type annotation checks 0.11.0 (2017-06-26) =================== * Normalize path (#86) * Clear query and fragment parts in ``.with_path()`` (#85) 0.10.3 (2017-06-13) =================== * Prevent double URL arguments unquoting (#83) 0.10.2 (2017-05-05) =================== * Unexpected hash behavior (#75) 0.10.1 (2017-05-03) =================== * Unexpected compare behavior (#73) * Do not quote or unquote + if not a query string. (#74) 0.10.0 (2017-03-14) =================== * Added ``URL.build`` class method (#58) * Added ``path_qs`` attribute (#42) 0.9.8 (2017-02-16) ================== * Do not quote ``:`` in path 0.9.7 (2017-02-16) ================== * Load from pickle without _cache (#56) * Percent-encoded pluses in path variables become spaces (#59) 0.9.6 (2017-02-15) ================== * Revert backward incompatible change (BaseURL) 0.9.5 (2017-02-14) ================== * Fix BaseURL rich comparison support 0.9.4 (2017-02-14) ================== * Use BaseURL 0.9.3 (2017-02-14) ================== * Added BaseURL 0.9.2 (2017-02-08) ================== * Remove debug print 0.9.1 (2017-02-07) ================== * Do not lose tail chars (#45) 0.9.0 (2017-02-07) ================== * Allow to quote ``%`` in non strict mode (#21) * Incorrect parsing of query parameters with %3B (;) inside (#34) * Fix core dumps (#41) * ``tmpbuf`` - compiling error (#43) * Added ``URL.update_path()`` method * Added ``URL.update_query()`` method (#47) 0.8.1 (2016-12-03) ================== * Fix broken aiohttp: revert back ``quote`` / ``unquote``. 0.8.0 (2016-12-03) ================== * Support more verbose error messages in ``.with_query()`` (#24) * Don't percent-encode ``@`` and ``:`` in path (#32) * Don't expose ``yarl.quote`` and ``yarl.unquote``, these functions are part of private API 0.7.1 (2016-11-18) ================== * Accept not only ``str`` but all classes inherited from ``str`` also (#25) 0.7.0 (2016-11-07) ================== * Accept ``int`` as value for ``.with_query()`` 0.6.0 (2016-11-07) ================== * Explicitly use UTF8 encoding in ``setup.py`` (#20) * Properly unquote non-UTF8 strings (#19) 0.5.3 (2016-11-02) ================== * Don't use ``typing.NamedTuple`` fields but indexes on URL construction 0.5.2 (2016-11-02) ================== * Inline ``_encode`` class method 0.5.1 (2016-11-02) ================== * Make URL construction faster by removing extra classmethod calls 0.5.0 (2016-11-02) ================== * Add Cython optimization for quoting/unquoting * Provide binary wheels 0.4.3 (2016-09-29) ================== * Fix typing stubs 0.4.2 (2016-09-29) ================== * Expose ``quote()`` and ``unquote()`` as public API 0.4.1 (2016-09-28) ================== * Support empty values in query (``'/path?arg'``) 0.4.0 (2016-09-27) ================== * Introduce ``relative()`` (#16) 0.3.2 (2016-09-27) ================== * Typo fixes #15 0.3.1 (2016-09-26) ================== * Support sequence of pairs as ``with_query()`` parameter 0.3.0 (2016-09-26) ================== * Introduce ``is_default_port()`` 0.2.1 (2016-09-26) ================== * Raise ValueError for URLs like 'http://:8080/' 0.2.0 (2016-09-18) ================== * Avoid doubling slashes when joining paths (#13) * Appending path starting from slash is forbidden (#12) 0.1.4 (2016-09-09) ================== * Add ``kwargs`` support for ``with_query()`` (#10) 0.1.3 (2016-09-07) ================== * Document ``with_query()``, ``with_fragment()`` and ``origin()`` * Allow ``None`` for ``with_query()`` and ``with_fragment()`` 0.1.2 (2016-09-07) ================== * Fix links, tune docs theme. 0.1.1 (2016-09-06) ================== * Update README, old version used obsolete API 0.1.0 (2016-09-06) ================== * The library was deeply refactored, bytes are gone away but all accepted strings are encoded if needed. 0.0.1 (2016-08-30) ================== * The first release. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/README.rst0000644000175100001660000001360314774356277013674 0ustar00runnerdockeryarl ==== The module provides handy URL class for URL parsing and changing. .. image:: https://github.com/aio-libs/yarl/workflows/CI/badge.svg :target: https://github.com/aio-libs/yarl/actions?query=workflow%3ACI :align: right .. image:: https://codecov.io/gh/aio-libs/yarl/graph/badge.svg?flag=pytest :target: https://app.codecov.io/gh/aio-libs/yarl?flags[]=pytest :alt: Codecov coverage for the pytest-driven measurements .. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json :target: https://codspeed.io/aio-libs/yarl .. image:: https://badge.fury.io/py/yarl.svg :target: https://badge.fury.io/py/yarl .. image:: https://readthedocs.org/projects/yarl/badge/?version=latest :target: https://yarl.aio-libs.org .. image:: https://img.shields.io/pypi/pyversions/yarl.svg :target: https://pypi.python.org/pypi/yarl .. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat :target: https://matrix.to/#/%23aio-libs:matrix.org :alt: Matrix Room — #aio-libs:matrix.org .. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat :target: https://matrix.to/#/%23aio-libs-space:matrix.org :alt: Matrix Space — #aio-libs-space:matrix.org Introduction ------------ Url is constructed from ``str``: .. code-block:: pycon >>> from yarl import URL >>> url = URL('https://www.python.org/~guido?arg=1#frag') >>> url URL('https://www.python.org/~guido?arg=1#frag') All url parts: *scheme*, *user*, *password*, *host*, *port*, *path*, *query* and *fragment* are accessible by properties: .. code-block:: pycon >>> url.scheme 'https' >>> url.host 'www.python.org' >>> url.path '/~guido' >>> url.query_string 'arg=1' >>> url.query >>> url.fragment 'frag' All url manipulations produce a new url object: .. code-block:: pycon >>> url = URL('https://www.python.org') >>> url / 'foo' / 'bar' URL('https://www.python.org/foo/bar') >>> url / 'foo' % {'bar': 'baz'} URL('https://www.python.org/foo?bar=baz') Strings passed to constructor and modification methods are automatically encoded giving canonical representation as result: .. code-block:: pycon >>> url = URL('https://www.python.org/шлях') >>> url URL('https://www.python.org/%D1%88%D0%BB%D1%8F%D1%85') Regular properties are *percent-decoded*, use ``raw_`` versions for getting *encoded* strings: .. code-block:: pycon >>> url.path '/шлях' >>> url.raw_path '/%D1%88%D0%BB%D1%8F%D1%85' Human readable representation of URL is available as ``.human_repr()``: .. code-block:: pycon >>> url.human_repr() 'https://www.python.org/шлях' For full documentation please read https://yarl.aio-libs.org. Installation ------------ :: $ pip install yarl The library is Python 3 only! PyPI contains binary wheels for Linux, Windows and MacOS. If you want to install ``yarl`` on another operating system where wheels are not provided, the tarball will be used to compile the library from the source code. It requires a C compiler and and Python headers installed. To skip the compilation you must explicitly opt-in by using a PEP 517 configuration setting ``pure-python``, or setting the ``YARL_NO_EXTENSIONS`` environment variable to a non-empty value, e.g.: .. code-block:: console $ pip install yarl --config-settings=pure-python=false Please note that the pure-Python (uncompiled) version is much slower. However, PyPy always uses a pure-Python implementation, and, as such, it is unaffected by this variable. Dependencies ------------ YARL requires multidict_ and propcache_ libraries. API documentation ------------------ The documentation is located at https://yarl.aio-libs.org. Why isn't boolean supported by the URL query API? ------------------------------------------------- There is no standard for boolean representation of boolean values. Some systems prefer ``true``/``false``, others like ``yes``/``no``, ``on``/``off``, ``Y``/``N``, ``1``/``0``, etc. ``yarl`` cannot make an unambiguous decision on how to serialize ``bool`` values because it is specific to how the end-user's application is built and would be different for different apps. The library doesn't accept booleans in the API; a user should convert bools into strings using own preferred translation protocol. Comparison with other URL libraries ------------------------------------ * furl (https://pypi.python.org/pypi/furl) The library has rich functionality but the ``furl`` object is mutable. I'm afraid to pass this object into foreign code: who knows if the code will modify my url in a terrible way while I just want to send URL with handy helpers for accessing URL properties. ``furl`` has other non-obvious tricky things but the main objection is mutability. * URLObject (https://pypi.python.org/pypi/URLObject) URLObject is immutable, that's pretty good. Every URL change generates a new URL object. But the library doesn't do any decode/encode transformations leaving the end user to cope with these gory details. Source code ----------- The project is hosted on GitHub_ Please file an issue on the `bug tracker `_ if you have found a bug or have some suggestion in order to improve the library. Discussion list --------------- *aio-libs* google group: https://groups.google.com/forum/#!forum/aio-libs Feel free to post your questions and ideas here. Authors and License ------------------- The ``yarl`` package is written by Andrew Svetlov. It's *Apache 2* licensed and freely available. .. _GitHub: https://github.com/aio-libs/yarl .. _multidict: https://github.com/aio-libs/multidict .. _propcache: https://github.com/aio-libs/propcache ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743903941.5852761 yarl-1.19.0/docs/0000755000175100001660000000000014774356306013123 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/docs/Makefile0000644000175100001660000001703314774356277014576 0ustar00runnerdocker# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/yarl.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/yarl.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/yarl" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/yarl" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." spelling: $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling @echo @echo "Build finished." ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743903941.5852761 yarl-1.19.0/docs/_static/0000755000175100001660000000000014774356306014551 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/docs/_static/yarl-icon-128x128.png0000644000175100001660000004271614774356277020130 0ustar00runnerdockerPNG  IHDRL\gAMA a cHRMz&u0`:pQ<bKGD pHYs  tIME 02!. DIDATxidu{rzE7ؚ؈&EʢH–,ɖ4Ǜ&b"D{b~:l2mˢX  @4F[uמg~ܗ/  )_d4]Ysپ3/UݷMD3#~er;*Xkỳ%(2KGh.cKʎS4-jG})Yfdݵc 3;K1 ~戅tSJLZy#]I$oYy̞9 FOTk ۤ=ԝ5ݽWm<'[l29}&  S qP5AU[A z^icRN ~`lx0Ez0Tj/rtluִ/#&0Ҵi G vaoo qDޡ}$~󀌚_Itl(w#yhҳm&gO((EJB ")eWc)~}3DM^gq+lsZ5G$1&0:nŗM.>mWWEJB WԸ 5|Lc ZPf- x|M5޾Da ^ALdYhins5'}r4M8vHo3gYRZiweخ]t/T:@DQknuA^Ug>G>c/) p R 1ARfʁ ӵY``y$R|^{"~jˆZ9i M)Mȸ 5;oM(-!7MsI{z `/K;4Ŵ1عfjG;>^ò2?(xȃ.@^ryskOGR`ؽ6+8'2oK_kOIAAuk{8> 3C&p[Q=0M8/RGoeYǾW)6_k36/L%9x $K9eYCe?*txϱ:/o̗YIX213TEZM [_~O/ (:3)4M$T*QMzI?͟>H52gD)#8gxajHcY R˕;~$Iqo!Z4M4SmYm}7k_*H 1s+Ih![x 6jK AESᱽg}V4a9\`lڛ^ h`2q6BC&L$_a: &j3e~?g~8 pw&^O<8,CsHYb^K-ɦ69iM| 匸d qʖKoI e)168f宿U'Sz[x y>E}vguMt+>I6o "'% ' y9s'5"n3h b!{f@iLwWCK6j?ovIY%IRY5O,Sߤ~U{u߄ 6{_kT~M ~rP'} J#ᷔ\IRY&=}6>5c&km?&RJL}0y4}pT:,yҝz|f M&:+; @Er!|ULǔdSa f  ƿ2I|jZY9x$II:O~B&XIIȆTzLIX]އeWI*d1u.xgsw͸>|OcԻkml5 b ;;xxLK: C jտk=WgGaZEDA__Afsl/yK|M"b9[tXwL2ڜU&~3-U&x)?yEyVpASXo`\"oPU2 !%< c; qW?@Y 'GуyI0'n(pnOe=߭ ջDl &B_3טsȯ ~T ct\GVlmm_UHxA|/lo|^y:{Ѵ&eNA"Wt stA.p0CogLB~2:NEAޜvjZV>+]y҃<"J,&K(M:!z@sĉkO#XcGRHIy ȌZ>>7H,DXxPs|% -$r 6Grz:%>߲ Q,[q o=pIT1ofܥaYr濮$%M.`2PRX%ʧxe (taCfW̓ G,Tuatv*7ƀʇooEL;ܩ˻90ED~0C粒[싟Q*#3aIL*pH;9퐊Ӿ[/p3t}\?ՃjLɃP)aK6#0qNYzDiR!7UB$8$|xsd1vFg OjDkl#9}] $_d bVd +~b_6}6@v83,]zYE.[PS]G/}qPe)z8ҝjؾt‰GrI8w:HiWl_*A֙l0!Ρ۳Nb[qIL3Id )Y5cBן3g>}w$9𨝻݊ý @ * Ԑ[kM=]aXHx!K2%pЉ2g~3o]A#i^?4S61k7 Ьש:s"qje$=ȫ]b\A0Ra^؏v@od+|. 'EY]aTHNGٜubdBܽH̺beݢ,$`*N|t}ht4jll/3/,m“/J<0=>" i n(yu~9⽏'q,"Ba6U2,C؛~m8m`HY[e$!shclE b)ěDD`= 'sjuxW{V%T*{6:?˲cȴd`9Go`Oi9N| _ : 2HFVL`s[ ChZͭԼ{8[3 A"$(6`3^Kw+_"EƐ 22ZՈ..BiW*n^w%c{9;\cd:$+<3ld,%E*s$ )s,RH[FEdC0U?Dl1:4EA؍Ms"^C"j4!y수_ (Oäl%PGKk` +vH__x~UI<:?77WOZuw|?@iCN-p)tn0 =ca_4T@^`W5fhG, lGdÉy, 7AqU!ԥ00Ty؜W;;-S"x^'z^!W[tlS?"{^ d5D^Pg?W9r Cy^5&RrʳwԼ wM);̤YqpS8Bt1.W?K; *M9װ@:"tҙ֛~ e/ݮ:t$^~_m0CH )!+Wgk4.3`Umjswk'!|03IHdFIRŧFOe3!WCi/~OBidX({"{>7iӺ0q4¾Xa=*ͧB_Mv8w-dD8I}F*}?|G(wl`-`=|Y6ۡ4/٬?5 [ޕraa|;~%@B3X~/ HMH月@X b׾{EЀyzҩ `: Y>*m`Y"sM+h8dpz|7͑ /?/!=q$K; ɫ HlfN`x_{zyA י2 _Qibf;8/2oֿVe S@f2>$ɋw3l/-(g?}QxJLjCm;OH2d5ʼ8+0pMpY9u uwwx&%T w/p:>; H5'^[!^lvS%%raQFH)` lBZV~€tiZ{KyzaPz# ^šCęMZ>Oyd`sqZ}S#-n1SL}O[.p WS8W.tI[lx82}<=znqqKT l`rbWٝҴll;xhvD%qk;m0_䈊pA^y U,uջqBeTzMnk (h cs<:T!ltf/~ pZN5(EbXˇ |HaA [n%'rϞKw:IH@NƮWҤ=2Ů%t7j/v2tB-({&$x)jB"Bnj#kX@M6uB gqqXa5` x/3#i퍍{usd3aBT7'NxJ}.<7 /47\àޚ},dE[KtL&u6D 6|7-m_]։P:Vc}yMk fΊ&=;) HX=1/s/_޹Q5a?ZǰZ N|\ !Sl"[݆5SߢU6Jz@=Y740̌4%&<)b7ޝ鐬3NI.*0 Koܽ^f t "wYSJ1TC)Cf#ٻ =-`jS⶟O+"OHdOŒ|W+YS@슎@u=8OVm ΰ>c03`OvkxJ M0 0MA%?-\/.$4-0_z5_-3, 4&ֶံ=+m뉪7 äEH(ZX(+}u0X fF]j.fzxBlƷjRyX;e- jv@`C ftS:F1$v{8a`R+o>1@l5o19tl %Xҿ :.{5aݷ} g0I({: !%:H*`O`f:z,Dg/ćFP&Pۊ`M:fF&:w+QTܧ9`X G3f!jb C9l6CVSmN2 |܁>/_l+?B#x{GZ3!_D6äE7Qlɤ0ˀt<,$;\˟Tf?nNɕJ8 ]vؤdRP(n[#G? w14qQs0&N 7i6c`Pz[&)2[ Z8] ϼH^ʷbkUﱖO"N`6I"u7c2),\$]v3&TA XM|" 15OZ:ޅe#n@>) ܣfs"uz>:9++P\ν˶C}k~e^sbD{Ċ~3(l DC *29$!BuwK(E4qKUFZ(2uHHHx* HODtd3BdyY^# !jr3bqtrC"\`ATxx'<^R&BE]FɭQgMn"Y?_%j㝲70rfᩲ *'l֧YE2H^pbT!~ /F%II %C ` @VB`bv= TY8q6=B'"hͺuָ/ "ZN U:xЯפM--Eq9 I~IS,au}݉0U?ĺOd bP!~TV“Qs ~O}ۇF@ ==C 2S9+Uʼnq炽UT6vc m޽fы|  W*E,h6FE,Ӂl rZMNA!6`S$@*IUȁ eBh¹ (w"& 2YIrX},;ټ!oYzyG7I-93)"$3 HŹey/۵g7V@T`lĵDDRaҾjRDQG90$U6"Bޣx`0n!*d\VBdyS=GpC'|Pl2";Y 9PHHck1՟I1\~f(U=Wk6i33; IB\t GKՉp: )$! *\%1lJ\2BBT_%rwXPFX=s (4hV֠}Bm"<.5&*sE> f]p_ Ϯ3EuP$<1g'X%; Vypm$BHFtJQ8<_^$ưlV? H*NeH_-2+w3`vea$} BJ` /3$ I^m<4c^)fDYl 0F3&1[md`;~Hs޲Ћ<؀ 'w0)>> ޠxUR8k*$=I: l 483&Y*&an\}o*] m>'H'rĻ=3Dj7F Tfes3YnA BeD޶ Le‡-{(u RpwY\·«$5 ([播v"C w msR'd<sxET%م#UvaT `,ïa?&06[$ BH`+B1R?>*-DyFA"`_ ;s3_YRahM,+|'0Lg1 u8 -^X kxæ`G3ĠhoPz`d6AI$fؿ}7bD$H1lF:%\@exp 1[]l2s> 1B7Q1a1/bc]\xc6dI!e"~f6 0̺ P?UWylt6'!:M{H;T;ȭs%tweE1UiR].[3HMNMxs62S7s,as]bi7 9 sorAa3N*C-/G@y1,(pڮe޽Dws bߘMt1qx{xK T e; F? p."ڇ%NPZYA呪6-p@!g?ysu&'F..]h~!MC?I';,o!mj+lNM ]lPUFS R!S?6M^sruƒa3&e ݞΧZ=/vIO KǸ{: ~`FiWJNk`d0IRl`bnLwCSmV;Qs"T(06ȯP<2A-۽`"b06dawrC:#3Emv0maKݝ4Ÿ"IPTZ.TdƷ|HYx+~~ի #"3Xyi R gVƨX}&f&,Ac*rj<6C繷Z:"]Nvv5VEnazU: @CB« Z xf ?- &-rz륋=rI BCӉu;[KZY/f 5DyDWe4aGց9p> 8]iW\=0(Ŵ.RjGo`Z|h{i. ~YwyjguK6]H!9ހ߀&?mS͚,y<ŀ P$R3B%Waf yz :/6$UX Q2!:K.%n#y_ڍ7̙\Jμ9605C?O|BbhO<Π3j_NuG1+ꪾ2v.ETPRm y!o K}$A*sJ"T}b"߀%x˩!YQ鳎2)B9!t/#Ajt8 vXiɅۻ DDh`poPxFeݰVsB7U9Gyz0(\8b ^n]64#9ҰE&!MGa#a24<ϑ cEMTXe ٗFeYgxփD+/3 \ p R+3gθ-λW#w"UC՛qr-KKP09>.5ACGs}[_ԕÎνK (O }^<*^A*+ ^ի(;\hRY6_#zcf.\W] vT6'=^{֮ǀx>?pH8O"`˔ӸADUqMբ*`OmXdOp%u `@H w)w{: Q1\K=yPJ}[YY)GdVF ^iףTÕw\n>~]AD:Ư^)I6|ƷlQ(-ރhN#9G#ksRl3. ˨פhOS(m<íWA x억V[y73Z뙻ĔyӚTeQԎhmeXjp,T' f r%S/ y)]aqoma؟U`j$DX\l>g7Ɖ , h]*o>U<\aʟfVXe3j4G;6,jȷz~:`sΆȜ7YaE-A"(NM`'sm"sC\=8Fy5^L^`Q;uFtϰ؟8nu/­e rvA̙3O<1FՖ)# _6^jd`H޹B -Gkߣ**nĐB^9FD힐Vg -= N"}>6JeJPuMnbs7ݤC,G9`'׾9^vUTMNGTmY]熆6> v<ӧ/?ꓲ /pfb AHZ^{d 6b@ U/db>hQ,HIS4w=|_^"u5:efZp[y|1m_q#^S\/(]{,Uۗ-ذ$ɕsyd Jg=ϋj\ydO_{"o[Y5G?4LF0Dž^H΂IcW\i01 H1^g "J'W&揿F|/ٍgs{S@w[ذ$m??>Nc=U.+㏊c-wɕ;"i=%@JΈI vEV\SF}7vQ)בm-n #݃L *kbGˑ[q5{m@aV$[x{ p"'eL A=d,N;ԊWnFa_#!H@a\~`De,Ġljq8keV%Y㢴7eggǕ:\ܡJzq0w4C薼ғ8+m+$a vƥ`PUx 9oPkLQj];ٜܲN7b,AjdhSڗ¤X8#hbOʡM 7Y",!!orVѬb%׳ 'T(th<펖6\_H~Qmnn&IR6jf=g "U ٙSTؖHB BݷH5*^b@ dPj2y+eqkkRaxKoBIn Z9S]r_YbbCJ'!=bat)F ^vSR>TH&{-ҟT)7=p̬g13lfGr_QVJm7KRJaZkښ+"* 9{u@qyծ%ir$]hD#5%Kx@,S$镯˧/O2_E썥-gL"5'ekG(6661f%ylGQVesKILs6]b xI)xP尳 )h$I7>~ƥ?""X3[-(ɩkP?hYD8j.|$FQEZjmoop㶏V[* S8y<ϭu>Ino)e_[[ YȪ0Ɏ{ kO"?^ȿaW4u(\&CRAL4^W>G ӬNXIsl5P_s 'M2[vjЃ '~̟;<|cc,xW:4M$zhL(۴}yp[oy +8U>It=CT=[MҌmDLj7C7g5L|.DrXx(2f\F$VYV'niN0xliYfj_ֹ!pCxinllqEHNqZw,Kc5$g/PQq)rx3e97?k{vbb>W9oɰb)Yy̒F*eWg{{n+\r ۥvY`l8 Bl$td|yZx3JI)H(h_s[< _-cئcemaX٠Io} 3{hvbfG;΀Qp}#6$I"+]a$k_ֿ9^G֦Oi@Η?MdGu~j)IlXSjNs'TP^_yn(t;!?`8̆$I$!z>77ꎮ6[&pMNNB)RK"\aC5UYߤ$in@E#Hj4i,s0Ƅah4jZyw)CDu]׽͈{3dFYi=a^w;̜eYz$SJit 4M]+ i]͛^IqDZ[Wjm<}4FFlp1C|x D4C1&'˲,F]#-{p=WS#LdhBroe]?\0 1buMzFμQvk5EԟAS䞜ɀ蔔*I` Mp0%tEXtdate:create2018-10-06T12:06:19+00:00\JZ%tEXtdate:modify2018-10-06T12:06:19+00:00-IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/docs/api.rst0000644000175100001660000010260214774356277014436 0ustar00runnerdocker.. _yarl-api: Public API ========== .. module:: yarl .. currentmodule:: yarl The only public *yarl* class is :class:`URL`: .. doctest:: >>> from yarl import URL .. class:: URL(arg, *, encoded=False) Represents URL as :: [scheme:]//[user[:password]@]host[:port][/path][?query][#fragment] for absolute URLs and :: [/path][?query][#fragment] for relative ones (:ref:`yarl-api-relative-urls`). The URL structure is:: http://user:pass@example.com:8042/over/there?name=ferret#nose \__/ \__/ \__/ \_________/ \__/\_________/ \_________/ \__/ | | | | | | | | scheme user password host port path query fragment Internally all data are stored as *percent-encoded* strings for *user*, *path*, *query* and *fragment* URL parts and *IDNA-encoded* (:rfc:`5891`) for *host*. Constructor and modification operators perform *encoding* for all parts automatically. The library assumes all data uses *UTF-8* for *percent-encoded* tokens. .. doctest:: >>> URL('http://example.com/path/to/?arg1=a&arg2=b#fragment') URL('http://example.com/path/to/?arg1=a&arg2=b#fragment') Unless URL contain the only *ascii* characters there is no differences. But for *non-ascii* case *encoding* is applied. .. doctest:: >>> str(URL('http://εμπορικόσήμα.eu/шлях/這裡')) 'http://xn--jxagkqfkduily1i.eu/%D1%88%D0%BB%D1%8F%D1%85/%E9%80%99%E8%A3%A1' The same is true for *user*, *password*, *query* and *fragment* parts of URL. Already encoded URL is not changed: .. doctest:: >>> URL('http://xn--jxagkqfkduily1i.eu') URL('http://xn--jxagkqfkduily1i.eu') Use :meth:`~URL.human_repr` for getting human readable representation: .. doctest:: >>> url = URL('http://εμπορικόσήμα.eu/шлях/這裡') >>> str(url) 'http://xn--jxagkqfkduily1i.eu/%D1%88%D0%BB%D1%8F%D1%85/%E9%80%99%E8%A3%A1' >>> url.human_repr() 'http://εμπορικόσήμα.eu/шлях/這裡' .. note:: Sometimes encoding performed by *yarl* is not acceptable for certain WEB server. Passing ``encoded=True`` parameter prevents URL auto-encoding, user is responsible about URL correctness. Don't use this option unless there is no other way for keeping URL attributes not touched. Any URL manipulations don't guarantee correct encoding, URL parts could be re-quoted even if *encoded* parameter was explicitly set. URL properties -------------- There are two kinds of properties: *decoded* and *encoded* (with ``raw_`` prefix): .. attribute:: URL.scheme Scheme for absolute URLs, empty string for relative URLs or URLs starting with ``'//'`` (:ref:`yarl-api-relative-urls`). .. doctest:: >>> URL('http://example.com').scheme 'http' >>> URL('//example.com').scheme '' >>> URL('page.html').scheme '' .. attribute:: URL.user Decoded *user* part of URL, ``None`` if *user* is missing. .. doctest:: >>> URL('http://john@example.com').user 'john' >>> URL('http://бажан@example.com').user 'бажан' >>> URL('http://example.com').user is None True .. attribute:: URL.raw_user Encoded *user* part of URL, ``None`` if *user* is missing. .. doctest:: >>> URL('http://довбуш@example.com').raw_user '%D0%B4%D0%BE%D0%B2%D0%B1%D1%83%D1%88' >>> URL('http://example.com').raw_user is None True .. attribute:: URL.password Decoded *password* part of URL, ``None`` if *user* is missing. .. doctest:: >>> URL('http://john:pass@example.com').password 'pass' >>> URL('http://степан:пароль@example.com').password 'пароль' >>> URL('http://example.com').password is None True .. attribute:: URL.raw_password Encoded *password* part of URL, ``None`` if *user* is missing. .. doctest:: >>> URL('http://user:пароль@example.com').raw_password '%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C' .. attribute:: URL.host Encoded *host* part of URL, ``None`` for relative URLs (:ref:`yarl-api-relative-urls`). Brackets are stripped for IPv6. Host is converted to lowercase, address is validated and converted to compressed form. .. doctest:: >>> URL('http://example.com').host 'example.com' >>> URL('http://хост.домен').host 'хост.домен' >>> URL('page.html').host is None True >>> URL('http://[::1]').host '::1' .. attribute:: URL.raw_host IDNA decoded *host* part of URL, ``None`` for relative URLs (:ref:`yarl-api-relative-urls`). .. doctest:: >>> URL('http://хост.домен').raw_host 'xn--n1agdj.xn--d1acufc' >>> URL('http://[::1]').raw_host '::1' .. attribute:: URL.host_subcomponent :rfc:`3986#section-3.2.2` host subcomponent part of URL, ``None`` for relative URLs (:ref:`yarl-api-relative-urls`). .. doctest:: >>> URL('http://хост.домен').host_subcomponent 'xn--n1agdj.xn--d1acufc' >>> URL('http://[::1]').host_subcomponent '[::1]' .. versionadded:: 1.13 .. attribute:: URL.host_port_subcomponent :rfc:`3986#section-3.2.2` host and :rfc:`3986#section-3.2.3` port subcomponent part of URL, ``None`` for relative URLs (:ref:`yarl-api-relative-urls`). Trailing dots are stripped from the host to ensure this value can be used for an HTTP Host header. The port is omitted if it is the default port for the scheme. .. doctest:: >>> URL('http://хост.домен:81').host_port_subcomponent 'xn--n1agdj.xn--d1acufc:81' >>> URL('https://[::1]:8443').host_port_subcomponent '[::1]:8443' >>> URL('http://example.com./').host_port_subcomponent 'example.com' >>> URL('http://[::1]').host_port_subcomponent '[::1]' .. versionadded:: 1.17 .. attribute:: URL.port *port* part of URL, with scheme-based fallback. ``None`` for relative URLs (:ref:`yarl-api-relative-urls`) or for URLs without explicit port and :attr:`URL.scheme` without :ref:`default port substitution `. .. doctest:: >>> URL('http://example.com:8080').port 8080 >>> URL('http://example.com').port 80 >>> URL('page.html').port is None True .. attribute:: URL.explicit_port *explicit_port* part of URL, without scheme-based fallback. ``None`` for relative URLs (:ref:`yarl-api-relative-urls`) or for URLs without explicit port. .. doctest:: >>> URL('http://example.com:8080').explicit_port 8080 >>> URL('http://example.com').explicit_port is None True >>> URL('page.html').explicit_port is None True .. versionadded:: 1.3 .. attribute:: URL.authority Decoded *authority* part of URL, a combination of *user*, *password*, *host*, and *port*. ``authority = [ user [ ":" password ] "@" ] host [ ":" port ]``. *authority* is empty string if all parts are missing. .. doctest:: >>> URL('http://john:pass@example.com:8000').authority 'john:pass@example.com:8000' .. versionadded:: 1.5 .. attribute:: URL.raw_authority Encoded *authority* part of URL, a combination of *user*, *password*, *host*, and *port*. empty string if all parts are missing. .. doctest:: >>> URL('http://john:pass@хост.домен:8000').raw_authority 'john:pass@xn--n1agdj.xn--d1acufc:8000' .. versionadded:: 1.5 .. attribute:: URL.path Decoded *path* part of URL, ``'/'`` for absolute URLs without *path* part. .. doctest:: >>> URL('http://example.com/path/to').path '/path/to' >>> URL('http://example.com/шлях/сюди').path '/шлях/сюди' >>> URL('http://example.com').path '/' .. warning:: In many situations it is important to distinguish between path separators (a literal ``/``) and other forward slashes (a literal ``%2F``). Use :attr:`URL.path_safe` for these cases. .. attribute:: URL.path_safe Similar to :attr:`URL.path` except it doesn't decode ``%2F`` or ``%25``. This allows to distinguish between path separators (``/``) and encoded slashes (``%2F``). Note that ``%25`` is also not decoded to avoid issues with double unquoting of values. e.g. You can unquote the value with ``URL.path_safe.replace("%2F", "/").replace("%25", %")`` to get the same result as :meth:`URL.path`. If the ``%25`` was unquoted, it would be impossible to tell the difference between ``%2F`` and ``%252F``. .. versionadded:: 1.12 .. attribute:: URL.path_qs Decoded *path* part of URL and query string, ``'/'`` for absolute URLs without *path* part. .. doctest:: >>> URL('http://example.com/path/to?a1=a&a2=b').path_qs '/path/to?a1=a&a2=b' .. attribute:: URL.raw_path_qs Encoded *path* part of URL and query string, ``'/'`` for absolute URLs without *path* part. .. doctest:: >>> URL('http://example.com/шлях/сюди?ключ=знач').raw_path_qs '/%D1%88%D0%BB%D1%8F%D1%85/%D1%81%D1%8E%D0%B4%D0%B8?%D0%BA%D0%BB%D1%8E%D1%87=%D0%B7%D0%BD%D0%B0%D1%87' .. versionadded:: 0.15 .. attribute:: URL.raw_path Encoded *path* part of URL, ``'/'`` for absolute URLs without *path* part. .. doctest:: >>> URL('http://example.com/шлях/сюди').raw_path '/%D1%88%D0%BB%D1%8F%D1%85/%D1%81%D1%8E%D0%B4%D0%B8' .. attribute:: URL.query_string Decoded *query* part of URL, empty string if *query* is missing. .. doctest:: >>> URL('http://example.com/path?a1=a&a2=b').query_string 'a1=a&a2=b' >>> URL('http://example.com/path?ключ=знач').query_string 'ключ=знач' >>> URL('http://example.com/path').query_string '' .. attribute:: URL.raw_query_string Encoded *query* part of URL, empty string if *query* is missing. .. doctest:: >>> URL('http://example.com/path?ключ=знач').raw_query_string '%D0%BA%D0%BB%D1%8E%D1%87=%D0%B7%D0%BD%D0%B0%D1%87' .. attribute:: URL.fragment Encoded *fragment* part of URL, empty string if *fragment* is missing. .. doctest:: >>> URL('http://example.com/path#fragment').fragment 'fragment' >>> URL('http://example.com/path#якір').fragment 'якір' >>> URL('http://example.com/path').fragment '' .. attribute:: URL.raw_fragment Decoded *fragment* part of URL, empty string if *fragment* is missing. .. doctest:: >>> URL('http://example.com/path#якір').raw_fragment '%D1%8F%D0%BA%D1%96%D1%80' For *path* and *query* *yarl* supports additional helpers: .. attribute:: URL.parts A :class:`tuple` containing decoded *path* parts, ``('/',)`` for absolute URLs if *path* is missing. .. doctest:: >>> URL('http://example.com/path/to').parts ('/', 'path', 'to') >>> URL('http://example.com/шлях/сюди').parts ('/', 'шлях', 'сюди') >>> URL('http://example.com').parts ('/',) .. attribute:: URL.raw_parts A :class:`tuple` containing encoded *path* parts, ``('/',)`` for absolute URLs if *path* is missing. .. doctest:: >>> URL('http://example.com/шлях/сюди').raw_parts ('/', '%D1%88%D0%BB%D1%8F%D1%85', '%D1%81%D1%8E%D0%B4%D0%B8') .. attribute:: URL.name The last part of :attr:`parts`. .. doctest:: >>> URL('http://example.com/path/to').name 'to' >>> URL('http://example.com/шлях/сюди').name 'сюди' >>> URL('http://example.com/path/').name '' .. attribute:: URL.raw_name The last part of :attr:`raw_parts`. .. doctest:: >>> URL('http://example.com/шлях/сюди').raw_name '%D1%81%D1%8E%D0%B4%D0%B8' .. attribute:: URL.suffix The file extension of :attr:`name`. .. doctest:: >>> URL('http://example.com/path/to.txt').suffix '.txt' >>> URL('http://example.com/шлях.сюди').suffix '.сюди' >>> URL('http://example.com/path').suffix '' .. attribute:: URL.raw_suffix The file extension of :attr:`raw_name`. .. doctest:: >>> URL('http://example.com/шлях.сюди').raw_suffix '.%D1%81%D1%8E%D0%B4%D0%B8' .. attribute:: URL.suffixes A list of :attr:`name`'s file extensions. .. doctest:: >>> URL('http://example.com/path/to.tar.gz').suffixes ('.tar', '.gz') >>> URL('http://example.com/шлях.тут.ось').suffixes ('.тут', '.ось') >>> URL('http://example.com/path').suffixes () .. attribute:: URL.raw_suffixes A list of :attr:`raw_name`'s file extensions. .. doctest:: >>> URL('http://example.com/шлях.тут.ось').raw_suffixes ('.%D1%82%D1%83%D1%82', '.%D0%BE%D1%81%D1%8C') .. attribute:: URL.query A :class:`multidict.MultiDictProxy` representing parsed *query* parameters in decoded representation. Empty value if URL has no *query* part. .. doctest:: >>> URL('http://example.com/path?a1=a&a2=b').query >>> URL('http://example.com/path?ключ=знач').query >>> URL('http://example.com/path').query .. _yarl-api-relative-urls: Absolute and relative URLs -------------------------- The module supports both absolute and relative URLs. Absolute URL should start from either *scheme* or ``'//'``. .. attribute:: URL.absolute A check for absolute URLs. Return ``True`` for absolute ones (having *scheme* or starting with ``'//'``), ``False`` otherwise. .. doctest:: >>> URL('http://example.com').absolute True >>> URL('//example.com').absolute True >>> URL('/path/to').absolute False >>> URL('path').absolute False .. versionchanged:: 1.9.10 The :attr:`~yarl.URL.absolute` property is preferred over the ``is_absolute()`` method. New URL generation ------------------ URL is an immutable object, every operation described in the section generates a new :class:`URL` instance. .. method:: URL.build(*, scheme=..., authority=..., user=..., password=..., \ host=..., port=..., path=..., query=..., \ query_string=..., fragment=..., encoded=False) :classmethod: Creates and returns a new URL: .. doctest:: >>> URL.build(scheme="http", host="example.com") URL('http://example.com') >>> URL.build(scheme="http", host="example.com", query={"a": "b"}) URL('http://example.com/?a=b') >>> URL.build(scheme="http", host="example.com", query_string="a=b") URL('http://example.com/?a=b') >>> URL.build() URL('') Calling ``build`` method without arguments is equal to calling ``__init__`` without arguments. .. note:: Only one of ``query`` or ``query_string`` should be passed then ValueError will be raised. .. method:: URL.with_scheme(scheme) Return a new URL with *scheme* replaced: .. doctest:: >>> URL('http://example.com').with_scheme('https') URL('https://example.com') Returned URL may have a *different* ``port`` (:ref:`default port substitution `). .. method:: URL.with_user(user) Return a new URL with *user* replaced, auto-encode *user* if needed. Clear user/password if *user* is ``None``. .. doctest:: >>> URL('http://user:pass@example.com').with_user('new_user') URL('http://new_user:pass@example.com') >>> URL('http://user:pass@example.com').with_user('олекса') URL('http://%D0%BE%D0%BB%D0%B5%D0%BA%D1%81%D0%B0:pass@example.com') >>> URL('http://user:pass@example.com').with_user(None) URL('http://example.com') .. method:: URL.with_password(password) Return a new URL with *password* replaced, auto-encode *password* if needed. Clear password if ``None`` is passed. .. doctest:: >>> URL('http://user:pass@example.com').with_password('пароль') URL('http://user:%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@example.com') >>> URL('http://user:pass@example.com').with_password(None) URL('http://user@example.com') .. method:: URL.with_host(host) Return a new URL with *host* replaced, auto-encode *host* if needed. Changing *host* for relative URLs is not allowed, use :meth:`URL.join` instead. .. doctest:: >>> URL('http://example.com/path/to').with_host('python.org') URL('http://python.org/path/to') >>> URL('http://example.com/path').with_host('хост.домен') URL('http://xn--n1agdj.xn--d1acufc/path') .. method:: URL.with_port(port) Return a new URL with *port* replaced. Clear port to default if ``None`` is passed. .. doctest:: >>> URL('http://example.com:8888').with_port(9999) URL('http://example.com:9999') >>> URL('http://example.com:8888').with_port(None) URL('http://example.com') .. method:: URL.with_path(path, *, keep_query=False, keep_fragment=False) Return a new URL with *path* replaced, encode *path* if needed. If ``keep_query=True`` or ``keep_fragment=True`` it retains the existing query or fragment in the URL. .. versionchanged:: 1.18 Added *keep_query* and *keep_fragment* parameters. .. doctest:: >>> URL('http://example.com/').with_path('/path/to') URL('http://example.com/path/to') .. method:: URL.with_query(query) URL.with_query(**kwargs) Return a new URL with *query* part replaced. Unlike :meth:`update_query` the method replaces all query parameters. Accepts any :class:`~collections.abc.Mapping` (e.g. :class:`dict`, :class:`~multidict.MultiDict` instances) or :class:`str`, auto-encode the argument if needed. A sequence of ``(key, value)`` pairs is supported as well. Also it can take an arbitrary number of keyword arguments. Clear *query* if ``None`` is passed. .. note:: The library accepts :class:`str`, :class:`float`, :class:`int` and their subclasses except :class:`bool` as query argument values. If a mapping such as :class:`dict` is used, the values may also be :class:`list` or :class:`tuple` to represent a key has many values. Please see :ref:`yarl-bools-support` for the reason why :class:`bool` is not supported out-of-the-box. .. doctest:: >>> URL('http://example.com/path?a=b').with_query('c=d') URL('http://example.com/path?c=d') >>> URL('http://example.com/path?a=b').with_query({'c': 'd'}) URL('http://example.com/path?c=d') >>> URL('http://example.com/path?a=b').with_query({'c': [1, 2]}) URL('http://example.com/path?c=1&c=2') >>> URL('http://example.com/path?a=b').with_query({'кл': 'зн'}) URL('http://example.com/path?%D0%BA%D0%BB=%D0%B7%D0%BD') >>> URL('http://example.com/path?a=b').with_query(None) URL('http://example.com/path') >>> URL('http://example.com/path?a=b&b=1').with_query(b='2') URL('http://example.com/path?b=2') >>> URL('http://example.com/path?a=b&b=1').with_query([('b', '2')]) URL('http://example.com/path?b=2') .. versionchanged:: 1.5 Support :class:`list` and :class:`tuple` as a query parameter value. .. versionchanged:: 1.6 Support subclasses of :class:`int` (except :class:`bool`) and :class:`float` as a query parameter value. .. method:: URL.extend_query(query) URL.extend_query(**kwargs) Returns a new URL with *query* part extended. Unlike :meth:`update_query`, this method keeps duplicate keys. Returned :class:`URL` object will contain query string which extends parts from passed query parts (or parts of parsed query string). Accepts any :class:`~collections.abc.Mapping` (e.g. :class:`dict`, :class:`~multidict.MultiDict` instances) or :class:`str`, auto-encode the argument if needed. A sequence of ``(key, value)`` pairs is supported as well. Also it can take an arbitrary number of keyword arguments. Returns the same :class:`URL` if *query* of ``None`` is passed. .. note:: The library accepts :class:`str`, :class:`float`, :class:`int` and their subclasses except :class:`bool` as query argument values. If a mapping such as :class:`dict` is used, the values may also be :class:`list` or :class:`tuple` to represent a key has many values. Please see :ref:`yarl-bools-support` for the reason why :class:`bool` is not supported out-of-the-box. .. doctest:: >>> URL('http://example.com/path?a=b&b=1').extend_query(b='2') URL('http://example.com/path?a=b&b=1&b=2') >>> URL('http://example.com/path?a=b&b=1').extend_query([('b', '2')]) URL('http://example.com/path?a=b&b=1&b=2') >>> URL('http://example.com/path?a=b&c=e&c=f').extend_query(c='d') URL('http://example.com/path?a=b&c=e&c=f&c=d') .. versionadded:: 1.11.0 .. method:: URL.update_query(query) URL.update_query(**kwargs) Returns a new URL with *query* part updated. Unlike :meth:`with_query` the method does not replace query completely. Returned :class:`URL` object will contain query string which updated parts from passed query parts (or parts of parsed query string). Accepts any :class:`~collections.abc.Mapping` (e.g. :class:`dict`, :class:`~multidict.MultiDict` instances) or :class:`str`, auto-encode the argument if needed. A sequence of ``(key, value)`` pairs is supported as well. Also it can take an arbitrary number of keyword arguments. Clear *query* if ``None`` is passed. Mod operator (``%``) can be used as alternative to the direct call of :meth:`URL.update_query`. .. note:: The library accepts :class:`str`, :class:`float`, :class:`int` and their subclasses except :class:`bool` as query argument values. If a mapping such as :class:`dict` is used, the values may also be :class:`list` or :class:`tuple` to represent a key has many values. Please see :ref:`yarl-bools-support` for the reason why :class:`bool` is not supported out-of-the-box. .. doctest:: >>> URL('http://example.com/path?a=b').update_query('c=d') URL('http://example.com/path?a=b&c=d') >>> URL('http://example.com/path?a=b').update_query({'c': 'd'}) URL('http://example.com/path?a=b&c=d') >>> URL('http://example.com/path?a=b').update_query({'c': [1, 2]}) URL('http://example.com/path?a=b&c=1&c=2') >>> URL('http://example.com/path?a=b').update_query({'кл': 'зн'}) URL('http://example.com/path?a=b&%D0%BA%D0%BB=%D0%B7%D0%BD') >>> URL('http://example.com/path?a=b&b=1').update_query(b='2') URL('http://example.com/path?a=b&b=2') >>> URL('http://example.com/path?a=b&b=1').update_query([('b', '2')]) URL('http://example.com/path?a=b&b=2') >>> URL('http://example.com/path?a=b&c=e&c=f').update_query(c='d') URL('http://example.com/path?a=b&c=d') >>> URL('http://example.com/path?a=b').update_query('c=d&c=f') URL('http://example.com/path?a=b&c=d&c=f') >>> URL('http://example.com/path?a=b') % {'c': 'd'} URL('http://example.com/path?a=b&c=d') .. versionchanged:: 1.0 All multiple key/value pairs are applied to the multi-dictionary. .. versionadded:: 1.5 Support for mod operator (``%``) to update the URL's query part. .. versionchanged:: 1.5 Support :class:`list` and :class:`tuple` as a query parameter value. .. versionchanged:: 1.6 Support subclasses of :class:`int` (except :class:`bool`) and :class:`float` as a query parameter value. .. method:: URL.without_query_params(*query_params) Return a new URL whose *query* part does not contain specified ``query_params``. Accepts :class:`str` for ``query_params``. It does nothing if none of specified ``query_params`` are present in the query. .. versionadded:: 1.10.0 .. method:: URL.with_fragment(fragment) Return a new URL with *fragment* replaced, auto-encode *fragment* if needed. Clear *fragment* to default if ``None`` is passed. .. doctest:: >>> URL('http://example.com/path#frag').with_fragment('anchor') URL('http://example.com/path#anchor') >>> URL('http://example.com/path#frag').with_fragment('якір') URL('http://example.com/path#%D1%8F%D0%BA%D1%96%D1%80') >>> URL('http://example.com/path#frag').with_fragment(None) URL('http://example.com/path') .. method:: URL.with_name(name, *, keep_query=False, keep_fragment=False) Return a new URL with *name* (last part of *path*) replaced and cleaned up *query* and *fragment* parts. Name is encoded if needed. If ``keep_query=True`` or ``keep_fragment=True`` it retains the existing query or fragment in the URL. .. versionchanged:: 1.18 Added *keep_query* and *keep_fragment* parameters. .. doctest:: >>> URL('http://example.com/path/to?arg#frag').with_name('new') URL('http://example.com/path/new') >>> URL('http://example.com/path/to').with_name("ім'я") URL('http://example.com/path/%D1%96%D0%BC%27%D1%8F') .. method:: URL.with_suffix(suffix, *, keep_query=False, keep_fragment=False) Return a new URL with *suffix* (file extension of *name*) replaced and cleaned up *query* and *fragment* parts. Name is encoded if needed. If ``keep_query=True`` or ``keep_fragment=True`` it retains the existing query or fragment in the URL. .. versionchanged:: 1.18 Added *keep_query* and *keep_fragment* parameters. .. doctest:: >>> URL('http://example.com/path/to?arg#frag').with_suffix('.doc') URL('http://example.com/path/to.doc') >>> URL('http://example.com/path/to').with_suffix('.cуфікс') URL('http://example.com/path/to.c%D1%83%D1%84%D1%96%D0%BA%D1%81') .. attribute:: URL.parent A new URL with last part of *path* removed and cleaned up *query* and *fragment* parts. .. doctest:: >>> URL('http://example.com/path/to?arg#frag').parent URL('http://example.com/path') .. method:: URL.origin() A new URL with *scheme*, *host* and *port* parts only. *user*, *password*, *path*, *query* and *fragment* are removed. .. doctest:: >>> URL('http://example.com/path/to?arg#frag').origin() URL('http://example.com') >>> URL('http://user:pass@example.com/path').origin() URL('http://example.com') .. method:: URL.relative() A new *relative* URL with *path*, *query* and *fragment* parts only. *scheme*, *user*, *password*, *host* and *port* are removed. .. doctest:: >>> URL('http://example.com/path/to?arg#frag').relative() URL('/path/to?arg#frag') Division (``/``) operator creates a new URL with appended *path* parts and cleaned up *query* and *fragment* parts. The path is encoded if needed. .. doctest:: >>> url = URL('http://example.com/path?arg#frag') / 'to/subpath' >>> url URL('http://example.com/path/to/subpath') >>> url.parts ('/', 'path', 'to', 'subpath') >>> url = URL('http://example.com/path?arg#frag') / 'сюди' >>> url URL('http://example.com/path/%D1%81%D1%8E%D0%B4%D0%B8') .. method:: URL.joinpath(*other, encoded=False) Construct a new URL by with all ``other`` elements appended to *path*, and cleaned up *query* and *fragment* parts. Passing ``encoded=True`` parameter prevents path element auto-encoding, the caller is responsible for taking care of URL correctness. .. doctest:: >>> url = URL('http://example.com/path?arg#frag').joinpath('to', 'subpath') >>> url URL('http://example.com/path/to/subpath') >>> url.parts ('/', 'path', 'to', 'subpath') >>> url = URL('http://example.com/path?arg#frag').joinpath('сюди') >>> url URL('http://example.com/path/%D1%81%D1%8E%D0%B4%D0%B8') >>> url = URL('http://example.com/path').joinpath('%D1%81%D1%8E%D0%B4%D0%B8', encoded=True) >>> url URL('http://example.com/path/%D1%81%D1%8E%D0%B4%D0%B8') .. versionadded:: 1.9 .. method:: URL.__truediv__(url) Shortcut for :meth:`URL.joinpath` with a single element and ``encoded=False``. .. doctest:: >>> url = URL('http://example.com/path?arg#frag') / 'to' >>> url URL('http://example.com/path/to') >>> url.parts ('/', 'path', 'to') .. versionadded:: 0.9 .. method:: URL.join(url) Construct a full (“absolute”) URL by combining a “base URL” (``self``) with another URL (``url``). Informally, this uses components of the base URL, in particular the addressing scheme, the network location and (part of) the path, to provide missing components in the relative URL, e.g.: .. doctest:: >>> base = URL('http://example.com/path/index.html') >>> base.join(URL('page.html')) URL('http://example.com/path/page.html') .. note:: If ``url`` is an absolute URL (that is, starting with ``//`` or ``scheme://``), the URL‘s host name and/or scheme will be present in the result, e.g.: .. doctest:: >>> base = URL('http://example.com/path/index.html') >>> base.join(URL('//python.org/page.html')) URL('http://python.org/page.html') Human readable representation ----------------------------- All URL data is stored in encoded form internally. It's pretty good for passing ``str(url)`` everywhere URL string is accepted but quite bad for memorizing by humans. .. method:: URL.human_repr() Return decoded human readable string for URL representation. .. doctest:: >>> url = URL('http://εμπορικόσήμα.eu/這裡') >>> str(url) 'http://xn--jxagkqfkduily1i.eu/%E9%80%99%E8%A3%A1' >>> url.human_repr() 'http://εμπορικόσήμα.eu/這裡' .. _yarl-api-default-ports: Default port substitution ------------------------- :mod:`yarl` is aware about the following *scheme* -> *port* translations: +------------------+-------+ | scheme | port | +==================+=======+ | ``'http'`` | 80 | +------------------+-------+ | ``'https'`` | 443 | +------------------+-------+ | ``'ws'`` | 80 | +------------------+-------+ | ``'wss'`` | 443 | +------------------+-------+ .. method:: URL.is_default_port() A check for default port. Return ``True`` if URL's :attr:`~URL.port` is *default* for used :attr:`~URL.scheme`, ``False`` otherwise. Relative URLs have no default port. .. doctest:: >>> URL('http://example.com').is_default_port() True >>> URL('http://example.com:80').is_default_port() True >>> URL('http://example.com:8080').is_default_port() False >>> URL('/path/to').is_default_port() False Cache control ------------- IDNA conversion and host encoding are quite expensive operations, that's why the ``yarl`` library caches these calls by storing results in the global LRU cache. .. function:: cache_clear() Clear IDNA and host encoding cache. .. function:: cache_info() Return a dictionary with ``"idna_encode"``, ``"idna_decode"``, and ``"encode_host"`` keys, each value points to corresponding ``CacheInfo`` structure (see :func:`functools.lru_cache` for details): .. doctest:: :options: +SKIP >>> yarl.cache_info() {'idna_encode': CacheInfo(hits=5, misses=5, maxsize=256, currsize=5), 'idna_decode': CacheInfo(hits=24, misses=15, maxsize=256, currsize=15), 'encode_host': CacheInfo(hits=0, misses=0, maxsize=512, currsize=0)} .. versionchanged:: 1.16 ``ip_address``, and ``host_validate`` are deprecated in favor of a single ``encode_host`` cache. .. function:: cache_configure(*, idna_encode_size=256, idna_decode_size=256, encode_host_size=512) Set the IDNA encode, IDNA decode, and host encode cache sizes. Pass ``None`` to make the corresponding cache unbounded (may speed up host encoding operation a little but the memory footprint can be very high, please use with caution). .. versionchanged:: 1.16 ``ip_address_size`` and ``host_validate_size`` are deprecated in favor of a single ``encode_host`` cache. References ---------- :mod:`yarl` stays on shoulders of giants: several RFC documents and low-level :mod:`urllib.parse` which performs almost all gory work. The module borrowed design from :mod:`pathlib` in any place where it was possible. .. seealso:: :rfc:`5891` - Internationalized Domain Names in Applications (IDNA): Protocol Document describing non-ASCII domain name encoding. :rfc:`3987` - Internationalized Resource Identifiers This specifies conversion rules for non-ASCII characters in URL. :rfc:`3986` - Uniform Resource Identifiers This is the current standard (STD66). Any changes to :mod:`yarl` module should conform to this. Certain deviations could be observed, which are mostly for backward compatibility purposes and for certain de-facto parsing requirements as commonly observed in major browsers. :rfc:`2732` - Format for Literal IPv6 Addresses in URL's. This specifies the parsing requirements of IPv6 URLs. :rfc:`2396` - Uniform Resource Identifiers (URI): Generic Syntax Document describing the generic syntactic requirements for both Uniform Resource Names (URNs) and Uniform Resource Locators (URLs). :rfc:`2368` - The mailto URL scheme. Parsing requirements for mailto URL schemes. :rfc:`1808` - Relative Uniform Resource Locators This Request For Comments includes the rules for joining an absolute and a relative URL, including a fair number of "Abnormal Examples" which govern the treatment of border cases. :rfc:`1738` - Uniform Resource Locators (URL) This specifies the formal syntax and semantics of absolute URLs. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/docs/changes.rst0000644000175100001660000000054214774356277015275 0ustar00runnerdocker.. _yarl_changes: ========= Changelog ========= .. only:: not is_release To be included in v\ |release| (if present) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. towncrier-draft-entries:: |release| [UNRELEASED DRAFT] Released versions ^^^^^^^^^^^^^^^^^ .. include:: ../CHANGES.rst :start-after: .. towncrier release notes start ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/docs/conf.py0000644000175100001660000003160014774356277014431 0ustar00runnerdocker#!/usr/bin/env python3 # # yarl documentation build configuration file, created by # sphinx-quickstart on Mon Aug 29 19:55:36 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' import os import re from pathlib import Path PROJECT_ROOT_DIR = Path(__file__).parents[1].resolve() IS_RELEASE_ON_RTD = ( os.getenv("READTHEDOCS", "False") == "True" and os.environ["READTHEDOCS_VERSION_TYPE"] == "tag" ) if IS_RELEASE_ON_RTD: tags.add("is_release") _docs_path = Path(__file__).parent _version_path = _docs_path / "../yarl/__init__.py" with _version_path.open() as fp: try: _version_info = re.search( r"^__version__ = \"" r"(?P\d+)" r"\.(?P\d+)" r"\.(?P\d+)" r"(?P.*)?\"$", fp.read(), re.M, ).groupdict() except IndexError: raise RuntimeError("Unable to determine version.") # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ # stdlib-party extensions: "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.coverage", "sphinx.ext.doctest", "sphinx.ext.viewcode", # Third-party extensions: "alabaster", "sphinxcontrib.towncrier.ext", # provides `towncrier-draft-entries` directive "myst_parser", # extended markdown; https://pypi.org/project/myst-parser/ ] try: import sphinxcontrib.spelling # noqa extensions.append("sphinxcontrib.spelling") except ImportError: pass intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "multidict": ("https://multidict.aio-libs.org/en/stable", None), "propcache": ("https://propcache.aio-libs.org/en/stable", None), } # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # -- Project information ----------------------------------------------------- github_url = "https://github.com" github_repo_org = "aio-libs" github_repo_name = "yarl" github_repo_slug = f"{github_repo_org}/{github_repo_name}" github_repo_url = f"{github_url}/{github_repo_slug}" github_sponsors_url = f"{github_url}/sponsors" project = github_repo_name copyright = f"2016, Andrew Svetlov, {project} contributors and aio-libs team" author = "Andrew Svetlov and aio-libs team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = "{major}.{minor}".format(**_version_info) # The full version, including alpha/beta/rc tags. release = "{major}.{minor}.{patch}-{tag}".format(**_version_info) rst_epilog = f""" .. |project| replace:: {project} """ # pylint: disable=invalid-name # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Extension configuration ------------------------------------------------- # -- Options for extlinks extension --------------------------------------- extlinks = { "issue": (f"{github_repo_url}/issues/%s", "#%s"), "pr": (f"{github_repo_url}/pull/%s", "PR #%s"), "commit": (f"{github_repo_url}/commit/%s", "%s"), "gh": (f"{github_url}/%s", "GitHub: %s"), "user": (f"{github_sponsors_url}/%s", "@%s"), } # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "alabaster" html_theme_options = { "logo": "yarl-icon-128x128.png", "description": "Yet another URL library", "github_user": "aio-libs", "github_repo": "yarl", "github_button": True, "github_type": "star", "github_banner": True, "codecov_button": True, "pre_bg": "#FFF6E5", "note_bg": "#E5ECD1", "note_border": "#BFCF8C", "body_text": "#482C0A", "sidebar_text": "#49443E", "sidebar_header": "#4B4032", "sidebar_collapse": False, } # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. # # html_title = 'yarl v0.1.0' # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # # html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} html_sidebars = { "**": [ "about.html", "navigation.html", "searchbox.html", ] } # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = "yarldoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, "yarl.tex", "yarl Documentation", "Andrew Svetlov", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "yarl", "yarl Documentation", [author], 1)] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "yarl", "yarl Documentation", author, "yarl", "One line description of project.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False default_role = "any" nitpicky = True nitpick_ignore = [ ("envvar", "TMPDIR"), ] # -- Options for towncrier_draft extension ----------------------------------- towncrier_draft_autoversion_mode = "draft" # or: 'sphinx-version', 'sphinx-release' towncrier_draft_include_empty = True towncrier_draft_working_directory = PROJECT_ROOT_DIR # Not yet supported: towncrier_draft_config_path = 'pyproject.toml' # relative to cwd ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743903941.5852761 yarl-1.19.0/docs/contributing/0000755000175100001660000000000014774356306015632 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/docs/contributing/guidelines.rst0000644000175100001660000000176614774356277020535 0ustar00runnerdocker----------------- Contributing docs ----------------- We use Sphinx_ to generate our docs website. You can trigger the process locally by executing: .. code-block:: shell-session $ make doc It is also integrated with `Read The Docs`_ that builds and publishes each commit to the main branch and generates live docs previews for each pull request. The sources of the Sphinx_ documents use reStructuredText as a de-facto standard. But in order to make contributing docs more beginner-friendly, we've integrated `MyST parser`_ allowing us to also accept new documents written in an extended version of Markdown that supports using Sphinx directives and roles. `Read the docs `_ to learn more on how to use it. .. _MyST docs: https://myst-parser.readthedocs.io/en/latest/using/intro.html#writing-myst-in-sphinx .. _MyST parser: https://pypi.org/project/myst-parser/ .. _Read The Docs: https://readthedocs.org .. _Sphinx: https://www.sphinx-doc.org .. include:: ../../CHANGES/README.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/docs/contributing/release_guide.rst0000644000175100001660000000754214774356277021200 0ustar00runnerdocker************* Release Guide ************* Welcome to the |project| Release Guide! This page contains information on how to release a new version of |project| using the automated Continuous Delivery pipeline. .. tip:: The intended audience for this document is maintainers and core contributors. Pre-release activities ====================== 1. Check if there are any open Pull Requests that could be desired in the upcoming release. If there are any — merge them. If some are incomplete, try to get them ready. Don't forget to review the enclosed change notes per our guidelines. 2. Visually inspect the draft section of the :ref:`Changelog` page. Make sure the content looks consistent, uses the same writing style, targets the end-users and adheres to our documented guidelines. Most of the changelog sections will typically use the past tense or another way to relay the effect of the changes for the users, since the previous release. It should not target core contributors as the information they are normally interested in is already present in the Git history. Update the changelog fragments if you see any problems with this changelog section. 3. Optionally, test the previously published nightlies, that are available through GitHub Actions CI/CD artifacts, locally. 4. If you are satisfied with the above, inspect the changelog section categories in the draft. Presence of the breaking changes or features will hint you what version number segment to bump for the release. 5. Update the hardcoded version string in :file:`yarl/__init__.py`. Generate a new changelog from the fragments, and commit it along with the fragments removal and the Python module changes. Use the following commands, don't prepend a leading-``v`` before the version number. Just use the raw version number as per :pep:`440`. .. code-block:: shell-session [dir:yarl] $ yarl/__init__.py [dir:yarl] $ python -m towncrier build \ -- --version 'VERSION_WITHOUT_LEADING_V' [dir:yarl] $ git commit -v CHANGES{.rst,/} yarl/__init__.py .. seealso:: :ref:`Adding change notes with your PRs` Writing beautiful changelogs for humans The release stage ================= 1. Tag the commit with version and changelog changes, created during the preparation stage. If possible, make it GPG-signed. Prepend a leading ``v`` before the version number for the tag name. Add an extra sentence describing the release contents, in a few words. .. code-block:: shell-session [dir:yarl] $ git tag \ -s 'VERSION_WITH_LEADING_V' \ -m 'VERSION_WITH_LEADING_V' \ -m 'This release does X and Y.' 2. Push that tag to the upstream repository, which ``origin`` is considered to be in the example below. .. code-block:: shell-session [dir:yarl] $ git push origin 'VERSION_WITH_LEADING_V' 3. You can open the `GitHub Actions CI/CD workflow page `_ in your web browser to monitor the progress. But generally, you don't need to babysit the CI. 4. Check that web page or your email inbox for the notification with an approval request. GitHub will send it when it reaches the final "publishing" job. 5. Approve the deployment and wait for the CD workflow to complete. 6. Verify that the following things got created: - a PyPI release - a Git tag - a GitHub Releases page 7. Tell everyone you released a new version of |project| :) Depending on your mental capacity and the burnout stage, you are encouraged to post the updates in issues asking for the next release, contributed PRs, Bluesky, Twitter etc. You can also call out prominent contributors and thank them! .. _GitHub Actions CI/CD workflow: https://github.com/aio-libs/yarl/actions/workflows/ci-cd.yml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/docs/index.rst0000644000175100001660000001250014774356277014771 0ustar00runnerdocker.. yarl documentation master file, created by sphinx-quickstart on Mon Aug 29 19:55:36 2016. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. yarl ==== The module provides handy :class:`~yarl.URL` class for URL parsing and changing. Introduction ------------ URL is constructed from :class:`str`: .. doctest:: >>> from yarl import URL >>> url = URL('https://www.python.org/~guido?arg=1#frag') >>> url URL('https://www.python.org/~guido?arg=1#frag') All URL parts: *scheme*, *user*, *password*, *host*, *port*, *path*, *query* and *fragment* are accessible by properties: .. doctest:: >>> url.scheme 'https' >>> url.host 'www.python.org' >>> url.path '/~guido' >>> url.query_string 'arg=1' >>> url.query >>> url.fragment 'frag' All URL manipulations produces a new URL object: .. doctest:: >>> url.parent / 'downloads/source' URL('https://www.python.org/downloads/source') A URL object can be modified with ``/`` and ``%`` operators: .. doctest:: >>> url = URL('https://www.python.org') >>> url / 'foo' / 'bar' URL('https://www.python.org/foo/bar') >>> url / 'foo' % {'bar': 'baz'} URL('https://www.python.org/foo?bar=baz') Strings passed to constructor and modification methods are automatically encoded giving canonical representation as result: .. doctest:: >>> url = URL('https://www.python.org/шлях') >>> url URL('https://www.python.org/%D1%88%D0%BB%D1%8F%D1%85') Regular properties are *percent-decoded*, use ``raw_`` versions for getting *encoded* strings: .. doctest:: >>> url.path '/шлях' >>> url.raw_path '/%D1%88%D0%BB%D1%8F%D1%85' Human readable representation of URL is available as :meth:`~yarl.URL.human_repr`: .. doctest:: >>> url.human_repr() 'https://www.python.org/шлях' For full documentation please read :ref:`yarl-api` section. Installation ------------ :: $ pip install yarl The library is Python 3 only! PyPI contains binary wheels for Linux, Windows and MacOS. If you want to install ``yarl`` on another operating system (like *Alpine Linux*, which is not manylinux-compliant because of the missing glibc and therefore, cannot be used with our wheels) the the tarball will be used to compile the library from the source code. It requires a C compiler and and Python headers installed. To skip the compilation you must explicitly opt-in by using a PEP 517 configuration setting ``pure-python``, or setting the ``YARL_NO_EXTENSIONS`` environment variable to a non-empty value, e.g.: .. code-block:: console $ pip install yarl --config-settings=pure-python=false Please note that the pure-Python (uncompiled) version is much slower. However, PyPy always uses a pure-Python implementation, and, as such, it is unaffected by this variable. Dependencies ------------ ``yarl`` requires the :mod:`multidict` and :mod:`propcache ` libraries. It installs it automatically. API documentation ------------------ Open :ref:`yarl-api` for reading full list of available methods. Comparison with other URL libraries ------------------------------------ * furl (https://pypi.python.org/pypi/furl) The library has a rich functionality but ``furl`` object is mutable. I afraid to pass this object into foreign code: who knows if the code will modify my URL in a terrible way while I just want to send URL with handy helpers for accessing URL properties. ``furl`` has other non obvious tricky things but the main objection is mutability. * URLObject (https://pypi.python.org/pypi/URLObject) URLObject is immutable, that's pretty good. Every URL change generates a new URL object. But the library doesn't any decode/encode transformations leaving end user to cope with these gory details. .. _yarl-bools-support: Why isn't boolean supported by the URL query API? ------------------------------------------------- There is no standard for boolean representation of boolean values. Some systems prefer ``true``/``false``, others like ``yes``/``no``, ``on``/``off``, ``Y``/``N``, ``1``/``0``, etc. ``yarl`` cannot make an unambiguous decision on how to serialize :class:`bool` values because it is specific to how the end-user's application is built and would be different for different apps. The library doesn't accept booleans in the API; a user should convert bools into strings using own preferred translation protocol. Source code ----------- The project is hosted on GitHub_ Please file an issue on the `bug tracker `_ if you have found a bug or have some suggestion in order to improve the library. Discussion list --------------- *aio-libs* google group: https://groups.google.com/forum/#!forum/aio-libs Feel free to post your questions and ideas here. Authors and License ------------------- The ``yarl`` package is written by Andrew Svetlov. It's *Apache 2* licensed and freely available. Contents: .. toctree:: :maxdepth: 2 api .. toctree:: :caption: What's new changes .. toctree:: :caption: Contributing contributing/guidelines .. toctree:: :caption: Maintenance contributing/release_guide Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _GitHub: https://github.com/aio-libs/yarl ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/docs/make.bat0000644000175100001660000001706014774356277014543 0ustar00runnerdocker@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. epub3 to make an epub3 echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled echo. dummy to check syntax errors of document sources goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 1>NUL 2>NUL if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\yarl.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\yarl.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "epub3" ( %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) if "%1" == "dummy" ( %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy if errorlevel 1 exit /b 1 echo. echo.Build finished. Dummy builder generates no files. goto end ) :end ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/docs/spelling_wordlist.txt0000644000175100001660000000065114774356277017441 0ustar00runnerdockerBluesky Bugfixes Changelog Codecov Cython GPG IPv PRs PYX Towncrier Twitter UTF aiohttp armv ascii backend boolean booleans bools changelog changelogs config de decodable dev dists downstreams facto glibc google hardcoded hostnames macOS mailto manylinux multi nightlies pre pytest rc reStructuredText reencoding requote requoting runtimes sdist subclass subclasses subcomponent svetlov uncompiled unencoded unquoter v1 yarl ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/docs/yarl-icon-128x128.xcf0000644000175100001660000011401414774356277016465 0ustar00runnerdockergimp xcf fileBBBgimp-image-grid(style solid) (fgcolor (color-rgba 0.000000 0.000000 0.000000 1.000000)) (bgcolor (color-rgba 1.000000 1.000000 1.000000 1.000000)) (xspacing 10.000000) (yspacing 10.000000) (spacing-unit inches) (xoffset 0.000000) (yoffset 0.000000) (offset-unit inches) gamma0.45454999804496765 3 a ⁠U⁠R⁠L     gimp-text-layer(markup "URL") (font "Sans") (font-size 18.000000) (font-size-unit pixels) (antialias yes) (language "en-us") (base-direction ltr) (color (color-rgb 0.000000 0.000000 0.000000)) (justify left) (box-mode fixed) (box-width 174.000000) (box-height 165.000000) (box-unit pixels) (hinting yes) c  '                                                                                # $   H J   p r    V T  |   / 2   GI ~  h+,i  # %!% }!{    {z $$ || ǨX$  7 =    E  0 "N {     w  2        7  ~ H    i  "O   "    +  4   K Q 2  (_  I  0 + W   ; #   .     # " " " " % p" " ( ^" " +..     TȬS     @ @ @ @ @ @ @ @WR+)Layer       U  % 5 E@@aiohttp-icon-128x128.png      " : V/rRu82.).13            82.).13 Ķнٻ侢ڼѶҮ뼸Ƶ򩨫ѡƶͮ񼘜ȶƸ㰣񟢣贵ըʥҷäڷ ֶ ɷ ŦŹ ˩ Үˡ ܰ靈á® 鳲ܻ ۴ʱٯޱ׬𳵱ԪĦԪִշȨֳ˦򮧦ز՛ڸڲةݴܜĥ߷ۮݹ죣ʥܻХݲ Яʣ߷Ӧϣ̬󤢸ګݬ۵簾ټ¢ۡҦѥǡ並ڮܩꨭˬߨįԨⲰ ˪򰵽ӳìཱس ײۿ öɱ Ҿӱ ٲ ˽ݴ ݾ޷¾82.).13 ƭs`ZZUSQҭl[RO?//1?DD꽇\NONPPQJ1,,+3@@όX?:;?MONML8*++9@@￉S:;;:87AMMC,++*/>@|h[?:8778GMJ3*++6@jP^gF6676=L>+**-<\NKMP;99:C3--.//.4ۊcIIGIJJIBNepiTHIJHOS::;E^cOJLMMLL}ٙWLo?;;87651-;acNJNheB>9776/**)773++*0HKKVwzfgIEDA@;7786.++:MNMIlehFFCAA?9882,,.ENNGyeh EFBAA<86-,,+6LMAqeh EEB@AA@90++,@I?Vdh PKHGG>558>+2@AA<+A>Rԃffr lW:7;JNNH0+,9AA@0Nx975=kYNchgg ucB88ANOON9,,0?BAABB8^b+499855:HKNNXhiihP899:IOOE.--6BB?pR*,0899FOOPaiih_<8998>MOOL5,--.=BACF*--3998?MOONUgiliG899EOO@--,3@D>+--.79:IOO]irjX9989+--,18BOOReP]C::C=//.38-//9JNMMWJB=GKJJKBAIIHM[HIID>==;;9.?OOM56NPPTbfVPPMC=880ΠI1,1IOO@--APPOR[eiaQPPOFBA;994-f,-,:NOOJ2,,2KPOQX_iihYOPPKBB?997/-?IW*-.EOON;,--,991--}<5-,J@--2JQB..+TcOUgjjaRPHCCB<:6..O;/--++--39::9<:9;LdjjSFPPH7ACCT61?CCBVSA5.5::8@9:COXijja?1-6CCA9.-097gK8:=MPPajjiL99BOPPD/-1@CCt40-/=CCAa:-..2P_7::GPPOUgjjk[<::;IPPL5-..7CCK3/..4BCCE^+.-6{8::@OPP]jjkgF9::@OPP?..0>CC73//;DD=m+,,<:;>6;:DPQQPZjkcA:;;>NQP;.//MQQRbkN:;;:DQF0//.3BQ40//1=45`-6GQPPW[>;;=I7//:ULHHQIWDFBAACJJHGHHGNVVRRWh\YOIA;::;63KRQQOFLSRRSdSURRbkgRZLDD>;;91.;:1//=QQIDDFPR[jjsiHFFOZklljoll^wDEEC=5//1HOFEEKUgloeFCBHdllksjllhjMHHA6554Fc@2+,-.6EwaOk@9++*(5q\:/*(2jV757CMNOQPN^@6(.`O*,66=KMMNORah{;,RW+(*+28GMMViTJJKTB=>?gUGIJJH?:997FKLNMSvNMNNG?:6774+:MMOQNdfZNNKA@>8776.*-EMMNPPRgdSNNMD@@;772++*5KMMODeg^NNI@@?8775-++@MMFDCRhgWNOOMC@AA=8771++2JNNJAADEJhbQOOHAA@;885-,,MNNG37IPQRSSRQN>3587885Xa@OXg q;78ENNOSF?AHTTMAA@B=86Pi9KNP`l gms:7H[}qab b`̬dE@6**++-/5Se57GIOcb baiԩsHIC554=;CBFTUL`bbޯwPVQCC;8iCGUNG`bb`ⰭuHURDCC?14FCGRFF`bb`ᰫtBPRDCC6-17CFJCE`bbݮoAGODCC=/-03mCABCC|db bakԩeACFCCB4../3HDC6?DBmuab b`ŢXCD<@DD;//37D0/4=1//04RNGHHR[ǂb`bba`eǧ\ZZOGGJQHHLUdSRQ]q٫{ga``biҪtbd`TRR]hWRRVj[P^iafҭccYRRUgkbRRUSkefW`caiܭecc^SRR`kkjZQRRTOi{RQYc`VjҸѼ_\caVRQXikkeTRRSI{˜VQRR\[WUi˯óԫ_VX`YRRSdkk]RRMF{jbSRRNQWWUaḁ̀ZVWVRR\jkkhVRRPGFqkkj[RPFFQWWVXsд~cVVMEMRUgkkaSRJEGlllgUKDDFOVWVTplXTTLEEHR`llkZOEEHikll[IHHOWZWKSanw|{uk]RVTUThWACCBPjlldLHIKilljNMRQQSZ`|NUTTUSKOOLB:<<=NgllYIQRTp               420(*.4<ȿۼȾۿؿ󪲷ѾƿԾֹٿҼ๴¿н𨱷Ų˪ӵоش٫ἴпὴ䬴մѾгݸ®ݱӧĴæ׺۫Οة޽Ǧæߺ򽴳齩֯߶꾨Ũܰ𶧨뱨ҫݩ񰪩伬򯶿̬Ŭܪ¹ưеɦŷꩭ ȧýȸ ǧ̫槮úٿ竩к򾮯÷ۮƦĬث𱮶Թְٮ۶ӶƻӵͶ俶ȷٹŸ¸Ż Ὸ 軸 Ᾰ ⸶ཷ ݮܵ ֯ܵ ڱܷ೴ۺ縲Ƭίίݱ԰éڲݵ߸Ҵºù̴ÿ˹Կֹ׳༻޷̸»ཱϾDzúǫ"20(*.4<d?>mPRRN7/1?EE9006<<;CPYjllp>CPRRSt|kllhG;MQ_llz@MRRPjllV<<;kKRRK3/00TUTT^iXTTURH@z:<<5?Ce:==608OSSUoRH22ITTWhmbTTLDcF<==:1>gA<=:211BSSXgO9009QTTammkZSTTRQPHD:==<41Fy:=<51104LSR^`C11DTTYkmmfUTTRh·^<==811oL<=8110=RRgS501105NTTUemml^SSf;==;311==<3112GPu?0110?STS\lmmiU[;==611`?=711.0C\.0113JTVglkkm?=:211ZC;201100[Z0010.-.;;CR_nnjF@EGG6AGDtm:>>@OTVgnnlqώkJDGF9DC :>>HTTS\mnnodAm-AqsJ5=@ :>AQTTdnnmOKTTXknno^?>>==qicT?1223AE|=;xn;@OUUTWioX>>MJHTM6229G69A{v2:IUU^eD>>=^AAP>223?_GMCbϦbBDEIJJKMJJGyHJKEDDI]l]ha[XSTVVND?>>?;:RUURRTKRWVVWeniSUVVRIGA??>62DUURRNHKUVV\m|ziUVVULHHF??:326NUUT|PIHHPVVWhoZVVPHHC??=5332?TUUVrKHHJTVV`oofWVVTJHHG@??8334JUUXhGHHNVVYkoom_VVNHHD??<4332:RU[]GHHIRVVcoo iZVSIHHGA?>633EU]XGHHLUV\moo xbWLHHE?:3327N\WGHHPWgoo qXKIIB744AUVFGGI]noo ZVUSSDCEE@HR@AAC]ppoo [YXVVN7?IIH9>T?@@KXipp aXYVVT>36EII@49Y>@@?DTV^oppfXYVVI443@@NVVfppqWZWR:3445CII=443e=@@?GUVVZmppWYF4439GIID5441n:@@BQVVbppV;8544@IIH:3440}4?@@JVVXjpU8964437FIA441n2;@DSVVU^o?88543vB3Нr]X__LKKFAA?^S25ͣwvILMMGCNp476ϝiZOG9788מ}yu!20(*.4<                1.,#%(+.3<޷񴱭ۭתť˩ڪؽ۳ڴڭӪӳȪิ ¯޽Ѧ߸ۮܵ򱨧Ӿ׳Чаڭɬ׹īɪտﱻ׬ػﵴܾͼ򰪩ٶ·ٳѴڲüѸڶڰ˺޹Ŷ۰ùǸܼٺڳ򿮨ȯ㫨ſµھȰطԼüĭѭ°ɷͰخɽܾݴ ƶ ໺º 䯰  ɾծ ϸ¸㯯 鰯 簯  ݮ  ˮ  󼶷 ƾ Է Ĵ  Ƽ⻸ °޷޷Ųٸ޸ﴸԻĹߺﯸͰٷ༬رű공๴շ͹츴ǸǽոҸ໹𹳱߿򷲳𷼼򬭲.,#%(+.3<lm[=DQRRQ`SFUWWLCFD>:RK;;DQ[kllfC?OSTsllgE;=KRRPu24AORSTSSQI833;<<:9>NRSdllQ;<==MS>h/11;FA2114RSSTG7TjSTTXi^TTLC=9t;<<=86NTSSTUs2HSSO7/N^STTbmhWTTPFF@RSSH211-y3OTTWimml\STTLFFC?b7110/3N`x113ITP8011.l-ATTammhVTPGFF=110/.6Ov110:QC11/`.4KZkmm`SJFFBe1.6TbZPJ5B8334Y13?ennkTIIGN^~W;22@@wED;;==<@Y;>CU<@OYknnS?MTTRTlBEGG;VGF713;>>=FQ=ITTannobA=CRTRWxO.aFGGA6EGG>226=>=HNARTTVinnlL=>>HX~]C10JMGGD9GmGB5122:>=INKTTS]mnno\?=;Aq|XRN711:VFGGF=5R/1224==HQPRSTTUennhF;?hpVRTT@122bBGGB8CNu60226=UzlZRSTYlnnT<\mA=NTTK422/u9GGE;5~F6LJ012[ќcSTbobRzE<>>ESS<22/q2CH@8F?305vd;فTVkL<>>?LG3220^29C:6IDDCAgfgHIJJIKGDDB]HENG_eWVVS qNTUUNVca^l]m\VVTs 6:RUURIIRVV\kjnohWVT b0EUUTLHHLUVVWn}o`U^07OUUPHHPVVUookWn52@TUUSJHHJTVVUoaw825KUUNHHNVV^oky722AACEZ pqcM GII?5=@@BDP pnN; QHIIE638?@@CDO pq^?uXGIIH<344<@@DCU pjGvY@CSr߭yEkFIIG9344@@KSRR@X.2:eEII@4458:Hly]=@@CRWH1{041kBIF7344698Ud=@@IQ91c33;ZD<34588?vGIIJHBATMAB?HDCEFEcUWWN@UXYyVXWhva[Xj8PWWU?4HXUUXXYgh4AVWWJ549RVu\Z[\56LWWS;454DXapW_54 bool: truthy_values = {'', None, 'true', '1', 'on'} return setting_value.lower() in truthy_values def _get_setting_value( config_settings: _ConfigDict | None = None, config_setting_name: str | None = None, env_var_name: str | None = None, *, default: bool = False, ) -> bool: user_provided_setting_sources = ( (config_settings, config_setting_name, (KeyError, TypeError)), (os.environ, env_var_name, KeyError), ) for src_mapping, src_key, lookup_errors in user_provided_setting_sources: if src_key is None: continue with suppress(lookup_errors): # type: ignore[arg-type] return _is_truthy_setting_value(src_mapping[src_key]) # type: ignore[arg-type,index] return default def _make_pure_python(config_settings: _ConfigDict | None = None) -> bool: return _get_setting_value( config_settings, PURE_PYTHON_CONFIG_SETTING, PURE_PYTHON_ENV_VAR, default=PURE_PYTHON_MODE_CLI_FALLBACK, ) def _include_cython_line_tracing( config_settings: _ConfigDict | None = None, *, default: bool = False, ) -> bool: return _get_setting_value( config_settings, CYTHON_TRACING_CONFIG_SETTING, CYTHON_TRACING_ENV_VAR, default=default, ) @contextmanager def patched_distutils_cmd_install() -> Iterator[None]: """Make `install_lib` of `install` cmd always use `platlib`. :yields: None """ # Without this, build_lib puts stuff under `*.data/purelib/` folder orig_finalize = _distutils_install_cmd.finalize_options def new_finalize_options(self: _distutils_install_cmd) -> None: self.install_lib = self.install_platlib orig_finalize(self) _distutils_install_cmd.finalize_options = new_finalize_options # type: ignore[method-assign] try: yield finally: _distutils_install_cmd.finalize_options = orig_finalize # type: ignore[method-assign] @contextmanager def patched_dist_has_ext_modules() -> Iterator[None]: """Make `has_ext_modules` of `Distribution` always return `True`. :yields: None """ # Without this, build_lib puts stuff under `*.data/platlib/` folder orig_func = _DistutilsDistribution.has_ext_modules _DistutilsDistribution.has_ext_modules = lambda *args, **kwargs: True # type: ignore[method-assign] try: yield finally: _DistutilsDistribution.has_ext_modules = orig_func # type: ignore[method-assign] @contextmanager def patched_dist_get_long_description() -> Iterator[None]: """Make `has_ext_modules` of `Distribution` always return `True`. :yields: None """ # Without this, build_lib puts stuff under `*.data/platlib/` folder _orig_func = _DistutilsDistributionMetadata.get_long_description def _get_sanitized_long_description(self: _DistutilsDistributionMetadata) -> str: assert self.long_description is not None return sanitize_rst_roles(self.long_description) _DistutilsDistributionMetadata.get_long_description = ( # type: ignore[method-assign] _get_sanitized_long_description ) try: yield finally: _DistutilsDistributionMetadata.get_long_description = _orig_func # type: ignore[method-assign] def _exclude_dir_path( excluded_dir_path: Path, visited_directory: str, _visited_dir_contents: list[str], ) -> list[str]: """Prevent recursive directory traversal.""" # This stops the temporary directory from being copied # into self recursively forever. # Ref: https://github.com/aio-libs/yarl/issues/992 visited_directory_subdirs_to_ignore = [ subdir for subdir in _visited_dir_contents if excluded_dir_path == Path(visited_directory) / subdir ] if visited_directory_subdirs_to_ignore: print( f'Preventing `{excluded_dir_path !s}` from being ' 'copied into itself recursively...', file=_standard_error_stream, ) return visited_directory_subdirs_to_ignore @contextmanager def _in_temporary_directory(src_dir: Path) -> Iterator[None]: with TemporaryDirectory(prefix='.tmp-yarl-pep517-') as tmp_dir: tmp_dir_path = Path(tmp_dir) root_tmp_dir_path = tmp_dir_path.parent _exclude_tmpdir_parent = partial(_exclude_dir_path, root_tmp_dir_path) with chdir_cm(tmp_dir): tmp_src_dir = tmp_dir_path / 'src' copytree( src_dir, tmp_src_dir, ignore=_exclude_tmpdir_parent, symlinks=True, ) os.chdir(tmp_src_dir) yield @contextmanager def maybe_prebuild_c_extensions( line_trace_cython_when_unset: bool = False, build_inplace: bool = False, config_settings: _ConfigDict | None = None, ) -> Iterator[None]: """Pre-build C-extensions in a temporary directory, when needed. This context manager also patches metadata, setuptools and distutils. :param build_inplace: Whether to copy and chdir to a temporary location. :param config_settings: :pep:`517` config settings mapping. """ cython_line_tracing_requested = _include_cython_line_tracing( config_settings, default=line_trace_cython_when_unset, ) is_pure_python_build = _make_pure_python(config_settings) if is_pure_python_build: print("*********************", file=_standard_error_stream) print("* Pure Python build *", file=_standard_error_stream) print("*********************", file=_standard_error_stream) if cython_line_tracing_requested: _warn_that( f'The `{CYTHON_TRACING_CONFIG_SETTING !s}` setting requesting ' 'Cython line tracing is set, but building C-extensions is not. ' 'This option will not have any effect for in the pure-python ' 'build mode.', RuntimeWarning, stacklevel=999, ) yield return print("**********************", file=_standard_error_stream) print("* Accelerated build *", file=_standard_error_stream) print("**********************", file=_standard_error_stream) if not IS_CPYTHON: _warn_that( 'Building C-extensions under the runtimes other than CPython is ' 'unsupported and will likely fail. Consider passing the ' f'`{PURE_PYTHON_CONFIG_SETTING !s}` PEP 517 config setting.', RuntimeWarning, stacklevel=999, ) build_dir_ctx = ( nullcontext() if build_inplace else _in_temporary_directory(src_dir=Path.cwd().resolve()) ) with build_dir_ctx: config = _get_local_cython_config() cythonize_args = _make_cythonize_cli_args_from_config(config) with _patched_cython_env(config['env'], cython_line_tracing_requested): _cythonize_cli_cmd(cythonize_args) # type: ignore[no-untyped-call] with patched_distutils_cmd_install(): with patched_dist_has_ext_modules(): yield @patched_dist_get_long_description() def build_wheel( wheel_directory: str, config_settings: _ConfigDict | None = None, metadata_directory: str | None = None, ) -> str: """Produce a built wheel. This wraps the corresponding ``setuptools``' build backend hook. :param wheel_directory: Directory to put the resulting wheel in. :param config_settings: :pep:`517` config settings mapping. :param metadata_directory: :file:`.dist-info` directory path. """ with maybe_prebuild_c_extensions( line_trace_cython_when_unset=False, build_inplace=False, config_settings=config_settings, ): return _setuptools_build_wheel( wheel_directory=wheel_directory, config_settings=config_settings, metadata_directory=metadata_directory, ) @patched_dist_get_long_description() def build_editable( wheel_directory: str, config_settings: _ConfigDict | None = None, metadata_directory: str | None = None, ) -> str: """Produce a built wheel for editable installs. This wraps the corresponding ``setuptools``' build backend hook. :param wheel_directory: Directory to put the resulting wheel in. :param config_settings: :pep:`517` config settings mapping. :param metadata_directory: :file:`.dist-info` directory path. """ with maybe_prebuild_c_extensions( line_trace_cython_when_unset=True, build_inplace=True, config_settings=config_settings, ): return _setuptools_build_editable( wheel_directory=wheel_directory, config_settings=config_settings, metadata_directory=metadata_directory, ) def get_requires_for_build_wheel( config_settings: _ConfigDict | None = None, ) -> list[str]: """Determine additional requirements for building wheels. :param config_settings: :pep:`517` config settings mapping. """ is_pure_python_build = _make_pure_python(config_settings) if not is_pure_python_build and not IS_CPYTHON: _warn_that( 'Building C-extensions under the runtimes other than CPython is ' 'unsupported and will likely fail. Consider passing the ' f'`{PURE_PYTHON_CONFIG_SETTING !s}` PEP 517 config setting.', RuntimeWarning, stacklevel=999, ) c_ext_build_deps = [] if is_pure_python_build else [ 'Cython ~= 3.0.0; python_version >= "3.12"', 'Cython; python_version < "3.12"', ] return _setuptools_get_requires_for_build_wheel( config_settings=config_settings, ) + c_ext_build_deps build_sdist = patched_dist_get_long_description()(_setuptools_build_sdist) get_requires_for_build_editable = get_requires_for_build_wheel prepare_metadata_for_build_wheel = patched_dist_get_long_description()( _setuptools_prepare_metadata_for_build_wheel, ) prepare_metadata_for_build_editable = prepare_metadata_for_build_wheel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/packaging/pep517_backend/_compat.py0000644000175100001660000000134014774356277020610 0ustar00runnerdocker"""Cross-python stdlib shims.""" import os import sys from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path if sys.version_info >= (3, 11): from contextlib import chdir as chdir_cm from tomllib import loads as load_toml_from_string else: from tomli import loads as load_toml_from_string @contextmanager # type: ignore[no-redef] def chdir_cm(path: "os.PathLike[str]") -> Iterator[None]: """Temporarily change the current directory, recovering on exit.""" original_wd = Path.cwd() os.chdir(path) try: yield finally: os.chdir(original_wd) __all__ = ("chdir_cm", "load_toml_from_string") # noqa: WPS410 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/packaging/pep517_backend/_cython_configuration.py0000644000175100001660000000721014774356277023562 0ustar00runnerdocker# fmt: off from __future__ import annotations import os from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path from sys import version_info as _python_version_tuple from typing import TypedDict from expandvars import expandvars from ._compat import load_toml_from_string from ._transformers import get_cli_kwargs_from_config, get_enabled_cli_flags_from_config class Config(TypedDict): env: dict[str, str] flags: dict[str, bool] kwargs: dict[str, str] src: list[str] def get_local_cython_config() -> Config: """Grab optional build dependencies from pyproject.toml config. :returns: config section from ``pyproject.toml`` :rtype: dict This basically reads entries from:: [tool.local.cythonize] # Env vars provisioned during cythonize call src = ["src/**/*.pyx"] [tool.local.cythonize.env] # Env vars provisioned during cythonize call LDFLAGS = "-lssh" [tool.local.cythonize.flags] # This section can contain the following booleans: # * annotate — generate annotated HTML page for source files # * build — build extension modules using distutils # * inplace — build extension modules in place using distutils (implies -b) # * force — force recompilation # * quiet — be less verbose during compilation # * lenient — increase Python compat by ignoring some compile time errors # * keep-going — compile as much as possible, ignore compilation failures annotate = false build = false inplace = true force = true quiet = false lenient = false keep-going = false [tool.local.cythonize.kwargs] # This section can contain args that have values: # * exclude=PATTERN exclude certain file patterns from the compilation # * parallel=N run builds in N parallel jobs (default: calculated per system) exclude = "**.py" parallel = 12 [tool.local.cythonize.kwargs.directives] # This section can contain compiler directives # NAME = "VALUE" [tool.local.cythonize.kwargs.compile-time-env] # This section can contain compile time env vars # NAME = "VALUE" [tool.local.cythonize.kwargs.options] # This section can contain cythonize options # NAME = "VALUE" """ config_toml_txt = (Path.cwd().resolve() / 'pyproject.toml').read_text() config_mapping = load_toml_from_string(config_toml_txt) return config_mapping['tool']['local']['cythonize'] # type: ignore[no-any-return] def make_cythonize_cli_args_from_config(config: Config) -> list[str]: py_ver_arg = f'-{_python_version_tuple.major!s}' cli_flags = get_enabled_cli_flags_from_config(config['flags']) cli_kwargs = get_cli_kwargs_from_config(config['kwargs']) return cli_flags + [py_ver_arg] + cli_kwargs + ['--'] + config['src'] @contextmanager def patched_env(env: dict[str, str], cython_line_tracing_requested: bool) -> Iterator[None]: """Temporary set given env vars. :param env: tmp env vars to set :type env: dict :yields: None """ orig_env = os.environ.copy() expanded_env = {name: expandvars(var_val) for name, var_val in env.items()} # type: ignore[no-untyped-call] os.environ.update(expanded_env) if cython_line_tracing_requested: os.environ['CFLAGS'] = ' '.join(( os.getenv('CFLAGS', ''), '-DCYTHON_TRACE_NOGIL=1', # Implies CYTHON_TRACE=1 )).strip() try: yield finally: os.environ.clear() os.environ.update(orig_env) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/packaging/pep517_backend/_transformers.py0000644000175100001660000000706014774356277022057 0ustar00runnerdocker"""Data conversion helpers for the in-tree PEP 517 build backend.""" from collections.abc import Iterable, Iterator, Mapping from itertools import chain from re import sub as _substitute_with_regexp from typing import Union def _emit_opt_pairs(opt_pair: tuple[str, Union[dict[str, str], str]]) -> Iterator[str]: flag, flag_value = opt_pair flag_opt = f"--{flag!s}" if isinstance(flag_value, dict): sub_pairs: Iterable[tuple[str, ...]] = flag_value.items() else: sub_pairs = ((flag_value,),) yield from ("=".join(map(str, (flag_opt,) + pair)) for pair in sub_pairs) def get_cli_kwargs_from_config(kwargs_map: dict[str, str]) -> list[str]: """Make a list of options with values from config.""" return list(chain.from_iterable(map(_emit_opt_pairs, kwargs_map.items()))) def get_enabled_cli_flags_from_config(flags_map: Mapping[str, bool]) -> list[str]: """Make a list of enabled boolean flags from config.""" return [f"--{flag}" for flag, is_enabled in flags_map.items() if is_enabled] def sanitize_rst_roles(rst_source_text: str) -> str: """Replace RST roles with inline highlighting.""" pep_role_regex = r"""(?x) :pep:`(?P\d+)` """ pep_substitution_pattern = ( r"`PEP \g >`__" ) user_role_regex = r"""(?x) :user:`(?P[^`]+)(?:\s+(.*))?` """ user_substitution_pattern = ( r"`@\g " r">`__" ) issue_role_regex = r"""(?x) :issue:`(?P[^`]+)(?:\s+(.*))?` """ issue_substitution_pattern = ( r"`#\g " r">`__" ) pr_role_regex = r"""(?x) :pr:`(?P[^`]+)(?:\s+(.*))?` """ pr_substitution_pattern = ( r"`PR #\g " r">`__" ) commit_role_regex = r"""(?x) :commit:`(?P[^`]+)(?:\s+(.*))?` """ commit_substitution_pattern = ( r"`\g " r">`__" ) gh_role_regex = r"""(?x) :gh:`(?P[^`<]+)(?:\s+([^`]*))?` """ gh_substitution_pattern = r"GitHub: ``\g``" meth_role_regex = r"""(?x) (?::py)?:meth:`~?(?P[^`<]+)(?:\s+([^`]*))?` """ meth_substitution_pattern = r"``\g()``" role_regex = r"""(?x) (?::\w+)?:\w+:`(?P[^`<]+)(?:\s+([^`]*))?` """ substitution_pattern = r"``\g``" project_substitution_regex = r"\|project\|" project_substitution_pattern = "yarl" substitutions = ( (pep_role_regex, pep_substitution_pattern), (user_role_regex, user_substitution_pattern), (issue_role_regex, issue_substitution_pattern), (pr_role_regex, pr_substitution_pattern), (commit_role_regex, commit_substitution_pattern), (gh_role_regex, gh_substitution_pattern), (meth_role_regex, meth_substitution_pattern), (role_regex, substitution_pattern), (project_substitution_regex, project_substitution_pattern), ) rst_source_normalized_text = rst_source_text for regex, substitution in substitutions: rst_source_normalized_text = _substitute_with_regexp( regex, substitution, rst_source_normalized_text, ) return rst_source_normalized_text ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/packaging/pep517_backend/cli.py0000644000175100001660000000335114774356277017741 0ustar00runnerdocker# fmt: off from __future__ import annotations import sys from collections.abc import Sequence from itertools import chain from pathlib import Path from Cython.Compiler.CmdLine import parse_command_line as _split_cython_cli_args from Cython.Compiler.Main import compile as _translate_cython_cli_cmd from ._cython_configuration import get_local_cython_config as _get_local_cython_config from ._cython_configuration import ( make_cythonize_cli_args_from_config as _make_cythonize_cli_args_from_config, ) from ._cython_configuration import patched_env as _patched_cython_env _PROJECT_PATH = Path(__file__).parents[2] def run_main_program(argv: Sequence[str]) -> int | str: """Invoke ``translate-cython`` or fail.""" if len(argv) != 2: return 'This program only accepts one argument -- "translate-cython"' if argv[1] != 'translate-cython': return 'This program only implements the "translate-cython" subcommand' config = _get_local_cython_config() config['flags'] = {'keep-going': config['flags']['keep-going']} config['src'] = list( map( str, chain.from_iterable( map(_PROJECT_PATH.glob, config['src']), ), ), ) translate_cython_cli_args = _make_cythonize_cli_args_from_config(config) cython_options, cython_sources = _split_cython_cli_args( # type: ignore[no-untyped-call] translate_cython_cli_args, ) with _patched_cython_env(config['env'], cython_line_tracing_requested=True): return _translate_cython_cli_cmd( # type: ignore[no-any-return,no-untyped-call] cython_sources, cython_options, ).num_errors if __name__ == '__main__': sys.exit(run_main_program(argv=sys.argv)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/packaging/pep517_backend/hooks.py0000644000175100001660000000133414774356277020314 0ustar00runnerdocker"""PEP 517 build backend for optionally pre-building Cython.""" from contextlib import suppress as _suppress from setuptools.build_meta import * # Re-exporting PEP 517 hooks # pylint: disable=unused-wildcard-import,wildcard-import # noqa: F401, F403 # Re-exporting PEP 517 hooks from ._backend import ( # type: ignore[assignment] build_sdist, build_wheel, get_requires_for_build_wheel, prepare_metadata_for_build_wheel, ) with _suppress(ImportError): # Only succeeds w/ setuptools implementing PEP 660 # Re-exporting PEP 660 hooks from ._backend import ( # type: ignore[assignment] build_editable, get_requires_for_build_editable, prepare_metadata_for_build_editable, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/pyproject.toml0000644000175100001660000000633214774356277015122 0ustar00runnerdocker[build-system] requires = [ # NOTE: The following build dependencies are necessary for initial # NOTE: provisioning of the in-tree build backend located under # NOTE: `packaging/pep517_backend/`. "expandvars", "setuptools >= 47", # Minimum required for `version = attr:` "tomli; python_version < '3.11'", ] backend-path = ["packaging"] # requires `pip >= 20` or `pep517 >= 0.6.0` build-backend = "pep517_backend.hooks" # wraps `setuptools.build_meta` [tool.local.cythonize] # This attr can contain multiple globs src = ["yarl/*.pyx"] [tool.local.cythonize.env] # Env vars provisioned during cythonize call #CFLAGS = "-DCYTHON_TRACE=1 ${CFLAGS}" #LDFLAGS = "${LDFLAGS}" [tool.local.cythonize.flags] # This section can contain the following booleans: # * annotate — generate annotated HTML page for source files # * build — build extension modules using distutils # * inplace — build extension modules in place using distutils (implies -b) # * force — force recompilation # * quiet — be less verbose during compilation # * lenient — increase Python compat by ignoring some compile time errors # * keep-going — compile as much as possible, ignore compilation failures annotate = false build = false inplace = true force = true quiet = false lenient = false keep-going = false [tool.local.cythonize.kwargs] # This section can contain args that have values: # * exclude=PATTERN exclude certain file patterns from the compilation # * parallel=N run builds in N parallel jobs (default: calculated per system) # exclude = "**.py" # parallel = 12 [tool.local.cythonize.kwargs.directive] # This section can contain compiler directives. Ref: # https://cython.rtfd.io/en/latest/src/userguide/source_files_and_compilation.html#compiler-directives embedsignature = "True" emit_code_comments = "True" linetrace = "True" # Implies `profile=True` [tool.local.cythonize.kwargs.compile-time-env] # This section can contain compile time env vars [tool.local.cythonize.kwargs.option] # This section can contain cythonize options # Ref: https://github.com/cython/cython/blob/d6e6de9/Cython/Compiler/Options.py#L694-L730 #docstrings = "True" #embed_pos_in_docstring = "True" #warning_errors = "True" #error_on_unknown_names = "True" #error_on_uninitialized = "True" [tool.cibuildwheel] build-frontend = "build" before-test = [ # NOTE: Attempt to have pip pre-compile PyYAML wheel with our build # NOTE: constraints unset. The hope is that pip will cache that wheel # NOTE: and the test env provisioning stage will pick up PyYAML from # NOTE: said cache rather than attempting to build it with a conflicting. # NOTE: Version of Cython. # Ref: https://github.com/pypa/cibuildwheel/issues/1666 "PIP_CONSTRAINT= pip install PyYAML", ] test-requires = "-r requirements/test.txt" test-command = 'pytest -v -m "not hypothesis" --no-cov {project}/tests' # don't build PyPy wheels, install from source instead skip = "pp*" [tool.cibuildwheel.environment] COLOR = "yes" FORCE_COLOR = "1" MYPY_FORCE_COLOR = "1" PIP_CONSTRAINT = "requirements/cython.txt" PRE_COMMIT_COLOR = "always" PY_COLORS = "1" [tool.cibuildwheel.config-settings] pure-python = "false" [tool.cibuildwheel.windows] before-test = [] # Windows cmd has different syntax and pip chooses wheels ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/pytest.ini0000644000175100001660000000417014774356277014235 0ustar00runnerdocker[pytest] addopts = # `pytest-xdist`: --numprocesses=auto # Show 10 slowest invocations: --durations=10 # Report all the things == -rxXs: -ra # Show values of the local vars in errors/tracebacks: --showlocals # Autocollect and invoke the doctests from all modules: # https://docs.pytest.org/en/stable/doctest.html --doctest-modules # Pre-load the `pytest-cov` plugin early: -p pytest_cov # `pytest-cov`: --cov --cov-config=.coveragerc --cov-context=test # Fail on config parsing warnings: # --strict-config # Fail on non-existing markers: # * Deprecated since v6.2.0 but may be reintroduced later covering a # broader scope: # --strict # * Exists since v4.5.0 (advised to be used instead of `--strict`): --strict-markers doctest_optionflags = ALLOW_UNICODE ELLIPSIS # Marks tests with an empty parameterset as xfail(run=False) empty_parameter_set_mark = xfail faulthandler_timeout = 30 filterwarnings = error # FIXME: drop this once `pytest-cov` is updated. # Ref: https://github.com/pytest-dev/pytest-cov/issues/557 ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning # https://github.com/pytest-dev/pytest/issues/10977 and https://github.com/pytest-dev/pytest/pull/10894 ignore:ast\.(Num|NameConstant|Str) is deprecated and will be removed in Python 3\.14; use ast\.Constant instead:DeprecationWarning:_pytest ignore:Attribute s is deprecated and will be removed in Python 3\.14; use value instead:DeprecationWarning:_pytest # https://docs.pytest.org/en/stable/usage.html#creating-junitxml-format-files junit_duration_report = call # xunit1 contains more metadata than xunit2 so it's better for CI UIs: junit_family = xunit1 junit_logging = all junit_log_passing_tests = true junit_suite_name = yarl_test_suite # A mapping of markers to their descriptions allowed in strict mode: markers = minversion = 3.8.2 # Optimize pytest's lookup by restricting potentially deep dir tree scan: norecursedirs = build dist docs requirements venv virtualenv yarl.egg-info .* *.egg testpaths = tests/ xfail_strict = true ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743903941.5882761 yarl-1.19.0/requirements/0000755000175100001660000000000014774356306014716 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/requirements/cython.txt0000644000175100001660000000001714774356277016770 0ustar00runnerdockercython==3.0.12 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/requirements/dev.txt0000644000175100001660000000003514774356277016242 0ustar00runnerdocker-r test.txt -r towncrier.txt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/requirements/doc-spelling.txt0000644000175100001660000000014314774356277020044 0ustar00runnerdocker-r doc.txt sphinxcontrib-spelling==8.0.1; platform_system!="Windows" # We only use it in Azure CI ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/requirements/doc.txt0000644000175100001660000000011514774356277016230 0ustar00runnerdocker-r towncrier.txt myst-parser >= 0.10.0 sphinx==8.2.3 sphinxcontrib-towncrier ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/requirements/lint.txt0000644000175100001660000000002214774356277016426 0ustar00runnerdockerpre-commit==4.2.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/requirements/test.txt0000644000175100001660000000023314774356277016443 0ustar00runnerdocker-r cython.txt covdefaults hypothesis>=6.0 idna==3.10 multidict==6.3.2 propcache==0.3.1 pytest==8.3.5 pytest-cov>=2.3.1 pytest-xdist pytest_codspeed==3.2.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/requirements/towncrier.txt0000644000175100001660000000002314774356277017475 0ustar00runnerdockertowncrier==23.11.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743903941.6002762 yarl-1.19.0/setup.cfg0000644000175100001660000000411414774356306014014 0ustar00runnerdocker[bdist_wheel] universal = 0 [metadata] name = yarl version = attr: yarl.__version__ url = https://github.com/aio-libs/yarl project_urls = Chat: Matrix = https://matrix.to/#/#aio-libs:matrix.org Chat: Matrix Space = https://matrix.to/#/#aio-libs-space:matrix.org CI: GitHub Workflows = https://github.com/aio-libs/yarl/actions?query=branch:master Code of Conduct = https://github.com/aio-libs/.github/blob/master/CODE_OF_CONDUCT.md Coverage: codecov = https://codecov.io/github/aio-libs/yarl Docs: Changelog = https://yarl.aio-libs.org/en/latest/changes/ Docs: RTD = https://yarl.aio-libs.org GitHub: issues = https://github.com/aio-libs/yarl/issues GitHub: repo = https://github.com/aio-libs/yarl description = Yet another URL library long_description = file: README.rst, CHANGES.rst long_description_content_type = text/x-rst author = Andrew Svetlov author_email = andrew.svetlov@gmail.com maintainer = aiohttp team maintainer_email = team@aiohttp.org license = Apache-2.0 license_files = LICENSE NOTICE classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Cython Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 Topic :: Internet :: WWW/HTTP Topic :: Software Development :: Libraries :: Python Modules keywords = cython cext yarl [options] python_requires = >=3.9 packages = yarl zip_safe = False include_package_data = True install_requires = idna >= 2.0 multidict >= 4.0 propcache >= 0.2.1 [options.package_data] * = *.so [options.exclude_package_data] * = *.c *.h [pep8] max-line-length = 79 [flake8] extend-select = B950 ignore = E203,E301,E302,E501,E704,W503,W504,F811 max-line-length = 88 per-file-ignores = packaging/pep517_backend/hooks.py: F401 [isort] profile = black [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743903941.5902762 yarl-1.19.0/tests/0000755000175100001660000000000014774356306013335 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_cache.py0000644000175100001660000000567714774356277016037 0ustar00runnerdockerimport pytest import yarl # Don't check the actual behavior but make sure that calls are allowed def teardown_module() -> None: yarl.cache_configure() def test_cache_clear() -> None: yarl.cache_clear() def test_cache_info() -> None: info = yarl.cache_info() assert info.keys() == { "idna_encode", "idna_decode", "ip_address", "host_validate", "encode_host", } def test_cache_configure_default() -> None: yarl.cache_configure() def test_cache_configure_None() -> None: yarl.cache_configure( idna_decode_size=None, idna_encode_size=None, encode_host_size=None, ) def test_cache_configure_None_including_deprecated() -> None: msg = ( r"cache_configure\(\) no longer accepts the ip_address_size " r"or host_validate_size arguments, they are used to set the " r"encode_host_size instead and will be removed in the future" ) with pytest.warns(DeprecationWarning, match=msg): yarl.cache_configure( idna_decode_size=None, idna_encode_size=None, encode_host_size=None, ip_address_size=None, host_validate_size=None, ) assert yarl.cache_info()["idna_decode"].maxsize is None assert yarl.cache_info()["idna_encode"].maxsize is None assert yarl.cache_info()["encode_host"].maxsize is None def test_cache_configure_None_only_deprecated() -> None: msg = ( r"cache_configure\(\) no longer accepts the ip_address_size " r"or host_validate_size arguments, they are used to set the " r"encode_host_size instead and will be removed in the future" ) with pytest.warns(DeprecationWarning, match=msg): yarl.cache_configure( ip_address_size=None, host_validate_size=None, ) assert yarl.cache_info()["encode_host"].maxsize is None def test_cache_configure_explicit() -> None: yarl.cache_configure( idna_decode_size=128, idna_encode_size=128, encode_host_size=128, ) assert yarl.cache_info()["idna_decode"].maxsize == 128 assert yarl.cache_info()["idna_encode"].maxsize == 128 assert yarl.cache_info()["encode_host"].maxsize == 128 def test_cache_configure_waring() -> None: msg = ( r"cache_configure\(\) no longer accepts the ip_address_size " r"or host_validate_size arguments, they are used to set the " r"encode_host_size instead and will be removed in the future" ) with pytest.warns(DeprecationWarning, match=msg): yarl.cache_configure( idna_encode_size=1024, idna_decode_size=1024, ip_address_size=1024, host_validate_size=1024, ) assert yarl.cache_info()["encode_host"].maxsize == 1024 with pytest.warns(DeprecationWarning, match=msg): yarl.cache_configure(host_validate_size=None) assert yarl.cache_info()["encode_host"].maxsize is None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_cached_property.py0000644000175100001660000000105514774356277020131 0ustar00runnerdockerimport pytest from yarl._url import cached_property # type: ignore[attr-defined] class A: def __init__(self) -> None: self._cache: dict[str, int] = {} @cached_property def prop(self) -> int: """Docstring.""" return 1 def test_reify() -> None: a = A() assert 1 == a.prop def test_reify_class() -> None: assert isinstance(A.prop, cached_property) assert "Docstring." == A.prop.__doc__ def test_reify_assignment() -> None: a = A() with pytest.raises(AttributeError): a.prop = 123 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_normalize_path.py0000644000175100001660000000211014774356277017763 0ustar00runnerdockerimport pytest from yarl._path import normalize_path PATHS = [ # No dots ("", ""), ("/", "/"), ("//", "//"), ("///", "///"), ("path", "path"), # Single-dot ("path/to", "path/to"), ("././path/to", "path/to"), ("path/./to", "path/to"), ("path/././to", "path/to"), ("path/to/.", "path/to/"), ("path/to/./.", "path/to/"), ("/path/to/.", "/path/to/"), # Double-dots ("../path/to", "path/to"), ("path/../to", "to"), ("path/../../to", "to"), # absolute path root / is maintained; tests based on two # tests from web-platform-tests project's urltestdata.json ("/foo/../../../ton", "/ton"), ("/foo/../../../..bar", "/..bar"), # Non-ASCII characters ("μονοπάτι/../../να/ᴜɴɪ/ᴄᴏᴅᴇ", "να/ᴜɴɪ/ᴄᴏᴅᴇ"), ("μονοπάτι/../../να/𝕦𝕟𝕚/𝕔𝕠𝕕𝕖/.", "να/𝕦𝕟𝕚/𝕔𝕠𝕕𝕖/"), ] @pytest.mark.parametrize("original,expected", PATHS) def test_normalize_path(original: str, expected: str) -> None: assert normalize_path(original) == expected ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_pickle.py0000644000175100001660000000311314774356277016222 0ustar00runnerdockerimport pickle from yarl import URL # serialize def test_pickle() -> None: u1 = URL("picklepickle") hash(u1) v = pickle.dumps(u1) u2 = pickle.loads(v) assert u1._cache assert not u2._cache assert hash(u1) == hash(u2) def test_default_style_state() -> None: u = object.__new__(URL) val = ("set_state", "set_state", "set_state", "set_state", "set_state") u.__setstate__((None, {"_val": val})) assert u._val == val assert hash(u) != 1 def test_empty_url_is_not_cached() -> None: u = URL.__new__(URL) val = ("set_state", "set_state", "set_state", "set_state", "set_state") u.__setstate__((None, {"_val": val})) assert u._val == val assert hash(u) != 1 def test_pickle_does_not_pollute_cache() -> None: """Verify the unpickling does not pollute the cache. Since unpickle will call URL.__new__ with default args, we need to make sure that default args never end up in the pre_encoded_url or encode_url cache. """ u1 = URL.__new__(URL) u1._scheme = "this" u1._netloc = "never.appears.any.where.else.in.tests" u1._path = "" u1._query = "" u1._fragment = "" hash(u1) v = pickle.dumps(u1) u2: URL = pickle.loads(v) assert u1._cache assert hash(u1) == hash(u2) assert u2._scheme == "this" assert u2._netloc == "never.appears.any.where.else.in.tests" assert u2._path == "" assert u2._query == "" assert u2._fragment == "" # Verify unpickling did not the cache wrong scheme # for empty args. assert URL().scheme == "" assert URL("").scheme == "" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_quoting.py0000644000175100001660000004334114774356277016450 0ustar00runnerdockerfrom typing import Union import pytest from hypothesis import assume, example, given, note from hypothesis import strategies as st import yarl from yarl._quoting import NO_EXTENSIONS, _Quoter, _Unquoter from yarl._quoting_py import _Quoter as _PyQuoter from yarl._quoting_py import _Unquoter as _PyUnquoter if not NO_EXTENSIONS: from yarl._quoting_c import _Quoter as _CQuoter # type: ignore[import-not-found] from yarl._quoting_c import _Unquoter as _CUnquoter @pytest.fixture(params=[_PyQuoter, _CQuoter], ids=["py_quoter", "c_quoter"]) def quoter(request: pytest.FixtureRequest) -> Union[_PyQuoter, _CQuoter]: # type: ignore[misc,no-any-unimported] return request.param @pytest.fixture(params=[_PyUnquoter, _CUnquoter], ids=["py_unquoter", "c_unquoter"]) def unquoter(request: pytest.FixtureRequest) -> Union[_PyUnquoter, _CUnquoter]: # type: ignore[misc,no-any-unimported] return request.param quoters = [_PyQuoter, _CQuoter] quoter_ids = ["PyQuoter", "CQuoter"] unquoters = [_PyUnquoter, _CUnquoter] unquoter_ids = ["PyUnquoter", "CUnquoter"] else: @pytest.fixture(params=[_PyQuoter], ids=["py_quoter"]) def quoter(request: pytest.FixtureRequest) -> _PyQuoter: return request.param # type: ignore[no-any-return] @pytest.fixture(params=[_PyUnquoter], ids=["py_unquoter"]) def unquoter(request: pytest.FixtureRequest) -> _PyUnquoter: return request.param # type: ignore[no-any-return] quoters = [_PyQuoter] quoter_ids = ["PyQuoter"] unquoters = [_PyUnquoter] unquoter_ids = ["PyUnquoter"] @pytest.mark.skipif(NO_EXTENSIONS, reason="Extensions available but not imported") def test_quoting_c_loaded() -> None: assert "_quoting_c" in dir(yarl) def hexescape(char: str) -> str: """Escape char as RFC 2396 specifies""" hex_repr = hex(ord(char))[2:].upper() if len(hex_repr) == 1: hex_repr = "0%s" % hex_repr return "%" + hex_repr def test_quote_not_allowed_non_strict(quoter: type[_Quoter]) -> None: assert quoter()("%HH") == "%25HH" def test_quote_unfinished_tail_percent_non_strict(quoter: type[_Quoter]) -> None: assert quoter()("%") == "%25" def test_quote_unfinished_tail_digit_non_strict(quoter: type[_Quoter]) -> None: assert quoter()("%2") == "%252" def test_quote_unfinished_tail_safe_non_strict(quoter: type[_Quoter]) -> None: assert quoter()("%x") == "%25x" def test_quote_unfinished_tail_unsafe_non_strict(quoter: type[_Quoter]) -> None: assert quoter()("%#") == "%25%23" def test_quote_unfinished_tail_non_ascii_non_strict(quoter: type[_Quoter]) -> None: assert quoter()("%ß") == "%25%C3%9F" def test_quote_unfinished_tail_non_ascii2_non_strict(quoter: type[_Quoter]) -> None: assert quoter()("%€") == "%25%E2%82%AC" def test_quote_unfinished_tail_non_ascii3_non_strict(quoter: type[_Quoter]) -> None: assert quoter()("%🐍") == "%25%F0%9F%90%8D" def test_quote_from_bytes(quoter: type[_Quoter]) -> None: assert quoter()("archaeological arcana") == "archaeological%20arcana" assert quoter()("") == "" def test_quote_ignore_broken_unicode(quoter: type[_Quoter]) -> None: s = quoter()( "j\u001a\udcf4q\udcda/\udc97g\udcee\udccb\u000ch\udccb" "\u0018\udce4v\u001b\udce2\udcce\udccecom/y\udccepj\u0016" ) assert s == "j%1Aq%2Fg%0Ch%18v%1Bcom%2Fypj%16" assert quoter()(s) == s def test_unquote_to_bytes(unquoter: type[_Unquoter]) -> None: assert unquoter()("abc%20def") == "abc def" assert unquoter()("") == "" def test_never_quote(quoter: type[_Quoter]) -> None: # Make sure quote() does not quote letters, digits, and "_,.-~" do_not_quote = ( "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789" "_.-~" ) assert quoter()(do_not_quote) == do_not_quote assert quoter(qs=True)(do_not_quote) == do_not_quote def test_safe(quoter: type[_Quoter]) -> None: # Test setting 'safe' parameter does what it should do quote_by_default = "<>" assert quoter(safe=quote_by_default)(quote_by_default) == quote_by_default ret = quoter(safe=quote_by_default, qs=True)(quote_by_default) assert ret == quote_by_default _SHOULD_QUOTE = [chr(num) for num in range(32)] _SHOULD_QUOTE.append(r'<>#"{}|\^[]`') _SHOULD_QUOTE.append(chr(127)) # For 0x7F SHOULD_QUOTE = "".join(_SHOULD_QUOTE) @pytest.mark.parametrize("char", SHOULD_QUOTE) def test_default_quoting(char: str, quoter: type[_Quoter]) -> None: # Make sure all characters that should be quoted are by default sans # space (separate test for that). result = quoter()(char) assert hexescape(char) == result result = quoter(qs=True)(char) assert hexescape(char) == result # TODO: should it encode percent? def test_default_quoting_percent(quoter: type[_Quoter]) -> None: result = quoter()("%25") assert "%25" == result result = quoter(qs=True)("%25") assert "%25" == result result = quoter(requote=False)("%25") assert "%2525" == result def test_default_quoting_partial(quoter: type[_Quoter]) -> None: partial_quote = "ab[]cd" expected = "ab%5B%5Dcd" result = quoter()(partial_quote) assert expected == result result = quoter(qs=True)(partial_quote) assert expected == result def test_quoting_space(quoter: type[_Quoter]) -> None: # Make sure quote() and quote_plus() handle spaces as specified in # their unique way result = quoter()(" ") assert result == hexescape(" ") result = quoter(qs=True)(" ") assert result == "+" given = "a b cd e f" expect = given.replace(" ", hexescape(" ")) result = quoter()(given) assert expect == result expect = given.replace(" ", "+") result = quoter(qs=True)(given) assert expect == result def test_quoting_plus(quoter: type[_Quoter]) -> None: assert quoter(qs=False)("alpha+beta gamma") == "alpha+beta%20gamma" assert quoter(qs=True)("alpha+beta gamma") == "alpha%2Bbeta+gamma" assert quoter(safe="+", qs=True)("alpha+beta gamma") == "alpha+beta+gamma" def test_quote_with_unicode(quoter: type[_Quoter]) -> None: # Characters in Latin-1 range, encoded by default in UTF-8 given = "\u00a2\u00d8ab\u00ff" expect = "%C2%A2%C3%98ab%C3%BF" result = quoter()(given) assert expect == result # Characters in BMP, encoded by default in UTF-8 given = "\u6f22\u5b57" # "Kanji" expect = "%E6%BC%A2%E5%AD%97" result = quoter()(given) assert expect == result def test_quote_plus_with_unicode(quoter: type[_Quoter]) -> None: # Characters in Latin-1 range, encoded by default in UTF-8 given = "\u00a2\u00d8ab\u00ff" expect = "%C2%A2%C3%98ab%C3%BF" result = quoter(qs=True)(given) assert expect == result # Characters in BMP, encoded by default in UTF-8 given = "\u6f22\u5b57" # "Kanji" expect = "%E6%BC%A2%E5%AD%97" result = quoter(qs=True)(given) assert expect == result @pytest.mark.parametrize("num", list(range(128))) def test_unquoting(num: int, unquoter: type[_Unquoter]) -> None: # Make sure unquoting of all ASCII values works given = hexescape(chr(num)) expect = chr(num) result = unquoter()(given) assert expect == result if expect not in "+=&;": result = unquoter(qs=True)(given) assert expect == result # Expected value should be the same as given. # See https://url.spec.whatwg.org/#percent-encoded-bytes @pytest.mark.parametrize( ("input", "expected"), [ ("%", "%"), ("%2", "%2"), ("%x", "%x"), ("%€", "%€"), ("%2x", "%2x"), ("%2 ", "%2 "), ("% 2", "% 2"), ("%xa", "%xa"), ("%%", "%%"), ("%%3f", "%?"), ("%2%", "%2%"), ("%2%3f", "%2?"), ("%x%3f", "%x?"), ("%€%3f", "%€?"), ], ) def test_unquoting_bad_percent_escapes( unquoter: type[_Unquoter], input: str, expected: str ) -> None: assert unquoter()(input) == expected @pytest.mark.xfail( reason=""" FIXME: After conversion to bytes, should not cause UTF-8 decode fail. See https://url.spec.whatwg.org/#percent-encoded-bytes Refs: * https://github.com/aio-libs/yarl/pull/216 * https://github.com/aio-libs/yarl/pull/214 * https://github.com/aio-libs/yarl/pull/7 """, ) @pytest.mark.parametrize("urlencoded_string", ("%AB", "%AB%AB")) def test_unquoting_invalid_utf8_sequence( unquoter: type[_Unquoter], urlencoded_string: str ) -> None: with pytest.raises(ValueError): unquoter()(urlencoded_string) def test_unquoting_mixed_case_percent_escapes(unquoter: type[_Unquoter]) -> None: expected = "𝕦" assert expected == unquoter()("%F0%9D%95%A6") assert expected == unquoter()("%F0%9d%95%a6") assert expected == unquoter()("%f0%9D%95%a6") assert expected == unquoter()("%f0%9d%95%a6") def test_unquoting_parts(unquoter: type[_Unquoter]) -> None: # Make sure unquoting works when have non-quoted characters # interspersed given = "ab" + hexescape("c") + "d" expect = "abcd" result = unquoter()(given) assert expect == result result = unquoter(qs=True)(given) assert expect == result def test_quote_None(quoter: type[_Quoter]) -> None: assert quoter()(None) is None def test_unquote_None(unquoter: type[_Unquoter]) -> None: assert unquoter()(None) is None def test_quote_empty_string(quoter: type[_Quoter]) -> None: assert quoter()("") == "" def test_unquote_empty_string(unquoter: type[_Unquoter]) -> None: assert unquoter()("") == "" def test_quote_bad_types(quoter: type[_Quoter]) -> None: with pytest.raises(TypeError): quoter()(123) # type: ignore[call-overload] def test_unquote_bad_types(unquoter: type[_Unquoter]) -> None: with pytest.raises(TypeError): unquoter()(123) # type: ignore[call-overload] def test_quote_lowercase(quoter: type[_Quoter]) -> None: assert quoter()("%d1%84") == "%D1%84" def test_quote_unquoted(quoter: type[_Quoter]) -> None: assert quoter()("%41") == "A" def test_quote_space(quoter: type[_Quoter]) -> None: assert quoter()(" ") == "%20" # NULL # test to see if this would work to fix # coverage on this file. def test_quote_percent_last_character(quoter: type[_Quoter]) -> None: # % is last character in this case. assert quoter()("%") == "%25" def test_unquote_unsafe(unquoter: type[_Unquoter]) -> None: assert unquoter(unsafe="@")("%40") == "%40" def test_unquote_unsafe2(unquoter: type[_Unquoter]) -> None: assert unquoter(unsafe="@")("%40abc") == "%40abc" def test_unquote_unsafe3(unquoter: type[_Unquoter]) -> None: assert unquoter(qs=True)("a%2Bb=?%3D%2B%26") == "a%2Bb=?%3D%2B%26" def test_unquote_unsafe4(unquoter: type[_Unquoter]) -> None: assert unquoter(unsafe="@")("a@b") == "a%40b" @pytest.mark.parametrize( ("input", "expected"), [ ("%e2%82", "%e2%82"), ("%e2%82ac", "%e2%82ac"), ("%e2%82%f8", "%e2%82%f8"), ("%e2%82%2b", "%e2%82+"), ("%e2%82%e2%82%ac", "%e2%82€"), ("%e2%82%e2%82", "%e2%82%e2%82"), ], ) def test_unquote_non_utf8(unquoter: type[_Unquoter], input: str, expected: str) -> None: assert unquoter()(input) == expected def test_unquote_unsafe_non_utf8(unquoter: type[_Unquoter]) -> None: assert unquoter(unsafe="\n")("%e2%82%0a") == "%e2%82%0A" def test_unquote_plus_non_utf8(unquoter: type[_Unquoter]) -> None: assert unquoter(qs=True)("%e2%82%2b") == "%e2%82%2B" def test_quote_non_ascii(quoter: type[_Quoter]) -> None: assert quoter()("%F8") == "%F8" def test_quote_non_ascii2(quoter: type[_Quoter]) -> None: assert quoter()("a%F8b") == "a%F8b" def test_quote_percent_percent_encoded(quoter: type[_Quoter]) -> None: assert quoter()("%%3f") == "%25%3F" def test_quote_percent_digit_percent_encoded(quoter: type[_Quoter]) -> None: assert quoter()("%2%3f") == "%252%3F" def test_quote_percent_safe_percent_encoded(quoter: type[_Quoter]) -> None: assert quoter()("%x%3f") == "%25x%3F" def test_quote_percent_unsafe_percent_encoded(quoter: type[_Quoter]) -> None: assert quoter()("%#%3f") == "%25%23%3F" def test_quote_percent_non_ascii_percent_encoded(quoter: type[_Quoter]) -> None: assert quoter()("%ß%3f") == "%25%C3%9F%3F" def test_quote_percent_non_ascii2_percent_encoded(quoter: type[_Quoter]) -> None: assert quoter()("%€%3f") == "%25%E2%82%AC%3F" def test_quote_percent_non_ascii3_percent_encoded(quoter: type[_Quoter]) -> None: assert quoter()("%🐍%3f") == "%25%F0%9F%90%8D%3F" def test_quote_starts_with_percent(quoter: type[_Quoter]) -> None: assert quoter()("%a") == "%25a" def test_quote_ends_with_percent(quoter: type[_Quoter]) -> None: assert quoter()("a%") == "a%25" def test_quote_all_percent(quoter: type[_Quoter]) -> None: assert quoter()("%%%%") == "%25%25%25%25" class StrLike(str): """Str subclass.""" def test_quote_str_like(quoter: type[_Quoter]) -> None: assert quoter()(StrLike("abc")) == "abc" def test_unquote_str_like(unquoter: type[_Unquoter]) -> None: assert unquoter()(StrLike("abc")) == "abc" def test_quote_sub_delims(quoter: type[_Quoter]) -> None: assert quoter()("!$&'()*+,;=") == "!$&'()*+,;=" def test_requote_sub_delims(quoter: type[_Quoter]) -> None: assert quoter()("%21%24%26%27%28%29%2A%2B%2C%3B%3D") == "!$&'()*+,;=" def test_unquoting_plus(unquoter: type[_Unquoter]) -> None: assert unquoter(qs=False)("a+b") == "a+b" def test_unquote_plus_to_space(unquoter: type[_Unquoter]) -> None: assert unquoter(qs=True)("a+b") == "a b" def test_unquote_with_plus_plus_to_space(unquoter: type[_Unquoter]) -> None: assert unquoter(plus=True)("a+b") == "a b" def test_unquote_without_plus_plus(unquoter: type[_Unquoter]) -> None: assert unquoter(plus=False)("a+b") == "a+b" def test_unquote_plus_to_space_unsafe(unquoter: type[_Unquoter]) -> None: assert unquoter(unsafe="+", qs=True)("a+b") == "a+b" def test_unquote_multiple_unsafe(unquoter: type[_Unquoter]) -> None: assert unquoter(unsafe="!@#$")("a!@#$b") == "a%21%40%23%24b" def test_unquote_explict_empty_unsafe(unquoter: type[_Unquoter]) -> None: assert unquoter(unsafe="")("a!@#$b") == "a!@#$b" def test_quote_qs_with_colon(quoter: type[_Quoter]) -> None: s = quoter(safe="=+&?/:@", qs=True)("next=http%3A//example.com/") assert s == "next=http://example.com/" def test_quote_protected(quoter: type[_Quoter]) -> None: s = quoter(protected="/")("/path%2fto/three") assert s == "/path%2Fto/three" def test_quote_fastpath_safe(quoter: type[_Quoter]) -> None: s1 = "/path/to" s2 = quoter(safe="/")(s1) assert s1 is s2 def test_quote_fastpath_pct(quoter: type[_Quoter]) -> None: s1 = "abc%A0" s2 = quoter()(s1) assert s1 is s2 def test_quote_very_large_string(quoter: type[_Quoter]) -> None: # more than 8 KiB s = "abcфух%30%0a" * 1024 assert quoter()(s) == "abc%D1%84%D1%83%D1%850%0A" * 1024 def test_space(quoter: type[_Quoter]) -> None: s = "% A" assert quoter()(s) == "%25%20A" def test_quoter_path_with_plus(quoter: type[_Quoter]) -> None: s = "/test/x+y%2Bz/:+%2B/" assert "/test/x+y%2Bz/:+%2B/" == quoter(safe="@:", protected="/+")(s) def test_unquoter_path_with_plus(unquoter: type[_Unquoter]) -> None: s = "/test/x+y%2Bz/:+%2B/" assert "/test/x+y+z/:++/" == unquoter(unsafe="+")(s) @given(safe=st.text(), protected=st.text(), qs=st.booleans(), requote=st.booleans()) def test_fuzz__PyQuoter(safe: str, protected: str, qs: bool, requote: bool) -> None: # type: ignore[misc] """Verify that _PyQuoter can be instantiated with any valid arguments.""" _PyQuoter(safe=safe, protected=protected, qs=qs, requote=requote) @given(ignore=st.text(), unsafe=st.text(), qs=st.booleans()) def test_fuzz__PyUnquoter(ignore: str, unsafe: str, qs: bool) -> None: # type: ignore[misc] """Verify that _PyUnquoter can be instantiated with any valid arguments.""" _PyUnquoter(ignore=ignore, unsafe=unsafe, qs=qs) @example(text_input="0") @given( text_input=st.text( alphabet=st.characters(max_codepoint=127, blacklist_characters="%") ), ) @pytest.mark.parametrize("quoter", quoters, ids=quoter_ids) @pytest.mark.parametrize("unquoter", unquoters, ids=unquoter_ids) def test_quote_unquote_parameter( # type: ignore[misc] quoter: type[_PyQuoter], unquoter: type[_PyUnquoter], text_input: str, ) -> None: quote = quoter() unquote = unquoter() text_quoted = quote(text_input) note(f"text_quoted={text_quoted!r}") text_output = unquote(text_quoted) assert text_input == text_output @example(text_input="0") @given( text_input=st.text( alphabet=st.characters(max_codepoint=127, blacklist_characters="%") ), ) @pytest.mark.parametrize("quoter", quoters, ids=quoter_ids) @pytest.mark.parametrize("unquoter", unquoters, ids=unquoter_ids) def test_quote_unquote_parameter_requote( # type: ignore[misc] quoter: type[_PyQuoter], unquoter: type[_PyUnquoter], text_input: str, ) -> None: quote = quoter(requote=True) unquote = unquoter() text_quoted = quote(text_input) note(f"text_quoted={text_quoted!r}") text_output = unquote(text_quoted) assert text_input == text_output @example(text_input="0") @given( text_input=st.text( alphabet=st.characters(max_codepoint=127, blacklist_characters="%") ), ) @pytest.mark.parametrize("quoter", quoters, ids=quoter_ids) @pytest.mark.parametrize("unquoter", unquoters, ids=unquoter_ids) def test_quote_unquote_parameter_path_safe( # type: ignore[misc] quoter: type[_PyQuoter], unquoter: type[_PyUnquoter], text_input: str, ) -> None: quote = quoter() unquote = unquoter(ignore="/%", unsafe="+") assume("+" not in text_input and "/" not in text_input) text_quoted = quote(text_input) note(f"text_quoted={text_quoted!r}") text_output = unquote(text_quoted) assert text_input == text_output ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_quoting_benchmarks.py0000644000175100001660000000433214774356277020642 0ustar00runnerdocker"""codspeed benchmark for yarl._quoting module.""" from pytest_codspeed import BenchmarkFixture from yarl._quoting import _Quoter, _Unquoter QUOTER_SLASH_SAFE = _Quoter(safe="/") QUOTER = _Quoter() UNQUOTER = _Unquoter() QUERY_QUOTER = _Quoter(safe="?/:@", protected="=+&;", qs=True, requote=False) PATH_QUOTER = _Quoter(safe="@:", protected="/+", requote=False) LONG_PATH = "/path/to" * 100 LONG_QUERY = "a=1&b=2&c=3&d=4&e=5&f=6&g=7&h=8&i=9&j=0" * 25 LONG_QUERY_WITH_PCT = LONG_QUERY + "&d=%25%2F%3F%3A%40%26%3B%3D%2B" def test_quote_query_string(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): QUERY_QUOTER("a=1&b=2&c=3&d=4&e=5&f=6&g=7&h=8&i=9&j=0") def test_quoter_ascii(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): QUOTER_SLASH_SAFE("/path/to") def test_quote_long_path(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): PATH_QUOTER(LONG_PATH) def test_quoter_pct(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): QUOTER("abc%0a") def test_long_query(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): QUERY_QUOTER(LONG_QUERY) def test_long_query_with_pct(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): QUERY_QUOTER(LONG_QUERY_WITH_PCT) def test_quoter_quote_utf8(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): PATH_QUOTER("/шлях/файл") def test_unquoter_short(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): UNQUOTER("/path/to") def test_unquoter_long_ascii(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): UNQUOTER(LONG_QUERY) def test_unquoter_long_pct(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): UNQUOTER(LONG_QUERY_WITH_PCT) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_update_query.py0000644000175100001660000003632414774356277017474 0ustar00runnerdockerimport enum from typing import Optional, Union import pytest from multidict import MultiDict from yarl import URL # with_query def test_with_query() -> None: url = URL("http://example.com") assert str(url.with_query({"a": "1"})) == "http://example.com/?a=1" def test_update_query() -> None: url = URL("http://example.com/") assert str(url.update_query({"a": "1"})) == "http://example.com/?a=1" assert str(URL("test").update_query(a=1)) == "test?a=1" url = URL("http://example.com/?foo=bar") expected_url = URL("http://example.com/?foo=bar&baz=foo") assert url.update_query({"baz": "foo"}) == expected_url assert url.update_query(baz="foo") == expected_url assert url.update_query("baz=foo") == expected_url def test_update_query_with_args_and_kwargs() -> None: url = URL("http://example.com/") with pytest.raises(ValueError): url.update_query("a", foo="bar") # type: ignore[call-overload] def test_update_query_with_multiple_args() -> None: url = URL("http://example.com/") with pytest.raises(ValueError): url.update_query("a", "b") # type: ignore[call-overload] def test_update_query_with_none_arg() -> None: url = URL("http://example.com/?foo=bar&baz=foo") expected_url = URL("http://example.com/") assert url.update_query(None) == expected_url def test_update_query_with_empty_dict() -> None: url = URL("http://example.com/?foo=bar&baz=foo") assert url.update_query({}) == url def test_with_query_list_of_pairs() -> None: url = URL("http://example.com") assert str(url.with_query([("a", "1")])) == "http://example.com/?a=1" def test_with_query_list_non_pairs() -> None: url = URL("http://example.com") with pytest.raises(ValueError): url.with_query(["a=1", "b=2", "c=3"]) # type: ignore[list-item] def test_with_query_kwargs() -> None: url = URL("http://example.com") q = url.with_query(query="1", query2="1").query assert q == dict(query="1", query2="1") def test_with_query_kwargs_and_args_are_mutually_exclusive() -> None: url = URL("http://example.com") with pytest.raises(ValueError): url.with_query({"a": "2", "b": "4"}, a="1") # type: ignore[call-overload] def test_with_query_only_single_arg_is_supported() -> None: url = URL("http://example.com") u1 = url.with_query(b=3) u2 = URL("http://example.com/?b=3") assert u1 == u2 with pytest.raises(ValueError): url.with_query("a=1", "a=b") # type: ignore[call-overload] def test_with_query_empty_dict() -> None: url = URL("http://example.com/?a=b") new_url = url.with_query({}) assert new_url.query_string == "" assert str(new_url) == "http://example.com/" def test_with_query_empty_str() -> None: url = URL("http://example.com/?a=b") assert str(url.with_query("")) == "http://example.com/" def test_with_query_empty_value() -> None: url = URL("http://example.com/") assert str(url.with_query({"a": ""})) == "http://example.com/?a=" def test_with_query_str() -> None: url = URL("http://example.com") assert str(url.with_query("a=1&b=2")) == "http://example.com/?a=1&b=2" def test_with_query_str_non_ascii_and_spaces() -> None: url = URL("http://example.com") url2 = url.with_query("a=1 2&b=знач") assert url2.raw_query_string == "a=1+2&b=%D0%B7%D0%BD%D0%B0%D1%87" assert url2.query_string == "a=1 2&b=знач" def test_with_query_int() -> None: url = URL("http://example.com") assert url.with_query({"a": 1}) == URL("http://example.com/?a=1") def test_with_query_kwargs_int() -> None: url = URL("http://example.com") assert url.with_query(b=2) == URL("http://example.com/?b=2") def test_with_query_list_int() -> None: url = URL("http://example.com") assert str(url.with_query([("a", 1)])) == "http://example.com/?a=1" @pytest.mark.parametrize( ("query", "expected"), [ pytest.param({"a": []}, "", id="empty list"), pytest.param({"a": ()}, "", id="empty tuple"), pytest.param({"a": [1]}, "/?a=1", id="single list"), pytest.param({"a": (1,)}, "/?a=1", id="single tuple"), pytest.param({"a": [1, 2]}, "/?a=1&a=2", id="list"), pytest.param({"a": (1, 2)}, "/?a=1&a=2", id="tuple"), pytest.param({"a[]": [1, 2]}, "/?a%5B%5D=1&a%5B%5D=2", id="key with braces"), pytest.param({"&": [1, 2]}, "/?%26=1&%26=2", id="quote key"), pytest.param({"a": ["1", 2]}, "/?a=1&a=2", id="mixed types"), pytest.param({"&": ["=", 2]}, "/?%26=%3D&%26=2", id="quote key and value"), pytest.param({"a": 1, "b": [2, 3]}, "/?a=1&b=2&b=3", id="single then list"), pytest.param({"a": [1, 2], "b": 3}, "/?a=1&a=2&b=3", id="list then single"), pytest.param({"a": ["1&a=2", 3]}, "/?a=1%26a%3D2&a=3", id="ampersand then int"), pytest.param({"a": [1, "2&a=3"]}, "/?a=1&a=2%26a%3D3", id="int then ampersand"), ], ) def test_with_query_sequence( query: dict[str, Union[int, list[Union[str, int]], tuple[int, ...]]], expected: str ) -> None: url = URL("http://example.com") expected = "http://example.com{expected}".format_map(locals()) assert str(url.with_query(query)) == expected @pytest.mark.parametrize( "query", [ pytest.param({"a": [[1]]}, id="nested"), pytest.param([("a", [1, 2])], id="tuple list"), ], ) def test_with_query_sequence_invalid_use(query: object) -> None: url = URL("http://example.com") with pytest.raises(TypeError, match="Invalid variable type"): url.with_query(query) # type: ignore[call-overload] class _CStr(str): """Custom str.""" class _EmptyStrEr: def __str__(self) -> str: assert False class _CInt(int, _EmptyStrEr): """Custom int.""" class _CFloat(float, _EmptyStrEr): """Custom float.""" @pytest.mark.parametrize( ("value", "expected"), [ pytest.param("1", "1", id="str"), pytest.param(_CStr("1"), "1", id="custom str"), pytest.param(1, "1", id="int"), pytest.param(_CInt(1), "1", id="custom int"), pytest.param(1.1, "1.1", id="float"), pytest.param(_CFloat(1.1), "1.1", id="custom float"), ], ) def test_with_query_valid_type(value: Union[str, int, float], expected: str) -> None: url = URL("http://example.com") expected = "http://example.com/?a={expected}".format_map(locals()) assert str(url.with_query({"a": value})) == expected @pytest.mark.parametrize( ("value", "exc_type"), [ pytest.param(True, TypeError, id="bool"), pytest.param(None, TypeError, id="none"), pytest.param(float("inf"), ValueError, id="non-finite float"), pytest.param(float("nan"), ValueError, id="NaN float"), ], ) def test_with_query_invalid_type( value: Union[bool, float, None], exc_type: type[Exception] ) -> None: url = URL("http://example.com") with pytest.raises(exc_type): url.with_query({"a": value}) # type: ignore[dict-item] @pytest.mark.parametrize( ("value", "expected"), [ pytest.param("1", "1", id="str"), pytest.param(_CStr("1"), "1", id="custom str"), pytest.param(1, "1", id="int"), pytest.param(_CInt(1), "1", id="custom int"), pytest.param(1.1, "1.1", id="float"), pytest.param(_CFloat(1.1), "1.1", id="custom float"), ], ) def test_with_query_list_valid_type( value: Union[str, int, float], expected: str ) -> None: url = URL("http://example.com") expected = "http://example.com/?a={expected}".format_map(locals()) assert str(url.with_query([("a", value)])) == expected @pytest.mark.parametrize( ("value"), [pytest.param(True, id="bool"), pytest.param(None, id="none")] ) def test_with_query_list_invalid_type(value: Optional[bool]) -> None: url = URL("http://example.com") with pytest.raises(TypeError): url.with_query([("a", value)]) # type: ignore[list-item] def test_with_int_enum() -> None: class IntEnum(int, enum.Enum): A = 1 url = URL("http://example.com/path") url2 = url.with_query(a=IntEnum.A) assert str(url2) == "http://example.com/path?a=1" def test_with_class_that_implements__int__() -> None: """Allow classes that implement __int__ to be used in query strings.""" class myint: def __int__(self) -> int: return 84 url = URL("http://example.com/path") url2 = url.with_query(a=myint()) assert str(url2) == "http://example.com/path?a=84" def test_with_float_enum() -> None: class FloatEnum(float, enum.Enum): A = 1.1 url = URL("http://example.com/path") url2 = url.with_query(a=FloatEnum.A) assert str(url2) == "http://example.com/path?a=1.1" def test_with_query_multidict() -> None: url = URL("http://example.com/path") q = MultiDict([("a", "b"), ("c", "d")]) assert str(url.with_query(q)) == "http://example.com/path?a=b&c=d" def test_with_multidict_with_spaces_and_non_ascii() -> None: url = URL("http://example.com") url2 = url.with_query({"a b": "ю б"}) assert url2.raw_query_string == "a+b=%D1%8E+%D0%B1" def test_with_query_multidict_with_unsafe() -> None: url = URL("http://example.com/path") url2 = url.with_query({"a+b": "?=+&;"}) assert url2.raw_query_string == "a%2Bb=?%3D%2B%26%3B" assert url2.query_string == "a%2Bb=?%3D%2B%26%3B" assert url2.query == {"a+b": "?=+&;"} def test_with_query_None() -> None: url = URL("http://example.com/path?a=b") assert url.with_query(None).query_string == "" def test_with_query_bad_type() -> None: url = URL("http://example.com") with pytest.raises(TypeError): url.with_query(123) # type: ignore[call-overload] def test_with_query_bytes() -> None: url = URL("http://example.com") with pytest.raises(TypeError): url.with_query(b"123") # type: ignore[arg-type] def test_with_query_bytearray() -> None: url = URL("http://example.com") with pytest.raises(TypeError): url.with_query(bytearray(b"123")) # type: ignore[arg-type] def test_with_query_memoryview() -> None: url = URL("http://example.com") with pytest.raises(TypeError): url.with_query(memoryview(b"123")) @pytest.mark.parametrize( ("query", "expected"), [ pytest.param([("key", "1;2;3")], "?key=1%3B2%3B3", id="tuple list semicolon"), pytest.param({"key": "1;2;3"}, "?key=1%3B2%3B3", id="mapping semicolon"), pytest.param([("key", "1&a=2")], "?key=1%26a%3D2", id="tuple list ampersand"), pytest.param({"key": "1&a=2"}, "?key=1%26a%3D2", id="mapping ampersand"), pytest.param([("&", "=")], "?%26=%3D", id="tuple list quote key"), pytest.param({"&": "="}, "?%26=%3D", id="mapping quote key"), pytest.param( [("a[]", "3")], "?a%5B%5D=3", id="quote one key braces", ), pytest.param( [("a[]", "3"), ("a[]", "4")], "?a%5B%5D=3&a%5B%5D=4", id="quote many key braces", ), ], ) def test_with_query_params( query: Union[list[tuple[str, ...]], dict[str, str]], expected: str ) -> None: url = URL("http://example.com/get") url2 = url.with_query(query) # type: ignore[arg-type] assert str(url2) == ("http://example.com/get" + expected) def test_with_query_only() -> None: url = URL() url2 = url.with_query(key="value") assert str(url2) == "?key=value" def test_with_query_complex_url() -> None: target_url = "http://example.com/?game=bulls+%26+cows" url = URL("/redir").with_query({"t": target_url}) assert url.query["t"] == target_url def test_update_query_multiple_keys() -> None: url = URL("http://example.com/path?a=1&a=2") u2 = url.update_query([("a", "3"), ("a", "4")]) assert str(u2) == "http://example.com/path?a=3&a=4" def test_update_query_with_non_ascii() -> None: url = URL("http://example.com/?foo=bar&baz=foo&%F0%9D%95%A6=%F0%9D%95%A6") assert url.update_query({"𝕦": "𝕦"}) == url def test_update_query_with_non_ascii_as_str() -> None: url = URL("http://example.com/?foo=bar&baz=foo&%F0%9D%95%A6=%F0%9D%95%A6") assert url.update_query("𝕦=𝕦") == url # mod operator def test_update_query_with_mod_operator() -> None: url = URL("http://example.com/") assert str(url % {"a": "1"}) == "http://example.com/?a=1" assert str(url % [("a", "1")]) == "http://example.com/?a=1" assert str(url % "a=1&b=2") == "http://example.com/?a=1&b=2" assert str(url % {"a": "1"} % {"b": "2"}) == "http://example.com/?a=1&b=2" assert str(url % {"a": "1"} % {"a": "3", "b": "2"}) == "http://example.com/?a=3&b=2" assert str(url / "foo" % {"a": "1"}) == "http://example.com/foo?a=1" def test_extend_query() -> None: url = URL("http://example.com/") assert str(url.extend_query({"a": "1"})) == "http://example.com/?a=1" assert str(URL("test").extend_query(a=1)) == "test?a=1" url = URL("http://example.com/?foo=bar") expected_url = URL("http://example.com/?foo=bar&baz=foo") assert url.extend_query({"baz": "foo"}) == expected_url assert url.extend_query(baz="foo") == expected_url assert url.extend_query("baz=foo") == expected_url def test_extend_query_with_args_and_kwargs() -> None: url = URL("http://example.com/") with pytest.raises(ValueError): url.extend_query("a", foo="bar") # type: ignore[call-overload] def test_extend_query_with_multiple_args() -> None: url = URL("http://example.com/") with pytest.raises(ValueError): url.extend_query("a", "b") # type: ignore[call-overload] def test_extend_query_with_none_arg() -> None: url = URL("http://example.com/?foo=bar&baz=foo") assert url.extend_query(None) == url def test_extend_query_with_empty_dict() -> None: url = URL("http://example.com/?foo=bar&baz=foo") assert url.extend_query({}) == url def test_extend_query_existing_keys() -> None: url = URL("http://example.com/?a=2") assert str(url.extend_query({"a": "1"})) == "http://example.com/?a=2&a=1" assert str(URL("test").extend_query(a=1)) == "test?a=1" url = URL("http://example.com/?foo=bar&baz=original") expected_url = URL("http://example.com/?foo=bar&baz=original&baz=foo") assert url.extend_query({"baz": "foo"}) == expected_url assert url.extend_query(baz="foo") == expected_url assert url.extend_query("baz=foo") == expected_url def test_extend_query_with_args_and_kwargs_with_existing() -> None: url = URL("http://example.com/?a=original") with pytest.raises(ValueError): url.extend_query("a", foo="bar") # type: ignore[call-overload] def test_extend_query_with_non_ascii() -> None: url = URL("http://example.com/?foo=bar&baz=foo") expected = URL("http://example.com/?foo=bar&baz=foo&%F0%9D%95%A6=%F0%9D%95%A6") assert url.extend_query({"𝕦": "𝕦"}) == expected def test_extend_query_with_non_ascii_as_str() -> None: url = URL("http://example.com/?foo=bar&baz=foo&") expected = URL("http://example.com/?foo=bar&baz=foo&%F0%9D%95%A6=%F0%9D%95%A6") assert url.extend_query("𝕦=𝕦") == expected def test_extend_query_with_non_ascii_same_key() -> None: url = URL("http://example.com/?foo=bar&baz=foo&%F0%9D%95%A6=%F0%9D%95%A6") expected = URL( "http://example.com/?foo=bar&baz=foo" "&%F0%9D%95%A6=%F0%9D%95%A6&%F0%9D%95%A6=%F0%9D%95%A6" ) assert url.extend_query({"𝕦": "𝕦"}) == expected ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_url.py0000644000175100001660000021363514774356277015571 0ustar00runnerdockerfrom enum import Enum from urllib.parse import SplitResult, quote, unquote import pytest from yarl import URL _WHATWG_C0_CONTROL_OR_SPACE = ( "\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 " ) _VERTICAL_COLON = "\ufe13" # normalizes to ":" _FULL_WITH_NUMBER_SIGN = "\uff03" # normalizes to "#" _ACCOUNT_OF = "\u2100" # normalizes to "a/c" def test_inheritance() -> None: with pytest.raises(TypeError) as ctx: class MyURL(URL): pass assert ( "Inheriting a class " ".MyURL'> " "from URL is forbidden" == str(ctx.value) ) def test_str_subclass() -> None: class S(str): pass assert str(URL(S("http://example.com"))) == "http://example.com" def test_is() -> None: u1 = URL("http://example.com") u2 = URL(u1) assert u1 is u2 def test_bool() -> None: assert URL("http://example.com") assert not URL() assert not URL("") def test_absolute_url_without_host() -> None: with pytest.raises(ValueError): URL("http://:8080/") def test_url_is_not_str() -> None: url = URL("http://example.com") assert not isinstance(url, str) # type: ignore[unreachable] def test_str() -> None: url = URL("http://example.com:8888/path/to?a=1&b=2") assert str(url) == "http://example.com:8888/path/to?a=1&b=2" def test_repr() -> None: url = URL("http://example.com") assert "URL('http://example.com')" == repr(url) def test_origin() -> None: url = URL("http://user:password@example.com:8888/path/to?a=1&b=2") assert URL("http://example.com:8888") == url.origin() def test_origin_is_equal_to_self() -> None: url = URL("http://example.com:8888") assert url.origin() == url def test_origin_with_no_auth() -> None: url = URL("http://example.com:8888/path/to?a=1&b=2") assert URL("http://example.com:8888") == url.origin() def test_origin_nonascii() -> None: url = URL("http://user:password@оун-упа.укр:8888/path/to?a=1&b=2") assert str(url.origin()) == "http://xn----8sb1bdhvc.xn--j1amh:8888" def test_origin_ipv6() -> None: url = URL("http://user:password@[::1]:8888/path/to?a=1&b=2") assert str(url.origin()) == "http://[::1]:8888" def test_origin_not_absolute_url() -> None: url = URL("/path/to?a=1&b=2") with pytest.raises(ValueError): url.origin() def test_origin_no_scheme() -> None: url = URL("//user:password@example.com:8888/path/to?a=1&b=2") with pytest.raises(ValueError): url.origin() def test_drop_dots() -> None: u = URL("http://example.com/path/../to") assert str(u) == "http://example.com/to" def test_abs_cmp() -> None: assert URL("http://example.com:8888") == URL("http://example.com:8888") assert URL("http://example.com:8888/") == URL("http://example.com:8888/") assert URL("http://example.com:8888/") == URL("http://example.com:8888") assert URL("http://example.com:8888") == URL("http://example.com:8888/") def test_abs_hash() -> None: url = URL("http://example.com:8888") url_trailing = URL("http://example.com:8888/") assert hash(url) == hash(url_trailing) # properties def test_scheme() -> None: url = URL("http://example.com") assert "http" == url.scheme def test_raw_user() -> None: url = URL("http://user@example.com") assert "user" == url.raw_user assert url.raw_user == SplitResult(*url._val).username def test_raw_user_non_ascii() -> None: url = URL("http://бажан@example.com") assert "%D0%B1%D0%B0%D0%B6%D0%B0%D0%BD" == url.raw_user assert url.raw_user == SplitResult(*url._val).username def test_no_user() -> None: url = URL("http://example.com") assert url.user is None def test_user_non_ascii() -> None: url = URL("http://бажан@example.com") assert "бажан" == url.user def test_raw_password() -> None: url = URL("http://user:password@example.com") assert "password" == url.raw_password assert url.raw_password == SplitResult(*url._val).password def test_raw_password_non_ascii() -> None: url = URL("http://user:пароль@example.com") assert "%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C" == url.raw_password assert url.raw_password == SplitResult(*url._val).password def test_password_non_ascii() -> None: url = URL("http://user:пароль@example.com") assert "пароль" == url.password def test_password_without_user() -> None: url = URL("http://:password@example.com") assert url.user is None assert "password" == url.password def test_empty_password_without_user() -> None: url = URL("http://:@example.com") assert url.user is None assert url.password == "" assert url.raw_password == "" assert url.raw_password == SplitResult(*url._val).password def test_user_empty_password() -> None: url = URL("http://user:@example.com") assert "user" == url.user assert "" == url.password def test_raw_host() -> None: url = URL("http://example.com") assert "example.com" == url.raw_host assert url.raw_host == SplitResult(*url._val).hostname @pytest.mark.parametrize( ("host"), [ ("example.com"), ("[::1]"), ("xn--gnter-4ya.com"), ], ) def test_host_subcomponent(host: str) -> None: url = URL(f"http://{host}") assert url.host_subcomponent == host @pytest.mark.parametrize( ("input", "result"), [ ("/", None), ("http://example.com", "example.com"), ("http://[::1]", "[::1]"), ("http://xn--gnter-4ya.com", "xn--gnter-4ya.com"), ("http://example.com.", "example.com"), ("https://example.com.", "example.com"), ("http://example.com:80", "example.com"), ("http://example.com:8080", "example.com:8080"), ("http://[::1]:8080", "[::1]:8080"), ], ) def test_host_port_subcomponent(input: str, result: str) -> None: url = URL(input) assert url.host_port_subcomponent == result def test_host_subcomponent_return_idna_encoded_host() -> None: url = URL("http://оун-упа.укр") assert url.host_subcomponent == "xn----8sb1bdhvc.xn--j1amh" def test_invalid_idna_hyphen_encoding() -> None: url = URL("http://x-----xn1agdj.tld") assert url.host == "x-----xn1agdj.tld" def test_invalid_idna_a_label_encoding() -> None: url = URL("http://xn--d.tld") assert url.raw_host == "xn--d.tld" def test_raw_host_non_ascii() -> None: url = URL("http://оун-упа.укр") assert "xn----8sb1bdhvc.xn--j1amh" == url.raw_host assert url.raw_host == SplitResult(*url._val).hostname def test_host_non_ascii() -> None: url = URL("http://оун-упа.укр") assert "оун-упа.укр" == url.host def test_localhost() -> None: url = URL("http://[::1]") assert "::1" == url.host def test_host_with_underscore() -> None: url = URL("http://abc_def.com") assert "abc_def.com" == url.host def test_raw_host_when_port_is_specified() -> None: url = URL("http://example.com:8888") assert "example.com" == url.raw_host assert url.raw_host == SplitResult(*url._val).hostname def test_raw_host_from_str_with_ipv4() -> None: url = URL("http://127.0.0.1:80") assert url.raw_host == "127.0.0.1" assert url.raw_host == SplitResult(*url._val).hostname def test_raw_host_from_str_with_ipv6() -> None: url = URL("http://[::1]:80") assert url.raw_host == "::1" assert url.raw_host == SplitResult(*url._val).hostname def test_authority_full() -> None: url = URL("http://user:passwd@host.com:8080/path") assert url.raw_authority == "user:passwd@host.com:8080" assert url.authority == "user:passwd@host.com:8080" def test_authority_short() -> None: url = URL("http://host.com/path") assert url.raw_authority == "host.com" def test_authority_full_nonasci() -> None: url = URL("http://степан:пароль@слава.укр:8080/path") assert url.raw_authority == ( "%D1%81%D1%82%D0%B5%D0%BF%D0%B0%D0%BD:" "%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@" "xn--80aaf8a3a.xn--j1amh:8080" ) assert url.authority == "степан:пароль@слава.укр:8080" def test_authority_unknown_scheme() -> None: v = "scheme://user:password@example.com:43/path/to?a=1&b=2" url = URL(v) assert str(url) == v def test_lowercase() -> None: url = URL("http://gitHUB.com") assert url.raw_host == "github.com" assert url.host == url.raw_host assert url.raw_host == SplitResult(*url._val).hostname def test_lowercase_nonascii() -> None: url = URL("http://Слава.Укр") assert url.raw_host == "xn--80aaf8a3a.xn--j1amh" assert url.raw_host == SplitResult(*url._val).hostname assert url.host == "слава.укр" def test_compressed_ipv6() -> None: url = URL("http://[1DEC:0:0:0::1]") assert url.raw_host == "1dec::1" assert url.host == url.raw_host assert url.raw_host == SplitResult(*url._val).hostname def test_ipv6_missing_left_bracket() -> None: with pytest.raises(ValueError, match="Invalid IPv6 URL"): URL("http://[1dec:0:0:0::1/") def test_ipv6_missing_right_bracket() -> None: with pytest.raises(ValueError, match="Invalid IPv6 URL"): URL("http://[1dec:0:0:0::1/") def test_ipv4_brackets_not_allowed() -> None: with pytest.raises(ValueError, match="An IPv4 address cannot be in brackets"): URL("http://[127.0.0.1]/") def test_ipfuture_brackets_not_allowed() -> None: with pytest.raises(ValueError, match="IPvFuture address is invalid"): URL("http://[v10]/") def test_ipv4_zone() -> None: # I'm unsure if it is correct. url = URL("http://1.2.3.4%тест%42:123") assert url.raw_host == "1.2.3.4%тест%42" assert url.host == url.raw_host assert url.raw_host == SplitResult(*url._val).hostname def test_port_for_explicit_port() -> None: url = URL("http://example.com:8888") assert 8888 == url.port assert url.explicit_port == SplitResult(*url._val).port def test_port_for_implicit_port() -> None: url = URL("http://example.com") assert 80 == url.port assert url.explicit_port == SplitResult(*url._val).port def test_port_for_relative_url() -> None: url = URL("/path/to") assert url.port is None assert url.explicit_port is None def test_port_for_unknown_scheme() -> None: url = URL("unknown://example.com") assert url.port is None assert url.explicit_port is None def test_explicit_zero_port() -> None: url = URL("http://example.com:0") assert url.explicit_port == 0 assert url.port == 0 def test_explicit_port_for_explicit_port() -> None: url = URL("http://example.com:8888") assert 8888 == url.explicit_port assert url.explicit_port == SplitResult(*url._val).port def test_explicit_port_for_implicit_port() -> None: url = URL("http://example.com") assert url.explicit_port is None assert url.explicit_port == SplitResult(*url._val).port def test_explicit_port_for_relative_url() -> None: url = URL("/path/to") assert url.explicit_port is None assert url.explicit_port == SplitResult(*url._val).port def test_explicit_port_for_unknown_scheme() -> None: url = URL("unknown://example.com") assert url.explicit_port is None assert url.explicit_port == SplitResult(*url._val).port def test_raw_path_string_empty() -> None: url = URL("http://example.com") assert "/" == url.raw_path def test_raw_path() -> None: url = URL("http://example.com/path/to") assert "/path/to" == url.raw_path def test_raw_path_non_ascii() -> None: url = URL("http://example.com/шлях/сюди") assert "/%D1%88%D0%BB%D1%8F%D1%85/%D1%81%D1%8E%D0%B4%D0%B8" == url.raw_path def test_path_non_ascii() -> None: url = URL("http://example.com/шлях/сюди") assert "/шлях/сюди" == url.path def test_path_with_spaces() -> None: url = URL("http://example.com/a b/test") assert "/a b/test" == url.path url = URL("http://example.com/a b") assert "/a b" == url.path def test_path_with_2F() -> None: """Path should decode %2F.""" url = URL("http://example.com/foo/bar%2fbaz") assert url.path == "/foo/bar/baz" def test_path_safe_with_2F() -> None: """Path safe should not decode %2F, otherwise it may look like a path separator.""" url = URL("http://example.com/foo/bar%2fbaz") assert url.path_safe == "/foo/bar%2Fbaz" def test_path_safe_with_25() -> None: """Path safe should not decode %25, otherwise it is prone to double unquoting.""" url = URL("http://example.com/foo/bar%252Fbaz") assert url.path_safe == "/foo/bar%252Fbaz" unquoted = url.path_safe.replace("%2F", "/").replace("%25", "%") assert unquoted == "/foo/bar%2Fbaz" def test_path_safe_with_no_netloc() -> None: """Path safe should not decode %2F, otherwise it may look like a path separator.""" url = URL("/foo/bar%2fbaz") assert url.path_safe == "/foo/bar%2Fbaz" url = URL("") assert url.path_safe == "" url = URL("http://example.com") assert url.path_safe == "/" @pytest.mark.parametrize( "original_path", [ "m+@bar/baz", "m%2B@bar/baz", "m%252B@bar/baz", "m%2F@bar/baz", ], ) def test_path_safe_only_round_trips(original_path: str) -> None: """Path safe can round trip with documented decode method.""" encoded_once = quote(original_path, safe="") encoded_twice = quote(encoded_once, safe="") url = URL(f"http://example.com/{encoded_twice}") unquoted = url.path_safe.replace("%2F", "/").replace("%25", "%") assert unquoted == f"/{encoded_once}" assert unquote(unquoted) == f"/{original_path}" def test_raw_path_for_empty_url() -> None: url = URL() assert "" == url.raw_path def test_raw_path_for_colon_and_at() -> None: url = URL("http://example.com/path:abc@123") assert url.raw_path == "/path:abc@123" def test_raw_query_string() -> None: url = URL("http://example.com?a=1&b=2") assert url.raw_query_string == "a=1&b=2" def test_raw_query_string_non_ascii() -> None: url = URL("http://example.com?б=в&ю=к") assert url.raw_query_string == "%D0%B1=%D0%B2&%D1%8E=%D0%BA" def test_query_string_non_ascii() -> None: url = URL("http://example.com?б=в&ю=к") assert url.query_string == "б=в&ю=к" def test_path_qs() -> None: url = URL("http://example.com/") assert url.path_qs == "/" url = URL("http://example.com/?б=в&ю=к") assert url.path_qs == "/?б=в&ю=к" url = URL("http://example.com/path?б=в&ю=к") assert url.path_qs == "/path?б=в&ю=к" url = URL("/path?б=в&ю=к") assert url.path_qs == "/path?б=в&ю=к" url = URL("") assert url.path_qs == "" url = URL("http://example.com") assert url.path_qs == "/" def test_raw_path_qs() -> None: url = URL("http://example.com/") assert url.raw_path_qs == "/" url = URL("http://example.com/?б=в&ю=к") assert url.raw_path_qs == "/?%D0%B1=%D0%B2&%D1%8E=%D0%BA" url = URL("http://example.com/path?б=в&ю=к") assert url.raw_path_qs == "/path?%D0%B1=%D0%B2&%D1%8E=%D0%BA" url = URL("http://example.com/шлях?a=1&b=2") assert url.raw_path_qs == "/%D1%88%D0%BB%D1%8F%D1%85?a=1&b=2" url = URL("/шлях?a=1&b=2") assert url.raw_path_qs == "/%D1%88%D0%BB%D1%8F%D1%85?a=1&b=2" url = URL("") assert url.raw_path_qs == "" url = URL("http://example.com") assert url.raw_path_qs == "/" def test_query_string_spaces() -> None: url = URL("http://example.com?a+b=c+d&e=f+g") assert url.query_string == "a b=c d&e=f g" # raw fragment def test_raw_fragment_empty() -> None: url = URL("http://example.com") assert "" == url.raw_fragment def test_raw_fragment() -> None: url = URL("http://example.com/path#anchor") assert "anchor" == url.raw_fragment def test_raw_fragment_non_ascii() -> None: url = URL("http://example.com/path#якір") assert "%D1%8F%D0%BA%D1%96%D1%80" == url.raw_fragment def test_raw_fragment_safe() -> None: url = URL("http://example.com/path#a?b/c:d@e") assert "a?b/c:d@e" == url.raw_fragment def test_fragment_non_ascii() -> None: url = URL("http://example.com/path#якір") assert "якір" == url.fragment def test_raw_parts_empty() -> None: url = URL("http://example.com") assert ("/",) == url.raw_parts def test_raw_parts() -> None: url = URL("http://example.com/path/to") assert ("/", "path", "to") == url.raw_parts def test_raw_parts_without_path() -> None: url = URL("http://example.com") assert ("/",) == url.raw_parts def test_raw_path_parts_with_2F_in_path() -> None: url = URL("http://example.com/path%2Fto/three") assert ("/", "path%2Fto", "three") == url.raw_parts def test_raw_path_parts_with_2f_in_path() -> None: url = URL("http://example.com/path%2fto/three") assert ("/", "path%2Fto", "three") == url.raw_parts def test_raw_parts_for_relative_path() -> None: url = URL("path/to") assert ("path", "to") == url.raw_parts def test_raw_parts_for_relative_path_starting_from_slash() -> None: url = URL("/path/to") assert ("/", "path", "to") == url.raw_parts def test_raw_parts_for_relative_double_path() -> None: url = URL("path/to") assert ("path", "to") == url.raw_parts def test_parts_for_empty_url() -> None: url = URL() assert ("",) == url.raw_parts def test_raw_parts_non_ascii() -> None: url = URL("http://example.com/шлях/сюди") assert ( "/", "%D1%88%D0%BB%D1%8F%D1%85", "%D1%81%D1%8E%D0%B4%D0%B8", ) == url.raw_parts def test_parts_non_ascii() -> None: url = URL("http://example.com/шлях/сюди") assert ("/", "шлях", "сюди") == url.parts def test_name_for_empty_url() -> None: url = URL() assert "" == url.raw_name def test_raw_name() -> None: url = URL("http://example.com/path/to#frag") assert "to" == url.raw_name def test_raw_name_root() -> None: url = URL("http://example.com/#frag") assert "" == url.raw_name def test_raw_name_root2() -> None: url = URL("http://example.com") assert "" == url.raw_name def test_raw_name_root3() -> None: url = URL("http://example.com/") assert "" == url.raw_name def test_relative_raw_name() -> None: url = URL("path/to") assert "to" == url.raw_name def test_relative_raw_name_starting_from_slash() -> None: url = URL("/path/to") assert "to" == url.raw_name def test_relative_raw_name_slash() -> None: url = URL("/") assert "" == url.raw_name def test_name_non_ascii() -> None: url = URL("http://example.com/шлях") assert url.name == "шлях" def test_suffix_for_empty_url() -> None: url = URL() assert "" == url.raw_suffix def test_raw_suffix() -> None: url = URL("http://example.com/path/to.txt#frag") assert ".txt" == url.raw_suffix def test_raw_suffix_root() -> None: url = URL("http://example.com/#frag") assert "" == url.raw_suffix def test_raw_suffix_root2() -> None: url = URL("http://example.com") assert "" == url.raw_suffix def test_raw_suffix_root3() -> None: url = URL("http://example.com/") assert "" == url.raw_suffix def test_relative_raw_suffix() -> None: url = URL("path/to") assert "" == url.raw_suffix def test_relative_raw_suffix_starting_from_slash() -> None: url = URL("/path/to") assert "" == url.raw_suffix def test_relative_raw_suffix_dot() -> None: url = URL(".") assert "" == url.raw_suffix def test_suffix_non_ascii() -> None: url = URL("http://example.com/шлях.суфікс") assert url.suffix == ".суфікс" def test_suffix_with_empty_name() -> None: url = URL("http://example.com/.hgrc") assert "" == url.raw_suffix def test_suffix_multi_dot() -> None: url = URL("http://example.com/doc.tar.gz") assert ".gz" == url.raw_suffix def test_suffix_with_dot_name() -> None: url = URL("http://example.com/doc.") assert "" == url.raw_suffix def test_suffixes_for_empty_url() -> None: url = URL() assert () == url.raw_suffixes def test_raw_suffixes() -> None: url = URL("http://example.com/path/to.txt#frag") assert (".txt",) == url.raw_suffixes def test_raw_suffixes_root() -> None: url = URL("http://example.com/#frag") assert () == url.raw_suffixes def test_raw_suffixes_root2() -> None: url = URL("http://example.com") assert () == url.raw_suffixes def test_raw_suffixes_root3() -> None: url = URL("http://example.com/") assert () == url.raw_suffixes def test_relative_raw_suffixes() -> None: url = URL("path/to") assert () == url.raw_suffixes def test_relative_raw_suffixes_starting_from_slash() -> None: url = URL("/path/to") assert () == url.raw_suffixes def test_relative_raw_suffixes_dot() -> None: url = URL(".") assert () == url.raw_suffixes def test_suffixes_non_ascii() -> None: url = URL("http://example.com/шлях.суфікс") assert url.suffixes == (".суфікс",) def test_suffixes_with_empty_name() -> None: url = URL("http://example.com/.hgrc") assert () == url.raw_suffixes def test_suffixes_multi_dot() -> None: url = URL("http://example.com/doc.tar.gz") assert (".tar", ".gz") == url.raw_suffixes def test_suffixes_with_dot_name() -> None: url = URL("http://example.com/doc.") assert () == url.raw_suffixes def test_plus_in_path() -> None: url = URL("http://example.com/test/x+y%2Bz/:+%2B/") assert "/test/x+y+z/:++/" == url.path def test_nonascii_in_qs() -> None: url = URL("http://example.com") url2 = url.with_query({"f\xf8\xf8": "f\xf8\xf8"}) assert "http://example.com/?f%C3%B8%C3%B8=f%C3%B8%C3%B8" == str(url2) def test_percent_encoded_in_qs() -> None: url = URL("http://example.com") url2 = url.with_query({"k%cf%80": "v%cf%80"}) assert str(url2) == "http://example.com/?k%25cf%2580=v%25cf%2580" assert url2.raw_query_string == "k%25cf%2580=v%25cf%2580" assert url2.query_string == "k%cf%80=v%cf%80" assert url2.query == {"k%cf%80": "v%cf%80"} # modifiers def test_parent_raw_path() -> None: url = URL("http://example.com/path/to") assert url.parent.raw_path == "/path" def test_parent_raw_parts() -> None: url = URL("http://example.com/path/to") assert url.parent.raw_parts == ("/", "path") def test_double_parent_raw_path() -> None: url = URL("http://example.com/path/to") assert url.parent.parent.raw_path == "/" def test_empty_parent_raw_path() -> None: url = URL("http://example.com/") assert url.parent.parent.raw_path == "/" def test_empty_parent_raw_path2() -> None: url = URL("http://example.com") assert url.parent.parent.raw_path == "/" def test_clear_fragment_on_getting_parent() -> None: url = URL("http://example.com/path/to#frag") assert URL("http://example.com/path") == url.parent def test_clear_fragment_on_getting_parent_toplevel() -> None: url = URL("http://example.com/#frag") assert URL("http://example.com/") == url.parent def test_clear_query_on_getting_parent() -> None: url = URL("http://example.com/path/to?a=b") assert URL("http://example.com/path") == url.parent def test_clear_query_on_getting_parent_toplevel() -> None: url = URL("http://example.com/?a=b") assert URL("http://example.com/") == url.parent # truediv def test_div_root() -> None: url = URL("http://example.com") / "path" / "to" assert str(url) == "http://example.com/path/to" assert url.raw_path == "/path/to" def test_div_root_with_slash() -> None: url = URL("http://example.com/") / "path" / "to" assert str(url) == "http://example.com/path/to" assert url.raw_path == "/path/to" def test_div() -> None: url = URL("http://example.com/path") / "to" assert str(url) == "http://example.com/path/to" assert url.raw_path == "/path/to" def test_div_with_slash() -> None: url = URL("http://example.com/path/") / "to" assert str(url) == "http://example.com/path/to" assert url.raw_path == "/path/to" def test_div_path_starting_from_slash_is_forbidden() -> None: url = URL("http://example.com/path/") with pytest.raises(ValueError): url / "/to/others" class StrEnum(str, Enum): spam = "ham" def __str__(self) -> str: return self.value def test_div_path_srting_subclass() -> None: url = URL("http://example.com/path/") / StrEnum.spam assert str(url) == "http://example.com/path/ham" def test_div_bad_type() -> None: url = URL("http://example.com/path/") with pytest.raises(TypeError): url / 3 # type: ignore[operator] def test_div_cleanup_query_and_fragment() -> None: url = URL("http://example.com/path?a=1#frag") assert str(url / "to") == "http://example.com/path/to" def test_div_for_empty_url() -> None: url = URL() / "a" assert url.raw_parts == ("a",) def test_div_for_relative_url() -> None: url = URL("a") / "b" assert url.raw_parts == ("a", "b") def test_div_for_relative_url_started_with_slash() -> None: url = URL("/a") / "b" assert url.raw_parts == ("/", "a", "b") def test_div_non_ascii() -> None: url = URL("http://example.com/сюди") url2 = url / "туди" assert url2.path == "/сюди/туди" assert url2.raw_path == "/%D1%81%D1%8E%D0%B4%D0%B8/%D1%82%D1%83%D0%B4%D0%B8" assert url2.parts == ("/", "сюди", "туди") assert url2.raw_parts == ( "/", "%D1%81%D1%8E%D0%B4%D0%B8", "%D1%82%D1%83%D0%B4%D0%B8", ) def test_div_percent_encoded() -> None: url = URL("http://example.com/path") url2 = url / "%cf%80" assert url2.path == "/path/%cf%80" assert url2.raw_path == "/path/%25cf%2580" assert url2.parts == ("/", "path", "%cf%80") assert url2.raw_parts == ("/", "path", "%25cf%2580") def test_div_with_colon_and_at() -> None: url = URL("http://example.com/base") / "path:abc@123" assert url.raw_path == "/base/path:abc@123" def test_div_with_dots() -> None: url = URL("http://example.com/base") / "../path/./to" assert url.raw_path == "/path/to" # joinpath @pytest.mark.parametrize( "base,to_join,expected", [ pytest.param("", ("path", "to"), "http://example.com/path/to", id="root"), pytest.param( "/", ("path", "to"), "http://example.com/path/to", id="root-with-slash" ), pytest.param("/path", ("to",), "http://example.com/path/to", id="path"), pytest.param( "/path/", ("to",), "http://example.com/path/to", id="path-with-slash" ), pytest.param( "/path", ("",), "http://example.com/path/", id="path-add-trailing-slash" ), pytest.param( "/path?a=1#frag", ("to",), "http://example.com/path/to", id="cleanup-query-and-fragment", ), pytest.param("", ("path/",), "http://example.com/path/", id="trailing-slash"), pytest.param( "", ( "path", "", ), "http://example.com/path/", id="trailing-slash-empty-string", ), pytest.param( "", ("path/", "to/"), "http://example.com/path/to/", id="duplicate-slash" ), pytest.param("", (), "http://example.com", id="empty-segments"), pytest.param( "/", ("path/",), "http://example.com/path/", id="base-slash-trailing-slash" ), pytest.param( "/", ("path/", "to/"), "http://example.com/path/to/", id="base-slash-duplicate-slash", ), pytest.param("/", (), "http://example.com", id="base-slash-empty-segments"), ], ) def test_joinpath(base: str, to_join: tuple[str, ...], expected: str) -> None: url = URL(f"http://example.com{base}") assert str(url.joinpath(*to_join)) == expected @pytest.mark.parametrize( "base,to_join,expected", [ pytest.param("path", "a", "path/a", id="default_default"), pytest.param("path", "./a", "path/a", id="default_relative"), pytest.param("path/", "a", "path/a", id="empty-segment_default"), pytest.param("path/", "./a", "path/a", id="empty-segment_relative"), pytest.param("path", ".//a", "path//a", id="default_empty-segment"), pytest.param("path/", ".//a", "path//a", id="empty-segment_empty_segment"), pytest.param("path//", "a", "path//a", id="empty-segments_default"), pytest.param("path//", "./a", "path//a", id="empty-segments_relative"), pytest.param("path//", ".//a", "path///a", id="empty-segments_empty-segment"), pytest.param("path", "a/", "path/a/", id="default_trailing-empty-segment"), pytest.param("path", "a//", "path/a//", id="default_trailing-empty-segments"), pytest.param("path", "a//b", "path/a//b", id="default_embedded-empty-segment"), pytest.param( "path/a/b/c/d/e", "a/../../../../../../c", "path/c", id="long-backtrack" ), pytest.param( "path/a/b/c/d/e", "a/../../../././../../../c", "path/c", id="long-backtrack-with-dots", ), pytest.param("path/a/../../d/e", "a/../c", "d/e/c", id="backtrack-in-both"), ], ) def test_joinpath_empty_segments(base: str, to_join: str, expected: str) -> None: url = URL(f"http://example.com/{base}") assert ( f"http://example.com/{expected}" == str(url.joinpath(to_join)) and str(url / to_join) == f"http://example.com/{expected}" ) def test_joinpath_backtrack_to_base() -> None: url = URL("http://example.com/../../c") new_url = url.joinpath("../../..") assert str(new_url) == "http://example.com" assert new_url.path == "/" assert new_url.raw_path == "/" def test_joinpath_single_empty_segments() -> None: """joining standalone empty segments does not create empty segments""" a = URL("/1//2///3") assert a.parts == ("/", "1", "", "2", "", "", "3") b = URL("scheme://host").joinpath(*a.parts[1:]) assert b.path == "/1/2/3" @pytest.mark.parametrize( "url,to_join,expected", [ pytest.param(URL(), ("a",), ("a",), id="empty-url"), pytest.param(URL("a"), ("b",), ("a", "b"), id="relative-path"), pytest.param(URL("a"), ("b", "", "c"), ("a", "b", "c"), id="empty-element"), pytest.param(URL("/a"), ("b"), ("/", "a", "b"), id="absolute-path"), pytest.param(URL(), ("a/",), ("a", ""), id="trailing-slash"), pytest.param(URL(), ("a/", "b/"), ("a", "b", ""), id="duplicate-slash"), pytest.param(URL(), (), ("",), id="empty-segments"), ], ) def test_joinpath_relative( url: URL, to_join: tuple[str, ...], expected: tuple[str, ...] ) -> None: assert url.joinpath(*to_join).raw_parts == expected @pytest.mark.parametrize( "url,to_join,encoded,e_path,e_raw_path,e_parts,e_raw_parts", [ pytest.param( "http://example.com/сюди", ("туди",), False, "/сюди/туди", "/%D1%81%D1%8E%D0%B4%D0%B8/%D1%82%D1%83%D0%B4%D0%B8", ("/", "сюди", "туди"), ("/", "%D1%81%D1%8E%D0%B4%D0%B8", "%D1%82%D1%83%D0%B4%D0%B8"), id="non-ascii", ), pytest.param( "http://example.com/path", ("%cf%80",), False, "/path/%cf%80", "/path/%25cf%2580", ("/", "path", "%cf%80"), ("/", "path", "%25cf%2580"), id="percent-encoded", ), pytest.param( "http://example.com/path", ("%cf%80",), True, "/path/π", "/path/%cf%80", ("/", "path", "π"), ("/", "path", "%cf%80"), id="encoded-percent-encoded", ), ], ) def test_joinpath_encoding( url: str, to_join: tuple[str, ...], encoded: bool, e_path: str, e_raw_path: str, e_parts: tuple[str, ...], e_raw_parts: tuple[str, ...], ) -> None: joined = URL(url).joinpath(*to_join, encoded=encoded) assert joined.path == e_path assert joined.raw_path == e_raw_path assert joined.parts == e_parts assert joined.raw_parts == e_raw_parts @pytest.mark.parametrize( "to_join,expected", [ pytest.param(("path:abc@123",), "/base/path:abc@123", id="with-colon-and-at"), pytest.param(("..", "path", ".", "to"), "/path/to", id="with-dots"), ], ) def test_joinpath_edgecases(to_join: tuple[str, ...], expected: str) -> None: url = URL("http://example.com/base").joinpath(*to_join) assert url.raw_path == expected def test_joinpath_path_starting_from_slash_is_forbidden() -> None: url = URL("http://example.com/path/") with pytest.raises( ValueError, match="Appending path .* starting from slash is forbidden" ): assert url.joinpath("/to/others") PATHS = [ # No dots ("", ""), ("path", "path"), # Single-dot ("path/to", "path/to"), ("././path/to", "path/to"), ("path/./to", "path/to"), ("path/././to", "path/to"), ("path/to/.", "path/to/"), ("path/to/./.", "path/to/"), # Double-dots ("../path/to", "path/to"), ("path/../to", "to"), ("path/../../to", "to"), # Non-ASCII characters ("μονοπάτι/../../να/ᴜɴɪ/ᴄᴏᴅᴇ", "να/ᴜɴɪ/ᴄᴏᴅᴇ"), ("μονοπάτι/../../να/𝕦𝕟𝕚/𝕔𝕠𝕕𝕖/.", "να/𝕦𝕟𝕚/𝕔𝕠𝕕𝕖/"), ] @pytest.mark.parametrize("original,expected", PATHS) def test_join_path_normalized(original: str, expected: str) -> None: """Test that joinpath normalizes paths.""" base_url = URL("http://example.com") new_url = base_url.joinpath(original) assert new_url.path == f"/{expected}" # with_path def test_with_path() -> None: url = URL("http://example.com") url2 = url.with_path("/test") assert str(url2) == "http://example.com/test" assert url2.raw_path == "/test" assert url2.path == "/test" def test_with_path_nonascii() -> None: url = URL("http://example.com") url2 = url.with_path("/π") assert str(url2) == "http://example.com/%CF%80" assert url2.raw_path == "/%CF%80" assert url2.path == "/π" def test_with_path_percent_encoded() -> None: url = URL("http://example.com") url2 = url.with_path("/%cf%80") assert str(url2) == "http://example.com/%25cf%2580" assert url2.raw_path == "/%25cf%2580" assert url2.path == "/%cf%80" def test_with_path_encoded() -> None: url = URL("http://example.com") url2 = url.with_path("/test", encoded=True) assert str(url2) == "http://example.com/test" assert url2.raw_path == "/test" assert url2.path == "/test" def test_with_path_encoded_nonascii() -> None: url = URL("http://example.com") url2 = url.with_path("/π", encoded=True) assert str(url2) == "http://example.com/π" assert url2.raw_path == "/π" assert url2.path == "/π" def test_with_path_encoded_percent_encoded() -> None: url = URL("http://example.com") url2 = url.with_path("/%cf%80", encoded=True) assert str(url2) == "http://example.com/%cf%80" assert url2.raw_path == "/%cf%80" assert url2.path == "/π" def test_with_path_dots() -> None: url = URL("http://example.com") assert str(url.with_path("/test/.")) == "http://example.com/test/" def test_with_path_relative() -> None: url = URL("/path") assert str(url.with_path("/new")) == "/new" def test_with_path_query() -> None: url = URL("http://example.com?a=b") assert str(url.with_path("/test")) == "http://example.com/test" def test_with_path_fragment() -> None: url = URL("http://example.com#frag") assert str(url.with_path("/test")) == "http://example.com/test" @pytest.mark.parametrize( ("original_url", "keep_query", "keep_fragment", "expected_url"), [ pytest.param( "http://example.com?a=b#frag", True, False, "http://example.com/test?a=b", id="query-only", ), pytest.param( "http://example.com?a=b#frag", False, True, "http://example.com/test#frag", id="fragment-only", ), pytest.param( "http://example.com?a=b#frag", True, True, "http://example.com/test?a=b#frag", id="all", ), pytest.param( "http://example.com?a=b#frag", False, False, "http://example.com/test", id="none", ), ], ) def test_with_path_keep_query_keep_fragment_flags( original_url: str, keep_query: bool, keep_fragment: bool, expected_url: str ) -> None: url = URL(original_url) url2 = url.with_path("/test", keep_query=keep_query, keep_fragment=keep_fragment) assert str(url2) == expected_url def test_with_path_empty() -> None: url = URL("http://example.com/test") assert str(url.with_path("")) == "http://example.com" def test_with_path_leading_slash() -> None: url = URL("http://example.com") assert url.with_path("test").path == "/test" # with_fragment def test_with_fragment() -> None: url = URL("http://example.com") url2 = url.with_fragment("frag") assert str(url2) == "http://example.com/#frag" assert url2.raw_fragment == "frag" assert url2.fragment == "frag" def test_with_fragment_safe() -> None: url = URL("http://example.com") u2 = url.with_fragment("a:b?c@d/e") assert str(u2) == "http://example.com/#a:b?c@d/e" def test_with_fragment_non_ascii() -> None: url = URL("http://example.com") url2 = url.with_fragment("фрагм") assert url2.raw_fragment == "%D1%84%D1%80%D0%B0%D0%B3%D0%BC" assert url2.fragment == "фрагм" def test_with_fragment_percent_encoded() -> None: url = URL("http://example.com") url2 = url.with_fragment("%cf%80") assert str(url2) == "http://example.com/#%25cf%2580" assert url2.raw_fragment == "%25cf%2580" assert url2.fragment == "%cf%80" def test_with_fragment_None() -> None: url = URL("http://example.com/path#frag") url2 = url.with_fragment(None) assert str(url2) == "http://example.com/path" def test_with_fragment_None_matching() -> None: url = URL("http://example.com/path") url2 = url.with_fragment(None) assert url is url2 def test_with_fragment_matching() -> None: url = URL("http://example.com/path#frag") url2 = url.with_fragment("frag") assert url is url2 def test_with_fragment_bad_type() -> None: url = URL("http://example.com") with pytest.raises(TypeError): url.with_fragment(123) # type: ignore[arg-type] # with_name def test_with_name() -> None: url = URL("http://example.com/a/b") assert url.raw_parts == ("/", "a", "b") url2 = url.with_name("c") assert url2.raw_parts == ("/", "a", "c") assert url2.parts == ("/", "a", "c") assert url2.raw_path == "/a/c" assert url2.path == "/a/c" @pytest.mark.parametrize( ("original_url", "keep_query", "keep_fragment", "expected_url"), [ pytest.param( "http://example.com/path/to?a=b#frag", True, False, "http://example.com/path/newname?a=b", id="query-only", ), pytest.param( "http://example.com/path/to?a=b#frag", False, True, "http://example.com/path/newname#frag", id="fragment-only", ), pytest.param( "http://example.com/path/to?a=b#frag", True, True, "http://example.com/path/newname?a=b#frag", id="all", ), pytest.param( "http://example.com/path/to?a=b#frag", False, False, "http://example.com/path/newname", id="none", ), ], ) def test_with_name_keep_query_keep_fragment_flags( original_url: str, keep_query: bool, keep_fragment: bool, expected_url: str ) -> None: url = URL(original_url) url2 = url.with_name("newname", keep_query=keep_query, keep_fragment=keep_fragment) assert str(url2) == expected_url def test_with_name_for_naked_path() -> None: url = URL("http://example.com") url2 = url.with_name("a") assert url2.raw_parts == ("/", "a") def test_with_name_for_relative_path() -> None: url = URL("a") url2 = url.with_name("b") assert url2.raw_parts == ("b",) def test_with_name_for_relative_path2() -> None: url = URL("a/b") url2 = url.with_name("c") assert url2.raw_parts == ("a", "c") def test_with_name_for_relative_path_starting_from_slash() -> None: url = URL("/a") url2 = url.with_name("b") assert url2.raw_parts == ("/", "b") def test_with_name_for_relative_path_starting_from_slash2() -> None: url = URL("/a/b") url2 = url.with_name("c") assert url2.raw_parts == ("/", "a", "c") def test_with_name_empty() -> None: url = URL("http://example.com/path/to").with_name("") assert str(url) == "http://example.com/path/" def test_with_name_non_ascii() -> None: url = URL("http://example.com/path").with_name("шлях") assert url.path == "/шлях" assert url.raw_path == "/%D1%88%D0%BB%D1%8F%D1%85" assert url.parts == ("/", "шлях") assert url.raw_parts == ("/", "%D1%88%D0%BB%D1%8F%D1%85") def test_with_name_percent_encoded() -> None: url = URL("http://example.com/path") url2 = url.with_name("%cf%80") assert url2.raw_parts == ("/", "%25cf%2580") assert url2.parts == ("/", "%cf%80") assert url2.raw_path == "/%25cf%2580" assert url2.path == "/%cf%80" def test_with_name_with_slash() -> None: with pytest.raises(ValueError): URL("http://example.com").with_name("a/b") def test_with_name_non_str() -> None: with pytest.raises(TypeError): URL("http://example.com").with_name(123) # type: ignore[arg-type] def test_with_name_within_colon_and_at() -> None: url = URL("http://example.com/oldpath").with_name("path:abc@123") assert url.raw_path == "/path:abc@123" def test_with_name_dot() -> None: with pytest.raises(ValueError): URL("http://example.com").with_name(".") def test_with_name_double_dot() -> None: with pytest.raises(ValueError): URL("http://example.com").with_name("..") # with_suffix def test_with_suffix() -> None: url = URL("http://example.com/a/b") assert url.raw_parts == ("/", "a", "b") url2 = url.with_suffix(".c") assert url2.raw_parts == ("/", "a", "b.c") assert url2.parts == ("/", "a", "b.c") assert url2.raw_path == "/a/b.c" assert url2.path == "/a/b.c" def test_with_suffix_encoded_suffix() -> None: url = URL("http://example.com/a/b") url2 = url.with_suffix(". c") assert url2.raw_parts == ("/", "a", "b.%20c") assert url2.parts == ("/", "a", "b. c") assert url2.raw_path == "/a/b.%20c" assert url2.path == "/a/b. c" def test_with_suffix_encoded_url() -> None: url = URL("http://example.com/a/b c") url2 = url.with_suffix(". d") url3 = url.with_suffix(".e") assert url2.raw_parts == ("/", "a", "b%20c.%20d") assert url2.parts == ("/", "a", "b c. d") assert url2.raw_path == "/a/b%20c.%20d" assert url2.path == "/a/b c. d" assert url3.raw_parts == ("/", "a", "b%20c.e") assert url3.parts == ("/", "a", "b c.e") assert url3.raw_path == "/a/b%20c.e" assert url3.path == "/a/b c.e" @pytest.mark.parametrize( ("original_url", "keep_query", "keep_fragment", "expected_url"), [ pytest.param( "http://example.com/path/to.txt?a=b#frag", True, False, "http://example.com/path/to.md?a=b", id="query-only", ), pytest.param( "http://example.com/path/to.txt?a=b#frag", False, True, "http://example.com/path/to.md#frag", id="fragment-only", ), pytest.param( "http://example.com/path/to.txt?a=b#frag", True, True, "http://example.com/path/to.md?a=b#frag", id="all", ), pytest.param( "http://example.com/path/to.txt?a=b#frag", False, False, "http://example.com/path/to.md", id="none", ), ], ) def test_with_suffix_keep_query_keep_fragment_flags( original_url: str, keep_query: bool, keep_fragment: bool, expected_url: str ) -> None: url = URL(original_url) url2 = url.with_suffix(".md", keep_query=keep_query, keep_fragment=keep_fragment) assert str(url2) == expected_url def test_with_suffix_for_naked_path() -> None: url = URL("http://example.com") with pytest.raises(ValueError) as excinfo: url.with_suffix(".a") (msg,) = excinfo.value.args assert msg == f"{url!r} has an empty name" def test_with_suffix_for_relative_path() -> None: url = URL("a") url2 = url.with_suffix(".b") assert url2.raw_parts == ("a.b",) def test_with_suffix_for_relative_path2() -> None: url = URL("a/b") url2 = url.with_suffix(".c") assert url2.raw_parts == ("a", "b.c") def test_with_suffix_for_relative_path_starting_from_slash() -> None: url = URL("/a") url2 = url.with_suffix(".b") assert url2.raw_parts == ("/", "a.b") def test_with_suffix_for_relative_path_starting_from_slash2() -> None: url = URL("/a/b") url2 = url.with_suffix(".c") assert url2.raw_parts == ("/", "a", "b.c") def test_with_suffix_empty() -> None: url = URL("http://example.com/path/to").with_suffix("") assert str(url) == "http://example.com/path/to" def test_with_suffix_non_ascii() -> None: url = URL("http://example.com/path").with_suffix(".шлях") assert url.path == "/path.шлях" assert url.raw_path == "/path.%D1%88%D0%BB%D1%8F%D1%85" assert url.parts == ("/", "path.шлях") assert url.raw_parts == ("/", "path.%D1%88%D0%BB%D1%8F%D1%85") def test_with_suffix_percent_encoded() -> None: url = URL("http://example.com/path") url2 = url.with_suffix(".%cf%80") assert url2.raw_parts == ("/", "path.%25cf%2580") assert url2.parts == ("/", "path.%cf%80") assert url2.raw_path == "/path.%25cf%2580" assert url2.path == "/path.%cf%80" def test_with_suffix_without_dot() -> None: with pytest.raises(ValueError) as excinfo: URL("http://example.com/a").with_suffix("b") (msg,) = excinfo.value.args assert msg == "Invalid suffix 'b'" def test_with_suffix_non_str() -> None: with pytest.raises(TypeError) as excinfo: URL("http://example.com").with_suffix(123) # type: ignore[arg-type] (msg,) = excinfo.value.args assert msg == "Invalid suffix type" def test_with_suffix_dot() -> None: with pytest.raises(ValueError) as excinfo: URL("http://example.com").with_suffix(".") (msg,) = excinfo.value.args assert msg == "Invalid suffix '.'" def test_with_suffix_with_slash() -> None: with pytest.raises(ValueError) as excinfo: URL("http://example.com/a").with_suffix("/.b") (msg,) = excinfo.value.args assert msg == "Invalid suffix '/.b'" def test_with_suffix_with_slash2() -> None: with pytest.raises(ValueError) as excinfo: URL("http://example.com/a").with_suffix(".b/.d") (msg,) = excinfo.value.args assert msg == "Invalid suffix '.b/.d'" def test_with_suffix_replace() -> None: url = URL("/a.b") url2 = url.with_suffix(".c") assert url2.raw_parts == ("/", "a.c") # is_absolute def test_is_absolute_for_relative_url() -> None: url = URL("/path/to") assert not url.is_absolute() assert not url.absolute def test_is_absolute_for_absolute_url() -> None: url = URL("http://example.com") assert url.is_absolute() assert url.absolute def test_is_non_absolute_for_empty_url() -> None: url = URL() assert not url.is_absolute() assert not url.absolute def test_is_non_absolute_for_empty_url2() -> None: url = URL("") assert not url.is_absolute() assert not url.absolute def test_is_absolute_path_starting_from_double_slash() -> None: url = URL("//www.python.org") assert url.is_absolute() assert url.absolute # is_default_port def test_is_default_port_for_relative_url() -> None: url = URL("/path/to") assert not url.is_default_port() def test_is_default_port_for_absolute_url_without_port() -> None: url = URL("http://example.com") assert url.is_default_port() def test_is_default_port_for_absolute_url_with_default_port() -> None: url = URL("http://example.com:80") assert url.is_default_port() assert str(url) == "http://example.com" def test_is_default_port_for_absolute_url_with_nondefault_port() -> None: url = URL("http://example.com:8080") assert not url.is_default_port() def test_is_default_port_for_unknown_scheme() -> None: url = URL("unknown://example.com:8080") assert not url.is_default_port() def test_handling_port_zero() -> None: url = URL("http://example.com:0") assert url.explicit_port == 0 assert url.explicit_port == SplitResult(*url._val).port assert str(url) == "http://example.com:0" assert not url.is_default_port() # def test_no_scheme() -> None: url = URL("example.com") assert url.raw_host is None assert url.raw_path == "example.com" assert str(url) == "example.com" def test_no_scheme2() -> None: url = URL("example.com/a/b") assert url.raw_host is None assert url.raw_path == "example.com/a/b" assert str(url) == "example.com/a/b" def test_from_non_allowed() -> None: with pytest.raises(TypeError): URL(1234) # type: ignore[arg-type] def test_from_idna() -> None: url = URL("http://xn--jxagkqfkduily1i.eu") assert "http://xn--jxagkqfkduily1i.eu" == str(url) url = URL("http://xn--einla-pqa.de/") # needs idna 2008 assert "http://xn--einla-pqa.de/" == str(url) def test_to_idna() -> None: url = URL("http://εμπορικόσήμα.eu") assert "http://xn--jxagkqfkduily1i.eu" == str(url) url = URL("http://einlaß.de/") assert "http://xn--einla-pqa.de/" == str(url) def test_from_ascii_login() -> None: url = URL("http://" "%D0%B2%D0%B0%D1%81%D1%8F" "@host:1234/") assert ("http://" "%D0%B2%D0%B0%D1%81%D1%8F" "@host:1234/") == str(url) def test_from_non_ascii_login() -> None: url = URL("http://бажан@host:1234/") assert ("http://%D0%B1%D0%B0%D0%B6%D0%B0%D0%BD@host:1234/") == str(url) def test_from_ascii_login_and_password() -> None: url = URL( "http://" "%D0%B2%D0%B0%D1%81%D1%8F" ":%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C" "@host:1234/" ) assert ( "http://" "%D0%B2%D0%B0%D1%81%D1%8F" ":%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C" "@host:1234/" ) == str(url) def test_from_non_ascii_login_and_password() -> None: url = URL("http://бажан:пароль@host:1234/") assert ( "http://" "%D0%B1%D0%B0%D0%B6%D0%B0%D0%BD" ":%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C" "@host:1234/" ) == str(url) def test_from_ascii_path() -> None: url = URL("http://example.com/" "%D0%BF%D1%83%D1%82%D1%8C/%D1%82%D1%83%D0%B4%D0%B0") assert ( "http://example.com/" "%D0%BF%D1%83%D1%82%D1%8C/%D1%82%D1%83%D0%B4%D0%B0" ) == str(url) def test_from_ascii_path_lower_case() -> None: url = URL("http://example.com/" "%d0%bf%d1%83%d1%82%d1%8c/%d1%82%d1%83%d0%b4%d0%b0") assert ( "http://example.com/" "%D0%BF%D1%83%D1%82%D1%8C/%D1%82%D1%83%D0%B4%D0%B0" ) == str(url) def test_from_non_ascii_path() -> None: url = URL("http://example.com/шлях/туди") assert ( "http://example.com/%D1%88%D0%BB%D1%8F%D1%85/%D1%82%D1%83%D0%B4%D0%B8" ) == str(url) def test_bytes() -> None: url = URL("http://example.com/шлях/туди") assert ( b"http://example.com/%D1%88%D0%BB%D1%8F%D1%85/%D1%82%D1%83%D0%B4%D0%B8" == bytes(url) ) def test_from_ascii_query_parts() -> None: url = URL( "http://example.com/" "?%D0%BF%D0%B0%D1%80%D0%B0%D0%BC" "=%D0%B7%D0%BD%D0%B0%D1%87" ) assert ( "http://example.com/" "?%D0%BF%D0%B0%D1%80%D0%B0%D0%BC" "=%D0%B7%D0%BD%D0%B0%D1%87" ) == str(url) def test_from_non_ascii_query_parts() -> None: url = URL("http://example.com/?парам=знач") assert ( "http://example.com/" "?%D0%BF%D0%B0%D1%80%D0%B0%D0%BC" "=%D0%B7%D0%BD%D0%B0%D1%87" ) == str(url) def test_from_non_ascii_query_parts2() -> None: url = URL("http://example.com/?п=з&ю=б") assert "http://example.com/?%D0%BF=%D0%B7&%D1%8E=%D0%B1" == str(url) def test_from_ascii_fragment() -> None: url = URL("http://example.com/" "#%D1%84%D1%80%D0%B0%D0%B3%D0%BC%D0%B5%D0%BD%D1%82") assert ( "http://example.com/" "#%D1%84%D1%80%D0%B0%D0%B3%D0%BC%D0%B5%D0%BD%D1%82" ) == str(url) def test_from_bytes_with_non_ascii_fragment() -> None: url = URL("http://example.com/#фрагмент") assert ( "http://example.com/" "#%D1%84%D1%80%D0%B0%D0%B3%D0%BC%D0%B5%D0%BD%D1%82" ) == str(url) def test_to_str() -> None: url = URL("http://εμπορικόσήμα.eu/") assert "http://xn--jxagkqfkduily1i.eu/" == str(url) def test_to_str_long() -> None: url = URL( "https://host-12345678901234567890123456789012345678901234567890" "-name:8888/" ) expected = ( "https://host-" "12345678901234567890123456789012345678901234567890" "-name:8888/" ) assert expected == str(url) def test_decoding_with_2F_in_path() -> None: url = URL("http://example.com/path%2Fto") assert "http://example.com/path%2Fto" == str(url) assert url == URL(str(url)) def test_decoding_with_26_and_3D_in_query() -> None: url = URL("http://example.com/?%26=%3D") assert "http://example.com/?%26=%3D" == str(url) assert url == URL(str(url)) def test_fragment_only_url() -> None: url = URL("#frag") assert str(url) == "#frag" def test_url_from_url() -> None: url = URL("http://example.com") assert URL(url) == url assert URL(url).raw_parts == ("/",) def test_lowercase_scheme() -> None: url = URL("HTTP://example.com") assert str(url) == "http://example.com" def test_str_for_empty_url() -> None: url = URL() assert "" == str(url) def test_parent_for_empty_url() -> None: url = URL() assert url == url.parent def test_parent_for_relative_url_with_child() -> None: url = URL("path/to") assert url.parent == URL("path") assert SplitResult(*url.parent._val).path == "path" def test_parent_for_relative_url() -> None: url = URL("path") assert url.parent == URL("") assert SplitResult(*url.parent._val).path == "" def test_parent_for_no_netloc_url() -> None: url = URL("/path/to") assert url.parent == URL("/path") def test_parent_for_top_level_no_netloc_url() -> None: url = URL("/") assert url.parent == URL("/") assert SplitResult(*url.parent._val).path == "/" def test_parent_for_absolute_url() -> None: url = URL("http://go.to/path/to") assert url.parent == URL("http://go.to/path") def test_parent_for_top_level_absolute_url() -> None: url = URL("http://go.to/") assert url.parent == URL("http://go.to/") assert SplitResult(*url.parent._val).path == "/" def test_empty_value_for_query() -> None: url = URL("http://example.com/path").with_query({"a": ""}) assert str(url) == "http://example.com/path?a=" def test_none_value_for_query() -> None: with pytest.raises(TypeError): URL("http://example.com/path").with_query({"a": None}) # type: ignore[dict-item] def test_decode_pct_in_path() -> None: url = URL("http://www.python.org/%7Eguido") assert "http://www.python.org/~guido" == str(url) def test_decode_pct_in_path_lower_case() -> None: url = URL("http://www.python.org/%7eguido") assert "http://www.python.org/~guido" == str(url) # join def test_join() -> None: base = URL("http://www.cwi.nl/%7Eguido/Python.html") url = URL("FAQ.html") url2 = base.join(url) assert str(url2) == "http://www.cwi.nl/~guido/FAQ.html" def test_join_absolute() -> None: base = URL("http://www.cwi.nl/%7Eguido/Python.html") url = URL("//www.python.org/%7Eguido") url2 = base.join(url) assert str(url2) == "http://www.python.org/~guido" def test_join_non_url() -> None: base = URL("http://example.com") with pytest.raises(TypeError): base.join("path/to") # type: ignore[arg-type] NORMAL = [ ("g:h", "g:h"), ("g", "http://a/b/c/g"), ("./g", "http://a/b/c/g"), ("g/", "http://a/b/c/g/"), ("/g", "http://a/g"), ("//g", "http://g"), ("?y", "http://a/b/c/d;p?y"), ("g?y", "http://a/b/c/g?y"), ("#s", "http://a/b/c/d;p?q#s"), ("g#s", "http://a/b/c/g#s"), ("g?y#s", "http://a/b/c/g?y#s"), (";x", "http://a/b/c/;x"), ("g;x", "http://a/b/c/g;x"), ("g;x?y#s", "http://a/b/c/g;x?y#s"), ("", "http://a/b/c/d;p?q"), (".", "http://a/b/c/"), ("./", "http://a/b/c/"), ("..", "http://a/b/"), ("../", "http://a/b/"), ("../g", "http://a/b/g"), ("../..", "http://a/"), ("../../", "http://a/"), ("../../g", "http://a/g"), ] @pytest.mark.parametrize("url,expected", NORMAL) def test_join_from_rfc_3986_normal(url: str, expected: str) -> None: # test case from https://tools.ietf.org/html/rfc3986.html#section-5.4 base = URL("http://a/b/c/d;p?q") url_obj = URL(url) expected_obj = URL(expected) assert base.join(url_obj) == expected_obj ABNORMAL = [ ("../../../g", "http://a/g"), ("../../../../g", "http://a/g"), ("/./g", "http://a/g"), ("/../g", "http://a/g"), ("g.", "http://a/b/c/g."), (".g", "http://a/b/c/.g"), ("g..", "http://a/b/c/g.."), ("..g", "http://a/b/c/..g"), ("./../g", "http://a/b/g"), ("./g/.", "http://a/b/c/g/"), ("g/./h", "http://a/b/c/g/h"), ("g/../h", "http://a/b/c/h"), ("g;x=1/./y", "http://a/b/c/g;x=1/y"), ("g;x=1/../y", "http://a/b/c/y"), ("g?y/./x", "http://a/b/c/g?y/./x"), ("g?y/../x", "http://a/b/c/g?y/../x"), ("g#s/./x", "http://a/b/c/g#s/./x"), ("g#s/../x", "http://a/b/c/g#s/../x"), ] @pytest.mark.parametrize("url,expected", ABNORMAL) def test_join_from_rfc_3986_abnormal(url: str, expected: str) -> None: # test case from https://tools.ietf.org/html/rfc3986.html#section-5.4.2 base = URL("http://a/b/c/d;p?q") url_obj = URL(url) expected_obj = URL(expected) assert base.join(url_obj) == expected_obj EMPTY_SEGMENTS = [ ( "https://web.archive.org/web/", "./https://github.com/aio-libs/yarl", "https://web.archive.org/web/https://github.com/aio-libs/yarl", ), ( "https://web.archive.org/web/https://github.com/", "aio-libs/yarl", "https://web.archive.org/web/https://github.com/aio-libs/yarl", ), ] @pytest.mark.parametrize("base,url,expected", EMPTY_SEGMENTS) def test_join_empty_segments(base: str, url: str, expected: str) -> None: base_obj = URL(base) url_obj = URL(url) expected_obj = URL(expected) joined = base_obj.join(url_obj) assert joined == expected_obj SIMPLE_BASE = "http://a/b/c/d" URLLIB_URLJOIN = [ ("", "http://a/b/c/g?y/./x", "http://a/b/c/g?y/./x"), ("", "http://a/./g", "http://a/./g"), ("svn://pathtorepo/dir1", "dir2", "svn://pathtorepo/dir2"), ("svn+ssh://pathtorepo/dir1", "dir2", "svn+ssh://pathtorepo/dir2"), ("ws://a/b", "g", "ws://a/g"), ("wss://a/b", "g", "wss://a/g"), # test for issue22118 duplicate slashes (SIMPLE_BASE + "/", "foo", SIMPLE_BASE + "/foo"), # Non-RFC-defined tests, covering variations of base and trailing # slashes ("http://a/b/c/d/e/", "../../f/g/", "http://a/b/c/f/g/"), ("http://a/b/c/d/e", "../../f/g/", "http://a/b/f/g/"), ("http://a/b/c/d/e/", "/../../f/g/", "http://a/f/g/"), ("http://a/b/c/d/e", "/../../f/g/", "http://a/f/g/"), ("http://a/b/c/d/e/", "../../f/g", "http://a/b/c/f/g"), ("http://a/b/", "../../f/g/", "http://a/f/g/"), ("a", "b", "b"), ("http:///", "..", "http:///"), ("a/", "b", "a/b"), ("a/b", "c", "a/c"), ("a/b/", "c", "a/b/c"), ( "https://x.org/", "/?text=Hello+G%C3%BCnter", "https://x.org/?text=Hello+G%C3%BCnter", ), ( "https://x.org/", "?text=Hello+G%C3%BCnter", "https://x.org/?text=Hello+G%C3%BCnter", ), ("http://example.com", "http://example.com", "http://example.com"), ("http://x.org", "https://x.org#fragment", "https://x.org#fragment"), ] @pytest.mark.parametrize("base,url,expected", URLLIB_URLJOIN) def test_join_cpython_urljoin(base: str, url: str, expected: str) -> None: # tests from cpython urljoin base_obj = URL(base) url_obj = URL(url) expected_obj = URL(expected) joined = base_obj.join(url_obj) assert joined == expected_obj def test_join_preserves_leading_slash() -> None: """Test that join preserves leading slash in path.""" base = URL.build(scheme="https", host="localhost", port=443) new = base.join(URL("") / "_msearch") assert str(new) == "https://localhost/_msearch" assert new.path == "/_msearch" def test_empty_authority() -> None: assert URL("http:///").authority == "" def test_split_result_non_decoded() -> None: with pytest.raises(ValueError): URL(SplitResult("http", "example.com", "path", "qs", "frag")) def test_split_result_encoded() -> None: url = URL(SplitResult("http", "example.com", "path", "qs", "frag"), encoded=True) assert str(url) == "http://example.com/path?qs#frag" def test_str_encoded() -> None: url = URL("http://example.com/path?qs#frag%2F%2D", encoded=True) assert str(url) == "http://example.com/path?qs#frag%2F%2D" def test_subclassed_str_encoded() -> None: class S(str): """Subclass of str.""" url = URL(S("http://example.com/path?qs#frag%2F%2D"), encoded=True) assert str(url) == "http://example.com/path?qs#frag%2F%2D" def test_human_repr() -> None: url = URL("http://бажан:пароль@хост.домен:8080/шлях/сюди?арг=вал#фраг") s = url.human_repr() assert URL(s) == url assert s == "http://бажан:пароль@хост.домен:8080/шлях/сюди?арг=вал#фраг" def test_human_repr_defaults() -> None: url = URL("шлях") s = url.human_repr() assert s == "шлях" def test_human_repr_default_port() -> None: url = URL("http://бажан:пароль@хост.домен/шлях/сюди?арг=вал#фраг") s = url.human_repr() assert URL(s) == url assert s == "http://бажан:пароль@хост.домен/шлях/сюди?арг=вал#фраг" def test_human_repr_ipv6() -> None: url = URL("http://[::1]:8080/path") s = url.human_repr() url2 = URL(s) assert url2 == url assert url2.host == "::1" assert s == "http://[::1]:8080/path" def test_human_repr_delimiters() -> None: url = URL.build( scheme="http", user=" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", password=" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", host="хост.домен", port=8080, path="/ !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", query={ " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" }, fragment=" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", ) s = url.human_repr() assert URL(s) == url assert ( s == "http:// !\"%23$%25&'()*+,-.%2F%3A;<=>%3F%40%5B\\%5D^_`{|}~" ": !\"%23$%25&'()*+,-.%2F%3A;<=>%3F%40%5B\\%5D^_`{|}~" "@хост.домен:8080" "/ !\"%23$%25&'()*+,-./:;<=>%3F@[\\]^_`{|}~" "? !\"%23$%25%26'()*%2B,-./:%3B<%3D>?@[\\]^_`{|}~" "= !\"%23$%25%26'()*%2B,-./:%3B<%3D>?@[\\]^_`{|}~" "# !\"#$%25&'()*+,-./:;<=>?@[\\]^_`{|}~" ) def test_human_repr_non_printable() -> None: url = URL.build( scheme="http", user="бажан\n\xad\u200b", password="пароль\n\xad\u200b", host="хост.домен", port=8080, path="/шлях\n\xad\u200b", query={"арг\n\xad\u200b": "вал\n\xad\u200b"}, fragment="фраг\n\xad\u200b", ) s = url.human_repr() assert URL(s) == url assert ( s == "http://бажан%0A%C2%AD%E2%80%8B:пароль%0A%C2%AD%E2%80%8B" "@хост.домен:8080" "/шлях%0A%C2%AD%E2%80%8B" "?арг%0A%C2%AD%E2%80%8B=вал%0A%C2%AD%E2%80%8B" "#фраг%0A%C2%AD%E2%80%8B" ) # relative def test_relative() -> None: url = URL("http://user:pass@example.com:8080/path?a=b#frag") rel = url.relative() assert str(rel) == "/path?a=b#frag" def test_relative_is_relative() -> None: url = URL("http://user:pass@example.com:8080/path?a=b#frag") rel = url.relative() assert not rel.is_absolute() assert not rel.absolute def test_relative_abs_parts_are_removed() -> None: url = URL("http://user:pass@example.com:8080/path?a=b#frag") rel = url.relative() assert not rel.scheme assert not rel.user assert not rel.password assert not rel.host assert not rel.port def test_relative_fails_on_rel_url() -> None: with pytest.raises(ValueError): URL("/path?a=b#frag").relative() def test_slash_and_question_in_query() -> None: u = URL("http://example.com/path?http://example.com/p?a#b") assert u.query_string == "http://example.com/p?a" def test_slash_and_question_in_fragment() -> None: u = URL("http://example.com/path#http://example.com/p?a") assert u.fragment == "http://example.com/p?a" def test_requoting() -> None: u = URL("http://127.0.0.1/?next=http%3A//example.com/") assert u.raw_query_string == "next=http://example.com/" assert str(u) == "http://127.0.0.1/?next=http://example.com/" def test_join_query_string() -> None: """Test that query strings are correctly joined.""" original = URL("http://127.0.0.1:62869") path_url = URL( "/api?start=2022-03-27T14:05:00%2B03:00&end=2022-03-27T16:05:00%2B03:00" ) assert path_url.query.get("start") == "2022-03-27T14:05:00+03:00" assert path_url.query.get("end") == "2022-03-27T16:05:00+03:00" new = original.join(path_url) assert new.query.get("start") == "2022-03-27T14:05:00+03:00" assert new.query.get("end") == "2022-03-27T16:05:00+03:00" def test_join_query_string_with_special_chars() -> None: """Test url joining when the query string has non-ascii params.""" original = URL("http://127.0.0.1") path_url = URL("/api?text=%D1%82%D0%B5%D0%BA%D1%81%D1%82") assert path_url.query.get("text") == "текст" new = original.join(path_url) assert new.query.get("text") == "текст" def test_join_encoded_url() -> None: """Test that url encoded urls are correctly joined.""" original = URL("http://127.0.0.1:62869") path_url = URL("/api/%34") assert original.path == "/" assert path_url.path == "/api/4" new = original.join(path_url) assert new.path == "/api/4" # cache def test_parsing_populates_cache() -> None: """Test that parsing a URL populates the cache.""" url = URL("http://user:password@example.com:80/path?a=b#frag") assert url._cache["raw_user"] == "user" assert url._cache["raw_password"] == "password" assert url._cache["raw_host"] == "example.com" assert url._cache["explicit_port"] == 80 assert url._cache["raw_query_string"] == "a=b" assert url._cache["raw_fragment"] == "frag" assert url._cache["scheme"] == "http" assert url._cache["raw_path"] == "/path" assert url.raw_user == "user" assert url.raw_password == "password" assert url.raw_host == "example.com" assert url.explicit_port == 80 assert url.raw_query_string == "a=b" assert url.raw_fragment == "frag" assert url.scheme == "http" url._cache.clear() # type: ignore[attr-defined] assert url.raw_user == "user" assert url.raw_password == "password" assert url.raw_host == "example.com" assert url.explicit_port == 80 assert url.raw_query_string == "a=b" assert url.raw_fragment == "frag" assert url.scheme == "http" assert url.raw_path == "/path" assert url._cache["raw_user"] == "user" assert url._cache["raw_password"] == "password" assert url._cache["raw_host"] == "example.com" assert url._cache["explicit_port"] == 80 assert url._cache["raw_query_string"] == "a=b" assert url._cache["raw_fragment"] == "frag" assert url._cache["scheme"] == "http" assert url._cache["raw_path"] == "/path" def test_relative_url_populates_cache() -> None: """Test that parsing a relative URL populates the cache.""" url = URL(".") assert url._cache["raw_query_string"] == "" assert url._cache["raw_fragment"] == "" assert url._cache["scheme"] == "" assert url._cache["raw_path"] == "." def test_parsing_populates_cache_for_single_dot() -> None: """Test that parsing a URL populates the cache for a single dot path.""" url = URL("http://example.com/.") # raw_path should be normalized to "/" assert url._cache["raw_path"] == "/" assert url._cache["raw_host"] == "example.com" assert url._cache["scheme"] == "http" assert url.raw_path == "/" @pytest.mark.parametrize( ("host", "is_authority"), [ *(("other_gen_delim_" + c, False) for c in "[]"), ], ) def test_build_with_invalid_ipv6_host(host: str, is_authority: bool) -> None: with pytest.raises(ValueError, match="Invalid IPv6 URL"): URL(f"http://{host}/") @pytest.mark.parametrize("byte", ["\r", "\n", "\t"]) def test_unsafe_url_bytes_are_removed(byte: str) -> None: url = URL(f"http://example.com{byte}/") assert str(url) == "http://example.com/" @pytest.mark.parametrize("byte", tuple(_WHATWG_C0_CONTROL_OR_SPACE)) def test_control_chars_are_removed(byte: str) -> None: url = URL(f"{byte}http://example.com/") assert str(url) == "http://example.com/" @pytest.mark.parametrize( "disallowed_unicode", [_VERTICAL_COLON, _FULL_WITH_NUMBER_SIGN, _ACCOUNT_OF] ) def test_url_with_invalid_unicode(disallowed_unicode: str) -> None: with pytest.raises( ValueError, match="contains invalid characters under NFKC normalization" ): URL(f"http://example.com{disallowed_unicode}80/") with pytest.raises( ValueError, match="contains invalid characters under NFKC normalization" ): URL(f"http://example.{disallowed_unicode}.com/frag") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_url_benchmarks.py0000644000175100001660000005046514774356277017766 0ustar00runnerdocker"""codspeed benchmarks for yarl.URL.""" import pytest from pytest_codspeed import BenchmarkFixture from yarl import URL MANY_HOSTS = [f"www.domain{i}.tld" for i in range(256)] MANY_URLS = [f"https://www.domain{i}.tld" for i in range(256)] MANY_IPV4_URLS = [f"http://127.0.0.{i}" for i in range(256)] MANY_IPV6_URLS = [f"http://[::1{i}]" for i in range(256)] BASE_URL_STR = "http://www.domain.tld" BASE_URL = URL(BASE_URL_STR) URL_WITH_USER_PASS_STR = "http://user:password@www.domain.tld" URL_WITH_USER_PASS = URL(URL_WITH_USER_PASS_STR) IPV6_QUERY_URL = URL("http://[::1]/req?query=1&query=2&query=3&query=4&query=5") URL_WITH_NOT_DEFAULT_PORT = URL("http://www.domain.tld:1234") QUERY_URL_STR = "http://www.domain.tld?query=1&query=2&query=3&query=4&query=5" LONG_QUERY_URL_STR = ( "http://www.domain.tld?query=1&query=2&query=3&query=4&query=5&query=6&query=7" "&query=8&query=9&query=10&query=11&query=12&query=13&query=14&query=15" "&query=16&query=17&query=18&query=19&query=20&query=21&query=22&query=23" "&query=24&query=25&query=26&query=27&query=28&query=29&query=30&query=31" "&query=32&query=33&query=34&query=35&query=36&query=37&query=38&query=39" "&query=40&query=41&query=42&query=43&query=44&query=45&query=46&query=47" "&query=48&query=49&query=50&query=51&query=52&query=53&query=54&query=55" "&query=56&query=57&query=58&query=59&query=60&query=61&query=62&query=63" ) LONG_QUERY_URL = URL(LONG_QUERY_URL_STR) QUERY_URL = URL(QUERY_URL_STR) URL_WITH_PATH_STR = "http://www.domain.tld/req" URL_WITH_PATH = URL(URL_WITH_PATH_STR) REL_URL = URL("/req") QUERY_SEQ = {str(i): tuple(str(j) for j in range(10)) for i in range(10)} SIMPLE_QUERY = {str(i): str(i) for i in range(10)} SIMPLE_INT_QUERY = {str(i): i for i in range(10)} QUERY_STRING = "x=y&z=1" class _SubClassedStr(str): """A subclass of str that does nothing.""" def test_url_build_with_host_and_port(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(host="www.domain.tld", path="/req", port=1234) def test_url_build_with_simple_query(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(host="www.domain.tld", query=SIMPLE_QUERY) def test_url_build_no_netloc(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(path="/req/req/req") def test_url_build_no_netloc_relative(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(path="req/req/req") def test_url_build_encoded_with_host_and_port(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(host="www.domain.tld", path="/req", port=1234, encoded=True) def test_url_build_with_host(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(host="domain") def test_url_build_access_username_password(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): url = URL.build(host="www.domain.tld", user="user", password="password") url.raw_user url.raw_password def test_url_build_access_raw_host(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): url = URL.build(host="www.domain.tld") url.raw_host def test_url_build_access_fragment(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): url = URL.build(host="www.domain.tld") url.fragment def test_url_build_access_raw_path(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): url = URL.build(host="www.domain.tld", path="/req") url.raw_path def test_url_build_with_different_hosts(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for host in MANY_HOSTS: URL.build(host=host) def test_url_build_with_host_path_and_port(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(host="www.domain.tld", port=1234) def test_url_make_no_netloc(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL("/req/req/req") def test_url_make_no_netloc_relative(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL("req/req/req") def test_url_make_with_host_path_and_port(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://www.domain.tld:1234/req") def test_url_make_encoded_with_host_path_and_port(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://www.domain.tld:1234/req", encoded=True) def test_url_make_with_host_and_path(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://www.domain.tld") def test_url_make_with_many_hosts(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for url in MANY_URLS: URL(url) def test_url_make_with_many_ipv4_hosts(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for url in MANY_IPV4_URLS: URL(url) def test_url_make_with_many_ipv6_hosts(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for url in MANY_IPV6_URLS: URL(url) def test_url_make_access_raw_host(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): url = URL("http://www.domain.tld") url.raw_host def test_raw_host_empty_cache(benchmark: BenchmarkFixture) -> None: url = URL("http://www.domain.tld") @benchmark def _run() -> None: for _ in range(100): url._cache.pop("raw_host", None) url.raw_host def test_url_make_access_fragment(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): url = URL("http://www.domain.tld") url.fragment def test_url_make_access_raw_path(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): url = URL("http://www.domain.tld/req") url.raw_path def test_url_make_access_username_password(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): url = URL("http://user:password@www.domain.tld") url.raw_user url.raw_password def test_url_make_empty_username(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://:password@www.domain.tld") def test_url_make_empty_password(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://user:@www.domain.tld") def test_url_make_with_ipv4_address_path_and_port(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://127.0.0.1:1234/req") def test_url_make_with_ipv4_address_and_path(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://127.0.0.1/req") def test_url_make_with_ipv6_address_path_and_port(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://[::1]:1234/req") def test_url_make_with_ipv6_address_and_path(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://[::1]/req") def test_extend_query_subclassed_str(benchmark: BenchmarkFixture) -> None: """Test extending a query with a subclassed str.""" subclassed_query = {str(i): _SubClassedStr(i) for i in range(10)} @benchmark def _run() -> None: for _ in range(25): BASE_URL.with_query(subclassed_query) def test_with_query_mapping(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.with_query(SIMPLE_QUERY) def test_with_query_mapping_int_values(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.with_query(SIMPLE_INT_QUERY) def test_with_query_sequence_mapping(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.with_query(QUERY_SEQ) def test_with_query_empty(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.with_query({}) def test_with_query_none(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.with_query(None) def test_update_query_mapping(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.update_query(SIMPLE_QUERY) def test_update_query_mapping_with_existing_query(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): QUERY_URL.update_query(SIMPLE_QUERY) def test_update_query_sequence_mapping(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.update_query(QUERY_SEQ) def test_update_query_empty(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.update_query({}) def test_update_query_none(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.update_query(None) def test_update_query_string(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.update_query(QUERY_STRING) def test_url_extend_query_simple_query_dict(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.extend_query(SIMPLE_QUERY) def test_url_extend_query_existing_query_simple_query_dict( benchmark: BenchmarkFixture, ) -> None: @benchmark def _run() -> None: for _ in range(25): QUERY_URL.extend_query(SIMPLE_QUERY) def test_url_extend_query_existing_query_string(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(25): QUERY_URL.extend_query(QUERY_STRING) def test_url_to_string(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): str(BASE_URL) def test_url_with_path_to_string(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): str(URL_WITH_PATH) def test_url_with_query_to_string(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): str(QUERY_URL) def test_url_with_fragment(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_fragment("fragment") def test_url_with_user(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_user("user") def test_url_with_password(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_password("password") def test_url_with_host(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_host("www.domain.tld") def test_url_with_port(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_port(1234) def test_url_with_scheme(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_scheme("https") def test_url_with_name(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_name("other.tld") def test_url_with_path(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_path("/req") def test_url_origin(benchmark: BenchmarkFixture) -> None: urls = [URL(BASE_URL_STR) for _ in range(100)] @benchmark def _run() -> None: for url in urls: url.origin() def test_url_origin_with_user_pass(benchmark: BenchmarkFixture) -> None: urls = [URL(URL_WITH_USER_PASS_STR) for _ in range(100)] @benchmark def _run() -> None: for url in urls: url.origin() def test_url_with_path_origin(benchmark: BenchmarkFixture) -> None: urls = [URL(URL_WITH_PATH_STR) for _ in range(100)] @benchmark def _run() -> None: for url in urls: url.origin() def test_url_with_path_relative(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): URL_WITH_PATH.relative() def test_url_with_path_parent(benchmark: BenchmarkFixture) -> None: cache = URL_WITH_PATH._cache @benchmark def _run() -> None: for _ in range(100): cache.pop("parent", None) URL_WITH_PATH.parent def test_url_join(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.join(REL_URL) def test_url_joinpath_encoded(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.joinpath("req", encoded=True) def test_url_joinpath_encoded_long(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.joinpath( "req/req/req/req/req/req/req/req/req/req/req/req/req/req", encoded=True ) def test_url_joinpath(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.joinpath("req", encoded=False) def test_url_joinpath_with_truediv(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL / "req/req/req" def test_url_equality(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL == BASE_URL BASE_URL == URL_WITH_PATH URL_WITH_PATH == URL_WITH_PATH def test_url_hash(benchmark: BenchmarkFixture) -> None: cache = BASE_URL._cache @benchmark def _run() -> None: for _ in range(100): cache.pop("hash", None) hash(BASE_URL) def test_is_default_port(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.is_default_port() URL_WITH_NOT_DEFAULT_PORT.is_default_port() def test_human_repr(benchmark: BenchmarkFixture) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.human_repr() URL_WITH_PATH.human_repr() QUERY_URL.human_repr() URL_WITH_NOT_DEFAULT_PORT.human_repr() IPV6_QUERY_URL.human_repr() REL_URL.human_repr() def test_query_string(benchmark: BenchmarkFixture) -> None: urls = [URL(QUERY_URL_STR) for _ in range(100)] @benchmark def _run() -> None: for url in urls: url.query_string def test_empty_query_string(benchmark: BenchmarkFixture) -> None: urls = [URL(BASE_URL_STR) for _ in range(100)] @benchmark def _run() -> None: for url in urls: url.query_string def test_empty_query_string_uncached(benchmark: BenchmarkFixture) -> None: urls = [URL(BASE_URL_STR) for _ in range(100)] @benchmark def _run() -> None: for url in urls: URL.query_string.wrapped(url) def test_query(benchmark: BenchmarkFixture) -> None: urls = [URL(QUERY_URL_STR) for _ in range(100)] @benchmark def _run() -> None: for url in urls: url.query def test_empty_query(benchmark: BenchmarkFixture) -> None: urls = [URL(BASE_URL_STR) for _ in range(100)] @benchmark def _run() -> None: for url in urls: url.query def test_url_host_port_subcomponent(benchmark: BenchmarkFixture) -> None: cache_non_default = URL_WITH_NOT_DEFAULT_PORT._cache cache = BASE_URL._cache @benchmark def _run() -> None: for _ in range(100): cache.pop("host_port_subcomponent", None) cache_non_default.pop("host_port_subcomponent", None) URL_WITH_NOT_DEFAULT_PORT.host_port_subcomponent BASE_URL.host_port_subcomponent def test_empty_path(benchmark: BenchmarkFixture) -> None: """Test accessing empty path.""" @benchmark def _run() -> None: for _ in range(100): BASE_URL.path def test_empty_path_uncached(benchmark: BenchmarkFixture) -> None: """Test accessing empty path without cache.""" @benchmark def _run() -> None: for _ in range(100): URL.path.wrapped(BASE_URL) def test_empty_path_safe(benchmark: BenchmarkFixture) -> None: """Test accessing empty path safe.""" @benchmark def _run() -> None: for _ in range(100): BASE_URL.path_safe def test_empty_path_safe_uncached(benchmark: BenchmarkFixture) -> None: """Test accessing empty path safe without cache.""" @benchmark def _run() -> None: for _ in range(100): URL.path_safe.wrapped(BASE_URL) def test_path_safe(benchmark: BenchmarkFixture) -> None: """Test accessing path safe.""" @benchmark def _run() -> None: for _ in range(100): URL_WITH_PATH.path_safe def test_path_safe_uncached(benchmark: BenchmarkFixture) -> None: """Test accessing path safe without cache.""" @benchmark def _run() -> None: for _ in range(100): URL.path_safe.wrapped(URL_WITH_PATH) def test_empty_raw_path_qs(benchmark: BenchmarkFixture) -> None: """Test accessing empty raw path with query.""" @benchmark def _run() -> None: for _ in range(100): BASE_URL.raw_path_qs def test_empty_raw_path_qs_uncached(benchmark: BenchmarkFixture) -> None: """Test accessing empty raw path with query without cache.""" @benchmark def _run() -> None: for _ in range(100): URL.raw_path_qs.wrapped(BASE_URL) def test_raw_path_qs(benchmark: BenchmarkFixture) -> None: """Test accessing raw path qs without query.""" @benchmark def _run() -> None: for _ in range(100): URL_WITH_PATH.raw_path_qs def test_raw_path_qs_uncached(benchmark: BenchmarkFixture) -> None: """Test accessing raw path qs without query and without cache.""" @benchmark def _run() -> None: for _ in range(100): URL.raw_path_qs.wrapped(URL_WITH_PATH) def test_raw_path_qs_with_query(benchmark: BenchmarkFixture) -> None: """Test accessing raw path qs with query.""" @benchmark def _run() -> None: for _ in range(100): IPV6_QUERY_URL.raw_path_qs def test_raw_path_qs_with_query_uncached(benchmark: BenchmarkFixture) -> None: """Test accessing raw path qs with query and without cache.""" @benchmark def _run() -> None: for _ in range(100): URL.raw_path_qs.wrapped(IPV6_QUERY_URL) @pytest.mark.parametrize("url", [QUERY_URL, LONG_QUERY_URL], ids=["short", "long"]) def test_parse_query_uncached(benchmark: BenchmarkFixture, url: URL) -> None: """Test parsing the query string without the cache.""" @benchmark def _run() -> None: for _ in range(100): URL._parsed_query.wrapped(url) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_url_build.py0000644000175100001660000002670714774356277016752 0ustar00runnerdockerimport pytest from yarl import URL # build classmethod def test_build_without_arguments() -> None: u = URL.build() assert str(u) == "" def test_build_simple() -> None: u = URL.build(scheme="http", host="127.0.0.1") assert str(u) == "http://127.0.0.1" def test_url_build_ipv6() -> None: u = URL.build(scheme="http", host="::1") assert str(u) == "http://[::1]" def test_url_build_ipv6_brackets_encoded() -> None: u = URL.build(scheme="http", host="[::1]", encoded=True) assert str(u) == "http://[::1]" def test_url_build_ipv6_brackets_not_encoded() -> None: u = URL.build(scheme="http", host="::1", encoded=False) assert str(u) == "http://[::1]" def test_url_ipv4_in_ipv6() -> None: u = URL.build(scheme="http", host="2001:db8:122:344::192.0.2.33") assert str(u) == "http://[2001:db8:122:344::c000:221]" def test_build_with_scheme() -> None: u = URL.build(scheme="blob", path="path") assert str(u) == "blob:path" def test_build_with_host() -> None: u = URL.build(host="127.0.0.1") assert str(u) == "//127.0.0.1" assert u == URL("//127.0.0.1") def test_build_with_scheme_and_host() -> None: u = URL.build(scheme="http", host="127.0.0.1") assert str(u) == "http://127.0.0.1" assert u == URL("http://127.0.0.1") @pytest.mark.parametrize( ("port", "exc", "match"), [ pytest.param( 8000, ValueError, r"""(?x) ^ Can't\ build\ URL\ with\ "port"\ but\ without\ "host"\. $ """, id="port-only", ), pytest.param( "", TypeError, r"^The port is required to be int, got .*\.$", id="port-str" ), ], ) def test_build_with_port(port: int, exc: type[Exception], match: str) -> None: with pytest.raises(exc, match=match): URL.build(port=port) def test_build_with_user() -> None: u = URL.build(scheme="http", host="127.0.0.1", user="foo") assert str(u) == "http://foo@127.0.0.1" def test_build_with_user_password() -> None: u = URL.build(scheme="http", host="127.0.0.1", user="foo", password="bar") assert str(u) == "http://foo:bar@127.0.0.1" def test_build_with_query_and_query_string() -> None: with pytest.raises(ValueError): URL.build( scheme="http", host="127.0.0.1", user="foo", password="bar", port=8000, path="/index.html", query=dict(arg="value1"), query_string="arg=value1", fragment="top", ) def test_build_with_all() -> None: u = URL.build( scheme="http", host="127.0.0.1", user="foo", password="bar", port=8000, path="/index.html", query_string="arg=value1", fragment="top", ) assert str(u) == "http://foo:bar@127.0.0.1:8000/index.html?arg=value1#top" def test_build_with_authority_and_host() -> None: with pytest.raises(ValueError): URL.build(authority="host.com", host="example.com") @pytest.mark.parametrize( ("host", "is_authority"), [ ("user:pass@host.com", True), ("user@host.com", True), ("host:com", False), ("not_percent_encoded%Zf", False), ("still_not_percent_encoded%fZ", False), *(("other_gen_delim_" + c, False) for c in "/?#[]"), ], ) def test_build_with_invalid_host(host: str, is_authority: bool) -> None: match = r"Host '[^']+' cannot contain '[^']+' \(at position \d+\)" if is_authority: match += ", if .* use 'authority' instead of 'host'" with pytest.raises(ValueError, match=f"{match}$"): URL.build(host=host) def test_build_with_authority() -> None: url = URL.build(scheme="http", authority="степан:bar@host.com:8000", path="/path") assert ( str(url) == "http://%D1%81%D1%82%D0%B5%D0%BF%D0%B0%D0%BD:bar@host.com:8000/path" ) def test_build_with_authority_no_leading_flash() -> None: msg = r"Path in a URL with authority should start with a slash \('/'\) if set" with pytest.raises(ValueError, match=msg): URL.build(scheme="http", authority="степан:bar@host.com:8000", path="path") def test_build_with_authority_without_encoding() -> None: url = URL.build( scheme="http", authority="foo:bar@host.com:8000", path="path", encoded=True ) assert str(url) == "http://foo:bar@host.com:8000/path" def test_build_with_authority_empty_host_no_scheme() -> None: url = URL.build(authority="", path="path") assert str(url) == "path" def test_build_with_authority_and_only_user() -> None: url = URL.build(scheme="https", authority="user:@foo.com", path="/path") assert str(url) == "https://user:@foo.com/path" def test_build_with_authority_with_port() -> None: url = URL.build(scheme="https", authority="foo.com:8080", path="/path") assert str(url) == "https://foo.com:8080/path" def test_build_with_authority_with_ipv6() -> None: url = URL.build(scheme="https", authority="[::1]", path="/path") assert str(url) == "https://[::1]/path" def test_build_with_authority_with_ipv6_and_port() -> None: url = URL.build(scheme="https", authority="[::1]:81", path="/path") assert str(url) == "https://[::1]:81/path" def test_query_str() -> None: u = URL.build(scheme="http", host="127.0.0.1", path="/", query_string="arg=value1") assert str(u) == "http://127.0.0.1/?arg=value1" def test_query_dict() -> None: u = URL.build(scheme="http", host="127.0.0.1", path="/", query=dict(arg="value1")) assert str(u) == "http://127.0.0.1/?arg=value1" def test_build_path_quoting() -> None: u = URL.build( scheme="http", host="127.0.0.1", path="/фотографія.jpg", query=dict(arg="Привіт"), ) assert u == URL("http://127.0.0.1/фотографія.jpg?arg=Привіт") assert str(u) == ( "http://127.0.0.1/" "%D1%84%D0%BE%D1%82%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D1%96%D1%8F.jpg?" "arg=%D0%9F%D1%80%D0%B8%D0%B2%D1%96%D1%82" ) def test_build_query_quoting() -> None: u = URL.build( scheme="http", host="127.0.0.1", path="/фотографія.jpg", query="arg=Привіт", ) assert u == URL("http://127.0.0.1/фотографія.jpg?arg=Привіт") assert str(u) == ( "http://127.0.0.1/" "%D1%84%D0%BE%D1%82%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D1%96%D1%8F.jpg?" "arg=%D0%9F%D1%80%D0%B8%D0%B2%D1%96%D1%82" ) def test_build_query_only() -> None: u = URL.build(query={"key": "value"}) assert str(u) == "?key=value" def test_build_drop_dots() -> None: u = URL.build(scheme="http", host="example.com", path="/path/../to") assert str(u) == "http://example.com/to" def test_build_encode() -> None: u = URL.build( scheme="http", host="оун-упа.укр", path="/шлях/криївка", query_string="ключ=знач", fragment="фраг", ) expected = ( "http://xn----8sb1bdhvc.xn--j1amh" "/%D1%88%D0%BB%D1%8F%D1%85/%D0%BA%D1%80%D0%B8%D1%97%D0%B2%D0%BA%D0%B0" "?%D0%BA%D0%BB%D1%8E%D1%87=%D0%B7%D0%BD%D0%B0%D1%87" "#%D1%84%D1%80%D0%B0%D0%B3" ) assert str(u) == expected def test_build_already_encoded() -> None: # resulting URL is invalid but not encoded u = URL.build( scheme="http", host="оун-упа.укр", path="/шлях/криївка", query_string="ключ=знач", fragment="фраг", encoded=True, ) assert str(u) == "http://оун-упа.укр/шлях/криївка?ключ=знач#фраг" def test_build_already_encoded_username_password() -> None: u = URL.build( scheme="http", host="x.org", path="/x/y/z", query_string="x=z", fragment="any", user="u", password="p", encoded=True, ) assert str(u) == "http://u:p@x.org/x/y/z?x=z#any" assert u.host_subcomponent == "x.org" def test_build_already_encoded_empty_host() -> None: u = URL.build( host="", path="/x/y/z", query_string="x=z", fragment="any", encoded=True, ) assert str(u) == "/x/y/z?x=z#any" assert u.host_subcomponent is None def test_build_percent_encoded() -> None: u = URL.build( scheme="http", host="%2d.org", user="u%2d", password="p%2d", path="/%2d", query_string="k%2d=v%2d", fragment="f%2d", ) assert str(u) == "http://u%252d:p%252d@%2d.org/%252d?k%252d=v%252d#f%252d" assert u.raw_host == "%2d.org" assert u.host == "%2d.org" assert u.raw_user == "u%252d" assert u.user == "u%2d" assert u.raw_password == "p%252d" assert u.password == "p%2d" assert u.raw_authority == "u%252d:p%252d@%2d.org" assert u.authority == "u%2d:p%2d@%2d.org:80" assert u.raw_path == "/%252d" assert u.path == "/%2d" assert u.query == {"k%2d": "v%2d"} assert u.raw_query_string == "k%252d=v%252d" assert u.query_string == "k%2d=v%2d" assert u.raw_fragment == "f%252d" assert u.fragment == "f%2d" def test_build_with_authority_percent_encoded() -> None: u = URL.build(scheme="http", authority="u%2d:p%2d@%2d.org") assert str(u) == "http://u%252d:p%252d@%2d.org" assert u.raw_host == "%2d.org" assert u.host == "%2d.org" assert u.raw_user == "u%252d" assert u.user == "u%2d" assert u.raw_password == "p%252d" assert u.password == "p%2d" assert u.raw_authority == "u%252d:p%252d@%2d.org" assert u.authority == "u%2d:p%2d@%2d.org:80" def test_build_with_authority_percent_encoded_already_encoded() -> None: u = URL.build(scheme="http", authority="u%2d:p%2d@%2d.org", encoded=True) assert str(u) == "http://u%2d:p%2d@%2d.org" assert u.raw_host == "%2d.org" assert u.host == "%2d.org" assert u.user == "u-" assert u.raw_user == "u%2d" assert u.password == "p-" assert u.raw_password == "p%2d" assert u.authority == "u-:p-@%2d.org:80" assert u.raw_authority == "u%2d:p%2d@%2d.org" def test_build_with_authority_with_path_with_leading_slash() -> None: u = URL.build(scheme="http", host="example.com", path="/path_with_leading_slash") assert str(u) == "http://example.com/path_with_leading_slash" def test_build_with_authority_with_empty_path() -> None: u = URL.build(scheme="http", host="example.com", path="") assert str(u) == "http://example.com" def test_build_with_authority_with_path_without_leading_slash() -> None: with pytest.raises(ValueError): URL.build(scheme="http", host="example.com", path="path_without_leading_slash") def test_build_with_none_host() -> None: with pytest.raises(TypeError, match="NoneType is illegal for.*host"): URL.build(scheme="http", host=None) # type: ignore[arg-type] def test_build_with_none_path() -> None: with pytest.raises(TypeError): URL.build(scheme="http", host="example.com", path=None) # type: ignore[arg-type] def test_build_with_none_query_string() -> None: with pytest.raises(TypeError): URL.build(scheme="http", host="example.com", query_string=None) # type: ignore[arg-type] def test_build_with_none_fragment() -> None: with pytest.raises(TypeError): URL.build(scheme="http", host="example.com", fragment=None) # type: ignore[arg-type] def test_build_uppercase_host() -> None: u = URL.build( host="UPPER.case", encoded=False, ) assert u.host == "upper.case" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_url_cmp_and_hash.py0000644000175100001660000000333314774356277020245 0ustar00runnerdockerfrom yarl import URL # comparison and hashing def test_ne_str() -> None: url = URL("http://example.com/") assert url != "http://example.com/" def test_eq() -> None: url = URL("http://example.com/") assert url == URL("http://example.com/") def test_hash() -> None: assert hash(URL("http://example.com/")) == hash(URL("http://example.com/")) def test_hash_double_call() -> None: url = URL("http://example.com/") assert hash(url) == hash(url) def test_le_less() -> None: url1 = URL("http://example1.com/") url2 = URL("http://example2.com/") assert url1 <= url2 def test_le_eq() -> None: url1 = URL("http://example.com/") url2 = URL("http://example.com/") assert url1 <= url2 def test_le_not_implemented() -> None: url = URL("http://example1.com/") assert url.__le__(123) is NotImplemented def test_lt() -> None: url1 = URL("http://example1.com/") url2 = URL("http://example2.com/") assert url1 < url2 def test_lt_not_implemented() -> None: url = URL("http://example1.com/") assert url.__lt__(123) is NotImplemented def test_ge_more() -> None: url1 = URL("http://example1.com/") url2 = URL("http://example2.com/") assert url2 >= url1 def test_ge_eq() -> None: url1 = URL("http://example.com/") url2 = URL("http://example.com/") assert url2 >= url1 def test_ge_not_implemented() -> None: url = URL("http://example1.com/") assert url.__ge__(123) is NotImplemented def test_gt() -> None: url1 = URL("http://example1.com/") url2 = URL("http://example2.com/") assert url2 > url1 def test_gt_not_implemented() -> None: url = URL("http://example1.com/") assert url.__gt__(123) is NotImplemented ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_url_parsing.py0000644000175100001660000004376114774356277017315 0ustar00runnerdockerfrom urllib.parse import SplitResult import pytest from yarl import URL class TestScheme: def test_scheme_path(self) -> None: u = URL("scheme:path") assert u.scheme == "scheme" assert u.host is None assert u.path == "path" assert u.query_string == "" assert u.fragment == "" def test_scheme_path_other(self) -> None: u = URL("scheme:path:other") assert u.scheme == "scheme" assert u.host is None assert u.path == "path:other" assert u.query_string == "" assert u.fragment == "" def test_complex_scheme(self) -> None: u = URL("allow+chars-33.:path") assert u.scheme == "allow+chars-33." assert u.host is None assert u.path == "path" assert u.query_string == "" assert u.fragment == "" def test_scheme_only(self) -> None: u = URL("simple:") assert u.scheme == "simple" assert u.host is None assert u.path == "" assert u.query_string == "" assert u.fragment == "" def test_no_scheme1(self) -> None: u = URL("google.com:80") assert u.scheme == "google.com" assert u.host is None assert u.path == "80" assert u.query_string == "" assert u.fragment == "" def test_no_scheme2(self) -> None: u = URL("google.com:80/root") assert u.scheme == "google.com" assert u.host is None assert u.path == "80/root" assert u.query_string == "" assert u.fragment == "" def test_not_a_scheme1(self) -> None: u = URL("not_cheme:path") assert u.scheme == "" assert u.host is None assert u.path == "not_cheme:path" assert u.query_string == "" assert u.fragment == "" def test_not_a_scheme2(self) -> None: u = URL("signals37:book") assert u.scheme == "signals37" assert u.host is None assert u.path == "book" assert u.query_string == "" assert u.fragment == "" def test_scheme_rel_path1(self) -> None: u = URL(":relative-path") assert u.scheme == "" assert u.host is None assert u.path == ":relative-path" assert u.query_string == "" assert u.fragment == "" def test_scheme_rel_path2(self) -> None: u = URL(":relative/path") assert u.scheme == "" assert u.host is None assert u.path == ":relative/path" assert u.query_string == "" assert u.fragment == "" def test_scheme_weird(self) -> None: u = URL("://and-this") assert u.scheme == "" assert u.host is None assert u.path == "://and-this" assert u.query_string == "" assert u.fragment == "" class TestHost: def test_canonical(self) -> None: u = URL("scheme://host/path") assert u.scheme == "scheme" assert u.host == "host" assert u.path == "/path" assert u.query_string == "" assert u.fragment == "" def test_absolute_no_scheme(self) -> None: u = URL("//host/path") assert u.scheme == "" assert u.host == "host" assert u.path == "/path" assert u.query_string == "" assert u.fragment == "" def test_absolute_no_scheme_complex_host(self) -> None: u = URL("//host+path") assert u.scheme == "" assert u.host == "host+path" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_absolute_no_scheme_simple_host(self) -> None: u = URL("//host") assert u.scheme == "" assert u.host == "host" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_weird_host(self) -> None: u = URL("//this+is$also&host!") assert u.scheme == "" assert u.host == "this+is$also&host!" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_scheme_no_host(self) -> None: u = URL("scheme:/host/path") assert u.scheme == "scheme" assert u.host is None assert u.path == "/host/path" assert u.query_string == "" assert u.fragment == "" def test_scheme_no_host2(self) -> None: u = URL("scheme:///host/path") assert u.scheme == "scheme" assert u.host is None assert u.path == "/host/path" assert u.query_string == "" assert u.fragment == "" def test_no_scheme_no_host(self) -> None: u = URL("scheme//host/path") assert u.scheme == "" assert u.host is None assert u.path == "scheme//host/path" assert u.query_string == "" assert u.fragment == "" def test_ipv4(self) -> None: u = URL("//127.0.0.1/") assert u.scheme == "" assert u.host == "127.0.0.1" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_ipv6(self) -> None: u = URL("//[::1]/") assert u.scheme == "" assert u.host == "::1" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_ipvfuture_address(self) -> None: u = URL("//[v1.-1]/") assert u.scheme == "" assert u.host == "v1.-1" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" class TestPort: def test_canonical(self) -> None: u = URL("//host:80/path") assert u.scheme == "" assert u.host == "host" assert u.port == 80 assert u.path == "/path" assert u.query_string == "" assert u.fragment == "" def test_no_path(self) -> None: u = URL("//host:80") assert u.scheme == "" assert u.host == "host" assert u.port == 80 assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_no_host(self) -> None: u = URL("//:77") assert u.scheme == "" assert u.host == "" assert u.port == 77 assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_double_port(self) -> None: with pytest.raises(ValueError): URL("//h:22:80/") def test_bad_port(self) -> None: with pytest.raises(ValueError): URL("//h:no/path") def test_another_bad_port(self) -> None: with pytest.raises(ValueError): URL("//h:22:no/path") def test_bad_port_again(self) -> None: with pytest.raises(ValueError): URL("//h:-80/path") class TestUserInfo: def test_canonical(self) -> None: u = URL("sch://user@host/") assert u.scheme == "sch" assert u.user == "user" assert u.host == "host" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_user_pass(self) -> None: u = URL("//user:pass@host") assert u.scheme == "" assert u.user == "user" assert u.password == "pass" assert u.host == "host" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_complex_userinfo(self) -> None: u = URL("//user:pas:and:more@host") assert u.scheme == "" assert u.user == "user" assert u.password == "pas:and:more" assert u.host == "host" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_no_user(self) -> None: u = URL("//:pas:@host") assert u.scheme == "" assert u.user is None assert u.password == "pas:" assert u.host == "host" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_weird_user(self) -> None: u = URL("//!($&')*+,;=@host") assert u.scheme == "" assert u.user == "!($&')*+,;=" assert u.password is None assert u.host == "host" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_weird_user2(self) -> None: u = URL("//user@info@ya.ru") assert u.scheme == "" assert u.user == "user@info" assert u.password is None assert u.host == "ya.ru" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_weird_user3(self) -> None: u = URL("//%5Bsome%5D@host") assert u.scheme == "" assert u.user == "[some]" assert u.password is None assert u.host == "host" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" class TestQuery_String: def test_simple(self) -> None: u = URL("?query") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "" assert u.query_string == "query" assert u.fragment == "" def test_scheme_query(self) -> None: u = URL("http:?query") assert u.scheme == "http" assert u.user is None assert u.password is None assert u.host is None assert u.path == "" assert u.query_string == "query" assert u.fragment == "" def test_abs_url_query(self) -> None: u = URL("//host?query") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host == "host" assert u.path == "/" assert u.query_string == "query" assert u.fragment == "" def test_abs_url_path_query(self) -> None: u = URL("//host/path?query") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host == "host" assert u.path == "/path" assert u.query_string == "query" assert u.fragment == "" def test_double_question_mark(self) -> None: u = URL("//ho?st/path?query") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host == "ho" assert u.path == "/" assert u.query_string == "st/path?query" assert u.fragment == "" def test_complex_query(self) -> None: u = URL("?a://b:c@d.e/f?g#h") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "" assert u.query_string == "a://b:c@d.e/f?g" assert u.fragment == "h" def test_query_in_fragment(self) -> None: u = URL("#?query") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "" assert u.query_string == "" assert u.fragment == "?query" class TestFragment: def test_simple(self) -> None: u = URL("#frag") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "" assert u.query_string == "" assert u.fragment == "frag" def test_scheme_frag(self) -> None: u = URL("http:#frag") assert u.scheme == "http" assert u.user is None assert u.password is None assert u.host is None assert u.path == "" assert u.query_string == "" assert u.fragment == "frag" def test_host_frag(self) -> None: u = URL("//host#frag") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host == "host" assert u.path == "/" assert u.query_string == "" assert u.fragment == "frag" def test_scheme_path_frag(self) -> None: u = URL("//host/path#frag") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host == "host" assert u.path == "/path" assert u.query_string == "" assert u.fragment == "frag" def test_scheme_query_frag(self) -> None: u = URL("//host?query#frag") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host == "host" assert u.path == "/" assert u.query_string == "query" assert u.fragment == "frag" def test_host_frag_query(self) -> None: u = URL("//ho#st/path?query") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host == "ho" assert u.path == "/" assert u.query_string == "" assert u.fragment == "st/path?query" def test_complex_frag(self) -> None: u = URL("#a://b:c@d.e/f?g#h") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "" assert u.query_string == "" assert u.fragment == "a://b:c@d.e/f?g#h" class TestStripEmptyParts: def test_all_empty_http(self) -> None: with pytest.raises(ValueError): URL("http://@:?#") def test_all_empty(self) -> None: u = URL("//@:?#") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host == "" assert u.path == "" assert u.query_string == "" assert u.fragment == "" def test_path_only(self) -> None: u = URL("///path") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "/path" assert u.query_string == "" assert u.fragment == "" def test_empty_user(self) -> None: u = URL("//@host") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host == "host" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_empty_port(self) -> None: u = URL("//host:") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host == "host" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_empty_port_and_path(self) -> None: u = URL("//host:/") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host == "host" assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_empty_path_only(self) -> None: u = URL("/") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "/" assert u.query_string == "" assert u.fragment == "" def test_relative_path_only(self) -> None: u = URL("path") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "path" assert u.query_string == "" assert u.fragment == "" def test_path(self) -> None: u = URL("/path") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "/path" assert u.query_string == "" assert u.fragment == "" def test_empty_query_with_path(self) -> None: u = URL("/path?") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "/path" assert u.query_string == "" assert u.fragment == "" def test_empty_query(self) -> None: u = URL("?") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "" assert u.query_string == "" assert u.fragment == "" def test_empty_query_with_frag(self) -> None: u = URL("?#frag") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "" assert u.query_string == "" assert u.fragment == "frag" def test_path_empty_frag(self) -> None: u = URL("/path#") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "/path" assert u.query_string == "" assert u.fragment == "" def test_empty_path(self) -> None: u = URL("#") assert u.scheme == "" assert u.user is None assert u.password is None assert u.host is None assert u.path == "" assert u.query_string == "" assert u.fragment == "" @pytest.mark.parametrize( ("scheme"), [ ("http"), ("https"), ("ws"), ("wss"), ("ftp"), ], ) def test_schemes_that_require_host(scheme: str) -> None: """Verify that schemes that require a host raise with empty host.""" expect = ( "Invalid URL: host is required for " f"absolute urls with the {scheme} scheme" ) with pytest.raises(ValueError, match=expect): URL(f"{scheme}://:1") @pytest.mark.parametrize( ("url", "hostname", "hostname_without_brackets"), [ ("http://[::1]", "[::1]", "::1"), ("http://[::1]:8080", "[::1]", "::1"), ("http://127.0.0.1:8080", "127.0.0.1", "127.0.0.1"), ( "http://xn--jxagkqfkduily1i.eu", "xn--jxagkqfkduily1i.eu", "xn--jxagkqfkduily1i.eu", ), ], ) def test_url_round_trips( url: str, hostname: str, hostname_without_brackets: str ) -> None: """Verify that URLs round-trip correctly.""" parsed = URL(url) assert SplitResult(*parsed._val).hostname == hostname_without_brackets assert parsed.raw_host == hostname_without_brackets assert parsed.host_subcomponent == hostname assert str(parsed) == url assert str(URL(str(parsed))) == url ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_url_query.py0000644000175100001660000001675014774356277017015 0ustar00runnerdockerfrom collections.abc import Sequence from urllib.parse import parse_qs, urlencode import pytest from multidict import MultiDict, MultiDictProxy from yarl import URL # ======================================== # Basic chars in query values # ======================================== URLS_WITH_BASIC_QUERY_VALUES: list[tuple[URL, MultiDict[str]]] = [ # Empty strings, keys and values ( URL("http://example.com"), MultiDict(), ), ( URL("http://example.com?a="), MultiDict([("a", "")]), ), # ASCII chars ( URL("http://example.com?a+b=c+d"), MultiDict({"a b": "c d"}), ), ( URL("http://example.com?a=1&b=2"), MultiDict([("a", "1"), ("b", "2")]), ), ( URL("http://example.com?a=1&b=2&a=3"), MultiDict([("a", "1"), ("b", "2"), ("a", "3")]), ), # Non-ASCI BMP chars ( URL("http://example.com?ключ=знач"), MultiDict({"ключ": "знач"}), ), ( URL("http://example.com?foo=ᴜɴɪᴄᴏᴅᴇ"), MultiDict({"foo": "ᴜɴɪᴄᴏᴅᴇ"}), ), # Non-BMP chars ( URL("http://example.com?bar=𝕦𝕟𝕚𝕔𝕠𝕕𝕖"), MultiDict({"bar": "𝕦𝕟𝕚𝕔𝕠𝕕𝕖"}), ), ] @pytest.mark.parametrize( "original_url, expected_query", URLS_WITH_BASIC_QUERY_VALUES, ) def test_query_basic_parsing(original_url: URL, expected_query: MultiDict[str]) -> None: assert isinstance(original_url.query, MultiDictProxy) assert original_url.query == expected_query @pytest.mark.parametrize( "original_url, expected_query", URLS_WITH_BASIC_QUERY_VALUES, ) def test_query_basic_update_query( original_url: URL, expected_query: MultiDict[str] ) -> None: new_url = original_url.update_query({}) assert new_url == original_url def test_query_dont_unqoute_twice() -> None: sample_url = "http://base.place?" + urlencode({"a": "/////"}) query = urlencode({"url": sample_url}) full_url = "http://test_url.aha?" + query url = URL(full_url) assert url.query["url"] == sample_url # ======================================== # Reserved chars in query values # ======================================== # See https://github.com/python/cpython#87133, which introduced a new # `separator` keyword argument to `urllib.parse.parse_qs` (among others). # If the name doesn't exist as a variable in the function bytecode, the # test is expected to fail. _SEMICOLON_XFAIL = pytest.mark.xfail( condition="separator" not in parse_qs.__code__.co_varnames, reason=( "Python versions < 3.9.2 lack a fix for " 'CVE-2021-23336 dropping ";" as a valid query parameter separator, ' "making this test fail." ), strict=True, ) URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES = [ # Ampersand (URL("http://127.0.0.1/?a=10&b=20"), 2, "10"), (URL("http://127.0.0.1/?a=10%26b=20"), 1, "10&b=20"), (URL("http://127.0.0.1/?a=10%3Bb=20"), 1, "10;b=20"), # Semicolon, which is *not* a query parameter separator as of RFC3986 (URL("http://127.0.0.1/?a=10;b=20"), 1, "10;b=20"), (URL("http://127.0.0.1/?a=10%26b=20"), 1, "10&b=20"), (URL("http://127.0.0.1/?a=10%3Bb=20"), 1, "10;b=20"), ] URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES_W_XFAIL = [ # Ampersand *URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES[:3], # Semicolon, which is *not* a query parameter separator as of RFC3986 # Mark the first of these as expecting to fail on old Python patch releases. pytest.param(*URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES[3], marks=_SEMICOLON_XFAIL), *URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES[4:], ] @pytest.mark.parametrize( "original_url, expected_query_len, expected_value_a", URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES_W_XFAIL, ) def test_query_separators_from_parsing( original_url: URL, expected_query_len: int, expected_value_a: str, ) -> None: assert len(original_url.query) == expected_query_len assert original_url.query["a"] == expected_value_a @pytest.mark.parametrize( "original_url, expected_query_len, expected_value_a", URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES_W_XFAIL, ) def test_query_separators_from_update_query( original_url: URL, expected_query_len: int, expected_value_a: str, ) -> None: new_url = original_url.update_query({"c": expected_value_a}) assert new_url.query["a"] == expected_value_a assert new_url.query["c"] == expected_value_a @pytest.mark.parametrize( "original_url, expected_query_len, expected_value_a", URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES, ) def test_query_separators_from_with_query( original_url: URL, expected_query_len: int, expected_value_a: str, ) -> None: new_url = original_url.with_query({"c": expected_value_a}) assert new_url.query["c"] == expected_value_a @pytest.mark.parametrize( "original_url, expected_query_len, expected_value_a", URLS_WITH_RESERVED_CHARS_IN_QUERY_VALUES, ) def test_query_from_empty_update_query( original_url: URL, expected_query_len: int, expected_value_a: str, ) -> None: new_url = original_url.update_query({}) assert new_url.query["a"] == original_url.query["a"] if "b" in original_url.query: assert new_url.query["b"] == original_url.query["b"] @pytest.mark.parametrize( ("original_query_string", "keys_to_drop", "expected_query_string"), [ ("a=10&b=M%C3%B9a+xu%C3%A2n&u%E1%BB%91ng=cafe", ["a"], "b=Mùa xuân&uống=cafe"), ("a=10&b=M%C3%B9a+xu%C3%A2n", ["b"], "a=10"), ("a=10&b=M%C3%B9a+xu%C3%A2n&c=30", ["b"], "a=10&c=30"), ( "a=10&b=M%C3%B9a+xu%C3%A2n&u%E1%BB%91ng=cafe", ["uống"], "a=10&b=Mùa xuân", ), ("a=10&b=M%C3%B9a+xu%C3%A2n", ["a", "b"], ""), ], ) def test_without_query_params( original_query_string: str, keys_to_drop: Sequence[str], expected_query_string: str ) -> None: url = URL(f"http://example.com?{original_query_string}") new_url = url.without_query_params(*keys_to_drop) assert new_url.query_string == expected_query_string assert new_url is not url @pytest.mark.parametrize( ("original_query_string", "keys_to_drop"), [ ("a=10&b=M%C3%B9a+xu%C3%A2n&c=30", ["invalid_key"]), ("a=10&b=M%C3%B9a+xu%C3%A2n", []), ], ) def test_skip_dropping_query_params( original_query_string: str, keys_to_drop: Sequence[str] ) -> None: url = URL(f"http://example.com?{original_query_string}") new_url = url.without_query_params(*keys_to_drop) assert new_url is url def test_update_query_rejects_bytes() -> None: url = URL("http://example.com") with pytest.raises(TypeError): url.update_query(b"foo=bar") # type: ignore[arg-type] def test_update_query_rejects_bytearray() -> None: url = URL("http://example.com") with pytest.raises(TypeError): url.update_query(bytearray(b"foo=bar")) # type: ignore[arg-type] def test_update_query_rejects_memoryview() -> None: url = URL("http://example.com") with pytest.raises(TypeError): url.update_query(memoryview(b"foo=bar")) def test_update_query_rejects_invalid_type() -> None: url = URL("http://example.com") with pytest.raises(TypeError): url.update_query(42) # type: ignore[call-overload] def test_update_query_with_sequence_of_pairs() -> None: url = URL("http://example.com") new_url = url.update_query([("a", "1"), ("b", "2")]) assert new_url.query == MultiDict([("a", "1"), ("b", "2")]) assert new_url.query_string == "a=1&b=2" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/tests/test_url_update_netloc.py0000644000175100001660000002174514774356277020476 0ustar00runnerdockerimport pytest from yarl import URL # with_* def test_with_scheme() -> None: url = URL("http://example.com") assert str(url.with_scheme("https")) == "https://example.com" def test_with_scheme_uppercased() -> None: url = URL("http://example.com") assert str(url.with_scheme("HTTPS")) == "https://example.com" @pytest.mark.parametrize( ("scheme"), [ ("http"), ("https"), ("HTTP"), ], ) def test_with_scheme_for_relative_url(scheme: str) -> None: """Test scheme can be set for relative URL.""" lower_scheme = scheme.lower() msg = ( "scheme replacement is not allowed for " f"relative URLs for the {lower_scheme} scheme" ) with pytest.raises(ValueError, match=msg): assert URL("path/to").with_scheme(scheme) def test_with_scheme_for_relative_file_url() -> None: """Test scheme can be set for relative file URL.""" expected = URL("file:///absolute/path") assert expected.with_scheme("file") == expected def test_with_scheme_invalid_type() -> None: url = URL("http://example.com") with pytest.raises(TypeError): assert str(url.with_scheme(123)) # type: ignore[arg-type] def test_with_user() -> None: url = URL("http://example.com") assert str(url.with_user("john")) == "http://john@example.com" def test_with_user_non_ascii() -> None: url = URL("http://example.com") url2 = url.with_user("бажан") assert url2.raw_user == "%D0%B1%D0%B0%D0%B6%D0%B0%D0%BD" assert url2.user == "бажан" assert url2.raw_authority == "%D0%B1%D0%B0%D0%B6%D0%B0%D0%BD@example.com" assert url2.authority == "бажан@example.com:80" def test_with_user_percent_encoded() -> None: url = URL("http://example.com") url2 = url.with_user("%cf%80") assert url2.raw_user == "%25cf%2580" assert url2.user == "%cf%80" assert url2.raw_authority == "%25cf%2580@example.com" assert url2.authority == "%cf%80@example.com:80" def test_with_user_for_relative_url() -> None: with pytest.raises(ValueError): URL("path/to").with_user("user") def test_with_user_invalid_type() -> None: url = URL("http://example.com:123") with pytest.raises(TypeError): url.with_user(123) # type: ignore[arg-type] def test_with_user_None() -> None: url = URL("http://john@example.com") assert str(url.with_user(None)) == "http://example.com" def test_with_user_ipv6() -> None: url = URL("http://john:pass@[::1]:8080/") assert str(url.with_user(None)) == "http://[::1]:8080/" def test_with_user_None_when_password_present() -> None: url = URL("http://john:pass@example.com") assert str(url.with_user(None)) == "http://example.com" def test_with_password() -> None: url = URL("http://john@example.com") assert str(url.with_password("pass")) == "http://john:pass@example.com" def test_with_password_ipv6() -> None: url = URL("http://john:pass@[::1]:8080/") assert str(url.with_password(None)) == "http://john@[::1]:8080/" def test_with_password_non_ascii() -> None: url = URL("http://john@example.com") url2 = url.with_password("пароль") assert url2.raw_password == "%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C" assert url2.password == "пароль" assert url2.raw_authority == "john:%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C@example.com" assert url2.authority == "john:пароль@example.com:80" def test_with_password_percent_encoded() -> None: url = URL("http://john@example.com") url2 = url.with_password("%cf%80") assert url2.raw_password == "%25cf%2580" assert url2.password == "%cf%80" assert url2.raw_authority == "john:%25cf%2580@example.com" assert url2.authority == "john:%cf%80@example.com:80" def test_with_password_non_ascii_with_colon() -> None: url = URL("http://john@example.com") url2 = url.with_password("п:а") assert url2.raw_password == "%D0%BF%3A%D0%B0" assert url2.password == "п:а" def test_with_password_for_relative_url() -> None: with pytest.raises(ValueError): URL("path/to").with_password("pass") def test_with_password_None() -> None: url = URL("http://john:pass@example.com") assert str(url.with_password(None)) == "http://john@example.com" def test_with_password_invalid_type() -> None: url = URL("http://example.com:123") with pytest.raises(TypeError): url.with_password(123) # type: ignore[arg-type] def test_with_password_and_empty_user() -> None: url = URL("http://example.com") url2 = url.with_password("pass") assert url2.password == "pass" assert url2.user is None assert str(url2) == "http://:pass@example.com" def test_from_str_with_host_ipv4() -> None: url = URL("http://host:80") url = url.with_host("192.168.1.1") assert url.raw_host == "192.168.1.1" def test_from_str_with_host_ipv6() -> None: url = URL("http://host:80") url = url.with_host("::1") assert url.raw_host == "::1" def test_with_host() -> None: url = URL("http://example.com:123") assert str(url.with_host("example.org")) == "http://example.org:123" def test_with_host_empty() -> None: url = URL("http://example.com:123") with pytest.raises(ValueError): url.with_host("") def test_with_host_non_ascii() -> None: url = URL("http://example.com:123") url2 = url.with_host("оун-упа.укр") assert url2.raw_host == "xn----8sb1bdhvc.xn--j1amh" assert url2.host == "оун-упа.укр" assert url2.raw_authority == "xn----8sb1bdhvc.xn--j1amh:123" assert url2.authority == "оун-упа.укр:123" @pytest.mark.parametrize( ("host", "is_authority"), [ ("user:pass@host.com", True), ("user@host.com", True), ("host:com", False), ("not_percent_encoded%Zf", False), ("still_not_percent_encoded%fZ", False), *(("other_gen_delim_" + c, False) for c in "/?#[]"), ], ) def test_with_invalid_host(host: str, is_authority: bool) -> None: url = URL("http://example.com:123") match = r"Host '[^']+' cannot contain '[^']+' \(at position \d+\)" if is_authority: match += ", if .* use 'authority' instead of 'host'" with pytest.raises(ValueError, match=f"{match}$"): url.with_host(host=host) def test_with_host_percent_encoded() -> None: url = URL("http://%25cf%2580%cf%80:%25cf%2580%cf%80@example.com:123") url2 = url.with_host("%cf%80.org") assert url2.raw_host == "%cf%80.org" assert url2.host == "%cf%80.org" assert url2.raw_authority == "%25cf%2580%CF%80:%25cf%2580%CF%80@%cf%80.org:123" assert url2.authority == "%cf%80π:%cf%80π@%cf%80.org:123" def test_with_host_for_relative_url() -> None: with pytest.raises(ValueError): URL("path/to").with_host("example.com") def test_with_host_invalid_type() -> None: url = URL("http://example.com:123") with pytest.raises(TypeError): url.with_host(None) # type: ignore[arg-type] def test_with_port() -> None: url = URL("http://example.com") assert str(url.with_port(8888)) == "http://example.com:8888" def test_with_default_port_normalization() -> None: url = URL("http://example.com") assert str(url.with_scheme("https")) == "https://example.com" assert str(url.with_scheme("https").with_port(443)) == "https://example.com" assert str(url.with_port(443).with_scheme("https")) == "https://example.com" def test_with_custom_port_normalization() -> None: url = URL("http://example.com") u88 = url.with_port(88) assert str(u88) == "http://example.com:88" assert str(u88.with_port(80)) == "http://example.com" assert str(u88.with_scheme("https")) == "https://example.com:88" def test_with_explicit_port_normalization() -> None: url = URL("http://example.com") u80 = url.with_port(80) assert str(u80) == "http://example.com" assert str(u80.with_port(81)) == "http://example.com:81" assert str(u80.with_scheme("https")) == "https://example.com:80" def test_with_port_with_no_port() -> None: url = URL("http://example.com") assert str(url.with_port(None)) == "http://example.com" def test_with_port_ipv6() -> None: url = URL("http://[::1]:8080/") assert str(url.with_port(81)) == "http://[::1]:81/" def test_with_port_keeps_query_and_fragment() -> None: url = URL("http://example.com/?a=1#frag") assert str(url.with_port(8888)) == "http://example.com:8888/?a=1#frag" def test_with_port_percent_encoded() -> None: url = URL("http://user%name:pass%word@example.com/") assert str(url.with_port(808)) == "http://user%25name:pass%25word@example.com:808/" def test_with_port_for_relative_url() -> None: with pytest.raises(ValueError): URL("path/to").with_port(1234) def test_with_port_invalid_type() -> None: with pytest.raises(TypeError): URL("http://example.com").with_port("123") # type: ignore[arg-type] with pytest.raises(TypeError): URL("http://example.com").with_port(True) def test_with_port_invalid_range() -> None: with pytest.raises(ValueError): URL("http://example.com").with_port(-1) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/towncrier.toml0000644000175100001660000000427214774356277015120 0ustar00runnerdocker[tool.towncrier] package = "yarl" filename = "CHANGES.rst" directory = "CHANGES/" title_format = "v{version}" template = "CHANGES/.TEMPLATE.rst" issue_format = "{issue}" # NOTE: The types are declared because: # NOTE: - there is no mechanism to override just the value of # NOTE: `tool.towncrier.type.misc.showcontent`; # NOTE: - and, we want to declare extra non-default types for # NOTE: clarity and flexibility. [[tool.towncrier.section]] path = "" [[tool.towncrier.type]] # Something we deemed an improper undesired behavior that got corrected # in the release to match pre-agreed expectations. directory = "bugfix" name = "Bug fixes" showcontent = true [[tool.towncrier.type]] # New behaviors, public APIs. That sort of stuff. directory = "feature" name = "Features" showcontent = true [[tool.towncrier.type]] # Declarations of future API removals and breaking changes in behavior. directory = "deprecation" name = "Deprecations (removal in next major release)" showcontent = true [[tool.towncrier.type]] # When something public gets removed in a breaking way. Could be # deprecated in an earlier release. directory = "breaking" name = "Removals and backward incompatible breaking changes" showcontent = true [[tool.towncrier.type]] # Notable updates to the documentation structure or build process. directory = "doc" name = "Improved documentation" showcontent = true [[tool.towncrier.type]] # Notes for downstreams about unobvious side effects and tooling. Changes # in the test invocation considerations and runtime assumptions. directory = "packaging" name = "Packaging updates and notes for downstreams" showcontent = true [[tool.towncrier.type]] # Stuff that affects the contributor experience. e.g. Running tests, # building the docs, setting up the development environment. directory = "contrib" name = "Contributor-facing changes" showcontent = true [[tool.towncrier.type]] # Changes that are hard to assign to any of the above categories. directory = "misc" name = "Miscellaneous internal changes" showcontent = true ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743903941.5922763 yarl-1.19.0/yarl/0000755000175100001660000000000014774356306013142 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/yarl/__init__.py0000644000175100001660000000043114774356277015260 0ustar00runnerdockerfrom ._query import Query, QueryVariable, SimpleQuery from ._url import URL, cache_clear, cache_configure, cache_info __version__ = "1.19.0" __all__ = ( "URL", "SimpleQuery", "QueryVariable", "Query", "cache_clear", "cache_configure", "cache_info", ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/yarl/_parse.py0000644000175100001660000001576514774356277015012 0ustar00runnerdocker"""URL parsing utilities.""" import re import unicodedata from functools import lru_cache from typing import Union from urllib.parse import scheme_chars, uses_netloc from ._quoters import QUOTER, UNQUOTER_PLUS # Leading and trailing C0 control and space to be stripped per WHATWG spec. # == "".join([chr(i) for i in range(0, 0x20 + 1)]) WHATWG_C0_CONTROL_OR_SPACE = ( "\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 " ) # Unsafe bytes to be removed per WHATWG spec UNSAFE_URL_BYTES_TO_REMOVE = ["\t", "\r", "\n"] USES_AUTHORITY = frozenset(uses_netloc) SplitURLType = tuple[str, str, str, str, str] def split_url(url: str) -> SplitURLType: """Split URL into parts.""" # Adapted from urllib.parse.urlsplit # Only lstrip url as some applications rely on preserving trailing space. # (https://url.spec.whatwg.org/#concept-basic-url-parser would strip both) url = url.lstrip(WHATWG_C0_CONTROL_OR_SPACE) for b in UNSAFE_URL_BYTES_TO_REMOVE: if b in url: url = url.replace(b, "") scheme = netloc = query = fragment = "" i = url.find(":") if i > 0 and url[0] in scheme_chars: for c in url[1:i]: if c not in scheme_chars: break else: scheme, url = url[:i].lower(), url[i + 1 :] has_hash = "#" in url has_question_mark = "?" in url if url[:2] == "//": delim = len(url) # position of end of domain part of url, default is end if has_hash and has_question_mark: delim_chars = "/?#" elif has_question_mark: delim_chars = "/?" elif has_hash: delim_chars = "/#" else: delim_chars = "/" for c in delim_chars: # look for delimiters; the order is NOT important wdelim = url.find(c, 2) # find first of this delim if wdelim >= 0 and wdelim < delim: # if found delim = wdelim # use earliest delim position netloc = url[2:delim] url = url[delim:] has_left_bracket = "[" in netloc has_right_bracket = "]" in netloc if (has_left_bracket and not has_right_bracket) or ( has_right_bracket and not has_left_bracket ): raise ValueError("Invalid IPv6 URL") if has_left_bracket: bracketed_host = netloc.partition("[")[2].partition("]")[0] # Valid bracketed hosts are defined in # https://www.rfc-editor.org/rfc/rfc3986#page-49 # https://url.spec.whatwg.org/ if bracketed_host[0] == "v": if not re.match(r"\Av[a-fA-F0-9]+\..+\Z", bracketed_host): raise ValueError("IPvFuture address is invalid") elif ":" not in bracketed_host: raise ValueError("An IPv4 address cannot be in brackets") if has_hash: url, _, fragment = url.partition("#") if has_question_mark: url, _, query = url.partition("?") if netloc and not netloc.isascii(): _check_netloc(netloc) return scheme, netloc, url, query, fragment def _check_netloc(netloc: str) -> None: # Adapted from urllib.parse._checknetloc # looking for characters like \u2100 that expand to 'a/c' # IDNA uses NFKC equivalence, so normalize for this check # ignore characters already included # but not the surrounding text n = netloc.replace("@", "").replace(":", "").replace("#", "").replace("?", "") normalized_netloc = unicodedata.normalize("NFKC", n) if n == normalized_netloc: return # Note that there are no unicode decompositions for the character '@' so # its currently impossible to have test coverage for this branch, however if the # one should be added in the future we want to make sure its still checked. for c in "/?#@:": # pragma: no branch if c in normalized_netloc: raise ValueError( f"netloc '{netloc}' contains invalid " "characters under NFKC normalization" ) @lru_cache # match the same size as urlsplit def split_netloc( netloc: str, ) -> tuple[Union[str, None], Union[str, None], Union[str, None], Union[int, None]]: """Split netloc into username, password, host and port.""" if "@" not in netloc: username: Union[str, None] = None password: Union[str, None] = None hostinfo = netloc else: userinfo, _, hostinfo = netloc.rpartition("@") username, have_password, password = userinfo.partition(":") if not have_password: password = None if "[" in hostinfo: _, _, bracketed = hostinfo.partition("[") hostname, _, port_str = bracketed.partition("]") _, _, port_str = port_str.partition(":") else: hostname, _, port_str = hostinfo.partition(":") if not port_str: return username or None, password, hostname or None, None try: port = int(port_str) except ValueError: raise ValueError("Invalid URL: port can't be converted to integer") if not (0 <= port <= 65535): raise ValueError("Port out of range 0-65535") return username or None, password, hostname or None, port def unsplit_result( scheme: str, netloc: str, url: str, query: str, fragment: str ) -> str: """Unsplit a URL without any normalization.""" if netloc or (scheme and scheme in USES_AUTHORITY) or url[:2] == "//": if url and url[:1] != "/": url = f"{scheme}://{netloc}/{url}" if scheme else f"{scheme}:{url}" else: url = f"{scheme}://{netloc}{url}" if scheme else f"//{netloc}{url}" elif scheme: url = f"{scheme}:{url}" if query: url = f"{url}?{query}" return f"{url}#{fragment}" if fragment else url @lru_cache # match the same size as urlsplit def make_netloc( user: Union[str, None], password: Union[str, None], host: Union[str, None], port: Union[int, None], encode: bool = False, ) -> str: """Make netloc from parts. The user and password are encoded if encode is True. The host must already be encoded with _encode_host. """ if host is None: return "" ret = host if port is not None: ret = f"{ret}:{port}" if user is None and password is None: return ret if password is not None: if not user: user = "" elif encode: user = QUOTER(user) if encode: password = QUOTER(password) user = f"{user}:{password}" elif user and encode: user = QUOTER(user) return f"{user}@{ret}" if user else ret def query_to_pairs(query_string: str) -> list[tuple[str, str]]: """Parse a query given as a string argument. Works like urllib.parse.parse_qsl with keep empty values. """ pairs: list[tuple[str, str]] = [] if not query_string: return pairs for k_v in query_string.split("&"): k, _, v = k_v.partition("=") pairs.append((UNQUOTER_PLUS(k), UNQUOTER_PLUS(v))) return pairs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/yarl/_path.py0000644000175100001660000000241314774356277014616 0ustar00runnerdocker"""Utilities for working with paths.""" from collections.abc import Sequence from contextlib import suppress def normalize_path_segments(segments: Sequence[str]) -> list[str]: """Drop '.' and '..' from a sequence of str segments""" resolved_path: list[str] = [] for seg in segments: if seg == "..": # ignore any .. segments that would otherwise cause an # IndexError when popped from resolved_path if # resolving for rfc3986 with suppress(IndexError): resolved_path.pop() elif seg != ".": resolved_path.append(seg) if segments and segments[-1] in (".", ".."): # do some post-processing here. # if the last segment was a relative dir, # then we need to append the trailing '/' resolved_path.append("") return resolved_path def normalize_path(path: str) -> str: # Drop '.' and '..' from str path prefix = "" if path and path[0] == "/": # preserve the "/" root element of absolute paths, copying it to the # normalised output as per sections 5.2.4 and 6.2.2.3 of rfc3986. prefix = "/" path = path[1:] segments = path.split("/") return prefix + "/".join(normalize_path_segments(segments)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/yarl/_query.py0000644000175100001660000000745214774356277015037 0ustar00runnerdocker"""Query string handling.""" import math from collections.abc import Iterable, Mapping, Sequence from typing import Any, SupportsInt, Union from multidict import istr from ._quoters import QUERY_PART_QUOTER, QUERY_QUOTER SimpleQuery = Union[str, SupportsInt, float] QueryVariable = Union[SimpleQuery, Sequence[SimpleQuery]] Query = Union[ None, str, Mapping[str, QueryVariable], Sequence[tuple[str, QueryVariable]] ] def query_var(v: SimpleQuery) -> str: """Convert a query variable to a string.""" cls = type(v) if cls is int: # Fast path for non-subclassed int return str(v) if isinstance(v, str): return v if isinstance(v, float): if math.isinf(v): raise ValueError("float('inf') is not supported") if math.isnan(v): raise ValueError("float('nan') is not supported") return str(float(v)) if cls is not bool and isinstance(v, SupportsInt): return str(int(v)) raise TypeError( "Invalid variable type: value " "should be str, int or float, got {!r} " "of type {}".format(v, cls) ) def get_str_query_from_sequence_iterable( items: Iterable[tuple[Union[str, istr], QueryVariable]], ) -> str: """Return a query string from a sequence of (key, value) pairs. value is a single value or a sequence of values for the key The sequence of values must be a list or tuple. """ quoter = QUERY_PART_QUOTER pairs = [ f"{quoter(k)}={quoter(v if type(v) is str else query_var(v))}" for k, val in items for v in ( val if type(val) is not str and isinstance(val, (list, tuple)) else (val,) ) ] return "&".join(pairs) def get_str_query_from_iterable( items: Iterable[tuple[Union[str, istr], SimpleQuery]] ) -> str: """Return a query string from an iterable. The iterable must contain (key, value) pairs. The values are not allowed to be sequences, only single values are allowed. For sequences, use `_get_str_query_from_sequence_iterable`. """ quoter = QUERY_PART_QUOTER # A listcomp is used since listcomps are inlined on CPython 3.12+ and # they are a bit faster than a generator expression. pairs = [ f"{quoter(k)}={quoter(v if type(v) is str else query_var(v))}" for k, v in items ] return "&".join(pairs) def get_str_query(*args: Any, **kwargs: Any) -> Union[str, None]: """Return a query string from supported args.""" query: Union[str, Mapping[str, QueryVariable], None] if kwargs: if args: msg = "Either kwargs or single query parameter must be present" raise ValueError(msg) query = kwargs elif len(args) == 1: query = args[0] else: raise ValueError("Either kwargs or single query parameter must be present") if query is None: return None if not query: return "" if type(query) is dict: return get_str_query_from_sequence_iterable(query.items()) if type(query) is str or isinstance(query, str): return QUERY_QUOTER(query) if isinstance(query, Mapping): return get_str_query_from_sequence_iterable(query.items()) if isinstance(query, (bytes, bytearray, memoryview)): # type: ignore[unreachable] msg = "Invalid query type: bytes, bytearray and memoryview are forbidden" raise TypeError(msg) if isinstance(query, Sequence): # We don't expect sequence values if we're given a list of pairs # already; only mappings like builtin `dict` which can't have the # same key pointing to multiple values are allowed to use # `_query_seq_pairs`. return get_str_query_from_iterable(query) raise TypeError( "Invalid query type: only str, mapping or " "sequence of (key, value) pairs is allowed" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/yarl/_quoters.py0000644000175100001660000000220214774356277015360 0ustar00runnerdocker"""Quoting and unquoting utilities for URL parts.""" from typing import Union from urllib.parse import quote from ._quoting import _Quoter, _Unquoter QUOTER = _Quoter(requote=False) REQUOTER = _Quoter() PATH_QUOTER = _Quoter(safe="@:", protected="/+", requote=False) PATH_REQUOTER = _Quoter(safe="@:", protected="/+") QUERY_QUOTER = _Quoter(safe="?/:@", protected="=+&;", qs=True, requote=False) QUERY_REQUOTER = _Quoter(safe="?/:@", protected="=+&;", qs=True) QUERY_PART_QUOTER = _Quoter(safe="?/:@", qs=True, requote=False) FRAGMENT_QUOTER = _Quoter(safe="?/:@", requote=False) FRAGMENT_REQUOTER = _Quoter(safe="?/:@") UNQUOTER = _Unquoter() PATH_UNQUOTER = _Unquoter(unsafe="+") PATH_SAFE_UNQUOTER = _Unquoter(ignore="/%", unsafe="+") QS_UNQUOTER = _Unquoter(qs=True) UNQUOTER_PLUS = _Unquoter(plus=True) # to match urllib.parse.unquote_plus def human_quote(s: Union[str, None], unsafe: str) -> Union[str, None]: if not s: return s for c in "%" + unsafe: if c in s: s = s.replace(c, f"%{ord(c):02X}") if s.isprintable(): return s return "".join(c if c.isprintable() else quote(c) for c in s) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/yarl/_quoting.py0000644000175100001660000000077214774356277015356 0ustar00runnerdockerimport os import sys from typing import TYPE_CHECKING __all__ = ("_Quoter", "_Unquoter") NO_EXTENSIONS = bool(os.environ.get("YARL_NO_EXTENSIONS")) # type: bool if sys.implementation.name != "cpython": NO_EXTENSIONS = True if TYPE_CHECKING or NO_EXTENSIONS: from ._quoting_py import _Quoter, _Unquoter else: try: from ._quoting_c import _Quoter, _Unquoter except ImportError: # pragma: no cover from ._quoting_py import _Quoter, _Unquoter # type: ignore[assignment] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/yarl/_quoting_c.pyx0000644000175100001660000003342214774356277016046 0ustar00runnerdocker# cython: language_level=3 from cpython.exc cimport PyErr_NoMemory from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc from cpython.unicode cimport ( PyUnicode_DATA, PyUnicode_DecodeASCII, PyUnicode_DecodeUTF8Stateful, PyUnicode_GET_LENGTH, PyUnicode_KIND, PyUnicode_READ, ) from libc.stdint cimport uint8_t, uint64_t from libc.string cimport memcpy, memset from string import ascii_letters, digits cdef str GEN_DELIMS = ":/?#[]@" cdef str SUB_DELIMS_WITHOUT_QS = "!$'()*," cdef str SUB_DELIMS = SUB_DELIMS_WITHOUT_QS + '+?=;' cdef str RESERVED = GEN_DELIMS + SUB_DELIMS cdef str UNRESERVED = ascii_letters + digits + '-._~' cdef str ALLOWED = UNRESERVED + SUB_DELIMS_WITHOUT_QS cdef str QS = '+&=;' DEF BUF_SIZE = 8 * 1024 # 8KiB cdef char BUFFER[BUF_SIZE] cdef inline Py_UCS4 _to_hex(uint8_t v) noexcept: if v < 10: return (v+0x30) # ord('0') == 0x30 else: return (v+0x41-10) # ord('A') == 0x41 cdef inline int _from_hex(Py_UCS4 v) noexcept: if '0' <= v <= '9': return (v) - 0x30 # ord('0') == 0x30 elif 'A' <= v <= 'F': return (v) - 0x41 + 10 # ord('A') == 0x41 elif 'a' <= v <= 'f': return (v) - 0x61 + 10 # ord('a') == 0x61 else: return -1 cdef inline int _is_lower_hex(Py_UCS4 v) noexcept: return 'a' <= v <= 'f' cdef inline Py_UCS4 _restore_ch(Py_UCS4 d1, Py_UCS4 d2): cdef int digit1 = _from_hex(d1) if digit1 < 0: return -1 cdef int digit2 = _from_hex(d2) if digit2 < 0: return -1 return (digit1 << 4 | digit2) cdef uint8_t ALLOWED_TABLE[16] cdef uint8_t ALLOWED_NOTQS_TABLE[16] cdef inline bint bit_at(uint8_t array[], uint64_t ch) noexcept: return array[ch >> 3] & (1 << (ch & 7)) cdef inline void set_bit(uint8_t array[], uint64_t ch) noexcept: array[ch >> 3] |= (1 << (ch & 7)) memset(ALLOWED_TABLE, 0, sizeof(ALLOWED_TABLE)) memset(ALLOWED_NOTQS_TABLE, 0, sizeof(ALLOWED_NOTQS_TABLE)) for i in range(128): if chr(i) in ALLOWED: set_bit(ALLOWED_TABLE, i) set_bit(ALLOWED_NOTQS_TABLE, i) if chr(i) in QS: set_bit(ALLOWED_NOTQS_TABLE, i) # ----------------- writer --------------------------- cdef struct Writer: char *buf Py_ssize_t size Py_ssize_t pos bint changed cdef inline void _init_writer(Writer* writer): writer.buf = &BUFFER[0] writer.size = BUF_SIZE writer.pos = 0 writer.changed = 0 cdef inline void _release_writer(Writer* writer): if writer.buf != BUFFER: PyMem_Free(writer.buf) cdef inline int _write_char(Writer* writer, Py_UCS4 ch, bint changed): cdef char * buf cdef Py_ssize_t size if writer.pos == writer.size: # reallocate size = writer.size + BUF_SIZE if writer.buf == BUFFER: buf = PyMem_Malloc(size) if buf == NULL: PyErr_NoMemory() return -1 memcpy(buf, writer.buf, writer.size) else: buf = PyMem_Realloc(writer.buf, size) if buf == NULL: PyErr_NoMemory() return -1 writer.buf = buf writer.size = size writer.buf[writer.pos] = ch writer.pos += 1 writer.changed |= changed return 0 cdef inline int _write_pct(Writer* writer, uint8_t ch, bint changed): if _write_char(writer, '%', changed) < 0: return -1 if _write_char(writer, _to_hex(ch >> 4), changed) < 0: return -1 return _write_char(writer, _to_hex(ch & 0x0f), changed) cdef inline int _write_utf8(Writer* writer, Py_UCS4 symbol): cdef uint64_t utf = symbol if utf < 0x80: return _write_pct(writer, utf, True) elif utf < 0x800: if _write_pct(writer, (0xc0 | (utf >> 6)), True) < 0: return -1 return _write_pct(writer, (0x80 | (utf & 0x3f)), True) elif 0xD800 <= utf <= 0xDFFF: # surogate pair, ignored return 0 elif utf < 0x10000: if _write_pct(writer, (0xe0 | (utf >> 12)), True) < 0: return -1 if _write_pct(writer, (0x80 | ((utf >> 6) & 0x3f)), True) < 0: return -1 return _write_pct(writer, (0x80 | (utf & 0x3f)), True) elif utf > 0x10FFFF: # symbol is too large return 0 else: if _write_pct(writer, (0xf0 | (utf >> 18)), True) < 0: return -1 if _write_pct(writer, (0x80 | ((utf >> 12) & 0x3f)), True) < 0: return -1 if _write_pct(writer, (0x80 | ((utf >> 6) & 0x3f)), True) < 0: return -1 return _write_pct(writer, (0x80 | (utf & 0x3f)), True) # --------------------- end writer -------------------------- cdef class _Quoter: cdef bint _qs cdef bint _requote cdef uint8_t _safe_table[16] cdef uint8_t _protected_table[16] def __init__( self, *, str safe='', str protected='', bint qs=False, bint requote=True, ): cdef Py_UCS4 ch self._qs = qs self._requote = requote if not self._qs: memcpy(self._safe_table, ALLOWED_NOTQS_TABLE, sizeof(self._safe_table)) else: memcpy(self._safe_table, ALLOWED_TABLE, sizeof(self._safe_table)) for ch in safe: if ord(ch) > 127: raise ValueError("Only safe symbols with ORD < 128 are allowed") set_bit(self._safe_table, ch) memset(self._protected_table, 0, sizeof(self._protected_table)) for ch in protected: if ord(ch) > 127: raise ValueError("Only safe symbols with ORD < 128 are allowed") set_bit(self._safe_table, ch) set_bit(self._protected_table, ch) def __call__(self, val): if val is None: return None if type(val) is not str: if isinstance(val, str): # derived from str val = str(val) else: raise TypeError("Argument should be str") return self._do_quote_or_skip(val) cdef str _do_quote_or_skip(self, str val): cdef Py_UCS4 ch cdef Py_ssize_t length = PyUnicode_GET_LENGTH(val) cdef Py_ssize_t idx = length cdef bint must_quote = 0 cdef Writer writer cdef int kind = PyUnicode_KIND(val) cdef const void *data = PyUnicode_DATA(val) # If everything in the string is in the safe # table and all ASCII, we can skip quoting while idx: idx -= 1 ch = PyUnicode_READ(kind, data, idx) if ch >= 128 or not bit_at(self._safe_table, ch): must_quote = 1 break if not must_quote: return val _init_writer(&writer) try: return self._do_quote(val, length, kind, data, &writer) finally: _release_writer(&writer) cdef str _do_quote( self, str val, Py_ssize_t length, int kind, const void *data, Writer *writer ): cdef Py_UCS4 ch cdef int changed cdef Py_ssize_t idx = 0 while idx < length: ch = PyUnicode_READ(kind, data, idx) idx += 1 if ch == '%' and self._requote and idx <= length - 2: ch = _restore_ch( PyUnicode_READ(kind, data, idx), PyUnicode_READ(kind, data, idx + 1) ) if ch != -1: idx += 2 if ch < 128: if bit_at(self._protected_table, ch): if _write_pct(writer, ch, True) < 0: raise continue if bit_at(self._safe_table, ch): if _write_char(writer, ch, True) < 0: raise continue changed = (_is_lower_hex(PyUnicode_READ(kind, data, idx - 2)) or _is_lower_hex(PyUnicode_READ(kind, data, idx - 1))) if _write_pct(writer, ch, changed) < 0: raise continue else: ch = '%' if self._write(writer, ch) < 0: raise if not writer.changed: return val else: return PyUnicode_DecodeASCII(writer.buf, writer.pos, "strict") cdef inline int _write(self, Writer *writer, Py_UCS4 ch): if self._qs: if ch == ' ': return _write_char(writer, '+', True) if ch < 128 and bit_at(self._safe_table, ch): return _write_char(writer, ch, False) return _write_utf8(writer, ch) cdef class _Unquoter: cdef str _ignore cdef bint _has_ignore cdef str _unsafe cdef bytes _unsafe_bytes cdef Py_ssize_t _unsafe_bytes_len cdef const unsigned char * _unsafe_bytes_char cdef bint _qs cdef bint _plus # to match urllib.parse.unquote_plus cdef _Quoter _quoter cdef _Quoter _qs_quoter def __init__(self, *, ignore="", unsafe="", qs=False, plus=False): self._ignore = ignore self._has_ignore = bool(self._ignore) self._unsafe = unsafe # unsafe may only be extended ascii characters (0-255) self._unsafe_bytes = self._unsafe.encode('ascii') self._unsafe_bytes_len = len(self._unsafe_bytes) self._unsafe_bytes_char = self._unsafe_bytes self._qs = qs self._plus = plus self._quoter = _Quoter() self._qs_quoter = _Quoter(qs=True) def __call__(self, val): if val is None: return None if type(val) is not str: if isinstance(val, str): # derived from str val = str(val) else: raise TypeError("Argument should be str") return self._do_unquote(val) cdef str _do_unquote(self, str val): cdef Py_ssize_t length = PyUnicode_GET_LENGTH(val) if length == 0: return val cdef list ret = [] cdef char buffer[4] cdef Py_ssize_t buflen = 0 cdef Py_ssize_t consumed cdef str unquoted cdef Py_UCS4 ch = 0 cdef Py_ssize_t idx = 0 cdef Py_ssize_t start_pct cdef int kind = PyUnicode_KIND(val) cdef const void *data = PyUnicode_DATA(val) cdef bint changed = 0 while idx < length: ch = PyUnicode_READ(kind, data, idx) idx += 1 if ch == '%' and idx <= length - 2: changed = 1 ch = _restore_ch( PyUnicode_READ(kind, data, idx), PyUnicode_READ(kind, data, idx + 1) ) if ch != -1: idx += 2 assert buflen < 4 buffer[buflen] = ch buflen += 1 try: unquoted = PyUnicode_DecodeUTF8Stateful(buffer, buflen, NULL, &consumed) except UnicodeDecodeError: start_pct = idx - buflen * 3 buffer[0] = ch buflen = 1 ret.append(val[start_pct : idx - 3]) try: unquoted = PyUnicode_DecodeUTF8Stateful(buffer, buflen, NULL, &consumed) except UnicodeDecodeError: buflen = 0 ret.append(val[idx - 3 : idx]) continue if not unquoted: assert consumed == 0 continue assert consumed == buflen buflen = 0 if self._qs and unquoted in '+=&;': ret.append(self._qs_quoter(unquoted)) elif ( (self._unsafe_bytes_len and unquoted in self._unsafe) or (self._has_ignore and unquoted in self._ignore) ): ret.append(self._quoter(unquoted)) else: ret.append(unquoted) continue else: ch = '%' if buflen: start_pct = idx - 1 - buflen * 3 ret.append(val[start_pct : idx - 1]) buflen = 0 if ch == '+': if ( (not self._qs and not self._plus) or (self._unsafe_bytes_len and self._is_char_unsafe(ch)) ): ret.append('+') else: changed = 1 ret.append(' ') continue if self._unsafe_bytes_len and self._is_char_unsafe(ch): changed = 1 ret.append('%') h = hex(ord(ch)).upper()[2:] for ch in h: ret.append(ch) continue ret.append(ch) if not changed: return val if buflen: ret.append(val[length - buflen * 3 : length]) return ''.join(ret) cdef inline bint _is_char_unsafe(self, Py_UCS4 ch): for i in range(self._unsafe_bytes_len): if ch == self._unsafe_bytes_char[i]: return True return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/yarl/_quoting_py.py0000644000175100001660000001525614774356277016071 0ustar00runnerdockerimport codecs import re from string import ascii_letters, ascii_lowercase, digits from typing import Union, cast, overload BASCII_LOWERCASE = ascii_lowercase.encode("ascii") BPCT_ALLOWED = {f"%{i:02X}".encode("ascii") for i in range(256)} GEN_DELIMS = ":/?#[]@" SUB_DELIMS_WITHOUT_QS = "!$'()*," SUB_DELIMS = SUB_DELIMS_WITHOUT_QS + "+&=;" RESERVED = GEN_DELIMS + SUB_DELIMS UNRESERVED = ascii_letters + digits + "-._~" ALLOWED = UNRESERVED + SUB_DELIMS_WITHOUT_QS _IS_HEX = re.compile(b"[A-Z0-9][A-Z0-9]") _IS_HEX_STR = re.compile("[A-Fa-f0-9][A-Fa-f0-9]") utf8_decoder = codecs.getincrementaldecoder("utf-8") class _Quoter: def __init__( self, *, safe: str = "", protected: str = "", qs: bool = False, requote: bool = True, ) -> None: self._safe = safe self._protected = protected self._qs = qs self._requote = requote @overload def __call__(self, val: str) -> str: ... @overload def __call__(self, val: None) -> None: ... def __call__(self, val: Union[str, None]) -> Union[str, None]: if val is None: return None if not isinstance(val, str): raise TypeError("Argument should be str") if not val: return "" bval = val.encode("utf8", errors="ignore") ret = bytearray() pct = bytearray() safe = self._safe safe += ALLOWED if not self._qs: safe += "+&=;" safe += self._protected bsafe = safe.encode("ascii") idx = 0 while idx < len(bval): ch = bval[idx] idx += 1 if pct: if ch in BASCII_LOWERCASE: ch = ch - 32 # convert to uppercase pct.append(ch) if len(pct) == 3: # pragma: no branch # peephole optimizer buf = pct[1:] if not _IS_HEX.match(buf): ret.extend(b"%25") pct.clear() idx -= 2 continue try: unquoted = chr(int(pct[1:].decode("ascii"), base=16)) except ValueError: ret.extend(b"%25") pct.clear() idx -= 2 continue if unquoted in self._protected: ret.extend(pct) elif unquoted in safe: ret.append(ord(unquoted)) else: ret.extend(pct) pct.clear() # special case, if we have only one char after "%" elif len(pct) == 2 and idx == len(bval): ret.extend(b"%25") pct.clear() idx -= 1 continue elif ch == ord("%") and self._requote: pct.clear() pct.append(ch) # special case if "%" is last char if idx == len(bval): ret.extend(b"%25") continue if self._qs and ch == ord(" "): ret.append(ord("+")) continue if ch in bsafe: ret.append(ch) continue ret.extend((f"%{ch:02X}").encode("ascii")) ret2 = ret.decode("ascii") if ret2 == val: return val return ret2 class _Unquoter: def __init__( self, *, ignore: str = "", unsafe: str = "", qs: bool = False, plus: bool = False, ) -> None: self._ignore = ignore self._unsafe = unsafe self._qs = qs self._plus = plus # to match urllib.parse.unquote_plus self._quoter = _Quoter() self._qs_quoter = _Quoter(qs=True) @overload def __call__(self, val: str) -> str: ... @overload def __call__(self, val: None) -> None: ... def __call__(self, val: Union[str, None]) -> Union[str, None]: if val is None: return None if not isinstance(val, str): raise TypeError("Argument should be str") if not val: return "" decoder = cast(codecs.BufferedIncrementalDecoder, utf8_decoder()) ret = [] idx = 0 while idx < len(val): ch = val[idx] idx += 1 if ch == "%" and idx <= len(val) - 2: pct = val[idx : idx + 2] if _IS_HEX_STR.fullmatch(pct): b = bytes([int(pct, base=16)]) idx += 2 try: unquoted = decoder.decode(b) except UnicodeDecodeError: start_pct = idx - 3 - len(decoder.buffer) * 3 ret.append(val[start_pct : idx - 3]) decoder.reset() try: unquoted = decoder.decode(b) except UnicodeDecodeError: ret.append(val[idx - 3 : idx]) continue if not unquoted: continue if self._qs and unquoted in "+=&;": to_add = self._qs_quoter(unquoted) if to_add is None: # pragma: no cover raise RuntimeError("Cannot quote None") ret.append(to_add) elif unquoted in self._unsafe or unquoted in self._ignore: to_add = self._quoter(unquoted) if to_add is None: # pragma: no cover raise RuntimeError("Cannot quote None") ret.append(to_add) else: ret.append(unquoted) continue if decoder.buffer: start_pct = idx - 1 - len(decoder.buffer) * 3 ret.append(val[start_pct : idx - 1]) decoder.reset() if ch == "+": if (not self._qs and not self._plus) or ch in self._unsafe: ret.append("+") else: ret.append(" ") continue if ch in self._unsafe: ret.append("%") h = hex(ord(ch)).upper()[2:] for ch in h: ret.append(ch) continue ret.append(ch) if decoder.buffer: ret.append(val[-len(decoder.buffer) * 3 :]) ret2 = "".join(ret) if ret2 == val: return val return ret2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/yarl/_url.py0000644000175100001660000015403314774356277014472 0ustar00runnerdockerimport re import sys import warnings from collections.abc import Mapping, Sequence from enum import Enum from functools import _CacheInfo, lru_cache from ipaddress import ip_address from typing import TYPE_CHECKING, Any, NoReturn, TypedDict, TypeVar, Union, overload from urllib.parse import SplitResult, uses_relative import idna from multidict import MultiDict, MultiDictProxy from propcache.api import under_cached_property as cached_property from ._parse import ( USES_AUTHORITY, SplitURLType, make_netloc, query_to_pairs, split_netloc, split_url, unsplit_result, ) from ._path import normalize_path, normalize_path_segments from ._query import ( Query, QueryVariable, SimpleQuery, get_str_query, get_str_query_from_iterable, get_str_query_from_sequence_iterable, ) from ._quoters import ( FRAGMENT_QUOTER, FRAGMENT_REQUOTER, PATH_QUOTER, PATH_REQUOTER, PATH_SAFE_UNQUOTER, PATH_UNQUOTER, QS_UNQUOTER, QUERY_QUOTER, QUERY_REQUOTER, QUOTER, REQUOTER, UNQUOTER, human_quote, ) DEFAULT_PORTS = {"http": 80, "https": 443, "ws": 80, "wss": 443, "ftp": 21} USES_RELATIVE = frozenset(uses_relative) # Special schemes https://url.spec.whatwg.org/#special-scheme # are not allowed to have an empty host https://url.spec.whatwg.org/#url-representation SCHEME_REQUIRES_HOST = frozenset(("http", "https", "ws", "wss", "ftp")) # reg-name: unreserved / pct-encoded / sub-delims # this pattern matches anything that is *not* in those classes. and is only used # on lower-cased ASCII values. NOT_REG_NAME = re.compile( r""" # any character not in the unreserved or sub-delims sets, plus % # (validated with the additional check for pct-encoded sequences below) [^a-z0-9\-._~!$&'()*+,;=%] | # % only allowed if it is part of a pct-encoded # sequence of 2 hex digits. %(?![0-9a-f]{2}) """, re.VERBOSE, ) _T = TypeVar("_T") if sys.version_info >= (3, 11): from typing import Self else: Self = Any class UndefinedType(Enum): """Singleton type for use with not set sentinel values.""" _singleton = 0 UNDEFINED = UndefinedType._singleton class CacheInfo(TypedDict): """Host encoding cache.""" idna_encode: _CacheInfo idna_decode: _CacheInfo ip_address: _CacheInfo host_validate: _CacheInfo encode_host: _CacheInfo class _InternalURLCache(TypedDict, total=False): _val: SplitURLType _origin: "URL" absolute: bool hash: int scheme: str raw_authority: str authority: str raw_user: Union[str, None] user: Union[str, None] raw_password: Union[str, None] password: Union[str, None] raw_host: Union[str, None] host: Union[str, None] host_subcomponent: Union[str, None] host_port_subcomponent: Union[str, None] port: Union[int, None] explicit_port: Union[int, None] raw_path: str path: str _parsed_query: list[tuple[str, str]] query: "MultiDictProxy[str]" raw_query_string: str query_string: str path_qs: str raw_path_qs: str raw_fragment: str fragment: str raw_parts: tuple[str, ...] parts: tuple[str, ...] parent: "URL" raw_name: str name: str raw_suffix: str suffix: str raw_suffixes: tuple[str, ...] suffixes: tuple[str, ...] def rewrite_module(obj: _T) -> _T: obj.__module__ = "yarl" return obj @lru_cache def encode_url(url_str: str) -> "URL": """Parse unencoded URL.""" cache: _InternalURLCache = {} host: Union[str, None] scheme, netloc, path, query, fragment = split_url(url_str) if not netloc: # netloc host = "" else: if ":" in netloc or "@" in netloc or "[" in netloc: # Complex netloc username, password, host, port = split_netloc(netloc) else: username = password = port = None host = netloc if host is None: if scheme in SCHEME_REQUIRES_HOST: msg = ( "Invalid URL: host is required for " f"absolute urls with the {scheme} scheme" ) raise ValueError(msg) else: host = "" host = _encode_host(host, validate_host=False) # Remove brackets as host encoder adds back brackets for IPv6 addresses cache["raw_host"] = host[1:-1] if "[" in host else host cache["explicit_port"] = port if password is None and username is None: # Fast path for URLs without user, password netloc = host if port is None else f"{host}:{port}" cache["raw_user"] = None cache["raw_password"] = None else: raw_user = REQUOTER(username) if username else username raw_password = REQUOTER(password) if password else password netloc = make_netloc(raw_user, raw_password, host, port) cache["raw_user"] = raw_user cache["raw_password"] = raw_password if path: path = PATH_REQUOTER(path) if netloc and "." in path: path = normalize_path(path) if query: query = QUERY_REQUOTER(query) if fragment: fragment = FRAGMENT_REQUOTER(fragment) cache["scheme"] = scheme cache["raw_path"] = "/" if not path and netloc else path cache["raw_query_string"] = query cache["raw_fragment"] = fragment self = object.__new__(URL) self._scheme = scheme self._netloc = netloc self._path = path self._query = query self._fragment = fragment self._cache = cache return self @lru_cache def pre_encoded_url(url_str: str) -> "URL": """Parse pre-encoded URL.""" self = object.__new__(URL) val = split_url(url_str) self._scheme, self._netloc, self._path, self._query, self._fragment = val self._cache = {} return self @lru_cache def build_pre_encoded_url( scheme: str, authority: str, user: Union[str, None], password: Union[str, None], host: str, port: Union[int, None], path: str, query_string: str, fragment: str, ) -> "URL": """Build a pre-encoded URL from parts.""" self = object.__new__(URL) self._scheme = scheme if authority: self._netloc = authority elif host: if port is not None: port = None if port == DEFAULT_PORTS.get(scheme) else port if user is None and password is None: self._netloc = host if port is None else f"{host}:{port}" else: self._netloc = make_netloc(user, password, host, port) else: self._netloc = "" self._path = path self._query = query_string self._fragment = fragment self._cache = {} return self def from_parts_uncached( scheme: str, netloc: str, path: str, query: str, fragment: str ) -> "URL": """Create a new URL from parts.""" self = object.__new__(URL) self._scheme = scheme self._netloc = netloc self._path = path self._query = query self._fragment = fragment self._cache = {} return self from_parts = lru_cache(from_parts_uncached) @rewrite_module class URL: # Don't derive from str # follow pathlib.Path design # probably URL will not suffer from pathlib problems: # it's intended for libraries like aiohttp, # not to be passed into standard library functions like os.open etc. # URL grammar (RFC 3986) # pct-encoded = "%" HEXDIG HEXDIG # reserved = gen-delims / sub-delims # gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" # / "*" / "+" / "," / ";" / "=" # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" # URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] # hier-part = "//" authority path-abempty # / path-absolute # / path-rootless # / path-empty # scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) # authority = [ userinfo "@" ] host [ ":" port ] # userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) # host = IP-literal / IPv4address / reg-name # IP-literal = "[" ( IPv6address / IPvFuture ) "]" # IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) # IPv6address = 6( h16 ":" ) ls32 # / "::" 5( h16 ":" ) ls32 # / [ h16 ] "::" 4( h16 ":" ) ls32 # / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 # / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 # / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 # / [ *4( h16 ":" ) h16 ] "::" ls32 # / [ *5( h16 ":" ) h16 ] "::" h16 # / [ *6( h16 ":" ) h16 ] "::" # ls32 = ( h16 ":" h16 ) / IPv4address # ; least-significant 32 bits of address # h16 = 1*4HEXDIG # ; 16 bits of address represented in hexadecimal # IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet # dec-octet = DIGIT ; 0-9 # / %x31-39 DIGIT ; 10-99 # / "1" 2DIGIT ; 100-199 # / "2" %x30-34 DIGIT ; 200-249 # / "25" %x30-35 ; 250-255 # reg-name = *( unreserved / pct-encoded / sub-delims ) # port = *DIGIT # path = path-abempty ; begins with "/" or is empty # / path-absolute ; begins with "/" but not "//" # / path-noscheme ; begins with a non-colon segment # / path-rootless ; begins with a segment # / path-empty ; zero characters # path-abempty = *( "/" segment ) # path-absolute = "/" [ segment-nz *( "/" segment ) ] # path-noscheme = segment-nz-nc *( "/" segment ) # path-rootless = segment-nz *( "/" segment ) # path-empty = 0 # segment = *pchar # segment-nz = 1*pchar # segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) # ; non-zero-length segment without any colon ":" # pchar = unreserved / pct-encoded / sub-delims / ":" / "@" # query = *( pchar / "/" / "?" ) # fragment = *( pchar / "/" / "?" ) # URI-reference = URI / relative-ref # relative-ref = relative-part [ "?" query ] [ "#" fragment ] # relative-part = "//" authority path-abempty # / path-absolute # / path-noscheme # / path-empty # absolute-URI = scheme ":" hier-part [ "?" query ] __slots__ = ("_cache", "_scheme", "_netloc", "_path", "_query", "_fragment") _cache: _InternalURLCache _scheme: str _netloc: str _path: str _query: str _fragment: str def __new__( cls, val: Union[str, SplitResult, "URL", UndefinedType] = UNDEFINED, *, encoded: bool = False, strict: Union[bool, None] = None, ) -> "URL": if strict is not None: # pragma: no cover warnings.warn("strict parameter is ignored") if type(val) is str: return pre_encoded_url(val) if encoded else encode_url(val) if type(val) is cls: return val if type(val) is SplitResult: if not encoded: raise ValueError("Cannot apply decoding to SplitResult") return from_parts(*val) if isinstance(val, str): return pre_encoded_url(str(val)) if encoded else encode_url(str(val)) if val is UNDEFINED: # Special case for UNDEFINED since it might be unpickling and we do # not want to cache as the `__set_state__` call would mutate the URL # object in the `pre_encoded_url` or `encoded_url` caches. self = object.__new__(URL) self._scheme = self._netloc = self._path = self._query = self._fragment = "" self._cache = {} return self raise TypeError("Constructor parameter should be str") @classmethod def build( cls, *, scheme: str = "", authority: str = "", user: Union[str, None] = None, password: Union[str, None] = None, host: str = "", port: Union[int, None] = None, path: str = "", query: Union[Query, None] = None, query_string: str = "", fragment: str = "", encoded: bool = False, ) -> "URL": """Creates and returns a new URL""" if authority and (user or password or host or port): raise ValueError( 'Can\'t mix "authority" with "user", "password", "host" or "port".' ) if port is not None and not isinstance(port, int): raise TypeError(f"The port is required to be int, got {type(port)!r}.") if port and not host: raise ValueError('Can\'t build URL with "port" but without "host".') if query and query_string: raise ValueError('Only one of "query" or "query_string" should be passed') if ( scheme is None # type: ignore[redundant-expr] or authority is None # type: ignore[redundant-expr] or host is None # type: ignore[redundant-expr] or path is None # type: ignore[redundant-expr] or query_string is None # type: ignore[redundant-expr] or fragment is None ): raise TypeError( 'NoneType is illegal for "scheme", "authority", "host", "path", ' '"query_string", and "fragment" args, use empty string instead.' ) if query: query_string = get_str_query(query) or "" if encoded: return build_pre_encoded_url( scheme, authority, user, password, host, port, path, query_string, fragment, ) self = object.__new__(URL) self._scheme = scheme _host: Union[str, None] = None if authority: user, password, _host, port = split_netloc(authority) _host = _encode_host(_host, validate_host=False) if _host else "" elif host: _host = _encode_host(host, validate_host=True) else: self._netloc = "" if _host is not None: if port is not None: port = None if port == DEFAULT_PORTS.get(scheme) else port if user is None and password is None: self._netloc = _host if port is None else f"{_host}:{port}" else: self._netloc = make_netloc(user, password, _host, port, True) path = PATH_QUOTER(path) if path else path if path and self._netloc: if "." in path: path = normalize_path(path) if path[0] != "/": msg = ( "Path in a URL with authority should " "start with a slash ('/') if set" ) raise ValueError(msg) self._path = path if not query and query_string: query_string = QUERY_QUOTER(query_string) self._query = query_string self._fragment = FRAGMENT_QUOTER(fragment) if fragment else fragment self._cache = {} return self def __init_subclass__(cls) -> NoReturn: raise TypeError(f"Inheriting a class {cls!r} from URL is forbidden") def __str__(self) -> str: if not self._path and self._netloc and (self._query or self._fragment): path = "/" else: path = self._path if (port := self.explicit_port) is not None and port == DEFAULT_PORTS.get( self._scheme ): # port normalization - using None for default ports to remove from rendering # https://datatracker.ietf.org/doc/html/rfc3986.html#section-6.2.3 host = self.host_subcomponent netloc = make_netloc(self.raw_user, self.raw_password, host, None) else: netloc = self._netloc return unsplit_result(self._scheme, netloc, path, self._query, self._fragment) def __repr__(self) -> str: return f"{self.__class__.__name__}('{str(self)}')" def __bytes__(self) -> bytes: return str(self).encode("ascii") def __eq__(self, other: object) -> bool: if type(other) is not URL: return NotImplemented path1 = "/" if not self._path and self._netloc else self._path path2 = "/" if not other._path and other._netloc else other._path return ( self._scheme == other._scheme and self._netloc == other._netloc and path1 == path2 and self._query == other._query and self._fragment == other._fragment ) def __hash__(self) -> int: if (ret := self._cache.get("hash")) is None: path = "/" if not self._path and self._netloc else self._path ret = self._cache["hash"] = hash( (self._scheme, self._netloc, path, self._query, self._fragment) ) return ret def __le__(self, other: object) -> bool: if type(other) is not URL: return NotImplemented return self._val <= other._val def __lt__(self, other: object) -> bool: if type(other) is not URL: return NotImplemented return self._val < other._val def __ge__(self, other: object) -> bool: if type(other) is not URL: return NotImplemented return self._val >= other._val def __gt__(self, other: object) -> bool: if type(other) is not URL: return NotImplemented return self._val > other._val def __truediv__(self, name: str) -> "URL": if not isinstance(name, str): return NotImplemented # type: ignore[unreachable] return self._make_child((str(name),)) def __mod__(self, query: Query) -> "URL": return self.update_query(query) def __bool__(self) -> bool: return bool(self._netloc or self._path or self._query or self._fragment) def __getstate__(self) -> tuple[SplitResult]: return (tuple.__new__(SplitResult, self._val),) def __setstate__( self, state: Union[tuple[SplitURLType], tuple[None, _InternalURLCache]] ) -> None: if state[0] is None and isinstance(state[1], dict): # default style pickle val = state[1]["_val"] else: unused: list[object] val, *unused = state self._scheme, self._netloc, self._path, self._query, self._fragment = val self._cache = {} def _cache_netloc(self) -> None: """Cache the netloc parts of the URL.""" c = self._cache split_loc = split_netloc(self._netloc) c["raw_user"], c["raw_password"], c["raw_host"], c["explicit_port"] = split_loc def is_absolute(self) -> bool: """A check for absolute URLs. Return True for absolute ones (having scheme or starting with //), False otherwise. Is is preferred to call the .absolute property instead as it is cached. """ return self.absolute def is_default_port(self) -> bool: """A check for default port. Return True if port is default for specified scheme, e.g. 'http://python.org' or 'http://python.org:80', False otherwise. Return False for relative URLs. """ if (explicit := self.explicit_port) is None: # If the explicit port is None, then the URL must be # using the default port unless its a relative URL # which does not have an implicit port / default port return self._netloc != "" return explicit == DEFAULT_PORTS.get(self._scheme) def origin(self) -> "URL": """Return an URL with scheme, host and port parts only. user, password, path, query and fragment are removed. """ # TODO: add a keyword-only option for keeping user/pass maybe? return self._origin @cached_property def _val(self) -> SplitURLType: return (self._scheme, self._netloc, self._path, self._query, self._fragment) @cached_property def _origin(self) -> "URL": """Return an URL with scheme, host and port parts only. user, password, path, query and fragment are removed. """ if not (netloc := self._netloc): raise ValueError("URL should be absolute") if not (scheme := self._scheme): raise ValueError("URL should have scheme") if "@" in netloc: encoded_host = self.host_subcomponent netloc = make_netloc(None, None, encoded_host, self.explicit_port) elif not self._path and not self._query and not self._fragment: return self return from_parts(scheme, netloc, "", "", "") def relative(self) -> "URL": """Return a relative part of the URL. scheme, user, password, host and port are removed. """ if not self._netloc: raise ValueError("URL should be absolute") return from_parts("", "", self._path, self._query, self._fragment) @cached_property def absolute(self) -> bool: """A check for absolute URLs. Return True for absolute ones (having scheme or starting with //), False otherwise. """ # `netloc`` is an empty string for relative URLs # Checking `netloc` is faster than checking `hostname` # because `hostname` is a property that does some extra work # to parse the host from the `netloc` return self._netloc != "" @cached_property def scheme(self) -> str: """Scheme for absolute URLs. Empty string for relative URLs or URLs starting with // """ return self._scheme @cached_property def raw_authority(self) -> str: """Encoded authority part of URL. Empty string for relative URLs. """ return self._netloc @cached_property def authority(self) -> str: """Decoded authority part of URL. Empty string for relative URLs. """ return make_netloc(self.user, self.password, self.host, self.port) @cached_property def raw_user(self) -> Union[str, None]: """Encoded user part of URL. None if user is missing. """ # not .username self._cache_netloc() return self._cache["raw_user"] @cached_property def user(self) -> Union[str, None]: """Decoded user part of URL. None if user is missing. """ if (raw_user := self.raw_user) is None: return None return UNQUOTER(raw_user) @cached_property def raw_password(self) -> Union[str, None]: """Encoded password part of URL. None if password is missing. """ self._cache_netloc() return self._cache["raw_password"] @cached_property def password(self) -> Union[str, None]: """Decoded password part of URL. None if password is missing. """ if (raw_password := self.raw_password) is None: return None return UNQUOTER(raw_password) @cached_property def raw_host(self) -> Union[str, None]: """Encoded host part of URL. None for relative URLs. When working with IPv6 addresses, use the `host_subcomponent` property instead as it will return the host subcomponent with brackets. """ # Use host instead of hostname for sake of shortness # May add .hostname prop later self._cache_netloc() return self._cache["raw_host"] @cached_property def host(self) -> Union[str, None]: """Decoded host part of URL. None for relative URLs. """ if (raw := self.raw_host) is None: return None if raw and raw[-1].isdigit() or ":" in raw: # IP addresses are never IDNA encoded return raw return _idna_decode(raw) @cached_property def host_subcomponent(self) -> Union[str, None]: """Return the host subcomponent part of URL. None for relative URLs. https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 `IP-literal = "[" ( IPv6address / IPvFuture ) "]"` Examples: - `http://example.com:8080` -> `example.com` - `http://example.com:80` -> `example.com` - `https://127.0.0.1:8443` -> `127.0.0.1` - `https://[::1]:8443` -> `[::1]` - `http://[::1]` -> `[::1]` """ if (raw := self.raw_host) is None: return None return f"[{raw}]" if ":" in raw else raw @cached_property def host_port_subcomponent(self) -> Union[str, None]: """Return the host and port subcomponent part of URL. Trailing dots are removed from the host part. This value is suitable for use in the Host header of an HTTP request. None for relative URLs. https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 `IP-literal = "[" ( IPv6address / IPvFuture ) "]"` https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3 port = *DIGIT Examples: - `http://example.com:8080` -> `example.com:8080` - `http://example.com:80` -> `example.com` - `http://example.com.:80` -> `example.com` - `https://127.0.0.1:8443` -> `127.0.0.1:8443` - `https://[::1]:8443` -> `[::1]:8443` - `http://[::1]` -> `[::1]` """ if (raw := self.raw_host) is None: return None if raw[-1] == ".": # Remove all trailing dots from the netloc as while # they are valid FQDNs in DNS, TLS validation fails. # See https://github.com/aio-libs/aiohttp/issues/3636. # To avoid string manipulation we only call rstrip if # the last character is a dot. raw = raw.rstrip(".") port = self.explicit_port if port is None or port == DEFAULT_PORTS.get(self._scheme): return f"[{raw}]" if ":" in raw else raw return f"[{raw}]:{port}" if ":" in raw else f"{raw}:{port}" @cached_property def port(self) -> Union[int, None]: """Port part of URL, with scheme-based fallback. None for relative URLs or URLs without explicit port and scheme without default port substitution. """ if (explicit_port := self.explicit_port) is not None: return explicit_port return DEFAULT_PORTS.get(self._scheme) @cached_property def explicit_port(self) -> Union[int, None]: """Port part of URL, without scheme-based fallback. None for relative URLs or URLs without explicit port. """ self._cache_netloc() return self._cache["explicit_port"] @cached_property def raw_path(self) -> str: """Encoded path of URL. / for absolute URLs without path part. """ return self._path if self._path or not self._netloc else "/" @cached_property def path(self) -> str: """Decoded path of URL. / for absolute URLs without path part. """ return PATH_UNQUOTER(self._path) if self._path else "/" if self._netloc else "" @cached_property def path_safe(self) -> str: """Decoded path of URL. / for absolute URLs without path part. / (%2F) and % (%25) are not decoded """ if self._path: return PATH_SAFE_UNQUOTER(self._path) return "/" if self._netloc else "" @cached_property def _parsed_query(self) -> list[tuple[str, str]]: """Parse query part of URL.""" return query_to_pairs(self._query) @cached_property def query(self) -> "MultiDictProxy[str]": """A MultiDictProxy representing parsed query parameters in decoded representation. Empty value if URL has no query part. """ return MultiDictProxy(MultiDict(self._parsed_query)) @cached_property def raw_query_string(self) -> str: """Encoded query part of URL. Empty string if query is missing. """ return self._query @cached_property def query_string(self) -> str: """Decoded query part of URL. Empty string if query is missing. """ return QS_UNQUOTER(self._query) if self._query else "" @cached_property def path_qs(self) -> str: """Decoded path of URL with query.""" return self.path if not (q := self.query_string) else f"{self.path}?{q}" @cached_property def raw_path_qs(self) -> str: """Encoded path of URL with query.""" if q := self._query: return f"{self._path}?{q}" if self._path or not self._netloc else f"/?{q}" return self._path if self._path or not self._netloc else "/" @cached_property def raw_fragment(self) -> str: """Encoded fragment part of URL. Empty string if fragment is missing. """ return self._fragment @cached_property def fragment(self) -> str: """Decoded fragment part of URL. Empty string if fragment is missing. """ return UNQUOTER(self._fragment) if self._fragment else "" @cached_property def raw_parts(self) -> tuple[str, ...]: """A tuple containing encoded *path* parts. ('/',) for absolute URLs if *path* is missing. """ path = self._path if self._netloc: return ("/", *path[1:].split("/")) if path else ("/",) if path and path[0] == "/": return ("/", *path[1:].split("/")) return tuple(path.split("/")) @cached_property def parts(self) -> tuple[str, ...]: """A tuple containing decoded *path* parts. ('/',) for absolute URLs if *path* is missing. """ return tuple(UNQUOTER(part) for part in self.raw_parts) @cached_property def parent(self) -> "URL": """A new URL with last part of path removed and cleaned up query and fragment. """ path = self._path if not path or path == "/": if self._fragment or self._query: return from_parts(self._scheme, self._netloc, path, "", "") return self parts = path.split("/") return from_parts(self._scheme, self._netloc, "/".join(parts[:-1]), "", "") @cached_property def raw_name(self) -> str: """The last part of raw_parts.""" parts = self.raw_parts if not self._netloc: return parts[-1] parts = parts[1:] return parts[-1] if parts else "" @cached_property def name(self) -> str: """The last part of parts.""" return UNQUOTER(self.raw_name) @cached_property def raw_suffix(self) -> str: name = self.raw_name i = name.rfind(".") return name[i:] if 0 < i < len(name) - 1 else "" @cached_property def suffix(self) -> str: return UNQUOTER(self.raw_suffix) @cached_property def raw_suffixes(self) -> tuple[str, ...]: name = self.raw_name if name.endswith("."): return () name = name.lstrip(".") return tuple("." + suffix for suffix in name.split(".")[1:]) @cached_property def suffixes(self) -> tuple[str, ...]: return tuple(UNQUOTER(suffix) for suffix in self.raw_suffixes) def _make_child(self, paths: "Sequence[str]", encoded: bool = False) -> "URL": """ add paths to self._path, accounting for absolute vs relative paths, keep existing, but do not create new, empty segments """ parsed: list[str] = [] needs_normalize: bool = False for idx, path in enumerate(reversed(paths)): # empty segment of last is not removed last = idx == 0 if path and path[0] == "/": raise ValueError( f"Appending path {path!r} starting from slash is forbidden" ) # We need to quote the path if it is not already encoded # This cannot be done at the end because the existing # path is already quoted and we do not want to double quote # the existing path. path = path if encoded else PATH_QUOTER(path) needs_normalize |= "." in path segments = path.split("/") segments.reverse() # remove trailing empty segment for all but the last path parsed += segments[1:] if not last and segments[0] == "" else segments if (path := self._path) and (old_segments := path.split("/")): # If the old path ends with a slash, the last segment is an empty string # and should be removed before adding the new path segments. old = old_segments[:-1] if old_segments[-1] == "" else old_segments old.reverse() parsed += old # If the netloc is present, inject a leading slash when adding a # path to an absolute URL where there was none before. if (netloc := self._netloc) and parsed and parsed[-1] != "": parsed.append("") parsed.reverse() if not netloc or not needs_normalize: return from_parts(self._scheme, netloc, "/".join(parsed), "", "") path = "/".join(normalize_path_segments(parsed)) # If normalizing the path segments removed the leading slash, add it back. if path and path[0] != "/": path = f"/{path}" return from_parts(self._scheme, netloc, path, "", "") def with_scheme(self, scheme: str) -> "URL": """Return a new URL with scheme replaced.""" # N.B. doesn't cleanup query/fragment if not isinstance(scheme, str): raise TypeError("Invalid scheme type") lower_scheme = scheme.lower() netloc = self._netloc if not netloc and lower_scheme in SCHEME_REQUIRES_HOST: msg = ( "scheme replacement is not allowed for " f"relative URLs for the {lower_scheme} scheme" ) raise ValueError(msg) return from_parts(lower_scheme, netloc, self._path, self._query, self._fragment) def with_user(self, user: Union[str, None]) -> "URL": """Return a new URL with user replaced. Autoencode user if needed. Clear user/password if user is None. """ # N.B. doesn't cleanup query/fragment if user is None: password = None elif isinstance(user, str): user = QUOTER(user) password = self.raw_password else: raise TypeError("Invalid user type") if not (netloc := self._netloc): raise ValueError("user replacement is not allowed for relative URLs") encoded_host = self.host_subcomponent or "" netloc = make_netloc(user, password, encoded_host, self.explicit_port) return from_parts(self._scheme, netloc, self._path, self._query, self._fragment) def with_password(self, password: Union[str, None]) -> "URL": """Return a new URL with password replaced. Autoencode password if needed. Clear password if argument is None. """ # N.B. doesn't cleanup query/fragment if password is None: pass elif isinstance(password, str): password = QUOTER(password) else: raise TypeError("Invalid password type") if not (netloc := self._netloc): raise ValueError("password replacement is not allowed for relative URLs") encoded_host = self.host_subcomponent or "" port = self.explicit_port netloc = make_netloc(self.raw_user, password, encoded_host, port) return from_parts(self._scheme, netloc, self._path, self._query, self._fragment) def with_host(self, host: str) -> "URL": """Return a new URL with host replaced. Autoencode host if needed. Changing host for relative URLs is not allowed, use .join() instead. """ # N.B. doesn't cleanup query/fragment if not isinstance(host, str): raise TypeError("Invalid host type") if not (netloc := self._netloc): raise ValueError("host replacement is not allowed for relative URLs") if not host: raise ValueError("host removing is not allowed") encoded_host = _encode_host(host, validate_host=True) if host else "" port = self.explicit_port netloc = make_netloc(self.raw_user, self.raw_password, encoded_host, port) return from_parts(self._scheme, netloc, self._path, self._query, self._fragment) def with_port(self, port: Union[int, None]) -> "URL": """Return a new URL with port replaced. Clear port to default if None is passed. """ # N.B. doesn't cleanup query/fragment if port is not None: if isinstance(port, bool) or not isinstance(port, int): raise TypeError(f"port should be int or None, got {type(port)}") if not (0 <= port <= 65535): raise ValueError(f"port must be between 0 and 65535, got {port}") if not (netloc := self._netloc): raise ValueError("port replacement is not allowed for relative URLs") encoded_host = self.host_subcomponent or "" netloc = make_netloc(self.raw_user, self.raw_password, encoded_host, port) return from_parts(self._scheme, netloc, self._path, self._query, self._fragment) def with_path( self, path: str, *, encoded: bool = False, keep_query: bool = False, keep_fragment: bool = False, ) -> "URL": """Return a new URL with path replaced.""" netloc = self._netloc if not encoded: path = PATH_QUOTER(path) if netloc: path = normalize_path(path) if "." in path else path if path and path[0] != "/": path = f"/{path}" query = self._query if keep_query else "" fragment = self._fragment if keep_fragment else "" return from_parts(self._scheme, netloc, path, query, fragment) @overload def with_query(self, query: Query) -> "URL": ... @overload def with_query(self, **kwargs: QueryVariable) -> "URL": ... def with_query(self, *args: Any, **kwargs: Any) -> "URL": """Return a new URL with query part replaced. Accepts any Mapping (e.g. dict, multidict.MultiDict instances) or str, autoencode the argument if needed. A sequence of (key, value) pairs is supported as well. It also can take an arbitrary number of keyword arguments. Clear query if None is passed. """ # N.B. doesn't cleanup query/fragment query = get_str_query(*args, **kwargs) or "" return from_parts_uncached( self._scheme, self._netloc, self._path, query, self._fragment ) @overload def extend_query(self, query: Query) -> "URL": ... @overload def extend_query(self, **kwargs: QueryVariable) -> "URL": ... def extend_query(self, *args: Any, **kwargs: Any) -> "URL": """Return a new URL with query part combined with the existing. This method will not remove existing query parameters. Example: >>> url = URL('http://example.com/?a=1&b=2') >>> url.extend_query(a=3, c=4) URL('http://example.com/?a=1&b=2&a=3&c=4') """ if not (new_query := get_str_query(*args, **kwargs)): return self if query := self._query: # both strings are already encoded so we can use a simple # string join query += new_query if query[-1] == "&" else f"&{new_query}" else: query = new_query return from_parts_uncached( self._scheme, self._netloc, self._path, query, self._fragment ) @overload def update_query(self, query: Query) -> "URL": ... @overload def update_query(self, **kwargs: QueryVariable) -> "URL": ... def update_query(self, *args: Any, **kwargs: Any) -> "URL": """Return a new URL with query part updated. This method will overwrite existing query parameters. Example: >>> url = URL('http://example.com/?a=1&b=2') >>> url.update_query(a=3, c=4) URL('http://example.com/?a=3&b=2&c=4') """ in_query: Union[str, Mapping[str, QueryVariable], None] if kwargs: if args: msg = "Either kwargs or single query parameter must be present" raise ValueError(msg) in_query = kwargs elif len(args) == 1: in_query = args[0] else: raise ValueError("Either kwargs or single query parameter must be present") if in_query is None: query = "" elif not in_query: query = self._query elif isinstance(in_query, Mapping): qm: MultiDict[QueryVariable] = MultiDict(self._parsed_query) qm.update(in_query) query = get_str_query_from_sequence_iterable(qm.items()) elif isinstance(in_query, str): qstr: MultiDict[str] = MultiDict(self._parsed_query) qstr.update(query_to_pairs(in_query)) query = get_str_query_from_iterable(qstr.items()) elif isinstance(in_query, (bytes, bytearray, memoryview)): # type: ignore[unreachable] msg = "Invalid query type: bytes, bytearray and memoryview are forbidden" raise TypeError(msg) elif isinstance(in_query, Sequence): # We don't expect sequence values if we're given a list of pairs # already; only mappings like builtin `dict` which can't have the # same key pointing to multiple values are allowed to use # `_query_seq_pairs`. qs: MultiDict[SimpleQuery] = MultiDict(self._parsed_query) qs.update(in_query) query = get_str_query_from_iterable(qs.items()) else: raise TypeError( "Invalid query type: only str, mapping or " "sequence of (key, value) pairs is allowed" ) return from_parts_uncached( self._scheme, self._netloc, self._path, query, self._fragment ) def without_query_params(self, *query_params: str) -> "URL": """Remove some keys from query part and return new URL.""" params_to_remove = set(query_params) & self.query.keys() if not params_to_remove: return self return self.with_query( tuple( (name, value) for name, value in self.query.items() if name not in params_to_remove ) ) def with_fragment(self, fragment: Union[str, None]) -> "URL": """Return a new URL with fragment replaced. Autoencode fragment if needed. Clear fragment to default if None is passed. """ # N.B. doesn't cleanup query/fragment if fragment is None: raw_fragment = "" elif not isinstance(fragment, str): raise TypeError("Invalid fragment type") else: raw_fragment = FRAGMENT_QUOTER(fragment) if self._fragment == raw_fragment: return self return from_parts( self._scheme, self._netloc, self._path, self._query, raw_fragment ) def with_name( self, name: str, *, keep_query: bool = False, keep_fragment: bool = False, ) -> "URL": """Return a new URL with name (last part of path) replaced. Query and fragment parts are cleaned up. Name is encoded if needed. """ # N.B. DOES cleanup query/fragment if not isinstance(name, str): raise TypeError("Invalid name type") if "/" in name: raise ValueError("Slash in name is not allowed") name = PATH_QUOTER(name) if name in (".", ".."): raise ValueError(". and .. values are forbidden") parts = list(self.raw_parts) if netloc := self._netloc: if len(parts) == 1: parts.append(name) else: parts[-1] = name parts[0] = "" # replace leading '/' else: parts[-1] = name if parts[0] == "/": parts[0] = "" # replace leading '/' query = self._query if keep_query else "" fragment = self._fragment if keep_fragment else "" return from_parts(self._scheme, netloc, "/".join(parts), query, fragment) def with_suffix( self, suffix: str, *, keep_query: bool = False, keep_fragment: bool = False, ) -> "URL": """Return a new URL with suffix (file extension of name) replaced. Query and fragment parts are cleaned up. suffix is encoded if needed. """ if not isinstance(suffix, str): raise TypeError("Invalid suffix type") if suffix and not suffix[0] == "." or suffix == "." or "/" in suffix: raise ValueError(f"Invalid suffix {suffix!r}") name = self.raw_name if not name: raise ValueError(f"{self!r} has an empty name") old_suffix = self.raw_suffix suffix = PATH_QUOTER(suffix) name = name + suffix if not old_suffix else name[: -len(old_suffix)] + suffix if name in (".", ".."): raise ValueError(". and .. values are forbidden") parts = list(self.raw_parts) if netloc := self._netloc: if len(parts) == 1: parts.append(name) else: parts[-1] = name parts[0] = "" # replace leading '/' else: parts[-1] = name if parts[0] == "/": parts[0] = "" # replace leading '/' query = self._query if keep_query else "" fragment = self._fragment if keep_fragment else "" return from_parts(self._scheme, netloc, "/".join(parts), query, fragment) def join(self, url: "URL") -> "URL": """Join URLs Construct a full (“absolute”) URL by combining a “base URL” (self) with another URL (url). Informally, this uses components of the base URL, in particular the addressing scheme, the network location and (part of) the path, to provide missing components in the relative URL. """ if type(url) is not URL: raise TypeError("url should be URL") scheme = url._scheme or self._scheme if scheme != self._scheme or scheme not in USES_RELATIVE: return url # scheme is in uses_authority as uses_authority is a superset of uses_relative if (join_netloc := url._netloc) and scheme in USES_AUTHORITY: return from_parts(scheme, join_netloc, url._path, url._query, url._fragment) orig_path = self._path if join_path := url._path: if join_path[0] == "/": path = join_path elif not orig_path: path = f"/{join_path}" elif orig_path[-1] == "/": path = f"{orig_path}{join_path}" else: # … # and relativizing ".." # parts[0] is / for absolute urls, # this join will add a double slash there path = "/".join([*self.parts[:-1], ""]) + join_path # which has to be removed if orig_path[0] == "/": path = path[1:] path = normalize_path(path) if "." in path else path else: path = orig_path return from_parts( scheme, self._netloc, path, url._query if join_path or url._query else self._query, url._fragment if join_path or url._fragment else self._fragment, ) def joinpath(self, *other: str, encoded: bool = False) -> "URL": """Return a new URL with the elements in other appended to the path.""" return self._make_child(other, encoded=encoded) def human_repr(self) -> str: """Return decoded human readable string for URL representation.""" user = human_quote(self.user, "#/:?@[]") password = human_quote(self.password, "#/:?@[]") if (host := self.host) and ":" in host: host = f"[{host}]" path = human_quote(self.path, "#?") if TYPE_CHECKING: assert path is not None query_string = "&".join( "{}={}".format(human_quote(k, "#&+;="), human_quote(v, "#&+;=")) for k, v in self.query.items() ) fragment = human_quote(self.fragment, "") if TYPE_CHECKING: assert fragment is not None netloc = make_netloc(user, password, host, self.explicit_port) return unsplit_result(self._scheme, netloc, path, query_string, fragment) _DEFAULT_IDNA_SIZE = 256 _DEFAULT_ENCODE_SIZE = 512 @lru_cache(_DEFAULT_IDNA_SIZE) def _idna_decode(raw: str) -> str: try: return idna.decode(raw.encode("ascii")) except UnicodeError: # e.g. '::1' return raw.encode("ascii").decode("idna") @lru_cache(_DEFAULT_IDNA_SIZE) def _idna_encode(host: str) -> str: try: return idna.encode(host, uts46=True).decode("ascii") except UnicodeError: return host.encode("idna").decode("ascii") @lru_cache(_DEFAULT_ENCODE_SIZE) def _encode_host(host: str, validate_host: bool) -> str: """Encode host part of URL.""" # If the host ends with a digit or contains a colon, its likely # an IP address. if host and (host[-1].isdigit() or ":" in host): raw_ip, sep, zone = host.partition("%") # If it looks like an IP, we check with _ip_compressed_version # and fall-through if its not an IP address. This is a performance # optimization to avoid parsing IP addresses as much as possible # because it is orders of magnitude slower than almost any other # operation this library does. # Might be an IP address, check it # # IP Addresses can look like: # https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 # - 127.0.0.1 (last character is a digit) # - 2001:db8::ff00:42:8329 (contains a colon) # - 2001:db8::ff00:42:8329%eth0 (contains a colon) # - [2001:db8::ff00:42:8329] (contains a colon -- brackets should # have been removed before it gets here) # Rare IP Address formats are not supported per: # https://datatracker.ietf.org/doc/html/rfc3986#section-7.4 # # IP parsing is slow, so its wrapped in an LRU try: ip = ip_address(raw_ip) except ValueError: pass else: # These checks should not happen in the # LRU to keep the cache size small host = ip.compressed if ip.version == 6: return f"[{host}%{zone}]" if sep else f"[{host}]" return f"{host}%{zone}" if sep else host # IDNA encoding is slow, skip it for ASCII-only strings if host.isascii(): # Check for invalid characters explicitly; _idna_encode() does this # for non-ascii host names. host = host.lower() if validate_host and (invalid := NOT_REG_NAME.search(host)): value, pos, extra = invalid.group(), invalid.start(), "" if value == "@" or (value == ":" and "@" in host[pos:]): # this looks like an authority string extra = ( ", if the value includes a username or password, " "use 'authority' instead of 'host'" ) raise ValueError( f"Host {host!r} cannot contain {value!r} (at position {pos}){extra}" ) from None return host return _idna_encode(host) @rewrite_module def cache_clear() -> None: """Clear all LRU caches.""" _idna_encode.cache_clear() _idna_decode.cache_clear() _encode_host.cache_clear() @rewrite_module def cache_info() -> CacheInfo: """Report cache statistics.""" return { "idna_encode": _idna_encode.cache_info(), "idna_decode": _idna_decode.cache_info(), "ip_address": _encode_host.cache_info(), "host_validate": _encode_host.cache_info(), "encode_host": _encode_host.cache_info(), } @rewrite_module def cache_configure( *, idna_encode_size: Union[int, None] = _DEFAULT_IDNA_SIZE, idna_decode_size: Union[int, None] = _DEFAULT_IDNA_SIZE, ip_address_size: Union[int, None, UndefinedType] = UNDEFINED, host_validate_size: Union[int, None, UndefinedType] = UNDEFINED, encode_host_size: Union[int, None, UndefinedType] = UNDEFINED, ) -> None: """Configure LRU cache sizes.""" global _idna_decode, _idna_encode, _encode_host # ip_address_size, host_validate_size are no longer # used, but are kept for backwards compatibility. if ip_address_size is not UNDEFINED or host_validate_size is not UNDEFINED: warnings.warn( "cache_configure() no longer accepts the " "ip_address_size or host_validate_size arguments, " "they are used to set the encode_host_size instead " "and will be removed in the future", DeprecationWarning, stacklevel=2, ) if encode_host_size is not None: for size in (ip_address_size, host_validate_size): if size is None: encode_host_size = None elif encode_host_size is UNDEFINED: if size is not UNDEFINED: encode_host_size = size elif size is not UNDEFINED: if TYPE_CHECKING: assert isinstance(size, int) assert isinstance(encode_host_size, int) encode_host_size = max(size, encode_host_size) if encode_host_size is UNDEFINED: encode_host_size = _DEFAULT_ENCODE_SIZE _encode_host = lru_cache(encode_host_size)(_encode_host.__wrapped__) _idna_decode = lru_cache(idna_decode_size)(_idna_decode.__wrapped__) _idna_encode = lru_cache(idna_encode_size)(_idna_encode.__wrapped__) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903935.0 yarl-1.19.0/yarl/py.typed0000644000175100001660000000001614774356277014645 0ustar00runnerdocker# Placeholder ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743903941.5932763 yarl-1.19.0/yarl.egg-info/0000755000175100001660000000000014774356306014634 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903941.0 yarl-1.19.0/yarl.egg-info/PKG-INFO0000644000175100001660000021411314774356305015732 0ustar00runnerdockerMetadata-Version: 2.4 Name: yarl Version: 1.19.0 Summary: Yet another URL library Home-page: https://github.com/aio-libs/yarl Author: Andrew Svetlov Author-email: andrew.svetlov@gmail.com Maintainer: aiohttp team Maintainer-email: team@aiohttp.org License: Apache-2.0 Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org Project-URL: CI: GitHub Workflows, https://github.com/aio-libs/yarl/actions?query=branch:master Project-URL: Code of Conduct, https://github.com/aio-libs/.github/blob/master/CODE_OF_CONDUCT.md Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/yarl Project-URL: Docs: Changelog, https://yarl.aio-libs.org/en/latest/changes/ Project-URL: Docs: RTD, https://yarl.aio-libs.org Project-URL: GitHub: issues, https://github.com/aio-libs/yarl/issues Project-URL: GitHub: repo, https://github.com/aio-libs/yarl Keywords: cython,cext,yarl Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Cython Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.9 Description-Content-Type: text/x-rst License-File: LICENSE License-File: NOTICE Requires-Dist: idna>=2.0 Requires-Dist: multidict>=4.0 Requires-Dist: propcache>=0.2.1 Dynamic: license-file yarl ==== The module provides handy URL class for URL parsing and changing. .. image:: https://github.com/aio-libs/yarl/workflows/CI/badge.svg :target: https://github.com/aio-libs/yarl/actions?query=workflow%3ACI :align: right .. image:: https://codecov.io/gh/aio-libs/yarl/graph/badge.svg?flag=pytest :target: https://app.codecov.io/gh/aio-libs/yarl?flags[]=pytest :alt: Codecov coverage for the pytest-driven measurements .. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json :target: https://codspeed.io/aio-libs/yarl .. image:: https://badge.fury.io/py/yarl.svg :target: https://badge.fury.io/py/yarl .. image:: https://readthedocs.org/projects/yarl/badge/?version=latest :target: https://yarl.aio-libs.org .. image:: https://img.shields.io/pypi/pyversions/yarl.svg :target: https://pypi.python.org/pypi/yarl .. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat :target: https://matrix.to/#/%23aio-libs:matrix.org :alt: Matrix Room — #aio-libs:matrix.org .. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat :target: https://matrix.to/#/%23aio-libs-space:matrix.org :alt: Matrix Space — #aio-libs-space:matrix.org Introduction ------------ Url is constructed from ``str``: .. code-block:: pycon >>> from yarl import URL >>> url = URL('https://www.python.org/~guido?arg=1#frag') >>> url URL('https://www.python.org/~guido?arg=1#frag') All url parts: *scheme*, *user*, *password*, *host*, *port*, *path*, *query* and *fragment* are accessible by properties: .. code-block:: pycon >>> url.scheme 'https' >>> url.host 'www.python.org' >>> url.path '/~guido' >>> url.query_string 'arg=1' >>> url.query >>> url.fragment 'frag' All url manipulations produce a new url object: .. code-block:: pycon >>> url = URL('https://www.python.org') >>> url / 'foo' / 'bar' URL('https://www.python.org/foo/bar') >>> url / 'foo' % {'bar': 'baz'} URL('https://www.python.org/foo?bar=baz') Strings passed to constructor and modification methods are automatically encoded giving canonical representation as result: .. code-block:: pycon >>> url = URL('https://www.python.org/шлях') >>> url URL('https://www.python.org/%D1%88%D0%BB%D1%8F%D1%85') Regular properties are *percent-decoded*, use ``raw_`` versions for getting *encoded* strings: .. code-block:: pycon >>> url.path '/шлях' >>> url.raw_path '/%D1%88%D0%BB%D1%8F%D1%85' Human readable representation of URL is available as ``.human_repr()``: .. code-block:: pycon >>> url.human_repr() 'https://www.python.org/шлях' For full documentation please read https://yarl.aio-libs.org. Installation ------------ :: $ pip install yarl The library is Python 3 only! PyPI contains binary wheels for Linux, Windows and MacOS. If you want to install ``yarl`` on another operating system where wheels are not provided, the tarball will be used to compile the library from the source code. It requires a C compiler and and Python headers installed. To skip the compilation you must explicitly opt-in by using a PEP 517 configuration setting ``pure-python``, or setting the ``YARL_NO_EXTENSIONS`` environment variable to a non-empty value, e.g.: .. code-block:: console $ pip install yarl --config-settings=pure-python=false Please note that the pure-Python (uncompiled) version is much slower. However, PyPy always uses a pure-Python implementation, and, as such, it is unaffected by this variable. Dependencies ------------ YARL requires multidict_ and propcache_ libraries. API documentation ------------------ The documentation is located at https://yarl.aio-libs.org. Why isn't boolean supported by the URL query API? ------------------------------------------------- There is no standard for boolean representation of boolean values. Some systems prefer ``true``/``false``, others like ``yes``/``no``, ``on``/``off``, ``Y``/``N``, ``1``/``0``, etc. ``yarl`` cannot make an unambiguous decision on how to serialize ``bool`` values because it is specific to how the end-user's application is built and would be different for different apps. The library doesn't accept booleans in the API; a user should convert bools into strings using own preferred translation protocol. Comparison with other URL libraries ------------------------------------ * furl (https://pypi.python.org/pypi/furl) The library has rich functionality but the ``furl`` object is mutable. I'm afraid to pass this object into foreign code: who knows if the code will modify my url in a terrible way while I just want to send URL with handy helpers for accessing URL properties. ``furl`` has other non-obvious tricky things but the main objection is mutability. * URLObject (https://pypi.python.org/pypi/URLObject) URLObject is immutable, that's pretty good. Every URL change generates a new URL object. But the library doesn't do any decode/encode transformations leaving the end user to cope with these gory details. Source code ----------- The project is hosted on GitHub_ Please file an issue on the `bug tracker `_ if you have found a bug or have some suggestion in order to improve the library. Discussion list --------------- *aio-libs* google group: https://groups.google.com/forum/#!forum/aio-libs Feel free to post your questions and ideas here. Authors and License ------------------- The ``yarl`` package is written by Andrew Svetlov. It's *Apache 2* licensed and freely available. .. _GitHub: https://github.com/aio-libs/yarl .. _multidict: https://github.com/aio-libs/multidict .. _propcache: https://github.com/aio-libs/propcache ========= Changelog ========= .. 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/#adding-a-news-entry we named the news folder "changes". WARNING: Don't drop the next directive! .. towncrier release notes start 1.19.0 ====== *(2025-04-05)* Bug fixes --------- - Fixed entire name being re-encoded when using ``yarl.URL.with_suffix()`` -- by `@NTFSvolume `__. *Related issues and pull requests on GitHub:* `#1468 `__. Features -------- - Started building armv7l wheels for manylinux -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1495 `__. Contributor-facing changes -------------------------- - GitHub Actions CI/CD is now configured to manage caching pip-ecosystem dependencies using `re-actors/cache-python-deps`_ -- an action by `@webknjaz `__ that takes into account ABI stability and the exact version of Python runtime. .. _`re-actors/cache-python-deps`: https://github.com/marketplace/actions/cache-python-deps *Related issues and pull requests on GitHub:* `#1471 `__. - Increased minimum `propcache`_ version to 0.2.1 to fix failing tests -- by `@bdraco `__. .. _`propcache`: https://github.com/aio-libs/propcache *Related issues and pull requests on GitHub:* `#1479 `__. - Added all hidden folders to pytest's ``norecursedirs`` to prevent it from trying to collect tests there -- by `@lysnikolaou `__. *Related issues and pull requests on GitHub:* `#1480 `__. Miscellaneous internal changes ------------------------------ - Improved accuracy of type annotations -- by `@Dreamsorcerer `__. *Related issues and pull requests on GitHub:* `#1484 `__. - Improved performance of parsing query strings -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1493 `__, `#1497 `__. - Improved performance of the C unquoter -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1496 `__, `#1498 `__. ---- 1.18.3 ====== *(2024-12-01)* Bug fixes --------- - Fixed uppercase ASCII hosts being rejected by ``URL.build()()`` and ``yarl.URL.with_host()`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#954 `__, `#1442 `__. Miscellaneous internal changes ------------------------------ - Improved performances of multiple path properties on cache miss -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1443 `__. ---- 1.18.2 ====== *(2024-11-29)* No significant changes. ---- 1.18.1 ====== *(2024-11-29)* Miscellaneous internal changes ------------------------------ - Improved cache performance when ``~yarl.URL`` objects are constructed from ``yarl.URL.build()`` with ``encoded=True`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1432 `__. - Improved cache performance for operations that produce a new ``~yarl.URL`` object -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1434 `__, `#1436 `__. ---- 1.18.0 ====== *(2024-11-21)* Features -------- - Added ``keep_query`` and ``keep_fragment`` flags in the ``yarl.URL.with_path()``, ``yarl.URL.with_name()`` and ``yarl.URL.with_suffix()`` methods, allowing users to optionally retain the query string and fragment in the resulting URL when replacing the path -- by `@paul-nameless `__. *Related issues and pull requests on GitHub:* `#111 `__, `#1421 `__. Contributor-facing changes -------------------------- - Started running downstream ``aiohttp`` tests in CI -- by `@Cycloctane `__. *Related issues and pull requests on GitHub:* `#1415 `__. Miscellaneous internal changes ------------------------------ - Improved performance of converting ``~yarl.URL`` to a string -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1422 `__. ---- 1.17.2 ====== *(2024-11-17)* Bug fixes --------- - Stopped implicitly allowing the use of Cython pre-release versions when building the distribution package -- by `@ajsanchezsanz `__ and `@markgreene74 `__. *Related issues and pull requests on GitHub:* `#1411 `__, `#1412 `__. - Fixed a bug causing ``~yarl.URL.port`` to return the default port when the given port was zero -- by `@gmacon `__. *Related issues and pull requests on GitHub:* `#1413 `__. Features -------- - Make error messages include details of incorrect type when ``port`` is not int in ``yarl.URL.build()``. -- by `@Cycloctane `__. *Related issues and pull requests on GitHub:* `#1414 `__. Packaging updates and notes for downstreams ------------------------------------------- - Stopped implicitly allowing the use of Cython pre-release versions when building the distribution package -- by `@ajsanchezsanz `__ and `@markgreene74 `__. *Related issues and pull requests on GitHub:* `#1411 `__, `#1412 `__. Miscellaneous internal changes ------------------------------ - Improved performance of the ``yarl.URL.joinpath()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1418 `__. ---- 1.17.1 ====== *(2024-10-30)* Miscellaneous internal changes ------------------------------ - Improved performance of many ``~yarl.URL`` methods -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1396 `__, `#1397 `__, `#1398 `__. - Improved performance of passing a `dict` or `str` to ``yarl.URL.extend_query()`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1401 `__. ---- 1.17.0 ====== *(2024-10-28)* Features -------- - Added ``~yarl.URL.host_port_subcomponent`` which returns the ``3986#section-3.2.2`` host and ``3986#section-3.2.3`` port subcomponent -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1375 `__. ---- 1.16.0 ====== *(2024-10-21)* Bug fixes --------- - Fixed blocking I/O to load Python code when creating a new ``~yarl.URL`` with non-ascii characters in the network location part -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1342 `__. Removals and backward incompatible breaking changes --------------------------------------------------- - Migrated to using a single cache for encoding hosts -- by `@bdraco `__. Passing ``ip_address_size`` and ``host_validate_size`` to ``yarl.cache_configure()`` is deprecated in favor of the new ``encode_host_size`` parameter and will be removed in a future release. For backwards compatibility, the old parameters affect the ``encode_host`` cache size. *Related issues and pull requests on GitHub:* `#1348 `__, `#1357 `__, `#1363 `__. Miscellaneous internal changes ------------------------------ - Improved performance of constructing ``~yarl.URL`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1336 `__. - Improved performance of calling ``yarl.URL.build()`` and constructing unencoded ``~yarl.URL`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1345 `__. - Reworked the internal encoding cache to improve performance on cache hit -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1369 `__. ---- 1.15.5 ====== *(2024-10-18)* Miscellaneous internal changes ------------------------------ - Improved performance of the ``yarl.URL.joinpath()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1304 `__. - Improved performance of the ``yarl.URL.extend_query()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1305 `__. - Improved performance of the ``yarl.URL.origin()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1306 `__. - Improved performance of the ``yarl.URL.with_path()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1307 `__. - Improved performance of the ``yarl.URL.with_query()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1308 `__, `#1328 `__. - Improved performance of the ``yarl.URL.update_query()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1309 `__, `#1327 `__. - Improved performance of the ``yarl.URL.join()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1313 `__. - Improved performance of ``~yarl.URL`` equality checks -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1315 `__. - Improved performance of ``~yarl.URL`` methods that modify the network location -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1316 `__. - Improved performance of the ``yarl.URL.with_fragment()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1317 `__. - Improved performance of calculating the hash of ``~yarl.URL`` objects -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1318 `__. - Improved performance of the ``yarl.URL.relative()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1319 `__. - Improved performance of the ``yarl.URL.with_name()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1320 `__. - Improved performance of ``~yarl.URL.parent`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1321 `__. - Improved performance of the ``yarl.URL.with_scheme()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1322 `__. ---- 1.15.4 ====== *(2024-10-16)* Miscellaneous internal changes ------------------------------ - Improved performance of the quoter when all characters are safe -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1288 `__. - Improved performance of unquoting strings -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1292 `__, `#1293 `__. - Improved performance of calling ``yarl.URL.build()`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1297 `__. ---- 1.15.3 ====== *(2024-10-15)* Bug fixes --------- - Fixed ``yarl.URL.build()`` failing to validate paths must start with a ``/`` when passing ``authority`` -- by `@bdraco `__. The validation only worked correctly when passing ``host``. *Related issues and pull requests on GitHub:* `#1265 `__. Removals and backward incompatible breaking changes --------------------------------------------------- - Removed support for Python 3.8 as it has reached end of life -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1203 `__. Miscellaneous internal changes ------------------------------ - Improved performance of constructing ``~yarl.URL`` when the net location is only the host -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1271 `__. ---- 1.15.2 ====== *(2024-10-13)* Miscellaneous internal changes ------------------------------ - Improved performance of converting ``~yarl.URL`` to a string -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1234 `__. - Improved performance of ``yarl.URL.joinpath()`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1248 `__, `#1250 `__. - Improved performance of constructing query strings from ``~multidict.MultiDict`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1256 `__. - Improved performance of constructing query strings with ``int`` values -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1259 `__. ---- 1.15.1 ====== *(2024-10-12)* Miscellaneous internal changes ------------------------------ - Improved performance of calling ``yarl.URL.build()`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1222 `__. - Improved performance of all ``~yarl.URL`` methods that create new ``~yarl.URL`` objects -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1226 `__. - Improved performance of ``~yarl.URL`` methods that modify the network location -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1229 `__. ---- 1.15.0 ====== *(2024-10-11)* Bug fixes --------- - Fixed validation with ``yarl.URL.with_scheme()`` when passed scheme is not lowercase -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1189 `__. Features -------- - Started building ``armv7l`` wheels -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1204 `__. Miscellaneous internal changes ------------------------------ - Improved performance of constructing unencoded ``~yarl.URL`` objects -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1188 `__. - Added a cache for parsing hosts to reduce overhead of encoding ``~yarl.URL`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1190 `__. - Improved performance of constructing query strings from ``~collections.abc.Mapping`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1193 `__. - Improved performance of converting ``~yarl.URL`` objects to strings -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1198 `__. ---- 1.14.0 ====== *(2024-10-08)* Packaging updates and notes for downstreams ------------------------------------------- - Switched to using the ``propcache`` package for property caching -- by `@bdraco `__. The ``propcache`` package is derived from the property caching code in ``yarl`` and has been broken out to avoid maintaining it for multiple projects. *Related issues and pull requests on GitHub:* `#1169 `__. Contributor-facing changes -------------------------- - Started testing with Hypothesis -- by `@webknjaz `__ and `@bdraco `__. Special thanks to `@Zac-HD `__ for helping us get started with this framework. *Related issues and pull requests on GitHub:* `#860 `__. Miscellaneous internal changes ------------------------------ - Improved performance of ``yarl.URL.is_default_port()`` when no explicit port is set -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1168 `__. - Improved performance of converting ``~yarl.URL`` to a string when no explicit port is set -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1170 `__. - Improved performance of the ``yarl.URL.origin()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1175 `__. - Improved performance of encoding hosts -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1176 `__. ---- 1.13.1 ====== *(2024-09-27)* Miscellaneous internal changes ------------------------------ - Improved performance of calling ``yarl.URL.build()`` with ``authority`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1163 `__. ---- 1.13.0 ====== *(2024-09-26)* Bug fixes --------- - Started rejecting ASCII hostnames with invalid characters. For host strings that look like authority strings, the exception message includes advice on what to do instead -- by `@mjpieters `__. *Related issues and pull requests on GitHub:* `#880 `__, `#954 `__. - Fixed IPv6 addresses missing brackets when the ``~yarl.URL`` was converted to a string -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1157 `__, `#1158 `__. Features -------- - Added ``~yarl.URL.host_subcomponent`` which returns the ``3986#section-3.2.2`` host subcomponent -- by `@bdraco `__. The only current practical difference between ``~yarl.URL.raw_host`` and ``~yarl.URL.host_subcomponent`` is that IPv6 addresses are returned bracketed. *Related issues and pull requests on GitHub:* `#1159 `__. ---- 1.12.1 ====== *(2024-09-23)* No significant changes. ---- 1.12.0 ====== *(2024-09-23)* Features -------- - Added ``~yarl.URL.path_safe`` to be able to fetch the path without ``%2F`` and ``%25`` decoded -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1150 `__. Removals and backward incompatible breaking changes --------------------------------------------------- - Restore decoding ``%2F`` (``/``) in ``URL.path`` -- by `@bdraco `__. This change restored the behavior before `#1057 `__. *Related issues and pull requests on GitHub:* `#1151 `__. Miscellaneous internal changes ------------------------------ - Improved performance of processing paths -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1143 `__. ---- 1.11.1 ====== *(2024-09-09)* Bug fixes --------- - Allowed scheme replacement for relative URLs if the scheme does not require a host -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#280 `__, `#1138 `__. - Allowed empty host for URL schemes other than the special schemes listed in the WHATWG URL spec -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1136 `__. Features -------- - Loosened restriction on integers as query string values to allow classes that implement ``__int__`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1139 `__. Miscellaneous internal changes ------------------------------ - Improved performance of normalizing paths -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1137 `__. ---- 1.11.0 ====== *(2024-09-08)* Features -------- - Added ``URL.extend_query()()`` method, which can be used to extend parameters without replacing same named keys -- by `@bdraco `__. This method was primarily added to replace the inefficient hand rolled method currently used in ``aiohttp``. *Related issues and pull requests on GitHub:* `#1128 `__. Miscellaneous internal changes ------------------------------ - Improved performance of the Cython ``cached_property`` implementation -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1122 `__. - Simplified computing ports by removing unnecessary code -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1123 `__. - Improved performance of encoding non IPv6 hosts -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1125 `__. - Improved performance of ``URL.build()()`` when the path, query string, or fragment is an empty string -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1126 `__. - Improved performance of the ``URL.update_query()()`` method -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1130 `__. - Improved performance of processing query string changes when arguments are ``str`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1131 `__. ---- 1.10.0 ====== *(2024-09-06)* Bug fixes --------- - Fixed joining a path when the existing path was empty -- by `@bdraco `__. A regression in ``URL.join()()`` was introduced in `#1082 `__. *Related issues and pull requests on GitHub:* `#1118 `__. Features -------- - Added ``URL.without_query_params()()`` method, to drop some parameters from query string -- by `@hongquan `__. *Related issues and pull requests on GitHub:* `#774 `__, `#898 `__, `#1010 `__. - The previously protected types ``_SimpleQuery``, ``_QueryVariable``, and ``_Query`` are now available for use externally as ``SimpleQuery``, ``QueryVariable``, and ``Query`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1050 `__, `#1113 `__. Contributor-facing changes -------------------------- - Replaced all ``~typing.Optional`` with ``~typing.Union`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1095 `__. Miscellaneous internal changes ------------------------------ - Significantly improved performance of parsing the network location -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1112 `__. - Added internal types to the cache to prevent future refactoring errors -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1117 `__. ---- 1.9.11 ====== *(2024-09-04)* Bug fixes --------- - Fixed a ``TypeError`` with ``MultiDictProxy`` and Python 3.8 -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1084 `__, `#1105 `__, `#1107 `__. Miscellaneous internal changes ------------------------------ - Improved performance of encoding hosts -- by `@bdraco `__. Previously, the library would unconditionally try to parse a host as an IP Address. The library now avoids trying to parse a host as an IP Address if the string is not in one of the formats described in ``3986#section-3.2.2``. *Related issues and pull requests on GitHub:* `#1104 `__. ---- 1.9.10 ====== *(2024-09-04)* Bug fixes --------- - ``URL.join()()`` has been changed to match ``3986`` and align with ``/ operation()`` and ``URL.joinpath()()`` when joining URLs with empty segments. Previously ``urllib.parse.urljoin`` was used, which has known issues with empty segments (`python/cpython#84774 `_). Due to the semantics of ``URL.join()()``, joining an URL with scheme requires making it relative, prefixing with ``./``. .. code-block:: pycon >>> URL("https://web.archive.org/web/").join(URL("./https://github.com/aio-libs/yarl")) URL('https://web.archive.org/web/https://github.com/aio-libs/yarl') Empty segments are honored in the base as well as the joined part. .. code-block:: pycon >>> URL("https://web.archive.org/web/https://").join(URL("github.com/aio-libs/yarl")) URL('https://web.archive.org/web/https://github.com/aio-libs/yarl') -- by `@commonism `__ This change initially appeared in 1.9.5 but was reverted in 1.9.6 to resolve a problem with query string handling. *Related issues and pull requests on GitHub:* `#1039 `__, `#1082 `__. Features -------- - Added ``~yarl.URL.absolute`` which is now preferred over ``URL.is_absolute()`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1100 `__. ---- 1.9.9 ===== *(2024-09-04)* Bug fixes --------- - Added missing type on ``~yarl.URL.port`` -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1097 `__. ---- 1.9.8 ===== *(2024-09-03)* Features -------- - Covered the ``~yarl.URL`` object with types -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1084 `__. - Cache parsing of IP Addresses when encoding hosts -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1086 `__. Contributor-facing changes -------------------------- - Covered the ``~yarl.URL`` object with types -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1084 `__. Miscellaneous internal changes ------------------------------ - Improved performance of handling ports -- by `@bdraco `__. *Related issues and pull requests on GitHub:* `#1081 `__. ---- 1.9.7 ===== *(2024-09-01)* Removals and backward incompatible breaking changes --------------------------------------------------- - Removed support ``3986#section-3.2.3`` port normalization when the scheme is not one of ``http``, ``https``, ``wss``, or ``ws`` -- by `@bdraco `__. Support for port normalization was recently added in `#1033 `__ and contained code that would do blocking I/O if the scheme was not one of the four listed above. The code has been removed because this library is intended to be safe for usage with ``asyncio``. *Related issues and pull requests on GitHub:* `#1076 `__. Miscellaneous internal changes ------------------------------ - Improved performance of property caching -- by `@bdraco `__. The ``reify`` implementation from ``aiohttp`` was adapted to replace the internal ``cached_property`` implementation. *Related issues and pull requests on GitHub:* `#1070 `__. ---- 1.9.6 ===== *(2024-08-30)* Bug fixes --------- - Reverted ``3986`` compatible ``URL.join()()`` honoring empty segments which was introduced in `#1039 `__. This change introduced a regression handling query string parameters with joined URLs. The change was reverted to maintain compatibility with the previous behavior. *Related issues and pull requests on GitHub:* `#1067 `__. ---- 1.9.5 ===== *(2024-08-30)* Bug fixes --------- - Joining URLs with empty segments has been changed to match ``3986``. Previously empty segments would be removed from path, breaking use-cases such as .. code-block:: python URL("https://web.archive.org/web/") / "https://github.com/" Now ``/ operation()`` and ``URL.joinpath()()`` keep empty segments, but do not introduce new empty segments. e.g. .. code-block:: python URL("https://example.org/") / "" does not introduce an empty segment. -- by `@commonism `__ and `@youtux `__ *Related issues and pull requests on GitHub:* `#1026 `__. - The default protocol ports of well-known URI schemes are now taken into account during the normalization of the URL string representation in accordance with ``3986#section-3.2.3``. Specified ports are removed from the ``str`` representation of a ``~yarl.URL`` if the port matches the scheme's default port -- by `@commonism `__. *Related issues and pull requests on GitHub:* `#1033 `__. - ``URL.join()()`` has been changed to match ``3986`` and align with ``/ operation()`` and ``URL.joinpath()()`` when joining URLs with empty segments. Previously ``urllib.parse.urljoin`` was used, which has known issues with empty segments (`python/cpython#84774 `_). Due to the semantics of ``URL.join()()``, joining an URL with scheme requires making it relative, prefixing with ``./``. .. code-block:: pycon >>> URL("https://web.archive.org/web/").join(URL("./https://github.com/aio-libs/yarl")) URL('https://web.archive.org/web/https://github.com/aio-libs/yarl') Empty segments are honored in the base as well as the joined part. .. code-block:: pycon >>> URL("https://web.archive.org/web/https://").join(URL("github.com/aio-libs/yarl")) URL('https://web.archive.org/web/https://github.com/aio-libs/yarl') -- by `@commonism `__ *Related issues and pull requests on GitHub:* `#1039 `__. Removals and backward incompatible breaking changes --------------------------------------------------- - Stopped decoding ``%2F`` (``/``) in ``URL.path``, as this could lead to code incorrectly treating it as a path separator -- by `@Dreamsorcerer `__. *Related issues and pull requests on GitHub:* `#1057 `__. - Dropped support for Python 3.7 -- by `@Dreamsorcerer `__. *Related issues and pull requests on GitHub:* `#1016 `__. Improved documentation ---------------------- - On the ``Contributing docs`` page, a link to the ``Towncrier philosophy`` has been fixed. *Related issues and pull requests on GitHub:* `#981 `__. - The pre-existing ``/ magic method()`` has been documented in the API reference -- by `@commonism `__. *Related issues and pull requests on GitHub:* `#1026 `__. Packaging updates and notes for downstreams ------------------------------------------- - A flaw in the logic for copying the project directory into a temporary folder that led to infinite recursion when ``TMPDIR`` was set to a project subdirectory path. This was happening in Fedora and its downstream due to the use of `pyproject-rpm-macros `__. It was only reproducible with ``pip wheel`` and was not affecting the ``pyproject-build`` users. -- by `@hroncok `__ and `@webknjaz `__ *Related issues and pull requests on GitHub:* `#992 `__, `#1014 `__. - Support Python 3.13 and publish non-free-threaded wheels *Related issues and pull requests on GitHub:* `#1054 `__. Contributor-facing changes -------------------------- - The CI/CD setup has been updated to test ``arm64`` wheels under macOS 14, except for Python 3.7 that is unsupported in that environment -- by `@webknjaz `__. *Related issues and pull requests on GitHub:* `#1015 `__. - Removed unused type ignores and casts -- by `@hauntsaninja `__. *Related issues and pull requests on GitHub:* `#1031 `__. Miscellaneous internal changes ------------------------------ - ``port``, ``scheme``, and ``raw_host`` are now ``cached_property`` -- by `@bdraco `__. ``aiohttp`` accesses these properties quite often, which cause ``urllib`` to build the ``_hostinfo`` property every time. ``port``, ``scheme``, and ``raw_host`` are now cached properties, which will improve performance. *Related issues and pull requests on GitHub:* `#1044 `__, `#1058 `__. ---- 1.9.4 (2023-12-06) ================== Bug fixes --------- - Started raising ``TypeError`` when a string value is passed into ``yarl.URL.build()`` as the ``port`` argument -- by `@commonism `__. Previously the empty string as port would create malformed URLs when rendered as string representations. (`#883 `__) Packaging updates and notes for downstreams ------------------------------------------- - The leading ``--`` has been dropped from the `PEP 517 `__ in-tree build backend config setting names. ``--pure-python`` is now just ``pure-python`` -- by `@webknjaz `__. The usage now looks as follows: .. code-block:: console $ python -m build \ --config-setting=pure-python=true \ --config-setting=with-cython-tracing=true (`#963 `__) Contributor-facing changes -------------------------- - A step-by-step ``Release Guide`` guide has been added, describing how to release *yarl* -- by `@webknjaz `__. This is primarily targeting maintainers. (`#960 `__) - Coverage collection has been implemented for the Cython modules -- by `@webknjaz `__. It will also be reported to Codecov from any non-release CI jobs. To measure coverage in a development environment, *yarl* can be installed in editable mode: .. code-block:: console $ python -Im pip install -e . Editable install produces C-files required for the Cython coverage plugin to map the measurements back to the PYX-files. `#961 `__ - It is now possible to request line tracing in Cython builds using the ``with-cython-tracing`` `PEP 517 `__ config setting -- `@webknjaz `__. This can be used in CI and development environment to measure coverage on Cython modules, but is not normally useful to the end-users or downstream packagers. Here's a usage example: .. code-block:: console $ python -Im pip install . --config-settings=with-cython-tracing=true For editable installs, this setting is on by default. Otherwise, it's off unless requested explicitly. The following produces C-files required for the Cython coverage plugin to map the measurements back to the PYX-files: .. code-block:: console $ python -Im pip install -e . Alternatively, the ``YARL_CYTHON_TRACING=1`` environment variable can be set to do the same as the `PEP 517 `__ config setting. `#962 `__ 1.9.3 (2023-11-20) ================== Bug fixes --------- - Stopped dropping trailing slashes in ``yarl.URL.joinpath()`` -- by `@gmacon `__. (`#862 `__, `#866 `__) - Started accepting string subclasses in ``yarl.URL.__truediv__()`` operations (``URL / segment``) -- by `@mjpieters `__. (`#871 `__, `#884 `__) - Fixed the human representation of URLs with square brackets in usernames and passwords -- by `@mjpieters `__. (`#876 `__, `#882 `__) - Updated type hints to include ``URL.missing_port()``, ``URL.__bytes__()`` and the ``encoding`` argument to ``yarl.URL.joinpath()`` -- by `@mjpieters `__. (`#891 `__) Packaging updates and notes for downstreams ------------------------------------------- - Integrated Cython 3 to enable building *yarl* under Python 3.12 -- by `@mjpieters `__. (`#829 `__, `#881 `__) - Declared modern ``setuptools.build_meta`` as the `PEP 517 `__ build backend in ``pyproject.toml`` explicitly -- by `@webknjaz `__. (`#886 `__) - Converted most of the packaging setup into a declarative ``setup.cfg`` config -- by `@webknjaz `__. (`#890 `__) - The packaging is replaced from an old-fashioned ``setup.py`` to an in-tree `PEP 517 `__ build backend -- by `@webknjaz `__. Whenever the end-users or downstream packagers need to build ``yarl`` from source (a Git checkout or an sdist), they may pass a ``config_settings`` flag ``--pure-python``. If this flag is not set, a C-extension will be built and included into the distribution. Here is how this can be done with ``pip``: .. code-block:: console $ python -m pip install . --config-settings=--pure-python=false This will also work with ``-e | --editable``. The same can be achieved via ``pypa/build``: .. code-block:: console $ python -m build --config-setting=--pure-python=false Adding ``-w | --wheel`` can force ``pypa/build`` produce a wheel from source directly, as opposed to building an ``sdist`` and then building from it. (`#893 `__) .. attention:: v1.9.3 was the only version using the ``--pure-python`` setting name. Later versions dropped the ``--`` prefix, making it just ``pure-python``. - Declared Python 3.12 supported officially in the distribution package metadata -- by `@edgarrmondragon `__. (`#942 `__) Contributor-facing changes -------------------------- - A regression test for no-host URLs was added per `#821 `__ and ``3986`` -- by `@kenballus `__. (`#821 `__, `#822 `__) - Started testing *yarl* against Python 3.12 in CI -- by `@mjpieters `__. (`#881 `__) - All Python 3.12 jobs are now marked as required to pass in CI -- by `@edgarrmondragon `__. (`#942 `__) - MyST is now integrated in Sphinx -- by `@webknjaz `__. This allows the contributors to author new documents in Markdown when they have difficulties with going straight RST. (`#953 `__) 1.9.2 (2023-04-25) ================== Bugfixes -------- - Fix regression with ``yarl.URL.__truediv__()`` and absolute URLs with empty paths causing the raw path to lack the leading ``/``. (`#854 `_) 1.9.1 (2023-04-21) ================== Bugfixes -------- - Marked tests that fail on older Python patch releases (< 3.7.10, < 3.8.8 and < 3.9.2) as expected to fail due to missing a security fix for CVE-2021-23336. (`#850 `_) 1.9.0 (2023-04-19) ================== This release was never published to PyPI, due to issues with the build process. Features -------- - Added ``URL.joinpath(*elements)``, to create a new URL appending multiple path elements. (`#704 `_) - Made ``URL.__truediv__()()`` return ``NotImplemented`` if called with an unsupported type — by `@michaeljpeters `__. (`#832 `_) Bugfixes -------- - Path normalization for absolute URLs no longer raises a ValueError exception when ``..`` segments would otherwise go beyond the URL path root. (`#536 `_) - Fixed an issue with update_query() not getting rid of the query when argument is None. (`#792 `_) - Added some input restrictions on with_port() function to prevent invalid boolean inputs or out of valid port inputs; handled incorrect 0 port representation. (`#793 `_) - Made ``yarl.URL.build()`` raise a ``TypeError`` if the ``host`` argument is ``None`` — by `@paulpapacz `__. (`#808 `_) - Fixed an issue with ``update_query()`` getting rid of the query when the argument is empty but not ``None``. (`#845 `_) Misc ---- - `#220 `_ 1.8.2 (2022-12-03) ================== This is the first release that started shipping wheels for Python 3.11. 1.8.1 (2022-08-01) ================== Misc ---- - `#694 `_, `#699 `_, `#700 `_, `#701 `_, `#702 `_, `#703 `_, `#739 `_ 1.8.0 (2022-08-01) ================== Features -------- - Added ``URL.raw_suffix``, ``URL.suffix``, ``URL.raw_suffixes``, ``URL.suffixes``, ``URL.with_suffix``. (`#613 `_) Improved Documentation ---------------------- - Fixed broken internal references to ``yarl.URL.human_repr()``. (`#665 `_) - Fixed broken external references to ``multidict:index`` docs. (`#665 `_) Deprecations and Removals ------------------------- - Dropped Python 3.6 support. (`#672 `_) Misc ---- - `#646 `_, `#699 `_, `#701 `_ 1.7.2 (2021-11-01) ================== Bugfixes -------- - Changed call in ``with_port()`` to stop reencoding parts of the URL that were already encoded. (`#623 `_) 1.7.1 (2021-10-07) ================== Bugfixes -------- - Fix 1.7.0 build error 1.7.0 (2021-10-06) ================== Features -------- - Add ``__bytes__()`` magic method so that ``bytes(url)`` will work and use optimal ASCII encoding. (`#582 `_) - Started shipping platform-specific arm64 wheels for Apple Silicon. (`#622 `_) - Started shipping platform-specific wheels with the ``musl`` tag targeting typical Alpine Linux runtimes. (`#622 `_) - Added support for Python 3.10. (`#622 `_) 1.6.3 (2020-11-14) ================== Bugfixes -------- - No longer loose characters when decoding incorrect percent-sequences (like ``%e2%82%f8``). All non-decodable percent-sequences are now preserved. `#517 `_ - Provide x86 Windows wheels. `#535 `_ ---- 1.6.2 (2020-10-12) ================== Bugfixes -------- - Provide generated ``.c`` files in TarBall distribution. `#530 `_ 1.6.1 (2020-10-12) ================== Features -------- - Provide wheels for ``aarch64``, ``i686``, ``ppc64le``, ``s390x`` architectures on Linux as well as ``x86_64``. `#507 `_ - Provide wheels for Python 3.9. `#526 `_ Bugfixes -------- - ``human_repr()`` now always produces valid representation equivalent to the original URL (if the original URL is valid). `#511 `_ - Fixed requoting a single percent followed by a percent-encoded character in the Cython implementation. `#514 `_ - Fix ValueError when decoding ``%`` which is not followed by two hexadecimal digits. `#516 `_ - Fix decoding ``%`` followed by a space and hexadecimal digit. `#520 `_ - Fix annotation of ``with_query()``/``update_query()`` methods for ``key=[val1, val2]`` case. `#528 `_ Removal ------- - Drop Python 3.5 support; Python 3.6 is the minimal supported Python version. ---- 1.6.0 (2020-09-23) ================== Features -------- - Allow for int and float subclasses in query, while still denying bool. `#492 `_ Bugfixes -------- - Do not requote arguments in ``URL.build()``, ``with_xxx()`` and in ``/`` operator. `#502 `_ - Keep IPv6 brackets in ``origin()``. `#504 `_ ---- 1.5.1 (2020-08-01) ================== Bugfixes -------- - Fix including relocated internal ``yarl._quoting_c`` C-extension into published PyPI dists. `#485 `_ Misc ---- - `#484 `_ ---- 1.5.0 (2020-07-26) ================== Features -------- - Convert host to lowercase on URL building. `#386 `_ - Allow using ``mod`` operator (``%``) for updating query string (an alias for ``update_query()`` method). `#435 `_ - Allow use of sequences such as ``list`` and ``tuple`` in the values of a mapping such as ``dict`` to represent that a key has many values:: url = URL("http://example.com") assert url.with_query({"a": [1, 2]}) == URL("http://example.com/?a=1&a=2") `#443 `_ - Support ``URL.build()`` with scheme and path (creates a relative URL). `#464 `_ - Cache slow IDNA encode/decode calls. `#476 `_ - Add ``@final`` / ``Final`` type hints `#477 `_ - Support URL authority/raw_authority properties and authority argument of ``URL.build()`` method. `#478 `_ - Hide the library implementation details, make the exposed public list very clean. `#483 `_ Bugfixes -------- - Fix tests with newer Python (3.7.6, 3.8.1 and 3.9.0+). `#409 `_ - Fix a bug where query component, passed in a form of mapping or sequence, is unquoted in unexpected way. `#426 `_ - Hide ``Query`` and ``QueryVariable`` type aliases in ``__init__.pyi``, now they are prefixed with underscore. `#431 `_ - Keep IPv6 brackets after updating port/user/password. `#451 `_ ---- 1.4.2 (2019-12-05) ================== Features -------- - Workaround for missing ``str.isascii()`` in Python 3.6 `#389 `_ ---- 1.4.1 (2019-11-29) ================== * Fix regression, make the library work on Python 3.5 and 3.6 again. 1.4.0 (2019-11-29) ================== * Distinguish an empty password in URL from a password not provided at all (#262) * Fixed annotations for optional parameters of ``URL.build`` (#309) * Use None as default value of ``user`` parameter of ``URL.build`` (#309) * Enforce building C Accelerated modules when installing from source tarball, use ``YARL_NO_EXTENSIONS`` environment variable for falling back to (slower) Pure Python implementation (#329) * Drop Python 3.5 support * Fix quoting of plus in path by pure python version (#339) * Don't create a new URL if fragment is unchanged (#292) * Included in error message the path that produces starting slash forbidden error (#376) * Skip slow IDNA encoding for ASCII-only strings (#387) 1.3.0 (2018-12-11) ================== * Fix annotations for ``query`` parameter (#207) * An incoming query sequence can have int variables (the same as for Mapping type) (#208) * Add ``URL.explicit_port`` property (#218) * Give a friendlier error when port can't be converted to int (#168) * ``bool(URL())`` now returns ``False`` (#272) 1.2.6 (2018-06-14) ================== * Drop Python 3.4 trove classifier (#205) 1.2.5 (2018-05-23) ================== * Fix annotations for ``build`` (#199) 1.2.4 (2018-05-08) ================== * Fix annotations for ``cached_property`` (#195) 1.2.3 (2018-05-03) ================== * Accept ``str`` subclasses in ``URL`` constructor (#190) 1.2.2 (2018-05-01) ================== * Fix build 1.2.1 (2018-04-30) ================== * Pin minimal required Python to 3.5.3 (#189) 1.2.0 (2018-04-30) ================== * Forbid inheritance, replace ``__init__`` with ``__new__`` (#171) * Support PEP-561 (provide type hinting marker) (#182) 1.1.1 (2018-02-17) ================== * Fix performance regression: don't encode empty ``netloc`` (#170) 1.1.0 (2018-01-21) ================== * Make pure Python quoter consistent with Cython version (#162) 1.0.0 (2018-01-15) ================== * Use fast path if quoted string does not need requoting (#154) * Speed up quoting/unquoting by ``_Quoter`` and ``_Unquoter`` classes (#155) * Drop ``yarl.quote`` and ``yarl.unquote`` public functions (#155) * Add custom string writer, reuse static buffer if available (#157) Code is 50-80 times faster than Pure Python version (was 4-5 times faster) * Don't recode IP zone (#144) * Support ``encoded=True`` in ``yarl.URL.build()`` (#158) * Fix updating query with multiple keys (#160) 0.18.0 (2018-01-10) =================== * Fallback to IDNA 2003 if domain name is not IDNA 2008 compatible (#152) 0.17.0 (2017-12-30) =================== * Use IDNA 2008 for domain name processing (#149) 0.16.0 (2017-12-07) =================== * Fix raising ``TypeError`` by ``url.query_string()`` after ``url.with_query({})`` (empty mapping) (#141) 0.15.0 (2017-11-23) =================== * Add ``raw_path_qs`` attribute (#137) 0.14.2 (2017-11-14) =================== * Restore ``strict`` parameter as no-op in ``quote`` / ``unquote`` 0.14.1 (2017-11-13) =================== * Restore ``strict`` parameter as no-op for sake of compatibility with aiohttp 2.2 0.14.0 (2017-11-11) =================== * Drop strict mode (#123) * Fix ``"ValueError: Unallowed PCT %"`` when there's a ``"%"`` in the URL (#124) 0.13.0 (2017-10-01) =================== * Document ``encoded`` parameter (#102) * Support relative URLs like ``'?key=value'`` (#100) * Unsafe encoding for QS fixed. Encode ``;`` character in value parameter (#104) * Process passwords without user names (#95) 0.12.0 (2017-06-26) =================== * Properly support paths without leading slash in ``URL.with_path()`` (#90) * Enable type annotation checks 0.11.0 (2017-06-26) =================== * Normalize path (#86) * Clear query and fragment parts in ``.with_path()`` (#85) 0.10.3 (2017-06-13) =================== * Prevent double URL arguments unquoting (#83) 0.10.2 (2017-05-05) =================== * Unexpected hash behavior (#75) 0.10.1 (2017-05-03) =================== * Unexpected compare behavior (#73) * Do not quote or unquote + if not a query string. (#74) 0.10.0 (2017-03-14) =================== * Added ``URL.build`` class method (#58) * Added ``path_qs`` attribute (#42) 0.9.8 (2017-02-16) ================== * Do not quote ``:`` in path 0.9.7 (2017-02-16) ================== * Load from pickle without _cache (#56) * Percent-encoded pluses in path variables become spaces (#59) 0.9.6 (2017-02-15) ================== * Revert backward incompatible change (BaseURL) 0.9.5 (2017-02-14) ================== * Fix BaseURL rich comparison support 0.9.4 (2017-02-14) ================== * Use BaseURL 0.9.3 (2017-02-14) ================== * Added BaseURL 0.9.2 (2017-02-08) ================== * Remove debug print 0.9.1 (2017-02-07) ================== * Do not lose tail chars (#45) 0.9.0 (2017-02-07) ================== * Allow to quote ``%`` in non strict mode (#21) * Incorrect parsing of query parameters with %3B (;) inside (#34) * Fix core dumps (#41) * ``tmpbuf`` - compiling error (#43) * Added ``URL.update_path()`` method * Added ``URL.update_query()`` method (#47) 0.8.1 (2016-12-03) ================== * Fix broken aiohttp: revert back ``quote`` / ``unquote``. 0.8.0 (2016-12-03) ================== * Support more verbose error messages in ``.with_query()`` (#24) * Don't percent-encode ``@`` and ``:`` in path (#32) * Don't expose ``yarl.quote`` and ``yarl.unquote``, these functions are part of private API 0.7.1 (2016-11-18) ================== * Accept not only ``str`` but all classes inherited from ``str`` also (#25) 0.7.0 (2016-11-07) ================== * Accept ``int`` as value for ``.with_query()`` 0.6.0 (2016-11-07) ================== * Explicitly use UTF8 encoding in ``setup.py`` (#20) * Properly unquote non-UTF8 strings (#19) 0.5.3 (2016-11-02) ================== * Don't use ``typing.NamedTuple`` fields but indexes on URL construction 0.5.2 (2016-11-02) ================== * Inline ``_encode`` class method 0.5.1 (2016-11-02) ================== * Make URL construction faster by removing extra classmethod calls 0.5.0 (2016-11-02) ================== * Add Cython optimization for quoting/unquoting * Provide binary wheels 0.4.3 (2016-09-29) ================== * Fix typing stubs 0.4.2 (2016-09-29) ================== * Expose ``quote()`` and ``unquote()`` as public API 0.4.1 (2016-09-28) ================== * Support empty values in query (``'/path?arg'``) 0.4.0 (2016-09-27) ================== * Introduce ``relative()`` (#16) 0.3.2 (2016-09-27) ================== * Typo fixes #15 0.3.1 (2016-09-26) ================== * Support sequence of pairs as ``with_query()`` parameter 0.3.0 (2016-09-26) ================== * Introduce ``is_default_port()`` 0.2.1 (2016-09-26) ================== * Raise ValueError for URLs like 'http://:8080/' 0.2.0 (2016-09-18) ================== * Avoid doubling slashes when joining paths (#13) * Appending path starting from slash is forbidden (#12) 0.1.4 (2016-09-09) ================== * Add ``kwargs`` support for ``with_query()`` (#10) 0.1.3 (2016-09-07) ================== * Document ``with_query()``, ``with_fragment()`` and ``origin()`` * Allow ``None`` for ``with_query()`` and ``with_fragment()`` 0.1.2 (2016-09-07) ================== * Fix links, tune docs theme. 0.1.1 (2016-09-06) ================== * Update README, old version used obsolete API 0.1.0 (2016-09-06) ================== * The library was deeply refactored, bytes are gone away but all accepted strings are encoded if needed. 0.0.1 (2016-08-30) ================== * The first release. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903941.0 yarl-1.19.0/yarl.egg-info/SOURCES.txt0000644000175100001660000000310214774356305016513 0ustar00runnerdocker.coveragerc CHANGES.rst LICENSE MANIFEST.in NOTICE README.rst pyproject.toml pytest.ini setup.cfg towncrier.toml CHANGES/.TEMPLATE.rst CHANGES/.gitignore CHANGES/README.rst docs/Makefile docs/api.rst docs/changes.rst docs/conf.py docs/index.rst docs/make.bat docs/spelling_wordlist.txt docs/yarl-icon-128x128.xcf docs/_static/yarl-icon-128x128.png docs/contributing/guidelines.rst docs/contributing/release_guide.rst packaging/README.md packaging/pep517_backend/__init__.py packaging/pep517_backend/__main__.py packaging/pep517_backend/_backend.py packaging/pep517_backend/_compat.py packaging/pep517_backend/_cython_configuration.py packaging/pep517_backend/_transformers.py packaging/pep517_backend/cli.py packaging/pep517_backend/hooks.py requirements/cython.txt requirements/dev.txt requirements/doc-spelling.txt requirements/doc.txt requirements/lint.txt requirements/test.txt requirements/towncrier.txt tests/test_cache.py tests/test_cached_property.py tests/test_normalize_path.py tests/test_pickle.py tests/test_quoting.py tests/test_quoting_benchmarks.py tests/test_update_query.py tests/test_url.py tests/test_url_benchmarks.py tests/test_url_build.py tests/test_url_cmp_and_hash.py tests/test_url_parsing.py tests/test_url_query.py tests/test_url_update_netloc.py yarl/__init__.py yarl/_parse.py yarl/_path.py yarl/_query.py yarl/_quoters.py yarl/_quoting.py yarl/_quoting_c.pyx yarl/_quoting_py.py yarl/_url.py yarl/py.typed yarl.egg-info/PKG-INFO yarl.egg-info/SOURCES.txt yarl.egg-info/dependency_links.txt yarl.egg-info/not-zip-safe yarl.egg-info/requires.txt yarl.egg-info/top_level.txt././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903941.0 yarl-1.19.0/yarl.egg-info/dependency_links.txt0000644000175100001660000000000114774356305020701 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903941.0 yarl-1.19.0/yarl.egg-info/not-zip-safe0000644000175100001660000000000114774356305017061 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903941.0 yarl-1.19.0/yarl.egg-info/requires.txt0000644000175100001660000000005214774356305017230 0ustar00runnerdockeridna>=2.0 multidict>=4.0 propcache>=0.2.1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743903941.0 yarl-1.19.0/yarl.egg-info/top_level.txt0000644000175100001660000000000514774356305017360 0ustar00runnerdockeryarl