Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b594ab5b2a | ||
|
|
6321c3f11b | ||
|
|
a4fd97b55a | ||
|
|
5c2fb8c32b | ||
|
|
19813d4d45 | ||
|
|
e5be3c146b | ||
|
|
cc7bae3c29 |
22 changed files with 401 additions and 27 deletions
2
Makefile
2
Makefile
|
|
@ -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
|
||||
|
|
|
|||
2
Pipfile
2
Pipfile
|
|
@ -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
8
Pipfile.lock
generated
|
|
@ -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
9
TODO
|
|
@ -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
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
108
src/byteb4rb1e/sphinxcontrib/testing/pytest/fixtures.py
Normal file
108
src/byteb4rb1e/sphinxcontrib/testing/pytest/fixtures.py
Normal 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
|
||||
|
|
@ -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 {}
|
||||
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
def test_default() -> None:
|
||||
assert 1 == 1
|
||||
17
tests/integration/conftest.py
Normal file
17
tests/integration/conftest.py
Normal 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
34
tox.ini
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue