Repository: aio-libs/yarl

Issue:
Regression: Trailing slash for yarl.URL ("http://localhost") / "path" / "" is dropped in 1.9.4
### Describe the bug

yarl 1.8.2 allowed to construct URLs with trailing slashes like that:
```
>>> str(yarl.URL ("http://localhost") / "path" / "") 
'http://localhost/path/'
```

yarl 1.9.4 drops the slash from that path. Possibly related to https://github.com/aio-libs/yarl/issues/862 :
```
>>> str(yarl.URL ("http://localhost") / "path" / "") 
'http://localhost/path'
```



### To Reproduce

Try 1.8.2 vs 1.9.4 with the code above.

### Expected behavior

Preserve the slash

### Logs/tracebacks

```python-traceback
-
```


### Python Version

```console
Python 3.11.6
```


### multidict Version

```console
$ python -m pip show multidict
Name: multidict
Version: 6.0.4
Summary: multidict implementation
Home-page: https://github.com/aio-libs/multidict
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache 2
Requires: 
Required-by: aiohttp, yarl
```


### yarl Version

```console
$ python -m pip show yarl
Name: yarl
Version: 1.9.4
Summary: Yet another URL library
Home-page: https://github.com/aio-libs/yarl
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache-2.0
Location: /home/alex/.local/share/virtualenvs/widget.backend-6HfWXuQ_/lib/python3.11/site-packages
Requires: idna, multidict
Required-by: aiohttp
```


### OS

Ubuntu 23.10

### Additional context

_No response_

### Code of Conduct

- [X] I agree to follow the aio-libs Code of Conduct

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aio-libs/yarl/pull/1026?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) Report
Attention: Patch coverage is `76.08696%` with `11 lines` in your changes missing coverage. Please review.
> Project coverage is 62.00%. Comparing base [(`7bca6cf`)](https://app.codecov.io/gh/aio-libs/yarl/commit/7bca6cf1ba3673018b9065c6102f791a0a7516c0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) to head [(`5ed39cb`)](https://app.codecov.io/gh/aio-libs/yarl/commit/5ed39cb51a90d0bc539f8e02f047456dcf439c0b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).

| [Files](https://app.codecov.io/gh/aio-libs/yarl/pull/1026?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | Patch % | Lines |
|---|---|---|
| [tests/test\_url.py](https://app.codecov.io/gh/aio-libs/yarl/pull/1026?src=pr&el=tree&filepath=tests%2Ftest_url.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-dGVzdHMvdGVzdF91cmwucHk=) | 73.33% | [8 Missing :warning: ](https://app.codecov.io/gh/aio-libs/yarl/pull/1026?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) |
| [yarl/\_url.py](https://app.codecov.io/gh/aio-libs/yarl/pull/1026?src=pr&el=tree&filepath=yarl%2F_url.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#diff-eWFybC9fdXJsLnB5) | 81.25% | [3 Missing :warning: ](https://app.codecov.io/gh/aio-libs/yarl/pull/1026?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           master    #1026       +/-   ##
===========================================
- Coverage   99.33%   62.00%   -37.33%     
===========================================
  Files          17       38       +21     
  Lines        3317     6546     +3229     
  Branches      325      354       +29     
===========================================
+ Hits         3295     4059      +764     
- Misses         22     2457     +2435     
- Partials        0       30       +30     
```

| [Flag](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | Coverage Δ | |
|---|---|---|
| [CI-GHA](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `61.97% <76.08%> (-37.27%)` | :arrow_down: |
| [MyPy](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `24.93% <50.00%> (?)` | |
| [OS-Linux](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.24% <100.00%> (+<0.01%)` | :arrow_up: |
| [OS-Windows](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.57% <100.00%> (+<0.01%)` | :arrow_up: |
| [OS-macOS](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.00% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.10.11](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.48% <100.00%> (+0.56%)` | :arrow_up: |
| [Py-3.10.14](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.12% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.11.9](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.12% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.12.3](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `98.91% <100.00%> (-0.21%)` | :arrow_down: |
| [Py-3.12.4](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.38% <100.00%> (?)` | |
| [Py-3.8.10](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `98.85% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.8.18](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.06% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.9.13](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `98.85% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.9.19](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.06% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-pypy7.3.11](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.38% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-pypy7.3.16](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.41% <100.00%> (+<0.01%)` | :arrow_up: |
| [VM-macos-latest](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.00% <100.00%> (+<0.01%)` | :arrow_up: |
| [VM-ubuntu-latest](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.24% <100.00%> (+<0.01%)` | :arrow_up: |
| [VM-windows-latest](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.57% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aio-libs/yarl/pull/1026?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).

commonism: @Dreamsorcerer can you help me figure out the issue with the codecov/ checks?

I can see it ran the unit tests, but failed to create proper coverage files?
e.g. https://app.codecov.io/gh/aio-libs/yarl/pull/1026/blob/tests/test_url.py#L859

Dreamsorcerer: > @Dreamsorcerer can you help me figure out the issue with the codecov/ checks?
> 
> I can see it ran the unit tests, but failed to create proper coverage files? e.g. https://app.codecov.io/gh/aio-libs/yarl/pull/1026/blob/tests/test_url.py#L859

I'd change the fail option to true, then it'll be obvious if any uploads have failed (and which jobs need to be rerun):
https://github.com/aio-libs/yarl/blob/master/.github/workflows/ci-cd.yml#L340
commonism: > I'd change the fail option to true, then it'll be obvious if any uploads have failed (and which jobs need to be rerun): https://github.com/aio-libs/yarl/blob/master/.github/workflows/ci-cd.yml#L340

It's not an upload failing.
[Test (3.12, Y, ubuntu-latest, false)](https://github.com/aio-libs/yarl/actions/runs/9598259348/job/26469466683?pr=1026#logs)
All tests ran, upload succeeded.

Yet the only tests which are run evaluated are the tests which have a annotation to return None
[def test_authority_full()](https://app.codecov.io/gh/aio-libs/yarl/pull/1026?flags%5B0%5D=Py-3.12.4&src=pr&el=tree&filepath=tests%2Ftest_url.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%20comments&utm_term=aio-libs#8b270584bb5bef11f7777857af051674-L201)

You can see test_url.py and tests/test_url.py exist.
https://app.codecov.io/gh/aio-libs/yarl/pull/1026/tree

Same issue: https://app.codecov.io/gh/aio-libs/yarl/pull/1025/tree
Same issue: https://app.codecov.io/gh/aio-libs/yarl/pull/1023/tree
Same issue: https://app.codecov.io/gh/aio-libs/yarl/pull/1019/tree
Okay: https://app.codecov.io/gh/aio-libs/yarl/pull/1018/tree
Okay: https://app.codecov.io/gh/aio-libs/yarl/pull/1016/tree

Same problem: https://app.codecov.io/gh/aio-libs/yarl/pull/1015/blob/tests/test_url.py
Dreamsorcerer: > Yet the only tests which are run evaluated are the tests which have a annotation to return None

Oh, that's because webknjaz added mypy coverage to the results. I think that's just confusing...

If you'd like to improve the situation, then you can add tests/ to coverage in pytest.ini and look at enabling the options we use elsewhere in .mypy.ini while fixing up the errors it highlights (maybe add a couple of options in each PR). You can use aiohttp-jinja2 as a reference: https://github.com/aio-libs/aiohttp-jinja2
commonism: > > Yet the only tests which are run evaluated are the tests which have a annotation to return None
> 
> Oh, that's because webknjaz added mypy coverage to the results. I think that's just confusing...

MyPy has it's own flag, the flag I chose [here](https://app.codecov.io/gh/aio-libs/yarl/pull/1026?flags%5B0%5D=Py-3.12.4&src=pr&el=tree&filepath=tests%2Ftest_url.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%20comments&utm_term=aio-libs#8b270584bb5bef11f7777857af051674-L201) is Py-3.12.4, still showing the mypy results?
For [Base](https://app.codecov.io/gh/aio-libs/yarl/commit/7bca6cf1ba3673018b9065c6102f791a0a7516c0/tree?flags%5B0%5D=Py-3.12.4) the file is [missing](https://app.codecov.io/gh/aio-libs/yarl/commit/7bca6cf1ba3673018b9065c6102f791a0a7516c0/blob/test_url.py?flags%5B0%5D=Py-3.12.4).

May be better to have somebody look into this who has an idea how this is supposed to work.
Currently it's broken for every PR.



Dreamsorcerer: Yeah, but it's clearly mypy coverage. See above if you want to improve either coverage.
commonism: To my understanding …

The coverage reported by [mypy](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/tree?flags%5B0%5D=MyPy) is "[tests/test_url.py](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/blob/tests/test_url.py?flags%5B0%5D=MyPy)", the coverage reported by [python 3.12.4](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/tree?search=&flags%5B0%5D=Py-3.12.4) is for "[test_url.py](https://app.codecov.io/gh/aio-libs/yarl/pull/1026/blob/test_url.py)".

The reported codecov numbers are combined values for unit testing and type checking.
The low value report is due to the mypy/typing use and does not reflect unit testing code coverage.

Due to a large number of single file posts, [codecov may rate limit](https://github.com/aio-libs/yarl/actions/runs/9640464656/job/26584495389#step:14:71) (seen in the scheduled CI runs), I guess this could be prevented by limiting the number of requests, combining all reports in a single upload.


Dreamsorcerer: Right, but if I understood correctly from the configs, tests/ also haven't been added to the unit test coverage report at all?

But, again, I'd enable the fail on error setting, so we don't have any missing uploads.
Dreamsorcerer: Oh, wait, I see what you mean. Codecov wouldn't load anything last time I looked. So, it looks like changing from codecov v3 to v4 resulted in the file paths changing for some reason...
Dreamsorcerer: Well, copying the config from aiohttp-jinja2, as I suggested, resolves the path, but now the yarl/ coverage seems to have disappeared: #1028.
webknjaz: @Dreamsorcerer there's a few points to check:
- what gets into codecov — "uploads" at https://app.codecov.io/gh/aio-libs/yarl/commit/7bca6cf1ba3673018b9065c6102f791a0a7516c0/tree?flags%5B0%5D=Py-3.12.4 has https://api.codecov.io/upload/gh/aio-libs/yarl/download?path=v4/raw/2024-06-14/F2143C32CD85E0BC827E552A5E512211/7bca6cf1ba3673018b9065c6102f791a0a7516c0/3eded8ad-f926-4cb5-b6a6-5cbce8a67443/48384b50-8891-45cb-8a42-87d70c33fd3e.txt which reveals what the Codecov CLI uploader sends to their server — it does only contain base filenames in the XML part, which is surprising
- then, I'd check what coverage.py produces in `coverage.xml` (it should be the same that it's in the payload
- if it's the fault of the coverage library and not Codecov, I'd try older versions — we've been attempting to update it recently in Ansible and hit some regressions related to forked subprocesses, for example

It is a good idea to bisect Codecov @ https://app.codecov.io/gh/aio-libs/yarl/commits.

Looks like https://app.codecov.io/gh/aio-libs/yarl/commit/60737a8bf57625cffb09674b788ab2f64320a448 is the last commit with proper paths and the following https://app.codecov.io/gh/aio-libs/yarl/commit/e74c1599d47e21523d3e78ad78966d9630b12cc0 is broken.

So it looks like https://github.com/aio-libs/yarl/pull/1007 might've influenced it, which could only come from the MyPy bump there or unpinned deps.

Comparing the last working (https://api.codecov.io/upload/gh/aio-libs/yarl/download?path=v4/raw/2024-06-06/F2143C32CD85E0BC827E552A5E512211/60737a8bf57625cffb09674b788ab2f64320a448/2bd0dbd5-af59-44bd-8e2b-479f8d54ef00.txt) and the first broken (https://api.codecov.io/upload/gh/aio-libs/yarl/download?path=v4/raw/2024-06-06/F2143C32CD85E0BC827E552A5E512211/e74c1599d47e21523d3e78ad78966d9630b12cc0/6a695c6c-3879-48c0-bf3d-62e2d036817a/d9f3f16e-284b-43ce-a191-ce045b11982f.txt) uploads (both from Py-3.11.9 @ ubuntu-latest), the first thing I noticed is that the file list before the XML chunk that was present earlier, disappeared. The second thing is that the older working upload had a `# path=./coverage.xml` comment and the newer one had `# path=/home/runner/work/yarl/yarl/coverage.xml`. Both have `<coverage version="7.5.3"` which means that the `coverage.py` version hasn't changed in between.

This points to some env changes, which might be versions of pytest/pytest-cov/codecov-action/codecov-cli etc.
webknjaz: @Dreamsorcerer you could also try producing an html report locally to see if `coverage.py` behaves well stand-alone.
webknjaz: Oh... There's actually more commits in between 60737a8bf57625cffb09674b788ab2f64320a448 and e74c1599d47e21523d3e78ad78966d9630b12cc0: https://github.com/aio-libs/yarl/commits/e74c1599d47e21523d3e78ad78966d9630b12cc0/.

And among those, there's a series of action bumps, which probably didn't have a chance to make uploads, being cancelled by newer merges starting more CI jobs. I compared our two commits' logs and the confirmed my suspicion:
```diff
- Download action repository 'codecov/codecov-action@v3.1.4' (SHA:eaaf4bedf32dbdc6b720b63067d99c4d77d6047d)
+ Download action repository 'codecov/codecov-action@v4' (SHA:125fc84a9a348dbcf27191600683ec096ec9021c)
```
*(https://github.com/aio-libs/yarl/actions/runs/9401318235/job/25893141828#step:1:41 vs. https://github.com/aio-libs/yarl/actions/runs/9405561693/job/25907242226#step:1:41)*

With that in mind, my new suspect is... :drum: :long_drum: :drum: ... https://github.com/aio-libs/yarl/pull/988!
webknjaz: Good news is that if you're persistent enough, it's possible to get them to merge your bugfix: https://github.com/codecov/uploader/pulls?q=sort%3Aupdated-desc+is%3Apr+author%3Awebknjaz+is%3Amerged / https://github.com/codecov/codecov-cli/pulls?q=sort%3Aupdated-desc+is%3Apr+is%3Amerged+author%3Awebknjaz.
webknjaz: To conclude, the current workaround for viewing the coverage would be `make cov; python -Im webbrowser htmlcov/index.html`.
commonism: Tried v3.1.4 - https://github.com/aio-libs/yarl/actions/runs/9691011881/job/26741939301
Can't say anything about the results as uploading fails due to rate limiting - https://github.com/codecov/codecov-action/issues/1293 may be related.


webknjaz: > Can't say anything about the results as uploading fails due to rate limiting - [codecov/codecov-action#1293](https://github.com/codecov/codecov-action/issues/1293) may be related.

I ended up hardcoding the token in pytest the other day (https://github.com/pytest-dev/pytest/pull/12516), to fight this problem. I think, we should do it everywhere for now.
webknjaz: Could you move the Codecov action update to a separate PR?
Also, the change notes are RST so it would be nice to use that markup. The RTD preview might be broken if you don't. It's a blocker, FYI.
commonism: Updated accordingly.
Where is the RTD preview located?
webknjaz: @commonism scroll down to the CI checks in this PR, and you'll see the status from Read The Docs. Click on the `Details` link. When it's red or in progress, it'll take you to the build log page. But once it succeeds, it leads to the preview.
commonism: I've had CI runs with good coverage some minutes ago, now the bug is back.

webknjaz: > Coverage looks better when looking into the Actions results.

Yes, I integrated putting that into the job summary directly from whatever coverage.py collected. There's no codecov integration at that point, so it doesn't stand in the way in that specific place.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index e5dd481..d2e92be 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -105,7 +105,7 @@ Bug fixes
 ---------
 
 - Stopped dropping trailing slashes in :py:meth:`~yarl.URL.joinpath` -- by :user:`gmacon`. (:issue:`862`, :issue:`866`)
-- Started accepting string subclasses in ``__truediv__()`` operations (``URL / segment``) -- by :user:`mjpieters`. (:issue:`871`, :issue:`884`)
+- 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`
@@ -174,7 +174,7 @@ Contributor-facing changes
 Bugfixes
 --------
 
-- Fix regression with ``__truediv__`` and absolute URLs with empty paths causing the raw path to lack the leading ``/``.
+- Fix regression with :meth:`~yarl.URL.__truediv__` and absolute URLs with empty paths causing the raw path to lack the leading ``/``.
   (`#854 <https://github.com/aio-libs/yarl/issues/854>`_)
 
 
@@ -196,7 +196,7 @@ Features
 --------
 
 - Added ``URL.joinpath(*elements)``, to create a new URL appending multiple path elements. (`#704 <https://github.com/aio-libs/yarl/issues/704>`_)
-- Made ``URL.__truediv__()`` return ``NotImplemented`` if called with an
+- Made :meth:`URL.__truediv__() <yarl.URL.__truediv__>` return ``NotImplemented`` if called with an
   unsupported type — by :user:`michaeljpeters`.
   (`#832 <https://github.com/aio-libs/yarl/issues/832>`_)
 
diff --git a/CHANGES/1026.bugfix.rst b/CHANGES/1026.bugfix.rst
new file mode 100644
index 0000000..eeae24c
--- /dev/null
+++ b/CHANGES/1026.bugfix.rst
@@ -0,0 +1,21 @@
+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 <yarl.URL.__truediv__>` and :meth:`URL.joinpath() <yarl.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`
diff --git a/CHANGES/1026.doc.rst b/CHANGES/1026.doc.rst
new file mode 100644
index 0000000..daabece
--- /dev/null
+++ b/CHANGES/1026.doc.rst
@@ -0,0 +1,2 @@
+The pre-existing :meth:`/ magic method <yarl.URL.__truediv__>`
+has been documented in the API reference -- by :user:`commonism`.
diff --git a/docs/api.rst b/docs/api.rst
index 5a4c325..e8e6eb5 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -844,6 +844,20 @@ The path is encoded if needed.
 
    .. 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”
diff --git a/yarl/_url.py b/yarl/_url.py
index 9cca27e..84b9519 100644
--- a/yarl/_url.py
+++ b/yarl/_url.py
@@ -713,28 +713,34 @@ class URL:
                 "Path in a URL with authority should start with a slash ('/') if set"
             )
 
-    def _make_child(self, segments, encoded=False):
-        """add segments to self._val.path, accounting for absolute vs relative paths"""
-        # keep the trailing slash if the last segment ends with /
-        parsed = [""] if segments and segments[-1][-1:] == "/" else []
-        for seg in reversed(segments):
-            if not seg:
-                continue
-            if seg[0] == "/":
+    def _make_child(self, paths, encoded=False):
+        """
+        add paths to self._val.path, accounting for absolute vs relative paths,
+        keep existing, but do not create new, empty segments
+        """
+        parsed = []
+        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 {seg!r} starting from slash is forbidden"
-                )
-            seg = seg if encoded else self._PATH_QUOTER(seg)
-            if "/" in seg:
-                parsed += (
-                    sub for sub in reversed(seg.split("/")) if sub and sub != "."
+                    f"Appending path {path!r} starting from slash is forbidden"
                 )
-            elif seg != ".":
-                parsed.append(seg)
+            path = path if encoded else self._PATH_QUOTER(path)
+            segments = [
+                segment for segment in reversed(path.split("/")) if segment != "."
+            ]
+            if not segments:
+                continue
+            # remove trailing empty segment for all but the last path
+            segment_slice_start = int(not last and segments[0] == "")
+            parsed += segments[segment_slice_start:]
         parsed.reverse()
-        old_path = self._val.path
-        if old_path:
-            parsed = [*old_path.rstrip("/").split("/"), *parsed]
+
+        if self._val.path and (old_path_segments := self._val.path.split("/")):
+            old_path_cutoff = -1 if old_path_segments[-1] == "" else None
+            parsed = [*old_path_segments[:old_path_cutoff], *parsed]
+
         if self.is_absolute():
             parsed = _normalize_path_segments(parsed)
             if parsed and parsed[0] != "":


Test patch:
diff --git a/tests/test_url.py b/tests/test_url.py
index 725e946..a8f1351 100644
--- a/tests/test_url.py
+++ b/tests/test_url.py
@@ -799,6 +799,9 @@ def test_div_with_dots():
         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",),
@@ -806,6 +809,15 @@ def test_div_with_dots():
             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"
         ),
@@ -827,6 +839,39 @@ def test_joinpath(base, to_join, expected):
     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"),
+    ],
+)
+def test_joinpath_empty_segments(base, to_join, expected):
+    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_single_empty_segments():
+    """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",
     [