New Release. More unit tests and fix ups!

modified:   README.rst
	modified:   miniuri/miniuri.py
	modified:   miniuri/tests/test_miniuri.py
	modified:   setup.py
This commit is contained in:
Russell Ballestrini 2024-09-25 10:02:57 -04:00
parent 143127cf7a
commit 06e48daa4d
4 changed files with 161 additions and 95 deletions

View file

@ -3,7 +3,7 @@ miniuri
miniuri is a universal URI parser class. 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 Installation

View file

@ -1,4 +1,7 @@
class Uri(object): from __future__ import unicode_literals
class Uri:
""" """
miniuri is a universal URI parser class. miniuri is a universal URI parser class.
The parser grants access to the following attributes: The parser grants access to the following attributes:
@ -14,136 +17,134 @@ class Uri(object):
port filename port filename
""" """
def _reset_attrs(self): def __init__(self, uri=None):
self.scheme = self.username = self.password = None self._reset_attributes()
self.hostname = self.port = self.path = None if uri:
self.filename = self.query = self.fragment = None 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 self.extension = None
def __init__(self, uri=None):
self._reset_attrs()
if uri:
self.uri = uri # invoke uri.setter
def __str__(self): def __str__(self):
return self.uri return self.uri or ""
@property @property
def uri(self): def uri(self):
"""build and return uri from attributes""" """Build and return URI from attributes."""
scheme = self.scheme + "://" if self.scheme else "" scheme = "{}://".format(self.scheme) if self.scheme else ""
authority = self.authority if self.authority else "" authority = self.authority or ""
return "".join([scheme, authority, self.relative_uri]) return "{}{}{}".format(scheme, authority, self.relative_uri)
@uri.setter @uri.setter
def uri(self, uri): def uri(self, uri):
"""parse and set all uri attributes""" """Parse and set all URI attributes."""
self._reset_attrs() self._reset_attributes()
if not uri:
if uri == "" or uri is None: return
return None
if "://" in uri: if "://" in uri:
self.scheme, uri = uri.split("://") self.scheme, uri = uri.split("://", 1)
elif uri.startswith("//"):
uri = uri.lstrip("//")
if "#" in uri: if "#" in uri:
uri, self.fragment = uri.split("#") uri, self.fragment = uri.split("#", 1)
if "?" in uri: if "?" in uri:
uri, self.query = uri.split("?") uri, self.query = uri.split("?", 1)
parts = uri.split("/", 1)
# invoke authority setter. self.authority = parts[0]
self.authority = uri.split("/")[0] self.path = "/" + parts[1] if len(parts) > 1 else None
# invoke path setter.
offset = 0
if self.authority:
offset = len(self.authority)
self.path = uri[offset:]
@property @property
def authority(self): def authority(self):
"""return a authority string from attributes""" """Return an authority string from attributes."""
if self.hostname: if self.hostname:
a = "" authority = ""
if self.username: if self.username:
a += self.username authority += self.username
if self.password: if self.password:
a += ":" + self.password authority += ":{}".format(self.password)
a += "@" authority += "@"
a += self.hostname authority += self.hostname
if self.port: if self.port:
a += ":" + self.port authority += ":{}".format(self.port)
return a return authority
@authority.setter @authority.setter
def authority(self, a): def authority(self, authority):
"""set all the attribute that makeup a authority""" """Set all the attributes that make up an authority."""
if a: if authority:
self.username = self.password = self.port = None self.username = self.password = self.port = None
self.hostname = a self.hostname = authority
if "@" in self.hostname: if "@" in self.hostname:
self.userinfo, self.hostname = a.split("@") # userinfo setter self.userinfo, self.hostname = authority.split("@", 1)
if ":" in self.hostname: 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 @property
def relative_uri(self): def relative_uri(self):
"""return everything that isn't part of the authority.""" """Return everything that isn't part of the authority."""
path = query = fragment = "" path = self.path or ""
if self.path: query = "?{}".format(self.query) if self.query else ""
path = self.path fragment = "#{}".format(self.fragment) if self.fragment else ""
if self.query: return "{}{}{}".format(path, query, fragment)
query = "?" + self.query
if self.fragment:
fragment = "#" + self.fragment
return "".join([path, query, fragment])
@property @property
def userinfo(self): def userinfo(self):
"""return username:password, username, or None""" """Return username:password, username, or None."""
if self.username: if self.username:
if self.password: return (
return self.username + ":" + self.password "{}:{}".format(self.username, self.password)
return self.username if self.password
else self.username
)
@userinfo.setter @userinfo.setter
def userinfo(self, info): def userinfo(self, info):
"""set username and password""" """Set username and password."""
self.username, self.password = info, None self.username, self.password = (info.split(":", 1) + [None])[:2]
if ":" in info:
self.username, self.password = info.split(":")
@property @property
def path(self): def path(self):
"""return path""" """Return path."""
p = self._path if self._path is not None else "" path = self._path or ""
f = self.filename if self.filename is not None else "" filename = self.filename or ""
if p or f: return "{}{}".format(path, filename) if path or filename else None
return p + f
@path.setter @path.setter
def path(self, new_path): def path(self, new_path):
if new_path: if new_path:
self.filename = new_path.split("/")[-1] 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: else:
self.filename = None self.filename = None
self._path = None self._path = None
@property @property
def filename(self): def filename(self):
"""return filename""" """Return filename."""
if self._filename: if self._filename:
if self.extension: return (
return "{}.{}".format(self._filename, self.extension) "{}.{}".format(self._filename, self.extension)
return self._filename if self.extension
else self._filename
)
@filename.setter @filename.setter
def filename(self, new_filename): def filename(self, new_filename):
if new_filename: if new_filename:
self._filename = new_filename.split(".")[0] parts = new_filename.rsplit(".", 1)
if "." in new_filename: self._filename = parts[0]
self.extension = new_filename.split(".")[-1] self.extension = parts[1] if len(parts) > 1 else None
else: else:
self._filename = None self._filename = None
self.extension = None

View file

@ -1,10 +1,8 @@
from unittest import TestCase from unittest import TestCase
from ..miniuri import Uri from ..miniuri import Uri
class TestMiniUri(TestCase): class TestMiniUri(TestCase):
def setUp(self): def setUp(self):
# "http://www.foxhop.net/samsung/HL-T5087SA/red-LED-failure" # "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") 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): def test_fragment(self):
self.assertEqual(self.u.fragment, "5") self.assertEqual(self.u.fragment, "5")
# test edge case URIs # Test edge case URIs
def test_uri_no_path(self): def test_uri_no_path(self):
self.u = Uri("https://fox:pass@www.foxhop.net:81?p=2#5") 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.password, "pass")
self.assertEqual(self.u.hostname, "www.foxhop.net") self.assertEqual(self.u.hostname, "www.foxhop.net")
self.assertEqual(self.u.port, "81") self.assertEqual(self.u.port, "81")
self.assertEqual(self.u.path, None) self.assertIsNone(self.u.path)
self.assertEqual(self.u.filename, None) self.assertIsNone(self.u.filename)
self.assertEqual(self.u.extension, None) self.assertIsNone(self.u.extension)
def test_uri_filename_only_path(self): def test_uri_filename_only_path(self):
self.u = Uri("https://fox:pass@www.foxhop.net:81/filename.jpg?p=2#5") 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.path, "/")
self.assertEqual(self.u.uri, "https://fox:pass@www.foxhop.net:81/?p=2#5") 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): def test_change_uri(self):
self.u.uri = "http://zell:sapp@wwws.foxhop.net:82/my/file.png?p=3#6" 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.relative_uri, "/about.html")
self.assertEqual(u.filename, "about.html") self.assertEqual(u.filename, "about.html")
self.assertEqual(u.extension, "html") self.assertEqual(u.extension, "html")
self.assertIsNone(u.fragment, None) self.assertIsNone(u.fragment)
self.assertIsNone(u.hostname, None) self.assertIsNone(u.hostname)
self.assertIsNone(u.scheme, None) self.assertIsNone(u.scheme)
self.assertIsNone(u.authority)
def test_empty_uri_sets_attrs_to_none(self): def test_empty_uri_sets_attrs_to_none(self):
self.u.uri = "" self.u.uri = ""
@ -210,7 +209,7 @@ class TestMiniUri(TestCase):
self.assertIsNone(self.u.authority) self.assertIsNone(self.u.authority)
def test_empty_uri_sets_attrs_to_none2(self): 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.u = Uri("")
self.assertEqual(self.u.uri, "") self.assertEqual(self.u.uri, "")
self.assertIsNone(self.u.scheme) self.assertIsNone(self.u.scheme)
@ -241,7 +240,7 @@ class TestMiniUri(TestCase):
self.assertIsNone(self.u.authority) self.assertIsNone(self.u.authority)
def test_none_uri_sets_attrs_to_none2(self): 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.u = Uri(None)
self.assertEqual(self.u.uri, "") self.assertEqual(self.u.uri, "")
self.assertIsNone(self.u.scheme) self.assertIsNone(self.u.scheme)
@ -271,3 +270,67 @@ class TestMiniUri(TestCase):
self.assertIsNone(self.u.extension) self.assertIsNone(self.u.extension)
self.assertIsNone(self.u.authority) 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)

View file

@ -4,7 +4,7 @@ from setuptools import setup, find_packages
setup( setup(
name="miniuri", name="miniuri",
version="1.0.9", version="1.1.0",
long_description=open("README.rst").read(), long_description=open("README.rst").read(),
description="miniuri: The Universal URI Parser", description="miniuri: The Universal URI Parser",
keywords="miniuri uri url parser", keywords="miniuri uri url parser",
@ -31,6 +31,9 @@ setup(
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "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: # built and uploaded to pypi with this:
#python setup.py sdist bdist_egg #python setup.py sdist bdist_egg
#twine upload dist/* #twine upload dist/*