Repository: aio-libs/yarl

Issue:
Explicit port 0 replaced with default port
### Please confirm the following

- [X] I agree to follow the [aio-libs Code of Conduct][CoC]

[CoC]: ../../.github/blob/master/CODE_OF_CONDUCT.md

- [X] I have checked the [current issues][issue search] for duplicates.

[issue search]: ../search?q=is%3Aissue&type=issues

- [X] I understand this is open source software provided for free and that I might not receive a timely response.
- [X] I am positive I am **NOT** reporting a (potential) security
vulnerability, to the best of my knowledge. *(These must be shared by
submitting [this report form][vulnerability report form] instead, if
any hesitation exists.)*

[vulnerability report form]: ../security/advisories/new

- [X] I am willing to submit a pull request with reporoducers as xfailing test cases or even entire fix. *(Assign this issue to me.)*

### Describe the bug

If a URL has an explicit port of `0` and a scheme with a default port (such as `http`), then the `port` and `explicit_port` attributes disagree on what the port is.

### To Reproduce

```
import yarl
u = yarl.URL("http://a.co:0")
assert u.port == u.explicit_port
```

### Expected behavior

If `explicit_port` is not `None`, then `port` should be the same as `explicit_port`.

### Logs/tracebacks

```python-traceback
Python 3.10.12 (main, Sep 11 2024, 15:47:36) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import yarl
>>> u = yarl.URL("http://a.co:0")
>>> assert u.port == u.explicit_port
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError
>>> u.port
80
>>> u.explicit_port
0
```


### Python Version

```console
$ python --version
Python 3.10.12
```


### multidict Version

```console
$ python -m pip show multidict
Name: multidict
Version: 6.1.0
Location: $PWD/.venv/lib/python3.10/site-packages
Requires: typing-extensions
Required-by: yarl
```


### propcache Version

```console
$ python -m pip show propcache
Name: propcache
Version: 0.2.0
Location: $PWD/.venv/lib/python3.10/site-packages
Requires:
Required-by: yarl
```


### yarl Version

```console
$ python -m pip show yarl
Name: yarl
Version: 1.17.2.dev0
Location: $PWD/.venv/lib/python3.10/site-packages
Editable project location: $PWD
Requires: idna, multidict, propcache
Required-by:
$ git describe
v1.17.1-1-gdfc41db
```


### OS

Ubuntu 22.04

### Additional context

I discovered this because Hypothesis can generate such a URL from its `hypothesis.provisional.urls` strategy. I think yarl's implementation of `port` (as `self.explicit_port or self._default_port`) is wrong because the URL grammar permits the port to be 0 (the production is just `port = *DIGIT`), but I can't think of a situation where a URL with port 0 would be useful.

Discussion:
codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/aio-libs/yarl/branches/gmacon:1408-zero-port-xfail)

### Merging #1413 will **not alter performance**

<sub>Comparing <code>gmacon:1408-zero-port-xfail</code> (8b183cb) with <code>master</code> (d6a65eb)</sub>



### Summary

`✅ 83` untouched benchmarks





codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aio-libs/yarl/pull/1413?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 96.06%. Comparing base [(`dfc41db`)](https://app.codecov.io/gh/aio-libs/yarl/commit/dfc41dbfcd5a91283ab6c5c21d70bc4b1c377789?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) to head [(`8b183cb`)](https://app.codecov.io/gh/aio-libs/yarl/commit/8b183cb6b4c4eb1b2d0474978080f3c18ad25eb6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs).
> Report is 1 commits behind head on master.

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


```diff
@@           Coverage Diff           @@
##           master    #1413   +/-   ##
=======================================
  Coverage   96.06%   96.06%           
=======================================
  Files          31       31           
  Lines        5765     5772    +7     
  Branches      344      345    +1     
=======================================
+ Hits         5538     5545    +7     
  Misses        201      201           
  Partials       26       26           
```

| [Flag](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/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/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `96.06% <100.00%> (+<0.01%)` | :arrow_up: |
| [MyPy](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `48.67% <50.00%> (-0.01%)` | :arrow_down: |
| [OS-Linux](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.55% <100.00%> (+<0.01%)` | :arrow_up: |
| [OS-Windows](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.62% <100.00%> (+<0.01%)` | :arrow_up: |
| [OS-macOS](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.30% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.10.11](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.28% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.10.15](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.51% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.11.10](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.51% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.11.9](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.28% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.12.7](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.51% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.13.0](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.51% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.9.13](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.23% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-3.9.20](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.47% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-pypy7.3.16](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.53% <100.00%> (+<0.01%)` | :arrow_up: |
| [Py-pypy7.3.17](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.55% <100.00%> (+<0.01%)` | :arrow_up: |
| [VM-macos-latest](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.30% <100.00%> (+<0.01%)` | :arrow_up: |
| [VM-ubuntu-latest](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.55% <100.00%> (+<0.01%)` | :arrow_up: |
| [VM-windows-latest](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.62% <100.00%> (+<0.01%)` | :arrow_up: |
| [pytest](https://app.codecov.io/gh/aio-libs/yarl/pull/1413/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | `99.55% <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/1413?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).


Patch:
diff --git a/CHANGES/1413.bugfix.rst b/CHANGES/1413.bugfix.rst
new file mode 100644
index 0000000..28b948d
--- /dev/null
+++ b/CHANGES/1413.bugfix.rst
@@ -0,0 +1,2 @@
+Fixed a bug causing ``URL.port`` to return the default port when the given port was zero
+-- by :user:`gmacon`.
diff --git a/CHANGES/1414.feature.rst b/CHANGES/1414.feature.rst
new file mode 100644
index 0000000..e34d624
--- /dev/null
+++ b/CHANGES/1414.feature.rst
@@ -0,0 +1,2 @@
+Make error messages include details of incorrect type when ``port`` is not int in :py:meth:`~yarl.URL.build`.
+-- by :user:`Cycloctane`.
diff --git a/yarl/_url.py b/yarl/_url.py
index b69149b..3b47d07 100644
--- a/yarl/_url.py
+++ b/yarl/_url.py
@@ -349,7 +349,7 @@ class URL:
                 'Can\'t mix "authority" with "user", "password", "host" or "port".'
             )
         if port is not None and not isinstance(port, int):
-            raise TypeError("The port is required to be 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:
@@ -784,7 +784,10 @@ class URL:
         scheme without default port substitution.
 
         """
-        return self.explicit_port or self._default_port
+        port = self.explicit_port
+        if port is None:
+            port = self._default_port
+        return port
 
     @cached_property
     def explicit_port(self) -> Union[int, None]:


Test patch:
diff --git a/tests/test_url.py b/tests/test_url.py
index a2d0cef..0a8ed90 100644
--- a/tests/test_url.py
+++ b/tests/test_url.py
@@ -380,6 +380,12 @@ def test_port_for_unknown_scheme():
     assert url.explicit_port is None
 
 
+def test_explicit_zero_port():
+    url = URL("http://example.com:0")
+    assert url.explicit_port == 0
+    assert url.port == 0
+
+
 def test_explicit_port_for_explicit_port():
     url = URL("http://example.com:8888")
     assert 8888 == url.explicit_port
diff --git a/tests/test_url_build.py b/tests/test_url_build.py
index a5339dd..43f1383 100644
--- a/tests/test_url_build.py
+++ b/tests/test_url_build.py
@@ -66,7 +66,7 @@ def test_build_with_scheme_and_host():
             id="port-only",
         ),
         pytest.param(
-            "", TypeError, r"^The port is required to be int\.$", id="port-str"
+            "", TypeError, r"^The port is required to be int, got .*\.$", id="port-str"
         ),
     ],
 )