-
Notifications
You must be signed in to change notification settings - Fork 0
Fix vscode integration #52
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
Changes from all commits
b9fd142
9de40d9
20b3932
f284ad5
50510cc
80edcc0
a757360
59600f5
4d34c84
baea771
a1cefac
66ead65
0451d62
8d41bd5
be81e55
a08f07c
efe1804
5d42980
9304a6d
af82bc3
ea24066
f57818e
ab20a4e
c77b825
3b45ea8
d51b5b8
b2ed0f8
88919ac
4ddc76b
1f6d814
b88efc3
c50aa2d
56a092d
8eb4557
653d6eb
d6c81ed
76c1e6e
5a249f1
40eb4f3
f61d360
24b1e70
d88b103
3fc0dfe
46619d1
48aed27
a127900
88f8f0b
cc62fcb
79af6a7
1b578db
c6e69e9
b65e6a1
0beff35
55b1406
28b74df
52ae32b
040127b
b991966
693b783
1a9f2cf
3028fbe
8a8f534
787dcd3
624b41d
ab49436
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -172,3 +172,7 @@ cython_debug/ | |
|
||
# Jupyter Book (via myst) build outputs | ||
_build | ||
|
||
!.logs/*.log | ||
|
||
**/envVars.txt |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
0.4.0 | ||
0.5.0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
from __future__ import annotations | ||
|
||
import sys | ||
import types | ||
from contextlib import suppress | ||
from functools import cached_property | ||
from pathlib import Path | ||
from typing import TYPE_CHECKING | ||
|
||
import _pytest._code.code | ||
import _pytest.nodes | ||
import _pytest.pathlib | ||
|
||
if TYPE_CHECKING: | ||
from collections.abc import Generator | ||
from os import PathLike | ||
from types import FunctionType, ModuleType | ||
from typing import Any, Final, Self | ||
|
||
import pytest | ||
|
||
CELL_PREFIX: Final[str] = "Cell" | ||
|
||
if sys.version_info < (3, 12): # pragma: no cover | ||
# Can't subclass `pathlib.Path` directly in python < 3.12 | ||
_Path = Path | ||
Path: Final[type] = type(_Path()) | ||
|
||
|
||
class CellPath(Path): | ||
"""Provide handling of Cells specified as `path/to/file[Celln]`.""" | ||
|
||
def __eq__(self, other: object) -> bool: | ||
"""Equality testing handled by `pathlib.Path`.""" | ||
return Path(self) == other | ||
|
||
def __hash__(self) -> int: | ||
"""Hashing handled by `pathlib.Path`.""" | ||
return super().__hash__() | ||
|
||
def exists(self, *args: Any, **kwargs: Any) -> bool: | ||
MusicalNinjaDad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""(Only) check that the notebook exists.""" | ||
# TODO: #33 Extend `CellPath.exists` to also check that the cell exists (if performance allows) | ||
return self.notebook.exists(*args, **kwargs) | ||
|
||
if sys.version_info < (3, 13): # pragma: no cover | ||
|
||
def relative_to(self, other: PathLike, *args: Any, **kwargs: Any) -> Self: | ||
"""Relative_to only works out-of-the-box on python 3.13 and above.""" | ||
return type(self)(f"{self.notebook.relative_to(other, *args, **kwargs)}[{self.cell}]") | ||
|
||
@cached_property | ||
def notebook(self) -> Path: | ||
"""Path of the notebook.""" | ||
return self.get_notebookpath(str(self)) | ||
|
||
@cached_property | ||
def cell(self) -> str: | ||
"""The cell specifier (e.g. "Cell0").""" | ||
return f"{CELL_PREFIX}{self.get_cellid(str(self))}" | ||
|
||
@classmethod | ||
MusicalNinjaDad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def is_cellpath(cls, path: str) -> bool: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): Assess robustness of is_cellpath string splitting logic. The current implementation relies on splitting the string based on the '[Cell' substring. Consider edge cases such as unexpected additional '[' characters or variations of file names that might inadvertently fulfill the condition. If these formats are strictly controlled, the code is acceptable; if not, enhanced validation might be warranted. Suggested implementation: import re
# (existing imports) @classmethod
def is_cellpath(cls, path: str) -> bool:
"""Determine whether a str is a valid representation of our pseudo-path."""
pattern = r'^(?P<notebook>.+\.ipynb)\[' + re.escape(CELL_PREFIX) + r'(?P<cellid>\d+)\]$'
return re.fullmatch(pattern, path) is not None If CELL_PREFIX is subject to change or defined elsewhere with different values, ensure that its usage with re.escape is valid across the codebase. |
||
"""Determine whether a str is a valid representation of our pseudo-path.""" | ||
return path.split(".")[-1].startswith("ipynb") and path.split(f"[{CELL_PREFIX}")[-1].removesuffix("]").isdigit() | ||
|
||
@classmethod | ||
def get_notebookpath(cls, path: str) -> Path: | ||
"""Return the real path of the notebook based on a pseudo-path.""" | ||
notebookpath = path.split(f"[{CELL_PREFIX}")[0] | ||
return Path(notebookpath) | ||
|
||
@classmethod | ||
def get_cellid(cls, path: str) -> int: | ||
"""Return the Cell id from the pseudo-path.""" | ||
cellid = path.split(f"[{CELL_PREFIX}")[-1].removesuffix("]") | ||
return int(cellid) | ||
|
||
@classmethod | ||
def to_nodeid(cls, path: str) -> str: | ||
""" | ||
Convert a pseudo-path to an equivalent nodeid. | ||
|
||
Examples: | ||
'notebook.ipynb[Cell0]::test_func' -> 'notebook.ipynb::Cell0::test_func' | ||
'notebook.ipynb[Cell1]' -> 'notebook.ipynb::Cell1' | ||
""" | ||
cellpath, *nodepath = path.split("::") | ||
notebookpath = f"{cls.get_notebookpath(cellpath)}" | ||
cell = f"{CELL_PREFIX}{cls.get_cellid(cellpath)}" | ||
return "::".join((notebookpath, cell, *nodepath)) | ||
|
||
class PytestItemMixin: | ||
"""Provides various overrides to handle our pseudo-path.""" | ||
|
||
# TODO: #51 Use metaclass to remove direct references to `CellPath` in `CellPath.PytestItemMixin` | ||
path: CellPath | ||
name: str | ||
|
||
def reportinfo(self) -> tuple[Path, int, str]: | ||
""" | ||
Returns tuple of notebook path, (linenumber=)0, Celln::testname. | ||
|
||
`reportinfo` is used by `location` and included as the header line in the report: | ||
``` | ||
==== FAILURES ==== | ||
___ reportinfo[2] ___ | ||
``` | ||
""" | ||
# `nodes.Item.location` calls `absolutepath()` and then `main._node_location_to_relpath` which caches the | ||
# results in `_bestrelpathcache[node_path]` very early in the test process. | ||
# As we provide the full CellPath as reportinfo[0] we need to patch `_pytest.nodes.absolutepath` in | ||
# `CellPath.patch_pytest_pathlib` (above) | ||
# | ||
# `TerminalReporter._locationline` adds a `<-` section if `nodeid.split("::")[0] != location[0]`. | ||
# Verbosity<2 tests runs are grouped by location[0] in the testlog. | ||
return self.path, 0, f"{self.path.cell}::{self.name}" | ||
|
||
def collect(self) -> Generator[pytest.Function, None, None]: | ||
"""Rebless children to include our overrides from the Mixin.""" | ||
# TODO(MusicalNinjaDad): #22 Handle Tests grouped in Class | ||
for item in super().collect(): # pytype: disable=attribute-error | ||
item_type = type(item) | ||
type_with_mixin = types.new_class(item_type.__name__, (CellPath.PytestItemMixin, item_type)) | ||
item.__class__ = type_with_mixin | ||
yield item | ||
|
||
@staticmethod | ||
def patch_pytest_absolutepath() -> dict[tuple[ModuleType, str], FunctionType]: | ||
"""Patch _pytest.pathlib functions.""" | ||
original_functions = {} | ||
|
||
# pytest has some unique handling to get the absolute path of a file. Possbily no longer needed with later | ||
# versions of pathlib? Hopefully we will be able to remove this patch with a later version of pytest. | ||
# | ||
# The original function is defined in _pytest.pathlib but | ||
# both `code` and `nodes` import it as `from .pathlib import absolutepath` | ||
# so we need to patch in both these namespaces... | ||
_pytest_absolutepath = _pytest.pathlib.absolutepath | ||
|
||
def _absolutepath(path: str | PathLike[str] | Path) -> Path: | ||
"""Return accurate absolute path for string representations of CellPath.""" | ||
# pytype: disable=attribute-error | ||
try: | ||
return path.absolute() # pytest prefers to avoid this, guessing for historical reasons??? | ||
except AttributeError: | ||
with suppress(AttributeError): # in case this is not a `str` but some other `PathLike` | ||
if CellPath.is_cellpath(path): | ||
return CellPath(path).absolute() | ||
return _pytest_absolutepath(path) | ||
# pytype: enable=attribute-error | ||
|
||
# 1. `code.Code.path` calls `absolutepath(self.raw.co_filename)` which is the info primarily used in | ||
# `TracebackEntry` and therefore relevant for failure reporting. | ||
original_functions[(_pytest._code.code, "absolutepath")] = _pytest_absolutepath # noqa: SLF001 | ||
_pytest._code.code.absolutepath = _absolutepath # noqa: SLF001 | ||
# 2. `nodes.Item.location` calls `absolutepath()` and then `main._node_location_to_relpath` which caches the | ||
# results of the `absolutepath()` call in `_bestrelpathcache[node_path]` very early in the test process. | ||
original_functions[(_pytest.nodes, "absolutepath")] = _pytest_absolutepath | ||
_pytest.nodes.absolutepath = _absolutepath | ||
|
||
return original_functions |
Uh oh!
There was an error while loading. Please reload this page.