diff --git a/.gitignore b/.gitignore
index c8f0442..4f0799d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ env/
bin/
build/
develop-eggs/
+dirhtml/
dist/
eggs/
lib/
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..e6ab173
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,17 @@
+version: 2
+
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.11"
+
+sphinx:
+ builder: dirhtml
+ configuration: docs/conf.py
+ fail_on_warning: true
+
+formats: all
+
+python:
+ install:
+ - requirements: docs/requirements.txt
diff --git a/Cargo.lock b/Cargo.lock
index 7758e9b..bb6c7cd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -276,7 +276,7 @@ dependencies = [
[[package]]
name = "url-py"
-version = "0.3.1"
+version = "0.3.2"
dependencies = [
"pyo3",
"url",
diff --git a/Cargo.toml b/Cargo.toml
index 16f620e..555690f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "url-py"
-version = "0.3.1"
+version = "0.3.2"
edition = "2021"
[lib]
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..d4bb2cb
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 0000000..1f3685d
--- /dev/null
+++ b/docs/api.rst
@@ -0,0 +1,7 @@
+API Reference
+=============
+
+.. automodule:: url
+ :members:
+ :undoc-members:
+ :imported-members:
diff --git a/docs/changes.rst b/docs/changes.rst
new file mode 100644
index 0000000..cebe747
--- /dev/null
+++ b/docs/changes.rst
@@ -0,0 +1,8 @@
+=========
+Changelog
+=========
+
+v0.3.2
+-------
+
+* Initial functional release with docs.
diff --git a/docs/compatibility.rst b/docs/compatibility.rst
new file mode 100644
index 0000000..1ccc3ad
--- /dev/null
+++ b/docs/compatibility.rst
@@ -0,0 +1,28 @@
+=============
+Compatibility
+=============
+
+``url.py`` is currently in beta so long as version numbers begin with a ``0``, meaning its public interface may change if issues are uncovered, though not typically without reason.
+Once it seems clear that the interfaces look correct (likely after ``url.py`` is in use for some period of time) versioning will move to `CalVer `_ and interfaces will not change in backwards-incompatible ways without deprecation periods.
+
+.. note::
+
+ Backwards compatibility is always defined relative to the URL specifications implemented by our underlying library (the ``url`` crate).
+ Changing a behavior which is explicitly incorrect according to the relevant specifications is not considered a backwards-incompatible change -- on the contrary, it's considered a bug fix.
+
+In the spirit of `having some explicit detail on url.py's public interfaces `, here is a non-exhaustive list of things which are *not* part of the ``url.py`` public interface, and therefore which may change without warning, even once no longer in beta:
+
+* All commonly understood indicators of privacy in Python -- in particular, (sub)packages, modules and identifiers beginning with a single underscore.
+ In the case of modules or packages, this includes *all* of their contents recursively, regardless of their naming.
+* All contents in the ``tests`` package unless explicitly indicated otherwise
+* The precise contents and wording of exception messages raised by any callable, private *or* public.
+* The precise contents of the ``__repr__`` of any type defined in the package.
+* The ability to *instantiate* exceptions defined anywhere in the package, with the sole exception of those explicitly indicating they are publicly instantiable.
+* The instantiation of any type with no public identifier, even if instances of it are returned by other public API.
+* The concrete types within the signature of a callable whenever they differ from their documented types.
+ In other words, if a function documents that it returns an argument of type ``Mapping[int, Sequence[str]]``, this is the promised return type, not whatever concrete type is returned which may be richer or have additional attributes and methods.
+ Changes to the signature will continue to guarantee this return type (or a broader one) but indeed are free to change the concrete type.
+* Subclassing of any class defined throughout the package.
+ Doing so is not supported for any object.
+
+If any API usage may be questionable, feel free to open a discussion (or issue if appropriate) to clarify.
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..26c2388
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,82 @@
+import importlib.metadata
+import re
+
+from url import URL
+
+GITHUB = URL.parse("https://github.com/")
+HOMEPAGE = GITHUB.join("crate-py/url")
+
+project = "url-py"
+author = "Julian Berman"
+copyright = f"2023, {author}"
+
+release = importlib.metadata.version("url-py")
+version = release.partition("-")[0]
+
+language = "en"
+default_role = "any"
+
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.autosectionlabel",
+ "sphinx.ext.coverage",
+ "sphinx.ext.doctest",
+ "sphinx.ext.extlinks",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.napoleon",
+ "sphinx.ext.todo",
+ "sphinx.ext.viewcode",
+ "sphinx_copybutton",
+ "sphinxcontrib.spelling",
+ "sphinxext.opengraph",
+]
+
+pygments_style = "lovelace"
+pygments_dark_style = "one-dark"
+
+html_theme = "furo"
+
+
+def entire_domain(host):
+ return r"http.?://" + re.escape(host) + r"($|/.*)"
+
+
+linkcheck_ignore = [
+ entire_domain("img.shields.io"),
+ f"{GITHUB}.*#.*",
+ str(HOMEPAGE.join("actions")),
+ str(HOMEPAGE.join("workflows/CI/badge.svg")),
+]
+
+# = Extensions =
+
+# -- autodoc --
+
+autodoc_default_options = {
+ "members": True,
+ "member-order": "bysource",
+}
+
+# -- autosectionlabel --
+
+autosectionlabel_prefix_document = True
+
+# -- intersphinx --
+
+intersphinx_mapping = {
+ "regret": ("https://regret.readthedocs.io/en/stable/", None),
+ "python": ("https://docs.python.org/", None),
+}
+
+# -- extlinks --
+
+extlinks = {
+ "gh": (str(HOMEPAGE) + "/%s", None),
+ "github": (str(GITHUB) + "/%s", None),
+}
+extlinks_detect_hardcoded_links = True
+
+# -- sphinxcontrib-spelling --
+
+spelling_word_list_filename = "spelling-wordlist.txt"
+spelling_show_suggestions = True
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..c59784d
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,9 @@
+Python bindings to the `Rust url crate `_.
+
+.. toctree::
+ :glob:
+ :hidden:
+
+ compatibility
+ api
+ changes
diff --git a/docs/requirements.in b/docs/requirements.in
new file mode 100644
index 0000000..2c71e41
--- /dev/null
+++ b/docs/requirements.in
@@ -0,0 +1,7 @@
+file:.#egg=url-py
+furo
+pygments-github-lexers
+sphinx-copybutton
+sphinx>5
+sphinxcontrib-spelling>5
+sphinxext-opengraph
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..0f130ff
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,106 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# pip-compile --strip-extras docs/requirements.in
+#
+alabaster==0.7.13
+ # via sphinx
+babel==2.13.0
+ # via sphinx
+beautifulsoup4==4.12.2
+ # via furo
+certifi==2023.7.22
+ # via requests
+charset-normalizer==3.3.0
+ # via requests
+contourpy==1.1.1
+ # via matplotlib
+cycler==0.12.1
+ # via matplotlib
+docutils==0.20.1
+ # via sphinx
+fonttools==4.43.1
+ # via matplotlib
+furo==2023.9.10
+ # via -r docs/requirements.in
+idna==3.4
+ # via requests
+imagesize==1.4.1
+ # via sphinx
+jinja2==3.1.2
+ # via sphinx
+kiwisolver==1.4.5
+ # via matplotlib
+markupsafe==2.1.3
+ # via jinja2
+matplotlib==3.8.0
+ # via sphinxext-opengraph
+numpy==1.26.1
+ # via
+ # contourpy
+ # matplotlib
+packaging==23.2
+ # via
+ # matplotlib
+ # sphinx
+pillow==10.1.0
+ # via matplotlib
+pyenchant==3.2.2
+ # via sphinxcontrib-spelling
+pygments==2.16.1
+ # via
+ # furo
+ # pygments-github-lexers
+ # sphinx
+pygments-github-lexers==0.0.5
+ # via -r docs/requirements.in
+pyparsing==3.1.1
+ # via matplotlib
+python-dateutil==2.8.2
+ # via matplotlib
+requests==2.31.0
+ # via sphinx
+six==1.16.0
+ # via python-dateutil
+snowballstemmer==2.2.0
+ # via sphinx
+soupsieve==2.5
+ # via beautifulsoup4
+sphinx==7.2.6
+ # via
+ # -r docs/requirements.in
+ # furo
+ # sphinx-basic-ng
+ # sphinx-copybutton
+ # sphinxcontrib-applehelp
+ # sphinxcontrib-devhelp
+ # sphinxcontrib-htmlhelp
+ # sphinxcontrib-qthelp
+ # sphinxcontrib-serializinghtml
+ # sphinxcontrib-spelling
+ # sphinxext-opengraph
+sphinx-basic-ng==1.0.0b2
+ # via furo
+sphinx-copybutton==0.5.2
+ # via -r docs/requirements.in
+sphinxcontrib-applehelp==1.0.7
+ # via sphinx
+sphinxcontrib-devhelp==1.0.5
+ # via sphinx
+sphinxcontrib-htmlhelp==2.0.4
+ # via sphinx
+sphinxcontrib-jsmath==1.0.1
+ # via sphinx
+sphinxcontrib-qthelp==1.0.6
+ # via sphinx
+sphinxcontrib-serializinghtml==1.1.9
+ # via sphinx
+sphinxcontrib-spelling==8.0.0
+ # via -r docs/requirements.in
+sphinxext-opengraph==0.8.2
+ # via -r docs/requirements.in
+file:.#egg=url-py
+ # via -r docs/requirements.in
+urllib3==2.0.7
+ # via requests
diff --git a/docs/spelling-wordlist.txt b/docs/spelling-wordlist.txt
new file mode 100644
index 0000000..af29c0b
--- /dev/null
+++ b/docs/spelling-wordlist.txt
@@ -0,0 +1,5 @@
+changelog
+instantiable
+instantiation
+iterable
+subclassing
diff --git a/noxfile.py b/noxfile.py
index 2cc4d15..a91c997 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -4,9 +4,20 @@
import nox
ROOT = Path(__file__).parent
-TESTS = ROOT / "tests"
PYPROJECT = ROOT / "pyproject.toml"
+DOCS = ROOT / "docs"
+TESTS = ROOT / "tests"
+
+REQUIREMENTS = dict(
+ docs=DOCS / "requirements.txt",
+ tests=ROOT / "test-requirements.txt",
+)
+REQUIREMENTS_IN = {
+ path.parent.joinpath(f"{path.stem}.in") for path in REQUIREMENTS.values()
+}
+
+SUPPORTED = ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.10"]
nox.options.sessions = []
@@ -20,18 +31,13 @@ def _session(fn):
return _session
-@session(python=["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3"])
+@session(python=SUPPORTED)
def tests(session):
"""
Run the test suite with a corresponding Python version.
"""
- session.install(ROOT, "-r", TESTS / "requirements.txt")
- if session.posargs == ["coverage"]:
- session.install("coverage[toml]")
- session.run("coverage", "run", "-m", "pytest")
- session.run("coverage", "report")
- else:
- session.run("pytest", *session.posargs, TESTS)
+ session.install(ROOT, "-r", REQUIREMENTS["tests"])
+ session.run("pytest", *session.posargs, TESTS)
@session(tags=["build"])
@@ -54,13 +60,63 @@ def style(session):
session.run("ruff", "check", TESTS, __file__)
+@session(tags=["docs"])
+@nox.parametrize(
+ "builder",
+ [
+ nox.param(name, id=name)
+ for name in [
+ "dirhtml",
+ "doctest",
+ "linkcheck",
+ "man",
+ "spelling",
+ ]
+ ],
+)
+def docs(session, builder):
+ """
+ Build the documentation using a specific Sphinx builder.
+ """
+ session.install("-r", REQUIREMENTS["docs"])
+ with TemporaryDirectory() as tmpdir_str:
+ tmpdir = Path(tmpdir_str)
+ argv = ["-n", "-T", "-W"]
+ if builder != "spelling":
+ argv += ["-q"]
+ posargs = session.posargs or [tmpdir / builder]
+ session.run(
+ "python",
+ "-m",
+ "sphinx",
+ "-b",
+ builder,
+ DOCS,
+ *argv,
+ *posargs,
+ )
+
+
+@session(tags=["docs", "style"], name="docs(style)")
+def docs_style(session):
+ """
+ Check the documentation style.
+ """
+ session.install(
+ "doc8",
+ "pygments",
+ "pygments-github-lexers",
+ )
+ session.run("python", "-m", "doc8", "--config", PYPROJECT, DOCS)
+
+
@session(default=False)
def requirements(session):
"""
Update the project's pinned requirements. Commit the result.
"""
session.install("pip-tools")
- for each in [TESTS / "requirements.in"]:
+ for each in REQUIREMENTS_IN:
session.run(
"pip-compile",
"--resolver",
diff --git a/pyproject.toml b/pyproject.toml
index e07f13f..587553f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,11 +30,18 @@ classifiers = [
]
[project.urls]
+Documentation = "https://url-py.readthedocs.io/"
Homepage = "https://github.com/crate-py/url"
Issues = "https://github.com/crate-py/url/issues/"
Funding = "https://github.com/sponsors/Julian"
Source = "https://github.com/crate-py/url"
+[tool.doc8]
+ignore = [
+ "D000", # see PyCQA/doc8#125
+ "D001", # one sentence per line, so max length doesn't make sense
+]
+
[tool.isort]
combine_as_imports = true
ensure_newline_before_comments = true
@@ -91,5 +98,6 @@ ignore = [
docstring-quotes = "double"
[tool.ruff.per-file-ignores]
+"docs/*" = ["ANN", "D"]
"tests/*" = ["ANN", "D", "RUF012"]
"noxfile.py" = ["ANN", "D100"]
diff --git a/test-requirements.in b/test-requirements.in
new file mode 100644
index 0000000..e079f8a
--- /dev/null
+++ b/test-requirements.in
@@ -0,0 +1 @@
+pytest
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..8528e8d
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,14 @@
+#
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+#
+# pip-compile --strip-extras test-requirements.in
+#
+iniconfig==2.0.0
+ # via pytest
+packaging==23.2
+ # via pytest
+pluggy==1.3.0
+ # via pytest
+pytest==7.4.2
+ # via -r test-requirements.in