Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update lint #8

Merged
merged 18 commits into from
Aug 5, 2023
50 changes: 30 additions & 20 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ permissions:
contents: read

jobs:
# Run pytest, integration tests, and linters
# Run tests, linters, and checkers via nox
test:
runs-on: ${{ matrix.platform }}

Expand All @@ -28,37 +28,43 @@ jobs:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy-3.8']

steps:
- uses: actions/checkout@v3
if: ${{ ! startsWith(matrix.python-version, 'pypy-') }}
- uses: actions/checkout@v1
if: ${{ startsWith(matrix.python-version, 'pypy-') }}
# Using actions/checkout@v2 or later with pypy causes an error
# (see https://foss.heptapod.net/pypy/pypy/-/issues/3640)
- name: Check out code
uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade setuptools pip wheel
python -m pip install tox
- name: Test with tox
run: tox -m ${{ matrix.python-version }}
if: ${{ ! (matrix.platform == 'windows-latest' && matrix.python-version == '3.8') && (matrix.python-version != 'pypy-3.8') }}
- name: Test with tox (Windows, py3.8)
python -m pip install nox

- name: Test with nox
run: nox
if: ${{ (matrix.python-version != '3.7') && ! (matrix.platform == 'windows-latest' && matrix.python-version == 'pypy-3.8') }}

# Skip type checkers on py3.7 because those package versions
# yield different results
- name: Test with nox (py3.7)
run: nox -s black pytest ruff
if: ${{ matrix.python-version == '3.7' }}

# TODO: Run tests on windows/pypy-3.8

- name: Integration test with nox
run: nox -t integration_test --python ${{ matrix.python-version }}
# Skip integration tests on Windows/py3.8 because
# there are no pre-built Windows/py3.8/numpy1.6.6 wheels.
# TODO: Also skip pyright on Windows for now, but investigate later.
run: tox -e black,docformatter,flake8,isort,mypy,py38
if: ${{ matrix.platform == 'windows-latest' && matrix.python-version == '3.8' }}
- name: Test with tox (pypy3.8)
# TODO: Skip integration tests on pypy; investigate issues
run: tox -e pypy3
if: ${{ matrix.python-version == 'pypy-3.8' }}
# Skip py3.9 because none of the bundles are for it.
# TODO: Investigate issues on Windows and pypy.
if: ${{ (matrix.python-version != '3.9') && ! (matrix.platform == 'windows-latest' && matrix.python-version == '3.8') && ! startsWith(matrix.python-version, 'pypy-') }}

- name: Upload failed outputs
uses: actions/upload-artifact@v3
with:
name: leda-outputs-${{ matrix.platform }}-${{ matrix.python-version}}
name: leda-outputs-${{ matrix.platform }}-${{ matrix.python-version }}
path: ~/leda_outputs/
retention-days: 1
if: ${{ failure() }}
Expand All @@ -84,14 +90,18 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
# Grab entire history for setuptools_scm
fetch-depth: 0

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade build twine

- name: Create packages
run: python -m build

- name: Run twine check
run: twine check dist/*
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
build
dist
*.egg-info
.mypy_cache
.nox
.pytest_cache
.ruff_cache
.tox
version.py
*.ipynb_checkpoints
2 changes: 1 addition & 1 deletion leda/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
DEFAULT_CELL_TIMEOUT = datetime.timedelta(minutes=10)


def main():
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"nb_path",
Expand Down
37 changes: 19 additions & 18 deletions leda/gen/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,30 @@
import datetime
import logging
import pathlib
from typing import IO, Any, List, Mapping, Optional, Union
from typing import IO, Any, Mapping

import cached_property
import nbformat

logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

# TODO: When we drop support for python3.7, switch to properly-typed,
# built-in functools.cached_property.


@dataclasses.dataclass(frozen=True)
class Report:
name: str

tag: Optional[str] = None
tag: str | None = None

params: Optional[Mapping[str, Any]] = None
additional_inject_code: Optional[str] = None
params: Mapping[str, Any] | None = None
additional_inject_code: str | None = None

cell_timeout: Optional[datetime.timedelta] = None
cell_timeout: datetime.timedelta | None = None

@cached_property.cached_property
@cached_property.cached_property # type: ignore[misc]
def full_name(self) -> str:
if self.tag:
parts = [self.name, self.tag]
Expand All @@ -36,11 +39,11 @@ def full_name(self) -> str:
return "-".join(parts)

@property
def handle(self) -> Union[str, IO]:
def handle(self) -> str | IO:
raise NotImplementedError

@cached_property.cached_property
def inject_code(self) -> Optional[str]:
@cached_property.cached_property # type: ignore[misc]
def inject_code(self) -> str | None:
if not self.params and not self.additional_inject_code:
return None

Expand Down Expand Up @@ -71,19 +74,19 @@ class _FileReport:
@dataclasses.dataclass(frozen=True)
class FileReport(Report, _FileReport):
@property
def handle(self) -> Union[str, IO]:
def handle(self) -> str | IO:
logger.info("Reading %s", self.nb_path)
return str(self.nb_path.expanduser())


@dataclasses.dataclass(frozen=True)
class ReportSet:
reports: List[Report] = dataclasses.field(hash=False)
reports: list[Report] = dataclasses.field(hash=False)


@dataclasses.dataclass()
class ReportModifier:
def modify(self, nb_contents: nbformat.NotebookNode):
def modify(self, nb_contents: nbformat.NotebookNode) -> None:
raise NotImplementedError


Expand All @@ -98,26 +101,24 @@ class ReportArtifact:
@dataclasses.dataclass()
class ReportGenerator:
def generate(
self, nb_contents: nbformat.NotebookNode, nb_name: Optional[str] = None
self, nb_contents: nbformat.NotebookNode, nb_name: str | None = None
) -> bytes:
raise NotImplementedError


@dataclasses.dataclass()
class ReportPublisher:
def publish(
self, report: Report, artifact: ReportArtifact
) -> Optional[str]:
def publish(self, report: Report, artifact: ReportArtifact) -> str | None:
raise NotImplementedError


@dataclasses.dataclass()
class ReportRunner:
def run(self, report: Report) -> Optional[str]:
def run(self, report: Report) -> str | None:
raise NotImplementedError


@dataclasses.dataclass()
class ReportSetRunner:
def run(self, report_set: ReportSet):
def run(self, report_set: ReportSet) -> None:
raise NotImplementedError
65 changes: 31 additions & 34 deletions leda/gen/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import logging
import os
import pathlib
from typing import Any, Dict, Optional, Tuple
from typing import Any

import jupyter_client.kernelspec
import nbconvert
from nbconvert import preprocessors
import nbformat
import packaging.version
import termcolor
Expand All @@ -21,45 +22,40 @@
logger.addHandler(logging.NullHandler())


class ExecutePreprocessorWithProgressBar(
nbconvert.preprocessors.ExecutePreprocessor
):
class ExecutePreprocessorWithProgressBar(preprocessors.ExecutePreprocessor):
"""Small extension to provide progress bar."""

progress = traitlets.Bool(default_value=False).tag( # pyright: ignore
config=True,
)
progress = traitlets.Bool(
default_value=False # pyright: ignore[reportGeneralTypeIssues]
).tag(config=True)

def __init__(self, **kwargs):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
# Progress bar state
self._num_cells: Optional[int] = None
self._pbar: Optional[tqdm.tqdm] = None
self._num_cells: int | None = None
self._pbar: tqdm.tqdm | None = None

def preprocess(
self,
nb: nbformat.NotebookNode,
resources: Optional[Dict] = None,
km: Optional[jupyter_client.KernelManager] = None, # pyright: ignore
) -> Tuple[nbformat.NotebookNode, Dict]:
resources: dict | None = None,
km: jupyter_client.KernelManager | None = None,
) -> tuple[nbformat.NotebookNode, dict]:
self._num_cells = len(nb["cells"])

result = super(ExecutePreprocessorWithProgressBar, self).preprocess(
nb, resources, km=km
)
result = super().preprocess(nb, resources, km=km)
if self._pbar is not None:
self._pbar.close()

# noinspection PyTypeChecker
return result

def preprocess_cell(
self,
cell: nbformat.NotebookNode,
resources: Dict,
resources: dict,
cell_index: int,
store_history: bool = True,
) -> Tuple[nbformat.NotebookNode, Dict]:
) -> tuple[nbformat.NotebookNode, dict]:
if self._pbar is None:
self._pbar = tqdm.tqdm(
desc="Executing notebook",
Expand All @@ -75,27 +71,25 @@ def preprocess_cell(
self._pbar.set_postfix_str(first_line)

# Note that preprocess_cell() will actually run the cell
result = super(
ExecutePreprocessorWithProgressBar, self
).preprocess_cell(cell, resources, cell_index)
result = super().preprocess_cell(cell, resources, cell_index)
self._pbar.update(1)

return result
return result # type: ignore[no-any-return]


@dataclasses.dataclass()
class MainStaticReportGenerator(leda.gen.base.ReportGenerator):
cell_timeout: Optional[datetime.timedelta] = None
kernel_name: Optional[str] = None
cell_timeout: datetime.timedelta | None = None
kernel_name: str | None = None
progress: bool = False

template_name: Optional[str] = None
theme: Optional[str] = None
template_name: str | None = None
theme: str | None = None

def __post_init__(self):
def __post_init__(self) -> None:
nbconvert_version = packaging.version.parse(nbconvert.__version__)
is_classic = self.template_name == "classic" or (
not self.template_name
and packaging.version.parse(nbconvert.__version__).major < 6
not self.template_name and nbconvert_version.major < 6
)
if is_classic and self.theme == "dark":
raise ValueError(
Expand All @@ -105,8 +99,10 @@ def __post_init__(self):
if self.theme not in (None, "light", "dark"):
raise ValueError(f"Unsupported theme: {self.theme!r}")

def _get_preprocessor(self) -> nbconvert.preprocessors.ExecutePreprocessor:
kwargs: Dict[str, Any] = {}
def _get_preprocessor(
self,
) -> preprocessors.ExecutePreprocessor:
kwargs: dict[str, Any] = {}

if self.cell_timeout:
kwargs["timeout"] = int(self.cell_timeout.total_seconds())
Expand All @@ -125,7 +121,7 @@ def _get_preprocessor(self) -> nbconvert.preprocessors.ExecutePreprocessor:
progress=self.progress, **kwargs
)

def _get_exporter_kwargs(self) -> Dict:
def _get_exporter_kwargs(self) -> dict:
# See https://nbconvert.readthedocs.io/en/latest/customizing.html#adding-additional-template-paths # noqa
exporter_kwargs = {
"extra_template_basedirs": str(
Expand All @@ -144,7 +140,7 @@ def _get_exporter_kwargs(self) -> Dict:
def generate(
self,
nb_contents: nbformat.NotebookNode,
nb_name: Optional[str] = None,
nb_name: str | None = None,
) -> bytes:
logger.info("Generating notebook")
preprocessor = self._get_preprocessor()
Expand All @@ -154,6 +150,7 @@ def generate(

logger.info("Generating HTML")
exporter = nbconvert.HTMLExporter(**self._get_exporter_kwargs())
body: str
body, _ = exporter.from_notebook_node(nb_contents)

logger.info("Modifying HTML")
Expand Down
2 changes: 1 addition & 1 deletion leda/gen/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# In 'lab' templates, the input cell and prompt are included in "jp-InputArea",
# but that would also include Markdown output, so we select the
# editor and prompt separately, to match the behavior of the "input"
# class in the 'classic" template.
# class in the "classic" template.
INPUT_SELECTORS_LAB: Selectors = [
"div.jp-InputArea-editor",
"div.jp-InputArea-prompt",
Expand Down
Loading