Repository: aio-libs/yarl

Issue:
Passwords without users
Currently, yarl refuses to set an password if a user isn't set.  That's a bit unfortunate because that’s exactly what you need if you’re building redis URLs: https://redis-py.readthedocs.io/en/latest/#redis.StrictRedis.from_url

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/aio-libs/yarl/pull/106?src=pr&el=h1) Report
> Merging [#106](https://codecov.io/gh/aio-libs/yarl/pull/106?src=pr&el=desc) into [master](https://codecov.io/gh/aio-libs/yarl/commit/6a091a171b0419dfa5ed67049d432d50d3301ce8?src=pr&el=desc) will **increase** coverage by `<.01%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/aio-libs/yarl/pull/106/graphs/tree.svg?src=pr&token=DjnOLyoIMo&width=650&height=150)](https://codecov.io/gh/aio-libs/yarl/pull/106?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #106      +/-   ##
==========================================
+ Coverage   99.63%   99.63%   +<.01%     
==========================================
  Files           2        2              
  Lines         544      546       +2     
  Branches      139      140       +1     
==========================================
+ Hits          542      544       +2     
  Partials        2        2
```


| [Impacted Files](https://codecov.io/gh/aio-libs/yarl/pull/106?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [yarl/\_\_init\_\_.py](https://codecov.io/gh/aio-libs/yarl/pull/106?src=pr&el=tree#diff-eWFybC9fX2luaXRfXy5weQ==) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aio-libs/yarl/pull/106?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aio-libs/yarl/pull/106?src=pr&el=footer). Last update [6a091a1...1baf675](https://codecov.io/gh/aio-libs/yarl/pull/106?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 5b9a667..49b78a2 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -10,6 +10,8 @@ CHANGES
 
 * Unsafe encoding for QS fixed. Encode `;` char in value param (#104)
 
+* Process passwords without user names (#95)
+
 0.12.0 (2017-06-26)
 -------------------
 
diff --git a/yarl/__init__.py b/yarl/__init__.py
index 41a3089..e1151c0 100644
--- a/yarl/__init__.py
+++ b/yarl/__init__.py
@@ -176,8 +176,11 @@ class URL:
                     netloc += ':{}'.format(val.port)
                 if val.username:
                     user = _quote(val.username)
-                    if val.password:
-                        user += ':' + _quote(val.password)
+                else:
+                    user = ''
+                if val.password:
+                    user += ':' + _quote(val.password)
+                if user:
                     netloc = user + '@' + netloc
 
             path = _quote(val[2], safe='+@:', protected='/+', strict=strict)
@@ -380,7 +383,10 @@ class URL:
 
         """
         # not .username
-        return self._val.username
+        ret = self._val.username
+        if not ret:
+            return None
+        return ret
 
     @cached_property
     def user(self):
@@ -590,7 +596,7 @@ class URL:
             ret = ret + ':' + str(port)
         if password:
             if not user:
-                raise ValueError("Non-empty password requires non-empty user")
+                user = ''
             user = user + ':' + password
         if user:
             ret = user + '@' + ret


Test patch:
diff --git a/tests/test_url.py b/tests/test_url.py
index b5d365e..d2582ff 100644
--- a/tests/test_url.py
+++ b/tests/test_url.py
@@ -77,6 +77,11 @@ def test_raw_user_non_ascii():
     assert '%D0%B2%D0%B0%D1%81%D1%8F' == url.raw_user
 
 
+def test_no_user():
+    url = URL('http://example.com')
+    assert url.user is None
+
+
 def test_user_non_ascii():
     url = URL('http://вася@example.com')
     assert 'вася' == url.user
@@ -97,6 +102,11 @@ def test_password_non_ascii():
     assert 'пароль' == url.password
 
 
+def test_password_without_user():
+    url = URL('http://:password@example.com')
+    assert 'password' == url.password
+
+
 def test_raw_host():
     url = URL('http://example.com')
     assert "example.com" == url.raw_host
diff --git a/tests/test_url_update_netloc.py b/tests/test_url_update_netloc.py
index 9790dd4..46bdab0 100644
--- a/tests/test_url_update_netloc.py
+++ b/tests/test_url_update_netloc.py
@@ -90,8 +90,10 @@ def test_with_password_invalid_type():
 
 def test_with_password_and_empty_user():
     url = URL('http://example.com')
-    with pytest.raises(ValueError):
-        assert str(url.with_password('pass'))
+    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():