././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9231503 sphinxtesters-0.2.4/README.rst0000644000000000000000000000735414467760231013100 0ustar00####################################################### Sphinxtesters - utilities for testing Sphinx extensions ####################################################### .. shared-text-body ********** Quickstart ********** If you have a directory containing a sphinx project, test that it builds with something like: .. code:: python class TestMyProject(SourcesBuilder): page_source_template = 'path/to/sphinx_dir' def test_basic_build(self): # Get doctree for page "a_page.rst" doctree = self.get_doctree('a_page') # Assert stuff about doctree version of page html = self.get_built_file('a_page.html') # Assert stuff about html version of page You can try adding other page content by using the ``rst_sources`` dictionary: .. code:: python class TestChangedProject(SourcesBuilder): page_source_template = 'path/to/sphinx_dir' rst_sources = {'a_page': """Replacement text for page""", 'b_page': """An entirely new page"""} def test_basic_build(self): a_doctree = self.get_doctree('a_page') b_doctree = self.get_doctree('b_page') # Your tests for the new page content here Set the text of the ``conf.py`` file with the ``conf_source`` attribute: .. code:: python class TestConfeddProject(SourcesBuilder): page_source_template = 'path/to/sphinx_dir' rst_sources = {'a_page': """Replacement text for page""", 'b_page': """An entirely new page"""} conf_source = """ # This overwrites existing conf.py """ def test_basic_build(self): a_doctree = self.get_doctree('a_page') b_doctree = self.get_doctree('b_page') # Your tests for the new page content here You don't need to set ``page_source_template``; if you don't, you start with a fresh project, where the only pages are the ones you specify in ``rst_sources``. .. code:: python class TestFreshProject(SourcesBuilder): rst_sources = {'a_page': """A new page""", 'b_page': """Another new page"""} conf_source = """ # Stuff for the conf.py file """ def test_basic_build(self): a_doctree = self.get_doctree('a_page') b_doctree = self.get_doctree('b_page') # Your tests for the new page content here See the tests for examples of using Sphinxtesters for testing builds of Sphinx projects. ************ Installation ************ :: pip install sphinxtesters **** Code **** See https://github.com/matthew-brett/sphinxtesters Released under the BSD two-clause license - see the file ``LICENSE`` in the source distribution. `travis-ci `_ kindly tests the code automatically under Python versions 2.7, and 3.3 through 3.6. The latest released version is at https://pypi.python.org/pypi/sphinxtesters ***** Tests ***** * Install ``sphinxtesters`` * Install the pytest_ testing framework:: pip install pytest * Run the tests with:: pytest sphinxtesters ******* Support ******* Please put up issues on the `sphinxtesters issue tracker`_. .. standalone-references .. |sphinxtesters-documentation| replace:: `sphinxtesters documentation`_ .. _sphinxtesters documentation: https://matthew-brett.github.com/sphinxtesters/sphinxtesters.html .. _documentation: https://matthew-brett.github.com/sphinxtesters .. _pandoc: http://pandoc.org .. _jupyter: jupyter.org .. _homebrew: brew.sh .. _sphinx: http://sphinx-doc.org .. _rest: http://docutils.sourceforge.net/rst.html .. _sphinxtesters issue tracker: https://github.com/matthew-brett/sphinxtesters/issues .. _pytest: https://pytest.org .. _mock: https://github.com/testing-cabal/mock ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730288923.4006407 sphinxtesters-0.2.4/pyproject.toml0000644000000000000000000000216014710416433014304 0ustar00[build-system] requires = ["flit_core >=3.4,<4"] build-backend = "flit_core.buildapi" [project] name = "sphinxtesters" authors = [ {name = "Matthew Brett", email = "matthew.brett@gmail.com"} ] classifiers = [ 'Development Status :: 3 - Alpha', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Topic :: Scientific/Engineering', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Operating System :: Unix', 'Operating System :: MacOS', ] readme = "README.rst" dynamic = ["version", "description"] # Check against requirements.txt dependencies = [ 'sphinx>=1.4', ] requires-python = ">=3.8" [project.urls] Home = "https://github.com/matthew-brett/sphinxtesters" Documentation = "https://github.com/matthew-brett/sphinxtesters" Source = "https://github.com/matthew-brett/sphinxtesters" [project.optional-dependencies] test = [ 'pytest' ] doc = [ 'ghp-import', 'numpydoc' ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730288694.6671653 sphinxtesters-0.2.4/sphinxtesters/__init__.py0000644000000000000000000000027014710416067016427 0ustar00""" Utilities for testing Sphinx extensions """ from .sphinxutils import (PageBuilder, SourcesBuilder, ModifiedPageBuilder, TempApp) __version__ = '0.2.4' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9255311 sphinxtesters-0.2.4/sphinxtesters/sphinxutils.py0000644000000000000000000003621314467760231017274 0ustar00""" Utilities for running sphinx tasks in-process """ import sys import os from os.path import join as pjoin, isdir, split as psplit, isfile import shutil from contextlib import contextmanager from copy import copy from tempfile import mkdtemp import pickle import warnings from docutils import nodes from docutils.parsers.rst import directives, roles import sphinx from sphinx.application import Sphinx from sphinx.domains.std import StandardDomain fresh_roles = copy(roles._roles) fresh_directives = copy(directives._directives) fresh_std_domain_init_labels = StandardDomain.initial_data['labels'].copy() @contextmanager def in_dir(path): """ Change into directory for duration of context """ cwd = os.getcwd() try: os.chdir(path) yield finally: os.chdir(cwd) def _visit_depart_attrs(cls): return [attr for attr in dir(cls) if attr.startswith('visit_') or attr.startswith('depart_')] def _get_visit_depart(cls): return {attr: getattr(cls, attr) for attr in _visit_depart_attrs(cls)} def _set_visit_depart(cls, to_set): for attr in _visit_depart_attrs(cls): if attr not in to_set: delattr(cls, attr) for attr, method in to_set.items(): setattr(cls, attr, method) class TestApp(Sphinx): # Default index filename. index_root = ('contents' if sphinx.version_info[0] < 2 else 'index') def __init__(self, *args, **kwargs): self._set_cache() with self.own_namespace(): super(TestApp, self).__init__(*args, **kwargs) def _set_cache(self): """ Initialize namespace from current state of docutils objects """ self._global_cache = dict( directives=copy(fresh_directives), roles=copy(fresh_roles), visit_depart = _get_visit_depart(nodes.GenericNodeVisitor), std_domain_init_labels = copy(fresh_std_domain_init_labels)) @contextmanager def own_namespace(self): """ Set docutils namespace for builds Sphinx uses global docutils objects. We need to isolate the effects of this sphinx application from others. * Store current state of docutils objects. * Take stored state of these objects from cache attached to `self` and put into docutils. * Yield to user of context manager. * Make sure any changes in state during yield go back into cache for future use by `self`. * Restore initial state of docutils objects. """ cache = self._global_cache _directives = directives._directives _roles = roles._roles GNV = nodes.GenericNodeVisitor _visit_depart = _get_visit_depart(GNV) _std_domain_init_labels = StandardDomain.initial_data['labels'] directives._directives = cache['directives'] roles._roles = cache['roles'] _set_visit_depart(GNV, cache['visit_depart']) StandardDomain.initial_data['labels'] = cache['std_domain_init_labels'] try: yield finally: # Reset docutils, Sphinx global state directives._directives = _directives roles._roles = _roles cache['visit_depart'] = _get_visit_depart(GNV) _set_visit_depart(GNV, _visit_depart) StandardDomain.initial_data['labels'] = _std_domain_init_labels def build(self, *args, **kwargs): with self.own_namespace(): return super(TestApp, self).build(*args, **kwargs) class TempApp(TestApp): """ An application pointing to its own temporary directory. The instance deletes its temporary directory when garbage collected. Parameters ---------- rst_text : str String containing ReST to build. conf_text : str, optional Text for configuration ``conf.py`` file. buildername : str, optional Name of default builder. status : file-like object or None, optional File-like object to which to write build status messages, or None for no build status messages. warningiserror : {True, False}, optional If True, raise an error for warning during the Sphinx build. """ def __init__(self, rst_text, conf_text='', buildername='html', status=sys.stdout, warningiserror=True): self.tmp_dir = tmp_dir = mkdtemp() with open(pjoin(tmp_dir, 'conf.py'), 'wt') as fobj: fobj.write(conf_text) # Write given RST text to master document. config = {} exec(conf_text, {}, config) master_doc = config.get('master_doc', self.index_root) with open(pjoin(tmp_dir, master_doc + '.rst'), 'wt') as fobj: fobj.write(rst_text) # Initialize cache of docutils global vars. self._set_cache() with self.own_namespace(): TestApp.__init__(self, srcdir=tmp_dir, confdir=tmp_dir, # Sphinx 1.8.0b1 does not allow srcdir==outdir outdir=pjoin(tmp_dir, 'build'), doctreedir=tmp_dir, buildername=buildername, status=status, warningiserror=warningiserror) def cleanup(self): if self.tmp_dir is None: return shutil.rmtree(self.tmp_dir) self.tmp_dir = None def __del__(self): # Sphinx application may or may not have a __del__ method. try: super(TempApp, self).__del__() except AttributeError: pass self.cleanup() class PageBuilder(object): """ Test class to build sphinx pages in temporary directory When child class has a name Pytest recognizes as a test class, Pytest will call :meth:`setup_class`. In this class method, :meth:`set_page_source` copies / makes / manipulates the source pages. It likely calls :meth:`modify_source` at the end, allowing you to hook in any other modifications. :meth:`setup_class` then initializes the Sphinx applicaton object, and builds the pages, using :meth:`build_source`. The default behavior is to initialize the source directory by copying from a template directory specified in ``page_source_template``. This can be None, to start with an empty source directory, before modifications by :meth:`modify_source`. """ # If True, assert that the build raised an error should_error = False # Builder builder = 'html' # Set to path containing any original sources that we copy to initialize # the source directory. Can be None (no pages copied). page_source_template = None # Name of default contents file (depends on Sphinx version) index_root = TestApp.index_root @classmethod def setup_class(cls): cls.build_error = None cls.build_path = mkdtemp() try: # Catch exceptions during test setup # Sets page_source, maybe modifies source cls.set_page_source() cls.out_dir = pjoin(cls.build_path, cls.builder) cls.doctree_dir = pjoin(cls.build_path, 'doctrees') # App to build the pages with warnings turned into errors cls.build_app = TestApp( cls.page_source, cls.page_source, cls.out_dir, cls.doctree_dir, cls.builder, warningiserror=True) except Exception as e: # Exceptions during test setup shutil.rmtree(cls.build_path) raise e cls.build_source() @classmethod def set_page_source(cls): """ Set directory containing page sources, maybe modify source. """ cls.page_source = pjoin(cls.build_path, 'source') if cls.page_source_template: shutil.copytree(cls.page_source_template, cls.page_source) else: os.mkdir(cls.page_source) cls.modify_source() @classmethod def modify_source(cls): """ Override to modify sources before initial build """ @classmethod def build_source(cls): try: # Catch exceptions during sphinx build cls.build_app.build(False, []) if cls.build_app.statuscode != 0: cls.build_error = "Unknown error" except Exception as e: # Exceptions during sphinx build cls.build_error = e # We will later check if a page build that should error, did error if cls.build_error is None or cls.should_error: return # An unexpected error - delete temp dir and report. shutil.rmtree(cls.build_path) raise RuntimeError('page build failed with build error {}' .format(cls.build_error)) def get_doctree(self, name): """ Return doctree given by `name` from pickle in doctree file """ with open(pjoin(self.doctree_dir, name + '.doctree'), 'rb') as fobj: content = fobj.read() return pickle.loads(content) @classmethod def get_built_file(cls, basename, encoding='utf8'): """ Contents of file in build dir with basename `basename` Parameters ---------- basename : str Basename of file to load, including extension. encoding : str, optional If None, return contents as bytes. If not None, decode contents with the given encoding. Returns ------- content : str or bytes Return text contents of file if `encoding` not None, else return binary contents of file. """ with open(pjoin(cls.out_dir, basename), 'rb') as fobj: content = fobj.read() return content if encoding is None else content.decode(encoding) def doctree2str(self, doctree): """ Return simple string representation from `doctree` """ return '\n'.join(str(p) for p in doctree.document[0]) def test_build_error(self): # Check whether an expected build error has occurred assert self.should_error == (self.build_error is not None) @classmethod def append_conf(cls, string): """ Append stuff to the conf.py file """ with open(pjoin(cls.page_source, 'conf.py'), 'a') as fobj: fobj.write(string) @classmethod def teardown_class(cls): if isdir(cls.build_path): shutil.rmtree(cls.build_path) class SourcesBuilder(PageBuilder): """ Build pages with text in class attribute ``rst_sources``. Class that stores page names, page contents as key, value pairs in the ``rst_sources`` class attribute. ``conf.py`` contents can go in the ``conf_source`` class attribute. """ # rst_sources is a dict with key, value pairs, where the keys are the page # names, with directory names separated by / regardless of platform we're # running on. ``.rst`` extension assumed (do not add it). The values are # strings giving the page contents. rst_sources = dict() # Contents for conf.py. Can be empty to use existing contents. If not # empty, then contents overwrites any existing conf.py file. conf_source = '' # Pages to be listed in the master document toctree toctree_pages = [] @classmethod def _touch(cls, fname): if isfile(fname): return with open(fname, 'wt') as fobj: fobj.write('') @classmethod def modify_source(cls): conf_fname = pjoin(cls.page_source, 'conf.py') if cls.conf_source: with open(conf_fname, 'wt') as fobj: fobj.write(cls.conf_source) else: cls._touch(conf_fname) for page_root, page_content in cls.rst_sources.items(): # page root may contain directories page_root = page_root.replace('/', os.path.sep) page_dir, page_base = psplit(page_root) page_dir = pjoin(cls.page_source, page_dir) if not isdir(page_dir): os.makedirs(page_dir) page_path = pjoin(page_dir, page_base + '.rst') with open(page_path, 'wt') as fobj: # Avoid warning (-> error) when page not included in toctree fobj.write(":orphan:\n\n") fobj.write(page_content) master_fname = cls._get_master() # Always write blank master document, if not already present. cls._touch(master_fname) # Write toctree to master doc, if needed cls.write_toctree(cls.toctree_pages, master_fname) @classmethod def get_conf_vars(cls, force=False): vars = {} with in_dir(cls.page_source): with open('conf.py', 'rt') as fobj: conf = fobj.read() exec(conf, {}, vars) return vars @classmethod def _get_master(cls): """ Return filename of master page for project """ master_doc = cls.get_conf_vars().get('master_doc', cls.index_root) return pjoin(cls.page_source, master_doc + '.rst') @classmethod def write_toctree(cls, page_list, master_fname=None): """ Write toctree directive for given page list """ if len(page_list) == 0: return if master_fname is None: master_fname = cls._get_master() with open(master_fname, 'at') as fobj: fobj.write("\n\n.. toctree::\n\n {0}\n\n".format( '\n'.join(page_list))) class ModifiedPageBuilder(PageBuilder): """ Add utilities for changing pages from template. This class now deprecated, please use PageBuilder instead. """ # Default page. Should specify a path-less page name that can be replaced # in modified builds. default_page = None @classmethod def setup_class(cls): warnings.warn('ModifedPageBuilder deprecated, please ' 'use PageBuilder instead', DeprecationWarning, stacklevel=2) super(ModifiedPageBuilder, cls).setup_class() @classmethod def replace_page(cls, file_like): """ Replace default page with contents of `file_like` """ out_fname = pjoin(cls.page_source, cls.default_page + '.rst') if hasattr(file_like, 'read'): contents = file_like.read() with open(out_fname, 'wt') as fobj: fobj.write(contents) return shutil.copyfile(file_like, out_fname) @classmethod def add_page(cls, file_like, out_name): """ Add another page from `file_like` with name `out_name` Parameters ---------- file_like : file-like or str File-like object or filename. out_name : str Base of filename for output. We will prepend the ``cls.page_source`` path, and add a ``.rst`` suffix. """ out_fname = pjoin(cls.page_source, out_name + '.rst') if hasattr(file_like, 'read'): contents = file_like.read() with open(out_fname, 'wt') as fobj: fobj.write(contents) else: shutil.copyfile(file_like, out_fname) with open(pjoin(cls.page_source, 'index.rst'), 'a') as fobj: fobj.write("\n\n.. toctree::\n\n {0}\n\n".format(out_name)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9256234 sphinxtesters-0.2.4/sphinxtesters/tests/__init__.py0000644000000000000000000000000014467760231017564 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9257898 sphinxtesters-0.2.4/sphinxtesters/tests/proj1/.gitignore0000644000000000000000000000001014467760231020477 0ustar00_build/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9258883 sphinxtesters-0.2.4/sphinxtesters/tests/proj1/README.md0000644000000000000000000000005214467760231017774 0ustar00# Test project A minimal Sphinx project. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9259982 sphinxtesters-0.2.4/sphinxtesters/tests/proj1/_static/.gitignore0000644000000000000000000000000014467760231022124 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9261227 sphinxtesters-0.2.4/sphinxtesters/tests/proj1/_static/README.txt0000644000000000000000000000046214467760231021646 0ustar00############################### Static directory for test pages ############################### We need this README file to make sure the ``_static`` directory gets created in the installation. The tests check for warnings in builds, and, when the ``_static`` directory is absent, this raises a warning. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9262164 sphinxtesters-0.2.4/sphinxtesters/tests/proj1/a_page.rst0000644000000000000000000000011014467760231020456 0ustar00######### A section ######### Some text. More text. Text is endless. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1692393624.926302 sphinxtesters-0.2.4/sphinxtesters/tests/proj1/conf.py0000644000000000000000000000006414467760231020017 0ustar00# The master toctree document. master_doc = 'index' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9264271 sphinxtesters-0.2.4/sphinxtesters/tests/proj1/index.rst0000644000000000000000000000027414467760231020364 0ustar00Here start the pages ==================== Contents: .. toctree:: :maxdepth: 2 a_page Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9265444 sphinxtesters-0.2.4/sphinxtesters/tests/test_modified_pagebuilder.py0000644000000000000000000000616314467760231023227 0ustar00""" Test ModifiedPageBuilder """ from io import StringIO from os.path import dirname, join as pjoin from sphinxtesters.sphinxutils import ModifiedPageBuilder from sphinxtesters.tmpdirs import in_dtemp import pytest HERE = dirname(__file__) PROJ1 = pjoin(HERE, 'proj1') class TestModifiedPageBuilder(ModifiedPageBuilder): # Replace page with file-like object page_source_template = PROJ1 default_page = 'a_page' _new_page = u""" Fancy title +++++++++++ Compelling text """ @classmethod def modify_source(cls): page_fobj = StringIO(cls._new_page) cls.replace_page(page_fobj) def test_a_build(self): doctree = self.get_doctree(self.default_page) doctree_str = self.doctree2str(doctree) expected = ( 'Fancy title\n' 'Compelling text') assert doctree_str == expected class TestFModifiedPageBuilder(TestModifiedPageBuilder): # Replace page, but with filename @classmethod def modify_source(cls): with in_dtemp(): with open('test.txt', 'wt') as fobj: fobj.write(cls._new_page) cls.replace_page('test.txt') def test_bad_pagebuilder(): """ Tests that warning on build generates error """ class TestBadPageBuilder(TestModifiedPageBuilder): _new_page = u""" Fancy title +++++++++++ :ref:`not-a-target` """ with pytest.raises(RuntimeError): TestBadPageBuilder.setup_class() class TestAppendConf(TestModifiedPageBuilder): # Test append_conf method @classmethod def modify_source(cls): super(TestAppendConf, cls).modify_source() cls.append_conf('# Spurious comment') def test_append_conf(self): with open(pjoin(PROJ1, 'conf.py'), 'rt') as fobj: before_contents = fobj.read() with open(pjoin(self.page_source, 'conf.py'), 'rt') as fobj: after_contents = fobj.read() assert (after_contents == before_contents + '# Spurious comment') class TestAddPage(TestModifiedPageBuilder): # Test ability to add a page @classmethod def modify_source(cls): page_fobj = StringIO(cls._new_page) cls.add_page(page_fobj, 'b_page') def test_a_build(self): doctree = self.get_doctree(self.default_page) doctree_str = self.doctree2str(doctree) expected = ( 'A section\n' 'Some text.\n' 'More text.\n' 'Text is endless.') assert doctree_str == expected expected = ( 'Fancy title\n' 'Compelling text') doctree = self.get_doctree('b_page') doctree_str = self.doctree2str(doctree) assert doctree_str == expected class TestAddFPage(TestAddPage): # Test ability to add a page as filename @classmethod def modify_source(cls): with in_dtemp(): with open('test.txt', 'wt') as fobj: fobj.write(cls._new_page) cls.add_page('test.txt', 'b_page') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9266932 sphinxtesters-0.2.4/sphinxtesters/tests/test_pagebuilder.py0000644000000000000000000001004114467760231021355 0ustar00""" Test PageBuilder """ from os.path import (dirname, join as pjoin, isdir, isfile) from sphinx.errors import ConfigError try: # Sphinx 1.8.0b1 from sphinx.errors import ApplicationError as AppError except ImportError: AppError = ConfigError from sphinxtesters.sphinxutils import PageBuilder import pytest HERE = dirname(__file__) PROJ1 = pjoin(HERE, 'proj1') class TestPageBuilder(PageBuilder): # Test a minmal source tree @classmethod def modify_source(cls): # Make an empty conf.py and contents.rst file with text cls.append_conf('') index_fname = pjoin(cls.page_source, cls.index_root + '.rst') with open(index_fname, 'wt') as fobj: fobj.write('some text') def test_build(self): assert isdir(self.out_dir) assert isdir(self.doctree_dir) index_fname = pjoin(self.out_dir, self.index_root + '.html') assert isfile(index_fname) doctree = self.get_doctree(self.index_root) assert doctree.document.astext() == 'some text' class TestMaster(PageBuilder): # Test we can change the master document @classmethod def modify_source(cls): cls.append_conf('master_doc = "foo"') with open(pjoin(cls.page_source, 'foo.rst'), 'wt') as fobj: fobj.write('more text') def test_build(self): index_fname = pjoin(self.out_dir, self.index_root + '.html') assert not isfile(index_fname) assert isfile(pjoin(self.out_dir, 'foo.html')) doctree = self.get_doctree('foo') assert doctree.document.astext() == 'more text' class TestTemplatePageBuilder(PageBuilder): page_source_template = PROJ1 def test_build(self): assert isdir(self.out_dir) assert isdir(self.doctree_dir) doctree = self.get_doctree('a_page') assert len(doctree.document) == 1 doctree_str = self.doctree2str(doctree) expected = ( 'A section\n' 'Some text.\n' 'More text.\n' 'Text is endless.') assert doctree_str == expected assert isfile(pjoin(self.doctree_dir, 'index.doctree')) html = self.get_built_file('a_page.html') assert 'Text is endless' in html def test_bad_pagebuilder(): class TestBadPageBuilder(PageBuilder): @classmethod def set_page_source(cls): cls.page_source = HERE # ConfigError as of Sphinx 1.6.6 # ApplicationError as of 1.8.0b1 # See imports. with pytest.raises((IOError, ConfigError, AppError)): TestBadPageBuilder.setup_class() class TestRewrite(TestTemplatePageBuilder): # Replace page, check we get replacement page _page = u""" Fancy title +++++++++++ Compelling text """ @classmethod def modify_source(cls): fname = pjoin(cls.page_source, 'a_page.rst') with open(fname, 'wt') as fobj: fobj.write(cls._page) def test_build(self): doctree = self.get_doctree('a_page') doctree_str = self.doctree2str(doctree) expected = ( 'Fancy title\n' 'Compelling text') assert doctree_str == expected def test_bad_pagebuilder_with_template(): """ Tests that warning on build generates error """ class TestBadPageBuilder(TestRewrite): _page = u""" Fancy title +++++++++++ :ref:`not-a-target` """ with pytest.raises(RuntimeError): TestBadPageBuilder.setup_class() class TestAppendConf(TestRewrite): # Test append_conf method @classmethod def modify_source(cls): super(TestAppendConf, cls).modify_source() cls.append_conf('# Spurious comment') def test_append_conf(self): with open(pjoin(PROJ1, 'conf.py'), 'rt') as fobj: before_contents = fobj.read() with open(pjoin(self.page_source, 'conf.py'), 'rt') as fobj: after_contents = fobj.read() assert (after_contents == before_contents + '# Spurious comment') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9268138 sphinxtesters-0.2.4/sphinxtesters/tests/test_proj1.py0000644000000000000000000000204714467760231020134 0ustar00""" Tests for proj1 build using sphinx extensions """ from os.path import (join as pjoin, dirname, isdir, exists, splitext) from sphinxtesters import ModifiedPageBuilder HERE = dirname(__file__) class Proj1Builder(ModifiedPageBuilder): """ Build using 'proj1' directory as template to modify """ page_source_template = pjoin(HERE, 'proj1') # default_page used in 'replace_page' class method default_page = 'a_page.rst' class TestProj1(Proj1Builder): def test_basic_build(self): assert isdir(self.out_dir) assert isdir(self.doctree_dir) doctree = self.get_doctree(splitext(self.default_page)[0]) assert len(doctree.document) == 1 doctree_str = self.doctree2str(doctree) expected = ( 'A section\n' 'Some text.\n' 'More text.\n' 'Text is endless.') assert doctree_str == expected assert exists(pjoin(self.doctree_dir, 'index.doctree')) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9269369 sphinxtesters-0.2.4/sphinxtesters/tests/test_sources_builder.py0000644000000000000000000000777714467760231022311 0ustar00""" Tests for SourcesBuilder utility """ from os.path import (join as pjoin, isdir, exists, dirname) from sphinxtesters import SourcesBuilder import pytest HERE = dirname(__file__) PROJ1 = pjoin(HERE, 'proj1') A_PAGE = """\ ######### A section ######### Some text. More text. Text is endless.""" A_DOCTREE = """\ A section Some text. More text. Text is endless.""" B_PAGE = """\ ############### Another section ############### Some more text.""" B_DOCTREE = """\ Another section Some more text.""" NO_TITLE_PAGE = """\ Just text, no title.""" NO_TITLE_DOCTREE = """\ Just text, no title.""" class CheckSources(SourcesBuilder): """ Template for testing some pages """ def test_structure(self): assert isdir(self.out_dir) assert isdir(self.doctree_dir) index_fname = pjoin(self.doctree_dir, self.index_root + '.doctree') assert exists(index_fname) for page_name in self.rst_sources: assert exists(pjoin(self.doctree_dir, page_name + '.doctree')) def check_page(self, page_name, expected_doctree): doctree = self.get_doctree(page_name) assert len(doctree.document) == 1 doctree_str = self.doctree2str(doctree) assert doctree_str == expected_doctree class TestAPage(CheckSources): rst_sources = dict(a_page=A_PAGE) expected_doctree = A_DOCTREE def test_page(self): page_name = list(self.rst_sources)[0] self.check_page(page_name, self.expected_doctree) class TestBPage(TestAPage): rst_sources = dict(b_page=B_PAGE) expected_doctree = B_DOCTREE class TestNoTitlePage(TestAPage): rst_sources = dict(no_title_page=NO_TITLE_PAGE) expected_doctree = NO_TITLE_DOCTREE class TestTemplateSourcesBuilder(SourcesBuilder): # Replace page using rst_sources page_source_template = PROJ1 rst_sources = {'a_page': u""" Fancy title +++++++++++ Compelling text """} def test_a_build(self): doctree = self.get_doctree('a_page') doctree_str = self.doctree2str(doctree) expected = ( 'Fancy title\n' 'Compelling text') assert doctree_str == expected def test_bad_souurcesbuilder_with_template(): """ Tests that warning on build generates error """ class TestBadSourcesBuilder(TestTemplateSourcesBuilder): rst_sources = {'a_page': u""" Fancy title +++++++++++ :ref:`not-a-target` """} with pytest.raises(RuntimeError): TestBadSourcesBuilder.setup_class() class TestAppendConf(TestTemplateSourcesBuilder): # Test append_conf method @classmethod def modify_source(cls): super(TestAppendConf, cls).modify_source() cls.append_conf('# Spurious comment') def test_append_conf(self): with open(pjoin(PROJ1, 'conf.py'), 'rt') as fobj: before_contents = fobj.read() with open(pjoin(self.page_source, 'conf.py'), 'rt') as fobj: after_contents = fobj.read() assert (after_contents == before_contents + '# Spurious comment') class TestNoTocTree(SourcesBuilder): # Test no toctree write rst_sources = dict(a_page=A_PAGE) def test_master(self): doctree = self.get_doctree(self.index_root) assert doctree.document.astext() == '' assert len(doctree.document.children) == 0 class TestTocTree(TestNoTocTree): # Test toctree write to master_doc toctree_pages = ['a_page'] master_name = TestNoTocTree.index_root def test_master(self): doctree = self.get_doctree(self.master_name) assert len(doctree.document.children) == 1 toctree = doctree.document.children[0] entries = toctree[0]['entries'] assert entries == [(None, 'a_page')] class TestFooTocTree(TestTocTree): # Test toctree write to another master_doc conf_source = 'master_doc = "foo"' master_name = 'foo' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9270487 sphinxtesters-0.2.4/sphinxtesters/tests/test_testapp.py0000644000000000000000000000200314467760231020551 0ustar00""" Test TempApp, TestApp classes """ from os.path import (join as pjoin, isdir) from sphinxtesters.sphinxutils import TempApp def assert_contents_equal(fname, contents, mode='t'): with open(fname, 'r' + mode) as fobj: f_contents = fobj.read() assert f_contents == contents def test_tempapp(): rst_txt = 'A simple page' app = TempApp(rst_txt) app.build() app_path = app.tmp_dir index_fname = pjoin(app_path, app.index_root + '.rst') assert_contents_equal(index_fname, rst_txt) assert_contents_equal(pjoin(app_path, 'conf.py'), '') app.cleanup() assert not isdir(app_path) def test_tempapp_master_doc(): rst_txt = 'A simple page' conf_txt = 'master_doc = "my_master"' app = TempApp(rst_txt, conf_txt) app.build() app_path = app.tmp_dir index_fname = pjoin(app_path, 'my_master.rst') assert_contents_equal(index_fname, rst_txt) assert_contents_equal(pjoin(app_path, 'conf.py'), conf_txt) app.cleanup() assert not isdir(app_path) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9271502 sphinxtesters-0.2.4/sphinxtesters/tests/test_tmpdirs.py0000644000000000000000000000161414467760231020562 0ustar00""" Test tmpdirs module """ from __future__ import division, print_function, absolute_import from os import unlink from os.path import isfile, isdir from ..tmpdirs import in_dtemp, dtemporize def test_in_dtemp(): # Test working in temporary directory with in_dtemp() as tmpdir: with open('test.txt', 'wt') as fobj: fobj.write('Some text') assert not isdir(tmpdir) def test_dtmeporize(): # Test decorator to work in temporary directory def func1(): with open('test.txt', 'wt') as fobj: fobj.write('Some text') @dtemporize def func2(): with open('test.txt', 'wt') as fobj: fobj.write('Some text') with in_dtemp(): func1() assert isfile('test.txt') unlink('test.txt') # Next one should be in a temporary directory, not here func2() assert not isfile('test.txt') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1692393624.9272485 sphinxtesters-0.2.4/sphinxtesters/tmpdirs.py0000644000000000000000000000125614467760231016363 0ustar00""" Utilities for running code in temporary directories """ from os import getcwd, chdir from tempfile import mkdtemp from functools import wraps from contextlib import contextmanager from shutil import rmtree @contextmanager def in_dtemp(): """ Change into temporary directory for duration of context """ tmpdir = mkdtemp() cwd = getcwd() try: chdir(tmpdir) yield tmpdir finally: chdir(cwd) rmtree(tmpdir) def dtemporize(func): """ Decorate a function to run in a temporary directory """ @wraps(func) def dfunc(*args, **kwargs): with in_dtemp(): return func(*args, **kwargs) return dfunc sphinxtesters-0.2.4/PKG-INFO0000644000000000000000000001155000000000000012426 0ustar00Metadata-Version: 2.1 Name: sphinxtesters Version: 0.2.4 Summary: Utilities for testing Sphinx extensions Author-email: Matthew Brett Requires-Python: >=3.8 Description-Content-Type: text/x-rst Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Scientific/Engineering Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Operating System :: MacOS Requires-Dist: sphinx>=1.4 Requires-Dist: ghp-import ; extra == "doc" Requires-Dist: numpydoc ; extra == "doc" Requires-Dist: pytest ; extra == "test" Project-URL: Documentation, https://github.com/matthew-brett/sphinxtesters Project-URL: Home, https://github.com/matthew-brett/sphinxtesters Project-URL: Source, https://github.com/matthew-brett/sphinxtesters Provides-Extra: doc Provides-Extra: test ####################################################### Sphinxtesters - utilities for testing Sphinx extensions ####################################################### .. shared-text-body ********** Quickstart ********** If you have a directory containing a sphinx project, test that it builds with something like: .. code:: python class TestMyProject(SourcesBuilder): page_source_template = 'path/to/sphinx_dir' def test_basic_build(self): # Get doctree for page "a_page.rst" doctree = self.get_doctree('a_page') # Assert stuff about doctree version of page html = self.get_built_file('a_page.html') # Assert stuff about html version of page You can try adding other page content by using the ``rst_sources`` dictionary: .. code:: python class TestChangedProject(SourcesBuilder): page_source_template = 'path/to/sphinx_dir' rst_sources = {'a_page': """Replacement text for page""", 'b_page': """An entirely new page"""} def test_basic_build(self): a_doctree = self.get_doctree('a_page') b_doctree = self.get_doctree('b_page') # Your tests for the new page content here Set the text of the ``conf.py`` file with the ``conf_source`` attribute: .. code:: python class TestConfeddProject(SourcesBuilder): page_source_template = 'path/to/sphinx_dir' rst_sources = {'a_page': """Replacement text for page""", 'b_page': """An entirely new page"""} conf_source = """ # This overwrites existing conf.py """ def test_basic_build(self): a_doctree = self.get_doctree('a_page') b_doctree = self.get_doctree('b_page') # Your tests for the new page content here You don't need to set ``page_source_template``; if you don't, you start with a fresh project, where the only pages are the ones you specify in ``rst_sources``. .. code:: python class TestFreshProject(SourcesBuilder): rst_sources = {'a_page': """A new page""", 'b_page': """Another new page"""} conf_source = """ # Stuff for the conf.py file """ def test_basic_build(self): a_doctree = self.get_doctree('a_page') b_doctree = self.get_doctree('b_page') # Your tests for the new page content here See the tests for examples of using Sphinxtesters for testing builds of Sphinx projects. ************ Installation ************ :: pip install sphinxtesters **** Code **** See https://github.com/matthew-brett/sphinxtesters Released under the BSD two-clause license - see the file ``LICENSE`` in the source distribution. `travis-ci `_ kindly tests the code automatically under Python versions 2.7, and 3.3 through 3.6. The latest released version is at https://pypi.python.org/pypi/sphinxtesters ***** Tests ***** * Install ``sphinxtesters`` * Install the pytest_ testing framework:: pip install pytest * Run the tests with:: pytest sphinxtesters ******* Support ******* Please put up issues on the `sphinxtesters issue tracker`_. .. standalone-references .. |sphinxtesters-documentation| replace:: `sphinxtesters documentation`_ .. _sphinxtesters documentation: https://matthew-brett.github.com/sphinxtesters/sphinxtesters.html .. _documentation: https://matthew-brett.github.com/sphinxtesters .. _pandoc: http://pandoc.org .. _jupyter: jupyter.org .. _homebrew: brew.sh .. _sphinx: http://sphinx-doc.org .. _rest: http://docutils.sourceforge.net/rst.html .. _sphinxtesters issue tracker: https://github.com/matthew-brett/sphinxtesters/issues .. _pytest: https://pytest.org .. _mock: https://github.com/testing-cabal/mock