Compare commits

...

7 commits
v0.1.0 ... dev

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
Tiara Rodney
19813d4d45
todo(1): in-progress 2025-06-06 22:40:57 +02:00
Tiara Rodney
e5be3c146b
todo(1): open 2025-06-06 22:40:57 +02:00
Tiara Rodney
cc7bae3c29
changes 2025-06-06 22:40:46 +02:00
22 changed files with 401 additions and 27 deletions

View file

@ -6,8 +6,6 @@ GPG_SIGNER_FINGERPRINT := "91CD826E74B0174D181903DEF97C70941CD8C4EF"
.PHONY: chore configure requirements-dev.txt requirements.txt publish archive
all: chore
chore: requirements.txt requirements-dev.txt bitbucket-pipelines.yaml
Pipfile.lock: Pipfile

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 = "*"

8
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "48c822d9e7bedd5ee25106b31093ce10a0071d7b12f7861e4e4cc46c5549c9ed"
"sha256": "4eac08fb6ab8c543a1fa6030c3c69a57aa6ae6de69e4f47ec483128bb61b2867"
},
"pipfile-spec": 6,
"requires": {
@ -32,9 +32,9 @@
"markers": "python_version >= '3.8'",
"version": "==2.17.0"
},
"byteb4rb1e-sphinxcontrib": {
"editable": true,
"path": "."
"byteb4rb1e.utils": {
"git": "https://bitbucket.org/byteb4rb1e/py-utils.git",
"ref": "d0dfa1cb12702e6d25f3a9eeab02968eda8d06ba"
},
"certifi": {
"hashes": [

9
TODO
View file

@ -47,3 +47,12 @@ Description: [Detailed explanation]
## Issues
ID: 1
Type: feature
Title: Experiment with repository layout and build environment
Status: in-progress
Priority: low
Created: 2025-06-06
Description: Find the best repository layout and build environment for my case
---

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,6 +30,7 @@ classifiers = [
]
dependencies = [
"sphinx>=5.1",
"byteb4rb1e.utils @ git+https://bitbucket.org/byteb4rb1e/py-utils.git@32ae99c5fa0174761f4053fce1130f3cd7a2a68b"
]
dynamic = ["version"]
requires-python = ">=3.8"
@ -39,6 +40,7 @@ Bitbucket = "https://bitbucket.org/byteb4rb1e/sphinxcontrib"
[tool.setuptools.packages.find]
where = ["src"]
namespaces = true
[tool.autopep8]

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

@ -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,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}