Compare commits

..

4 commits

Author SHA1 Message Date
Tiara Rodney
b594ab5b2a
feat(tox): add support for aux pos args 2025-06-28 00:30:07 +02:00
Tiara Rodney
6321c3f11b
fix(test): retrofit deprecations
pathlib support and sphinx.testing.path deprecation in Sphinx ver. 9  was
announced in Sphinx ver. 6 and enabled in Sphinx ver. 7, hence there are three
different environment states to support in regards to paths in Sphinx testing
environments.
2025-06-28 00:25:46 +02:00
Tiara Rodney
a4fd97b55a
dirty 2025-06-21 23:36:38 +02:00
Tiara Rodney
5c2fb8c32b
dirty 2025-06-21 23:19:14 +02:00
26 changed files with 420 additions and 195 deletions

View file

@ -6,7 +6,7 @@ GPG_SIGNER_FINGERPRINT := "91CD826E74B0174D181903DEF97C70941CD8C4EF"
.PHONY: chore configure requirements-dev.txt requirements.txt publish archive
chore: requirements.txt requirements-dev.txt bitbucket-pipelines.yml
chore: requirements.txt requirements-dev.txt bitbucket-pipelines.yaml
Pipfile.lock: Pipfile
python3 -m pipenv lock -v

View file

@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
byteb4rb1e_sphinxcontrib = { editable = true, path = '.'}
byteb4rb1e.sphinxcontrib = { editable = true, path = '.'}
[dev-packages]
sphinx = "*"

21
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "48c822d9e7bedd5ee25106b31093ce10a0071d7b12f7861e4e4cc46c5549c9ed"
"sha256": "4eac08fb6ab8c543a1fa6030c3c69a57aa6ae6de69e4f47ec483128bb61b2867"
},
"pipfile-spec": 6,
"requires": {
@ -32,8 +32,9 @@
"markers": "python_version >= '3.8'",
"version": "==2.17.0"
},
"byteb4rb1e.sphinxcontrib": {
"version": "==0.1.dev5+g3c348d5.d20250526"
"byteb4rb1e.utils": {
"git": "https://bitbucket.org/byteb4rb1e/py-utils.git",
"ref": "d0dfa1cb12702e6d25f3a9eeab02968eda8d06ba"
},
"certifi": {
"hashes": [
@ -381,11 +382,11 @@
},
"cachetools": {
"hashes": [
"sha256:82e73ba88f7b30228b5507dce1a1f878498fc669d972aef2dde4f3a3c24f103e",
"sha256:f225782b84438f828328fc2ad74346522f27e5b1440f4e9fd18b20ebfd1aa2cf"
"sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4",
"sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"
],
"markers": "python_version >= '3.9'",
"version": "==6.0.0"
"markers": "python_version >= '3.7'",
"version": "==5.5.2"
},
"certifi": {
"hashes": [
@ -832,11 +833,11 @@
},
"setuptools": {
"hashes": [
"sha256:49f7af965996f26d43c8ae34539c8d99c5042fbff34302ea151eaa9c207cd257",
"sha256:95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0"
"sha256:ca5cc1069b85dc23070a6628e6bcecb3292acac802399c7f8edc0100619f9009",
"sha256:f6ffc5f0142b1bd8d0ca94ee91b30c0ca862ffd50826da1ea85258a06fd94552"
],
"markers": "python_version >= '3.9'",
"version": "==80.8.0"
"version": "==80.7.1"
},
"setuptools-scm": {
"hashes": [

View file

@ -1,21 +1,3 @@
###################################
byteb4rb1e_sphinxcontrib.authorship
###################################
An implementation of the IMJE recommendations on authorship applied to Sphinx
documents.
.. code-block:: rst
.. contribution:: Review
:name: Adam Smith
:organization: Example Corp.
:group: contributions
.. code-block:: rst
.. acknowledgment::
:name: Adam Smith
:organization: Example Corp.
:group: contributions

View file

@ -12,7 +12,7 @@ via pip (PyPi)
.. code-block::
$> python3 -m pip install byteb4rb1e-sphinxcontrib
$> python3 -m pip install byteb4rb1ie-sphinxcontrib
via pip (Git)
-------------
@ -21,6 +21,20 @@ via pip (Git)
$> python3 -m pip git+https://bitbucket.org/byteb4rb1e/sphinxcontrib@master
via Git
-------
.. code-block::
$> git clone https://bitbucket.org/byteb4rb1e/sphinxcontrib
with pipenv
~~~~~~~~~~~
.. code-block::
$> sh ./configure --with-pipenv .pipenv
Usage
=====
@ -28,140 +42,5 @@ Usage
:caption: Sphinx Extensions
authorship
authorship_svc
svc_authorship
svc
Installation (Development)
==========================
.. code-block::
$> git clone https://bitbucket.org/byteb4rb1e/sphinxcontrib
.. warning::
(Non-MSYS2) Windows users MUST use ``.venv/Scripts/python3.exe``, instead of
``.venv/bin/python3``
.. warning::
(Non-MSYS2) Windows users MUST execute ``python3 -m pipenv install -d
--skip-lock``, instead of ``sh ./configure``.
with pipenv
-----------
``pipenv`` expected to be installed system-wide
.. code-block::
$> sh ./configure
with venv
---------
.. code-block::
$> python3 -m venv --system-site-packages .venv
$> .venv/bin/python3 -m pip install pipenv
$> .venv/bin/python3 -m pipenv run sh ./configure
with venv (but without configuration)
-------------------------------------
.. code-block::
$> python3 -m venv --system-site-packages .venv
$> .venv/bin/python3 -m pip install -r requirements-dev.txt
Development
===========
Static Code Analysis
--------------------
.. code-block::
$> python3 -m pipenv run test-static
Audit
~~~~~
.. code-block::
$> python3 -m tox -e audit
Format
~~~~~~
.. code-block::
$> python3 -m tox -e format -- --inline
Lint
~~~~
.. code-block::
$> python3 -m tox -e lint
Unit Testing
------------
.. code-block::
$> python3 -m pipenv run test-unit
Test Suite
~~~~~~~~~~
.. code-block::
$> python3 -m pipenv run test-unit -- -p tests/unit
Test Case
~~~~~~~~~
.. code-block::
$> python3 -m pipenv run test-unit -- -p tests/unit
Integration Testing
-------------------
.. code-block::
$> python3 -m pipenv run test-integration
Test Suite
~~~~~~~~~~
.. code-block::
$> python3 -m pipenv run test-integration -- \
-p tests/integration/byteb4rb1e_sphinxcontrib/authorship
Test Case
~~~~~~~~~
.. code-block::
$> python3 -m pipenv run test-integration -- \
-p tests/integration/byteb4rb1e_sphinxcontrib/authorship/test_setup.py
with a definitive Python and Sphinx major version (e.g. *Python* ``3.10``, and *Sphinx*
``6``)
.. code-block::
$> python3 -m tox -e py310-sphinx6-integration -- \
-p tests/unit
Documentation
-------------
.. code-block::
$> python3 -m pipenv run doc

View file

@ -1,3 +1,3 @@
#######################################
byteb4rb1e_sphinxcontrib.authorship_svc
byteb4rb1e_sphinxcontrib.svc_authorship
#######################################

View file

@ -7,7 +7,7 @@ requires = [
build-backend = "setuptools.build_meta"
[project]
name = "byteb4rb1e.sphinxcontrib"
name = "byteb4rb1e.sphinxcontrib.ext"
description = ""
authors = [
{ name = "Tiara Rodney", email = "tiara.rodney@byteb4rb1e.me" }
@ -30,19 +30,17 @@ classifiers = [
]
dependencies = [
"sphinx>=5.1",
"byteb4rb1e.utils @ git+https://bitbucket.org/byteb4rb1e/py-utils.git@32ae99c5fa0174761f4053fce1130f3cd7a2a68b"
]
dynamic = ["version"]
requires-python = ">=3.8"
[project.optional-dependencies]
git = ["pygit2>=1.18.0,<2",]
[project.urls]
Bitbucket = "https://bitbucket.org/byteb4rb1e/sphinxcontrib"
[tool.setuptools.packages.find]
where = ["src"]
namespaces = true
[tool.autopep8]

View file

@ -2,7 +2,7 @@
alabaster==1.0.0
babel==2.17.0
build==1.2.2.post1
cachetools==6.0.0
cachetools==5.5.2
certifi==2025.4.26
chardet==5.2.0
charset-normalizer==3.4.2
@ -37,7 +37,7 @@ requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.0.0
roman-numerals-py==3.1.0
setuptools==80.8.0
setuptools==80.7.1
setuptools-scm==8.3.1
snowballstemmer==3.0.1
sphinx==8.2.3

View file

@ -1,7 +1,7 @@
-i https://pypi.org/simple
alabaster==1.0.0
babel==2.17.0
byteb4rb1e.sphinxcontrib==0.1.dev5+g3c348d5.d20250526
-e .
certifi==2025.4.26
charset-normalizer==3.4.2
colorama==0.4.6

View file

@ -1,7 +0,0 @@
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata
def setup(app: Sphinx) -> ExtensionMetadata:
"""add this extension and its children to a Sphinx application"""
return {}

View file

@ -0,0 +1,86 @@
import importlib.resources
import mimetypes
from pathlib import Path
import urllib.request
import tarfile
from typing import Tuple, Optional, Dict
from io import IOBase
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.util.logging import getLogger
from byteb4rb1e.utils.urllib.request import PkgHandler
logger = getLogger(__name__)
_static_archives: Dict[str, IOBase] = {}
_opener: urllib.request.OpenerDirector = urllib.request.build_opener(
PkgHandler()
)
def on_config_inited(
app: Sphinx,
env: BuildEnvironment
):
"""
"""
global _static_archives
static_archives = app.config['html_static_archive']
if not static_archives:
return
if isinstance(static_archives, str):
static_archives = [static_archives]
for url in static_archives:
_static_archives[url] = _opener.open(url)
def on_env_updated(
app: Sphinx,
env: BuildEnvironment
):
"""
"""
static_dir = Path(app.outdir) / '_static'
for url, fh in _static_archives.items():
modes = {
'application/x-tar': 'r',
'application/x-tar+gzip': 'r:gz',
'application/x-tar+bzip2': 'r:bz2',
'application/x-tar+xz': 'r:xz',
}
mime_type = fh.headers.get('Content-Type')
if mime_type not in modes.keys():
raise Exception('')
archive = tarfile.open(fileobj=fh, mode=modes[mime_type])
for file in archive.getmembers():
if file.isdir():
continue
shadow_file = static_dir / file.name
if not shadow_file.exists():
logger.info(f'html_static_archive (extract): {url}::{file.name}')
archive.extract(file, path=static_dir, filter='data')
archive.close()
fh.close()
def setup(app: Sphinx):
app.add_config_value('html_static_archive', [], True)
app.connect('env-check-consistency', on_config_inited)
app.connect('env-check-consistency', on_env_updated)

View file

@ -0,0 +1,108 @@
from collections import namedtuple
import json
import logging
import os
from pathlib import Path
import shutil
from typing import Any, Tuple, Callable, Iterator, Dict
import warnings
import pytest
try:
from sphinx.deprecation import RemovedInSphinx90Warning
except ImportError:
RemovedInSphinx90Warning = None
from sphinx.testing.util import SphinxTestApp
from sphinx.testing.fixtures import make_app, test_params
USE_PATHLIB = True
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
from sphinx.testing.path import path as sphinxtesting_path
if w and issubclass(w[-1].message.__class__, RemovedInSphinx90Warning):
USE_PATHLIB = True
else:
USE_PATHLIB = False
from byteb4rb1e.testing.pytest.fixtures import current_test
_SPHINX_TESTAPP_COUNTER: Dict[str, int] = {}
_SPHINX_TESTSRC_COUNTER: Dict[str, int] = {}
@pytest.fixture
def mock_sphinx_testsrc_dir(
tmp_path: Path,
):
global _SPHINX_TESTSRC_COUNTER
_SPHINX_TESTSRC_COUNTER.setdefault(tmp_path, 0)
testsrc_id = _SPHINX_TESTSRC_COUNTER[tmp_path]
_SPHINX_TESTSRC_COUNTER[tmp_path] += 1
srcdir = tmp_path / f'sphinx-testsrc-{testsrc_id}'
def wrap():
srcdir.mkdir(parents=True)
(srcdir / 'conf.py').write_text("""
project = 'foobar'
master_doc = 'index'
""")
(srcdir / 'index.rst').write_text("""
######
Foobar
######
Hello world!
""")
return srcdir
return wrap
@pytest.fixture
def mock_sphinx_testapp(
make_app: Callable[[], SphinxTestApp],
tmp_path: Path,
caplog
):
global _SPHINX_TESTAPP_COUNTER
_SPHINX_TESTAPP_COUNTER.setdefault(tmp_path, 0)
testapp_id = _SPHINX_TESTAPP_COUNTER[tmp_path]
_SPHINX_TESTAPP_COUNTER[tmp_path] += 1
"""Provides the 'sphinx.application.Sphinx' object"""
def wrap(*args, **kwargs) -> Iterator[SphinxTestApp]:
assert kwargs.get('srcdir'), 'srcdir keyword argument missing'
caplog.set_level(logging.DEBUG)
logger = logging.getLogger()
basedir = tmp_path / f'sphinx-testapp-{testapp_id}'
srcdir = basedir / 'src'
builddir = basedir / 'build'
shutil.copytree(kwargs['srcdir'], srcdir, dirs_exist_ok=True)
kwargs['srcdir'] = sphinxtesting_path(srcdir) if not USE_PATHLIB else srcdir
kwargs.setdefault('builddir', sphinxtesting_path(builddir) if not USE_PATHLIB else builddir)
app_ = make_app(*args, **kwargs)
logger.debug(json.dumps({
'builder': app_.builder.name,
'srcdir': str(app_.srcdir),
'outdir': str(app_.outdir)
}, indent = 4))
return app_
return wrap

View file

@ -0,0 +1,117 @@
from pathlib import Path
import tarfile
import pytest
from byteb4rb1e.sphinxcontrib.testing.pytest.fixtures import (
mock_sphinx_testapp,
mock_sphinx_testsrc_dir
)
from byteb4rb1e.testing.pytest.decorators import run_in_subprocess_once
from byteb4rb1e.testing.pytest.fixtures import mock_system_site_package_dir
@run_in_subprocess_once()
def test_default(
mock_sphinx_testapp,
mock_sphinx_testsrc_dir,
mock_system_site_package_dir,
tmp_path,
) -> None:
"""
"""
pkg_dir = mock_system_site_package_dir('dummypkg')
mock_static_archive = tarfile.open(pkg_dir / 'data.tar', 'w')
(tmp_path / 'foobar.txt').write_text("Hello world!")
mock_static_archive.addfile(
mock_static_archive.gettarinfo(
arcname='foobar.txt',
fileobj=(tmp_path / 'foobar.txt').open('r')
)
)
mock_static_archive.close()
app = mock_sphinx_testapp(srcdir=mock_sphinx_testsrc_dir())
app.setup_extension('byteb4rb1e.sphinxcontrib.ext.html_static_archive')
app.config['html_static_archive'] = f'pkg://dummypkg/data.tar'
app.build(force_all=True)
assert (Path(app.outdir) / '_static' / 'foobar.txt').exists()
@run_in_subprocess_once()
def test_compression(
mock_sphinx_testapp,
mock_sphinx_testsrc_dir,
mock_system_site_package_dir,
tmp_path,
) -> None:
"""
"""
pkg_dir = mock_system_site_package_dir('dummypkg')
mock_static_archive = tarfile.open(pkg_dir / 'data.tar.gz', 'w:gz')
(tmp_path / 'foobar.txt').write_text("Hello world!")
mock_static_archive.addfile(
mock_static_archive.gettarinfo(
arcname='foobar.txt',
fileobj=(tmp_path / 'foobar.txt').open('r')
)
)
mock_static_archive.close()
app = mock_sphinx_testapp(srcdir=mock_sphinx_testsrc_dir())
app.setup_extension('byteb4rb1e.sphinxcontrib.ext.html_static_archive')
app.config['html_static_archive'] = f'pkg://dummypkg/data.tar.gz'
app.build(force_all=True)
assert (Path(app.outdir) / '_static' / 'foobar.txt').exists()
@run_in_subprocess_once()
def test_subdirs(
mock_sphinx_testapp,
mock_sphinx_testsrc_dir,
mock_system_site_package_dir,
tmp_path,
) -> None:
"""
"""
pkg_dir = mock_system_site_package_dir('dummypkg')
mock_static_archive = tarfile.open(pkg_dir / 'data.tar.gz', 'w:gz')
(tmp_path / 'foobar.txt').write_text("Hello world!")
mock_static_archive.addfile(
mock_static_archive.gettarinfo(
arcname='foo/bar/foobar.txt',
fileobj=(tmp_path / 'foobar.txt').open('r')
)
)
mock_static_archive.close()
app = mock_sphinx_testapp(srcdir=mock_sphinx_testsrc_dir())
app.setup_extension('byteb4rb1e.sphinxcontrib.ext.html_static_archive')
app.config['html_static_archive'] = f'pkg://dummypkg/data.tar.gz'
app.build(force_all=True)
assert (Path(app.outdir) / '_static' / 'foo' / 'bar' / 'foobar.txt').exists()

View file

@ -0,0 +1,32 @@
from pathlib import Path
import pytest
pytestmark = pytest.mark.pytest
from byteb4rb1e.sphinxcontrib.testing.pytest.fixtures import (
mock_sphinx_testapp,
mock_sphinx_testsrc_dir
)
def test_mock_sphinx_testsrc_dir(mock_sphinx_testsrc_dir):
"""
"""
srcdir = mock_sphinx_testsrc_dir()
assert (srcdir / 'conf.py').exists()
assert (srcdir / 'index.rst').exists()
def test_mock_sphinx_testapp(mock_sphinx_testapp, mock_sphinx_testsrc_dir):
"""
"""
srcdir = mock_sphinx_testsrc_dir()
app = mock_sphinx_testapp(srcdir=srcdir)
assert app.builder is not None
assert Path(app.srcdir).samefile(srcdir) is False # Should be copied to internal src
app.build(force_all=True)

View file

@ -1,2 +0,0 @@
def test_default() -> None:
assert 1 == 1

View file

@ -0,0 +1,17 @@
from pathlib import Path
import pytest
pytest_plugins = ['byteb4rb1e.sphinxcontrib.testing.pytest.fixtures']
_TESTS_ROOT = Path(__file__).resolve().parent
def pytest_configure(config):
# register an additional marker
config.addinivalue_line(
"markers", "pytest: test pytest integration"
)
@pytest.fixture(scope='session')
def rootdir() -> Path:
return _TESTS_ROOT

34
tox.ini
View file

@ -2,8 +2,9 @@
requires =
tox>=4.19
env_list =
py3{8-12}-{unit}
py3{8-12}-sphinx{5-8}-{integration}
unit-py3{9-13}
integration-py3{9-13}-sphinx{5-8}
integration-py3{9-13}-sphinx{5-8}-pytest8
lint
format
@ -35,17 +36,17 @@ deps =
commands =
black --check src tests
[testenv:py3{9-13}-unit]
[testenv:unit-py3{9-13}]
description = run type check on code base
labels = unit
deps =
{[testenv]deps}
pytest
commands =
pytest tests/unit --junitxml=test-reports/{env_name}.xml
pytest tests/unit --junitxml=test-reports/{env_name}.xml {posargs}
[testenv:py3{9-13}-sphinx{5-7}-integration]
description = run type check on code base
[testenv:integration-py3{9-13}-sphinx{5-7}]
description = run sphinx 5-7 integration tests
labels = integration
deps =
{[testenv]deps}
@ -54,14 +55,27 @@ deps =
sphinx7: sphinx>=7.0,<=8.0
pytest
commands =
pytest tests/integration --junitxml=test-reports/{env_name}.xml
pytest tests/integration --junitxml=test-reports/{env_name}.xml {posargs}
[testenv:py3{10-13}-sphinx8-integration]
description = run type check on code base
[testenv:integration-py3{10-13}-sphinx8]
description = run sphinx 8 integration tests
labels = integration
deps =
{[testenv]deps}
sphinx8: sphinx>=8.0,<=9.0
pytest
commands =
pytest tests/integration --junitxml=test-reports/{env_name}.xml
pytest tests/integration --junitxml=test-reports/{env_name}.xml {posargs}
[testenv:integration-py3{10-13}-sphinx{5-8}-pytest8]
description = run pytest 8 testing integration tests (excluding Python 3.9)
labels = integration
deps =
{[testenv]deps}
sphinx5: sphinx>=5.0,<=6.0
sphinx6: sphinx>=6.0,<=7.0
sphinx7: sphinx>=7.0,<=8.0
sphinx8: sphinx>=8.0,<=9.0
pytest8: pytest>=8.0,<=9.0
commands =
pytest tests/integration -m pytest --junitxml=test-reports/{env_name}.xml {posargs}