Repository: aio-libs/yarl

Issue:
regression in URL.build(…,port="") 
### Describe the bug

Input filtering for with_port #793 changed the logic to render the URL to compare the port to None.
https://github.com/aio-libs/yarl/commit/94b8679da05fd7b2d7bc86ed95e84c09f1946d75#r116844287
as it is possible to create a URL with a port="" via URL.build, this results in urls created with an empty port.


### To Reproduce

```
import yarl
u = yarl.URL.build(scheme="http", host="api.com", port="", path="/v1")
assert str(u) == "http://api.com/v1", str(u)
```

### Expected behavior

Filtering port="" in URL.build.
I'd even prefer to allow "" and convert to None to stay backwards compatible.

### Logs/tracebacks

```python-traceback
AssertionError: http://api.com:/v1
```


### Python Version

```console
$ python --version
Python 3.10.6
```


### multidict Version

```console
$ python -m pip show multidict
Name: multidict
Version: 6.0.2
```


### yarl Version

```console
$ python -m pip show yarl
Name: yarl
Version: 1.9.2
```


### OS

Linux

### Additional context

_No response_

### Code of Conduct

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

Discussion:
commonism: I did not include this in the PR - but the requirements file has a typo.
```
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 62fe39a..3b676e4 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -1,2 +1,2 @@
--r text.txt
+-r test.txt
 towncrier==23.11.0
 ```

webknjaz: > I did not include this in the PR - but the requirements file has a typo.
> 
> ```
> diff --git a/requirements/dev.txt b/requirements/dev.txt
> index 62fe39a..3b676e4 100644
> --- a/requirements/dev.txt
> +++ b/requirements/dev.txt
> @@ -1,2 +1,2 @@
> --r text.txt
> +-r test.txt
>  towncrier==23.11.0
> ```

Fixed that in https://github.com/aio-libs/yarl/commit/85b53c9f01e0f580405cc160e9f1ab168367b94e. Thanks!
Dreamsorcerer: > Actually… I'm not sure if it's a good idea to accept empty strings at all. Should we error out instead, and aim for stricter public API input validation in general?

Yeah, it's typed as int. I'm not sure we should add code that explicitly supports something the type checker rejects makes sense (and support `str` in general would be odd).
https://github.com/aio-libs/yarl/blob/85b53c9f01e0f580405cc160e9f1ab168367b94e/yarl/__init__.pyi#L61
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aio-libs/yarl/pull/959?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:
> Comparison is base [(`8d28329`)](https://app.codecov.io/gh/aio-libs/yarl/commit/8d2832906429c9282529541c6b29f19a0569fbe7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) 99.74% compared to head [(`d466951`)](https://app.codecov.io/gh/aio-libs/yarl/pull/959?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) 99.48%.
> Report is 7 commits behind head on master.


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


```diff
@@            Coverage Diff             @@
##           master     #959      +/-   ##
==========================================
- Coverage   99.74%   99.48%   -0.26%     
==========================================
  Files           4        4              
  Lines         772      774       +2     
  Branches      219      220       +1     
==========================================
  Hits          770      770              
- Misses          2        3       +1     
- Partials        0        1       +1     
```

| [Flag](https://app.codecov.io/gh/aio-libs/yarl/pull/959/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aio-libs) | Coverage Δ | |
|---|---|---|
| [unit](https://app.codecov.io/gh/aio-libs/yarl/pull/959/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.13%)` | :arrow_down: |

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/959?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).

webknjaz: @commonism looks like the PR title and description no longer correspond to its contents. This needs to be corrected.

Patch:
diff --git a/CHANGES/883.bugfix b/CHANGES/883.bugfix
new file mode 100644
index 0000000..9864377
--- /dev/null
+++ b/CHANGES/883.bugfix
@@ -0,0 +1,4 @@
+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.
diff --git a/yarl/_url.py b/yarl/_url.py
index c8f2acb..d5202db 100644
--- a/yarl/_url.py
+++ b/yarl/_url.py
@@ -233,6 +233,8 @@ class URL:
             raise ValueError(
                 'Can\'t mix "authority" with "user", "password", "host" or "port".'
             )
+        if not isinstance(port, (int, type(None))):
+            raise TypeError("The port is required to be int.")
         if port and not host:
             raise ValueError('Can\'t build URL with "port" but without "host".')
         if query and query_string:


Test patch:
diff --git a/tests/test_url_build.py b/tests/test_url_build.py
index 51969fa..ed07736 100644
--- a/tests/test_url_build.py
+++ b/tests/test_url_build.py
@@ -32,12 +32,28 @@ def test_build_with_scheme_and_host():
     assert u == URL("http://127.0.0.1")
 
 
-def test_build_with_port():
-    with pytest.raises(ValueError):
-        URL.build(port=8000)
-
-    u = URL.build(scheme="http", host="127.0.0.1", port=8000)
-    assert str(u) == "http://127.0.0.1:8000"
+@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\.$", id="port-str"
+        ),
+    ],
+)
+def test_build_with_port(port, exc, match):
+    print(match)
+    with pytest.raises(exc, match=match):
+        URL.build(port=port)
 
 
 def test_build_with_user():