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