diff --git a/README.rst b/README.rst index 82794f8..bfa4613 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ miniuri miniuri is a universal URI parser class. -Only 145 lines of Python (400 lines if you count unit tests). +Only 150 lines of Python (486 lines if you count unit tests). Installation diff --git a/miniuri/miniuri.py b/miniuri/miniuri.py index e7c35bf..a87bbb8 100644 --- a/miniuri/miniuri.py +++ b/miniuri/miniuri.py @@ -1,11 +1,14 @@ -class Uri(object): +from __future__ import unicode_literals + + +class Uri: """ miniuri is a universal URI parser class. The parser grants access to the following attributes: foo://username:password@test.com:808/go/to/index.php?pet=cat&name=bam#eye \_/ \_______________/ \______/ \_/ \___/ \_/ \_______________/\_/ - | | | | | | | | + | | | | | | | | | userinfo hostname | | | query fragment | \___________________________|/\________|____|_/ | | | | | | @@ -14,136 +17,134 @@ class Uri(object): port filename """ - def _reset_attrs(self): - self.scheme = self.username = self.password = None - self.hostname = self.port = self.path = None - self.filename = self.query = self.fragment = None + def __init__(self, uri=None): + self._reset_attributes() + if uri: + self.uri = uri + + def _reset_attributes(self): + self.scheme = None + self.username = None + self.password = None + self.hostname = None + self.port = None + self._path = None + self._filename = None + self.query = None + self.fragment = None self.extension = None - def __init__(self, uri=None): - self._reset_attrs() - if uri: - self.uri = uri # invoke uri.setter - def __str__(self): - return self.uri + return self.uri or "" @property def uri(self): - """build and return uri from attributes""" - scheme = self.scheme + "://" if self.scheme else "" - authority = self.authority if self.authority else "" - return "".join([scheme, authority, self.relative_uri]) + """Build and return URI from attributes.""" + scheme = "{}://".format(self.scheme) if self.scheme else "" + authority = self.authority or "" + return "{}{}{}".format(scheme, authority, self.relative_uri) @uri.setter def uri(self, uri): - """parse and set all uri attributes""" - self._reset_attrs() - - if uri == "" or uri is None: - return None - + """Parse and set all URI attributes.""" + self._reset_attributes() + if not uri: + return if "://" in uri: - self.scheme, uri = uri.split("://") - + self.scheme, uri = uri.split("://", 1) + elif uri.startswith("//"): + uri = uri.lstrip("//") if "#" in uri: - uri, self.fragment = uri.split("#") - + uri, self.fragment = uri.split("#", 1) if "?" in uri: - uri, self.query = uri.split("?") - - # invoke authority setter. - self.authority = uri.split("/")[0] - - # invoke path setter. - offset = 0 - if self.authority: - offset = len(self.authority) - self.path = uri[offset:] + uri, self.query = uri.split("?", 1) + parts = uri.split("/", 1) + self.authority = parts[0] + self.path = "/" + parts[1] if len(parts) > 1 else None @property def authority(self): - """return a authority string from attributes""" + """Return an authority string from attributes.""" if self.hostname: - a = "" + authority = "" if self.username: - a += self.username + authority += self.username if self.password: - a += ":" + self.password - a += "@" - a += self.hostname + authority += ":{}".format(self.password) + authority += "@" + authority += self.hostname if self.port: - a += ":" + self.port - return a + authority += ":{}".format(self.port) + return authority @authority.setter - def authority(self, a): - """set all the attribute that makeup a authority""" - if a: + def authority(self, authority): + """Set all the attributes that make up an authority.""" + if authority: self.username = self.password = self.port = None - self.hostname = a + self.hostname = authority if "@" in self.hostname: - self.userinfo, self.hostname = a.split("@") # userinfo setter + self.userinfo, self.hostname = authority.split("@", 1) if ":" in self.hostname: - self.hostname, self.port = self.hostname.split(":") + self.hostname, self.port = self.hostname.split(":", 1) + if not self.hostname: + self.hostname = None @property def relative_uri(self): - """return everything that isn't part of the authority.""" - path = query = fragment = "" - if self.path: - path = self.path - if self.query: - query = "?" + self.query - if self.fragment: - fragment = "#" + self.fragment - return "".join([path, query, fragment]) + """Return everything that isn't part of the authority.""" + path = self.path or "" + query = "?{}".format(self.query) if self.query else "" + fragment = "#{}".format(self.fragment) if self.fragment else "" + return "{}{}{}".format(path, query, fragment) @property def userinfo(self): - """return username:password, username, or None""" + """Return username:password, username, or None.""" if self.username: - if self.password: - return self.username + ":" + self.password - return self.username + return ( + "{}:{}".format(self.username, self.password) + if self.password + else self.username + ) @userinfo.setter def userinfo(self, info): - """set username and password""" - self.username, self.password = info, None - if ":" in info: - self.username, self.password = info.split(":") + """Set username and password.""" + self.username, self.password = (info.split(":", 1) + [None])[:2] @property def path(self): - """return path""" - p = self._path if self._path is not None else "" - f = self.filename if self.filename is not None else "" - if p or f: - return p + f + """Return path.""" + path = self._path or "" + filename = self.filename or "" + return "{}{}".format(path, filename) if path or filename else None @path.setter def path(self, new_path): if new_path: self.filename = new_path.split("/")[-1] - self._path = new_path.rstrip(self.filename) + self._path = new_path[: -len(self.filename)] if self.filename else new_path else: self.filename = None self._path = None @property def filename(self): - """return filename""" + """Return filename.""" if self._filename: - if self.extension: - return "{}.{}".format(self._filename, self.extension) - return self._filename + return ( + "{}.{}".format(self._filename, self.extension) + if self.extension + else self._filename + ) @filename.setter def filename(self, new_filename): if new_filename: - self._filename = new_filename.split(".")[0] - if "." in new_filename: - self.extension = new_filename.split(".")[-1] + parts = new_filename.rsplit(".", 1) + self._filename = parts[0] + self.extension = parts[1] if len(parts) > 1 else None else: self._filename = None + self.extension = None diff --git a/miniuri/tests/test_miniuri.py b/miniuri/tests/test_miniuri.py index 30a1a95..610c69a 100644 --- a/miniuri/tests/test_miniuri.py +++ b/miniuri/tests/test_miniuri.py @@ -1,10 +1,8 @@ from unittest import TestCase - from ..miniuri import Uri class TestMiniUri(TestCase): - def setUp(self): # "http://www.foxhop.net/samsung/HL-T5087SA/red-LED-failure" self.u = Uri("https://fox:pass@www.foxhop.net:81/path/filename.jpg?p=2#5") @@ -50,7 +48,7 @@ class TestMiniUri(TestCase): def test_fragment(self): self.assertEqual(self.u.fragment, "5") - # test edge case URIs + # Test edge case URIs def test_uri_no_path(self): self.u = Uri("https://fox:pass@www.foxhop.net:81?p=2#5") @@ -59,9 +57,9 @@ class TestMiniUri(TestCase): self.assertEqual(self.u.password, "pass") self.assertEqual(self.u.hostname, "www.foxhop.net") self.assertEqual(self.u.port, "81") - self.assertEqual(self.u.path, None) - self.assertEqual(self.u.filename, None) - self.assertEqual(self.u.extension, None) + self.assertIsNone(self.u.path) + self.assertIsNone(self.u.filename) + self.assertIsNone(self.u.extension) def test_uri_filename_only_path(self): self.u = Uri("https://fox:pass@www.foxhop.net:81/filename.jpg?p=2#5") @@ -81,7 +79,7 @@ class TestMiniUri(TestCase): self.assertEqual(self.u.path, "/") self.assertEqual(self.u.uri, "https://fox:pass@www.foxhop.net:81/?p=2#5") - # test_change_attributes: + # Test change attributes: def test_change_uri(self): self.u.uri = "http://zell:sapp@wwws.foxhop.net:82/my/file.png?p=3#6" @@ -190,9 +188,10 @@ class TestMiniUri(TestCase): self.assertEqual(u.relative_uri, "/about.html") self.assertEqual(u.filename, "about.html") self.assertEqual(u.extension, "html") - self.assertIsNone(u.fragment, None) - self.assertIsNone(u.hostname, None) - self.assertIsNone(u.scheme, None) + self.assertIsNone(u.fragment) + self.assertIsNone(u.hostname) + self.assertIsNone(u.scheme) + self.assertIsNone(u.authority) def test_empty_uri_sets_attrs_to_none(self): self.u.uri = "" @@ -210,7 +209,7 @@ class TestMiniUri(TestCase): self.assertIsNone(self.u.authority) def test_empty_uri_sets_attrs_to_none2(self): - # same as above but tests the constructor. + # Same as above but tests the constructor. self.u = Uri("") self.assertEqual(self.u.uri, "") self.assertIsNone(self.u.scheme) @@ -241,7 +240,7 @@ class TestMiniUri(TestCase): self.assertIsNone(self.u.authority) def test_none_uri_sets_attrs_to_none2(self): - # same as above but tests the constructor. + # Same as above but tests the constructor. self.u = Uri(None) self.assertEqual(self.u.uri, "") self.assertIsNone(self.u.scheme) @@ -271,3 +270,67 @@ class TestMiniUri(TestCase): self.assertIsNone(self.u.extension) self.assertIsNone(self.u.authority) + # New tests for additional edge cases + + def test_no_scheme(self): + u = Uri("//www.example.com/path") + self.assertIsNone(u.scheme) + self.assertEqual(u.authority, "www.example.com") + self.assertEqual(u.path, "/path") + self.assertIsNone(u.query) + self.assertIsNone(u.fragment) + + def test_no_authority(self): + u = Uri("/path/to/resource") + self.assertIsNone(u.scheme) + self.assertIsNone(u.authority) + self.assertEqual(u.path, "/path/to/resource") + self.assertIsNone(u.query) + self.assertIsNone(u.fragment) + + def test_no_path(self): + u = Uri("http://hostname") + self.assertEqual(u.scheme, "http") + self.assertEqual(u.authority, "hostname") + self.assertIsNone(u.path) + self.assertIsNone(u.query) + self.assertIsNone(u.fragment) + + def test_no_query_or_fragment(self): + u = Uri("http://hostname/path") + self.assertEqual(u.scheme, "http") + self.assertEqual(u.authority, "hostname") + self.assertEqual(u.path, "/path") + self.assertIsNone(u.query) + self.assertIsNone(u.fragment) + + def test_unicode_handling(self): + u = Uri("http://hostname/路径") + self.assertEqual(u.scheme, "http") + self.assertEqual(u.authority, "hostname") + self.assertEqual(u.path, "/路径") + self.assertIsNone(u.query) + self.assertIsNone(u.fragment) + + # Test for invalid inputs + def test_invalid_uri(self): + u = Uri("http://:80") + self.assertEqual(u.scheme, "http") + self.assertIsNone(u.username) + self.assertIsNone(u.password) + self.assertIsNone(u.hostname) + self.assertEqual(u.port, "80") + self.assertIsNone(u.path) + self.assertIsNone(u.query) + self.assertIsNone(u.fragment) + + def test_malformed_uri(self): + u = Uri("http://user@:80") + self.assertEqual(u.scheme, "http") + self.assertEqual(u.username, "user") + self.assertIsNone(u.password) + self.assertIsNone(u.hostname) + self.assertEqual(u.port, "80") + self.assertIsNone(u.path) + self.assertIsNone(u.query) + self.assertIsNone(u.fragment) diff --git a/setup.py b/setup.py index e9f7fed..b4fe394 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup, find_packages setup( name="miniuri", - version="1.0.9", + version="1.1.0", long_description=open("README.rst").read(), description="miniuri: The Universal URI Parser", keywords="miniuri uri url parser", @@ -31,6 +31,9 @@ setup( "Programming Language :: Python :: 3.8", "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", ], ) @@ -39,4 +42,3 @@ setup( # built and uploaded to pypi with this: #python setup.py sdist bdist_egg #twine upload dist/* -