Skip to content

Commit 343cd14

Browse files
0.8
1 parent 08f400c commit 343cd14

18 files changed

+244
-122
lines changed

.github/workflows/run_tox.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
strategy:
99
max-parallel: 4
1010
matrix:
11-
python-version: ['3.7', '3.8', '3.9', '3.10']
11+
python-version: ['3.8', '3.9', '3.10']
1212

1313
steps:
1414
- uses: actions/checkout@v1

doc/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
]
3838

3939
exec_code_working_dir = '../src'
40-
exec_code_folders = ['../src']
40+
exec_code_source_folders = ['../src', '../src', ]
4141

4242
# Add any paths that contain templates here, relative to this directory.
4343
templates_path = ['_templates']

doc/configuration.rst

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ The following configuration parameters are available:
3737
- ``Path`` or ``str``
3838
- The working directory where the code will be executed.
3939

40-
* - ``exec_code_folders``
40+
* - ``exec_code_source_folders``
4141
- | ``List`` of
4242
| ``Path`` or ``str``
4343
- | Additional folders that will be added to PYTHONPATH.
@@ -48,10 +48,10 @@ The following configuration parameters are available:
4848
- | The directory that is used to create the path to the
4949
| example files. Defaults to the parent folder of the ``conf.py``.
5050
51-
* - ``exec_code_stdout_encoding``
52-
- ``str``
53-
- | Encoding used to decode stdout.
54-
| The default depends on the operating system but should be ``utf-8``.
51+
* - ``exec_code_set_utf8_encoding``
52+
- ``True`` or ``False``
53+
- | True enforces utf-8 encoding (can fix encoding errors).
54+
| Default is ``False`` except on Windows where it is ``True``.
5555
5656

5757
If it's a relative path it will be resolved relative to the parent folder of the ``conf.py``
@@ -61,7 +61,7 @@ Example:
6161
.. code-block:: python
6262
6363
exec_code_working_dir = '..'
64-
exec_code_folders = ['../my_src']
64+
exec_code_source_folders = ['../my_src']
6565
exec_code_example_dir = '.'
6666
6767
If you are unsure what the values are you can run Sphinx build in verbose mode with ``-v -v``.
@@ -72,6 +72,6 @@ Log output for Example:
7272
.. code-block:: text
7373
7474
[exec-code] Working dir: C:\Python\sphinx-exec-code
75-
[exec-code] Folders: C:\Python\sphinx-exec-code\my_src
75+
[exec-code] Source folders: C:\Python\sphinx-exec-code\my_src
7676
[exec-code] Example dir: C:\Python\sphinx-exec-code\doc
77-
[exec-code] Stdout encoding: utf-8
77+
[exec-code] Set utf8 encoding: True

readme.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,15 @@ This code will be executed
3636
```
3737

3838
# Changelog
39+
#### 0.8 (18.07.2022)
40+
- Renamed ``exec_code_folders`` to ``exec_code_source_folders``
41+
- Changed type of parameter to specify stdout to a flag
42+
- Changed default for config parameter that sets encoding
43+
- Dropped support for Python 3.7
44+
3945
#### 0.7 (15.07.2022)
4046
- Added config parameter to specify stdout encoding
41-
- Only empty lines of the output get trimmed
47+
- Only empty lines of the output get trimmed
4248

4349
#### 0.6 (04.04.2022)
4450
- Fixed an issue where the line numbers for error messages were not correct

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ def load_version() -> str:
5252
"License :: OSI Approved :: Apache Software License",
5353
"Natural Language :: English",
5454
"Operating System :: OS Independent",
55-
"Programming Language :: Python :: 3.7",
5655
"Programming Language :: Python :: 3.8",
5756
"Programming Language :: Python :: 3.9",
5857
"Programming Language :: Python :: 3.10",

src/sphinx_exec_code/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.7'
1+
__version__ = '0.8'

src/sphinx_exec_code/code_exec.py

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,32 @@
33
import sys
44
from itertools import dropwhile
55
from pathlib import Path
6-
from typing import Iterable, Optional
7-
8-
from sphinx.errors import ConfigError
96

107
from sphinx_exec_code.code_exec_error import CodeException
11-
12-
WORKING_DIR: Optional[str] = None
13-
ADDITIONAL_FOLDERS: Optional[Iterable[str]] = None
14-
STDOUT_ENCODING: str = sys.stdout.encoding
15-
16-
17-
def setup_code_env(cwd: Path, folders: Iterable[Path], encoding: str):
18-
global WORKING_DIR, ADDITIONAL_FOLDERS, STDOUT_ENCODING
19-
WORKING_DIR = str(cwd)
20-
ADDITIONAL_FOLDERS = tuple(map(str, folders))
21-
STDOUT_ENCODING = encoding
8+
from sphinx_exec_code.configuration import PYTHONPATH_FOLDERS, SET_UTF8_ENCODING, WORKING_DIR
229

2310

2411
def execute_code(code: str, file: Path, first_loc: int) -> str:
25-
if WORKING_DIR is None or ADDITIONAL_FOLDERS is None:
26-
raise ConfigError('Working dir or additional folders are not set!')
12+
cwd: Path = WORKING_DIR.value
13+
encoding = 'utf-8' if SET_UTF8_ENCODING.value else None
14+
python_folders = PYTHONPATH_FOLDERS.value
2715

2816
env = os.environ.copy()
29-
try:
30-
env['PYTHONPATH'] = os.pathsep.join(ADDITIONAL_FOLDERS) + os.pathsep + env['PYTHONPATH']
31-
except KeyError:
32-
env['PYTHONPATH'] = os.pathsep.join(ADDITIONAL_FOLDERS)
3317

34-
run = subprocess.run([sys.executable, '-c', code], capture_output=True, cwd=WORKING_DIR, env=env)
18+
if python_folders:
19+
try:
20+
env['PYTHONPATH'] = os.pathsep.join(python_folders) + os.pathsep + env['PYTHONPATH']
21+
except KeyError:
22+
env['PYTHONPATH'] = os.pathsep.join(python_folders)
23+
24+
run = subprocess.run([sys.executable, '-c', code.encode('utf-8')], capture_output=True, text=True,
25+
encoding=encoding, cwd=cwd, env=env)
26+
3527
if run.returncode != 0:
36-
raise CodeException(code, file, first_loc, run.returncode, run.stderr.decode()) from None
28+
raise CodeException(code, file, first_loc, run.returncode, run.stderr) from None
3729

3830
# decode output and drop tailing spaces
39-
ret_str = (run.stdout.decode(encoding=STDOUT_ENCODING) + run.stderr.decode(encoding=STDOUT_ENCODING)).rstrip()
31+
ret_str = (run.stdout if run.stdout is not None else '' + run.stderr if run.stderr is not None else '').rstrip()
4032

4133
# drop leading empty lines
4234
ret_lines = list(dropwhile(lambda x: not x.strip(), ret_str.splitlines()))

src/sphinx_exec_code/code_format.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Iterable, Tuple
1+
from typing import Iterable, Tuple, List
22

33

44
class VisibilityMarkerError(Exception):
@@ -16,7 +16,7 @@ def __init__(self, marker: str):
1616

1717
self.do_add = True
1818
self.skip_empty = False
19-
self.lines = []
19+
self.lines: List[str] = []
2020

2121
def is_marker(self, line: str) -> bool:
2222
if line == self.start:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .values import EXAMPLE_DIR, PYTHONPATH_FOLDERS, SET_UTF8_ENCODING, WORKING_DIR
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from typing import Any, Final, Generic, Optional, Tuple, Type, TypeVar, Union
2+
3+
from sphinx.application import Sphinx as SphinxApp
4+
from sphinx.errors import ConfigError
5+
6+
from sphinx_exec_code.__const__ import log
7+
8+
TYPE_VALUE = TypeVar('TYPE_VALUE')
9+
10+
11+
class SphinxConfigValue(Generic[TYPE_VALUE]):
12+
SPHINX_TYPE: Union[Tuple[Type[Any], ...], Type[Any]]
13+
14+
def __init__(self, sphinx_name: str, initial_value: Optional[TYPE_VALUE] = None):
15+
self.sphinx_name: Final = sphinx_name
16+
self._value: Optional[TYPE_VALUE] = initial_value
17+
18+
@property
19+
def value(self) -> TYPE_VALUE:
20+
if self._value is None:
21+
raise ConfigError(f'{self.sphinx_name} is not set!')
22+
return self._value
23+
24+
def transform_value(self, app: SphinxApp, value):
25+
return value
26+
27+
def validate_value(self, value) -> TYPE_VALUE:
28+
return value
29+
30+
def from_app(self, app: SphinxApp) -> TYPE_VALUE:
31+
# load value
32+
value = self.transform_value(app, getattr(app.config, self.sphinx_name))
33+
34+
# log transformed value
35+
assert self.sphinx_name.startswith('exec_code_')
36+
name = self.sphinx_name[10:].replace('_', ' ').capitalize()
37+
log.debug(f'[exec-code] {name:s}: {value}')
38+
39+
# additional validation
40+
self._value = self.validate_value(value)
41+
return self._value
42+
43+
def add_config_value(self, app: SphinxApp, sphinx_default):
44+
app.add_config_value(self.sphinx_name, sphinx_default, 'env', self.SPHINX_TYPE)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from sphinx.application import Sphinx as SphinxApp
2+
3+
from sphinx_exec_code.configuration.base import SphinxConfigValue
4+
5+
6+
class SphinxConfigFlag(SphinxConfigValue[bool]):
7+
SPHINX_TYPE = bool
8+
9+
def transform_value(self, app: SphinxApp, value) -> bool:
10+
return bool(value)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from pathlib import Path
2+
from typing import Tuple
3+
4+
from sphinx.application import Sphinx as SphinxApp
5+
6+
from sphinx_exec_code.__const__ import log
7+
from sphinx_exec_code.configuration.base import SphinxConfigValue, TYPE_VALUE
8+
9+
10+
class SphinxConfigPath(SphinxConfigValue[TYPE_VALUE]):
11+
SPHINX_TYPE = (str, Path)
12+
13+
def make_path(self, app: SphinxApp, value) -> Path:
14+
try:
15+
path = Path(value)
16+
except Exception:
17+
raise ValueError(f'Could not create Path from "{value}" (type {type(value).__name__}) '
18+
f'(configured by {self.sphinx_name:s})') from None
19+
20+
if not path.is_absolute():
21+
path = (Path(app.confdir) / path).resolve()
22+
return path
23+
24+
def check_folder_exists(self, folder: Path) -> Path:
25+
if not folder.is_dir():
26+
raise FileNotFoundError(f'Directory "{folder}" not found! (configured by {self.sphinx_name:s})')
27+
return folder
28+
29+
30+
class SphinxConfigFolder(SphinxConfigPath[Path]):
31+
def transform_value(self, app: SphinxApp, value) -> Path:
32+
return self.make_path(app, value)
33+
34+
def validate_value(self, value: Path) -> Path:
35+
return self.check_folder_exists(value)
36+
37+
38+
class SphinxConfigMultipleFolderStr(SphinxConfigPath[Tuple[str, ...]]):
39+
SPHINX_TYPE = ()
40+
41+
def transform_value(self, app: SphinxApp, value) -> Tuple[Path, ...]:
42+
return tuple(self.make_path(app, p) for p in value)
43+
44+
def validate_value(self, value: Tuple[Path, ...]) -> Tuple[str, ...]:
45+
# check that folders exist
46+
for f in value:
47+
self.check_folder_exists(f)
48+
49+
# Search for a python package and print a warning if we find none
50+
# since this is the only reason to specify additional folders
51+
for f in value:
52+
package_found = False
53+
for _f in f.iterdir():
54+
if not _f.is_dir():
55+
continue
56+
57+
# log warning if we don't find a python package
58+
for file in _f.glob('__init__.py'):
59+
if file.name == '__init__.py':
60+
package_found = True
61+
break
62+
if package_found:
63+
break
64+
65+
if not package_found:
66+
log.warning(f'[exec-code] No Python packages found in {f}')
67+
68+
return tuple(map(str, value))
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .flag_config import SphinxConfigFlag
2+
from .path_config import SphinxConfigFolder, SphinxConfigMultipleFolderStr
3+
4+
EXAMPLE_DIR = SphinxConfigFolder('exec_code_example_dir')
5+
6+
# Options for code execution
7+
WORKING_DIR = SphinxConfigFolder('exec_code_working_dir')
8+
PYTHONPATH_FOLDERS = SphinxConfigMultipleFolderStr('exec_code_source_folders')
9+
SET_UTF8_ENCODING = SphinxConfigFlag('exec_code_set_utf8_encoding')

0 commit comments

Comments
 (0)