This commit is contained in:
Dima Gerasimov 2024-10-19 19:15:36 +01:00
parent bc7c3ac253
commit d1511929a8
33 changed files with 117 additions and 117 deletions

View file

@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
from .cfg import make_config
from .common import PathIsh, Paths, get_files
from .compat import assert_never
from .error import Res, unwrap, notnone
from .error import Res, notnone, unwrap
from .logging import (
make_logger,
)

View file

@ -7,11 +7,12 @@ import shutil
import sys
import tempfile
import traceback
from collections.abc import Iterable, Sequence
from contextlib import ExitStack
from itertools import chain
from pathlib import Path
from subprocess import PIPE, CompletedProcess, Popen, check_call, run
from typing import Any, Callable, Iterable, List, Optional, Sequence, Type
from typing import Any, Callable, List, Optional, Type
import click

View file

@ -7,10 +7,11 @@ from __future__ import annotations
import io
import pathlib
import sys
from collections.abc import Iterator, Sequence
from datetime import datetime
from functools import total_ordering
from pathlib import Path
from typing import IO, Any, Iterator, Sequence, Union
from typing import IO, Any, Union
PathIsh = Union[Path, str]

View file

@ -2,13 +2,13 @@ from .internal import assert_subpackage; assert_subpackage(__name__)
import logging
import sys
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Callable,
Iterator,
Optional,
Type,
TypeVar,
@ -21,7 +21,6 @@ import appdirs # type: ignore[import-untyped]
from . import warnings
PathIsh = Union[str, Path] # avoid circular import from .common

View file

@ -3,8 +3,9 @@ from __future__ import annotations
import importlib
import re
import sys
from collections.abc import Iterator
from contextlib import ExitStack, contextmanager
from typing import Any, Callable, Dict, Iterator, Optional, Type, TypeVar
from typing import Any, Callable, Dict, Optional, Type, TypeVar
Attrs = Dict[str, Any]

View file

@ -1,20 +1,18 @@
from __future__ import annotations
import os
from collections.abc import Iterable, Sequence
from glob import glob as do_glob
from pathlib import Path
from typing import (
TYPE_CHECKING,
Callable,
Generic,
Iterable,
List,
Sequence,
Tuple,
TypeVar,
Union,
)
from . import compat
from . import warnings
from . import compat, warnings
# some helper functions
# TODO start deprecating this? soon we'd be able to use Path | str syntax which is shorter and more explicit
@ -30,14 +28,14 @@ def get_files(
*,
sort: bool=True,
guess_compression: bool=True,
) -> Tuple[Path, ...]:
) -> tuple[Path, ...]:
"""
Helper function to avoid boilerplate.
Tuple as return type is a bit friendlier for hashing/caching, so hopefully makes sense
"""
# TODO FIXME mm, some wrapper to assert iterator isn't empty?
sources: List[Path]
sources: list[Path]
if isinstance(pp, Path):
sources = [pp]
elif isinstance(pp, str):
@ -54,7 +52,7 @@ def get_files(
# TODO ugh. very flaky... -3 because [<this function>, get_files(), <actual caller>]
return traceback.extract_stack()[-3].filename
paths: List[Path] = []
paths: list[Path] = []
for src in sources:
if src.parts[0] == '~':
src = src.expanduser()
@ -234,16 +232,14 @@ if not TYPE_CHECKING:
return types.asdict(*args, **kwargs)
# todo wrap these in deprecated decorator as well?
# TODO hmm how to deprecate these in runtime?
# tricky cause they are actually classes/types
from typing import Literal # noqa: F401
from .cachew import mcachew # noqa: F401
# this is kinda internal, should just use my.core.logging.setup_logger if necessary
from .logging import setup_logger
# TODO hmm how to deprecate these in runtime?
# tricky cause they are actually classes/types
from typing import Literal # noqa: F401
from .stats import Stats
from .types import (
Json,

View file

@ -3,9 +3,10 @@ Bindings for the 'core' HPI configuration
'''
import re
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Sequence
from typing import Optional
from . import PathIsh, warnings
@ -121,8 +122,8 @@ config = make_config(Config)
### tests start
from collections.abc import Iterator
from contextlib import contextmanager as ctx
from typing import Iterator
@ctx

View file

@ -9,8 +9,9 @@ import functools
import json
import sys
from collections import defaultdict
from collections.abc import Iterator, Mapping
from pathlib import Path
from typing import Any, Dict, Iterator, List, Mapping, Set, TypeVar
from typing import Any, Dict, List, Set, TypeVar
import click
from more_itertools import seekable

View file

@ -19,8 +19,9 @@ import ast
import logging
import os
import re
from collections.abc import Iterable, Sequence
from pathlib import Path
from typing import Any, Iterable, List, NamedTuple, Optional, Sequence, cast
from typing import Any, List, NamedTuple, Optional, cast
'''
None means that requirements weren't defined (different from empty requirements)

View file

@ -4,13 +4,12 @@ See https://beepb00p.xyz/mypy-error-handling.html#kiss for more detail
"""
import traceback
from collections.abc import Iterable, Iterator
from datetime import datetime
from itertools import tee
from typing import (
Any,
Callable,
Iterable,
Iterator,
List,
Literal,
Optional,

View file

@ -6,8 +6,9 @@ Contains various backwards compatibility/deprecation helpers relevant to HPI its
import inspect
import os
import re
from collections.abc import Iterator, Sequence
from types import ModuleType
from typing import Iterator, List, Optional, Sequence, TypeVar
from typing import List, Optional, TypeVar
from . import warnings

View file

@ -4,7 +4,8 @@ TODO doesn't really belong to 'core' morally, but can think of moving out later
from .internal import assert_subpackage; assert_subpackage(__name__)
from typing import Any, Dict, Iterable, Optional
from collections.abc import Iterable
from typing import Any, Dict, Optional
import click

View file

@ -122,8 +122,8 @@ def _wrap(j, parent=None) -> Tuple[Zoomable, List[Zoomable]]:
raise RuntimeError(f'Unexpected type: {type(j)} {j}')
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Iterator
class UnconsumedError(Exception):

View file

@ -22,7 +22,8 @@ def parse_org_datetime(s: str) -> datetime:
# TODO I guess want to borrow inspiration from bs4? element type <-> tag; and similar logic for find_one, find_all
from typing import Callable, Iterable, TypeVar
from collections.abc import Iterable
from typing import Callable, TypeVar
from orgparse import OrgNode

View file

@ -7,6 +7,7 @@ from __future__ import annotations
# todo not sure if belongs to 'core'. It's certainly 'more' core than actual modules, but still not essential
# NOTE: this file is meant to be importable without Pandas installed
import dataclasses
from collections.abc import Iterable, Iterator
from datetime import datetime, timezone
from pprint import pformat
from typing import (
@ -14,8 +15,6 @@ from typing import (
Any,
Callable,
Dict,
Iterable,
Iterator,
Literal,
Type,
TypeVar,

View file

@ -9,13 +9,12 @@ import dataclasses
import importlib
import inspect
import itertools
from collections.abc import Iterable, Iterator
from datetime import datetime
from typing import (
Any,
Callable,
Dict,
Iterable,
Iterator,
List,
NamedTuple,
Optional,

View file

@ -9,9 +9,10 @@ See the select_range function below
import re
import time
from collections.abc import Iterator
from datetime import date, datetime, timedelta
from functools import lru_cache
from typing import Any, Callable, Iterator, NamedTuple, Optional, Type
from functools import cache, lru_cache
from typing import Any, Callable, NamedTuple, Optional, Type
import more_itertools
@ -98,7 +99,7 @@ def parse_datetime_float(date_str: str) -> float:
# probably DateLike input? but a user could specify an order_key
# which is an epoch timestamp or a float value which they
# expect to be converted to a datetime to compare
@lru_cache(maxsize=None)
@cache
def _datelike_to_float(dl: Any) -> float:
if isinstance(dl, datetime):
return dl.timestamp()

View file

@ -1,7 +1,7 @@
import datetime
from dataclasses import asdict, is_dataclass
from decimal import Decimal
from functools import lru_cache
from functools import cache, lru_cache
from pathlib import Path
from typing import Any, Callable, NamedTuple, Optional
@ -57,7 +57,7 @@ def _default_encode(obj: Any) -> Any:
# could possibly run multiple times/raise warning if you provide different 'default'
# functions or change the kwargs? The alternative is to maintain all of this at the module
# level, which is just as annoying
@lru_cache(maxsize=None)
@cache
def _dumps_factory(**kwargs) -> Callable[[Any], str]:
use_default: DefaultEncoder = _default_encode
# if the user passed an additional 'default' parameter,

View file

@ -4,8 +4,9 @@ and yielding nothing (or a default) when its not available
"""
import warnings
from collections.abc import Iterable, Iterator
from functools import wraps
from typing import Any, Callable, Iterable, Iterator, Optional, TypeVar
from typing import Any, Callable, Optional, TypeVar
from .warnings import medium

View file

@ -3,10 +3,11 @@ from .internal import assert_subpackage; assert_subpackage(__name__)
import shutil
import sqlite3
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any, Callable, Iterator, Literal, Optional, Tuple, Union, overload
from typing import Any, Callable, Literal, Optional, Tuple, Union, overload
from .common import PathIsh
from .compat import assert_never

View file

@ -1,11 +1,13 @@
'''
Helpers for hpi doctor/stats functionality.
'''
from __future__ import annotations
import collections.abc
import importlib
import inspect
import typing
from collections.abc import Iterable, Iterator, Sequence
from contextlib import contextmanager
from datetime import datetime
from pathlib import Path
@ -13,20 +15,13 @@ from types import ModuleType
from typing import (
Any,
Callable,
Dict,
Iterable,
Iterator,
List,
Optional,
Protocol,
Sequence,
Union,
cast,
)
from .types import asdict
Stats = Dict[str, Any]
Stats = dict[str, Any]
class StatsFun(Protocol):
@ -55,10 +50,10 @@ def quick_stats():
def stat(
func: Union[Callable[[], Iterable[Any]], Iterable[Any]],
func: Callable[[], Iterable[Any]] | Iterable[Any],
*,
quick: bool = False,
name: Optional[str] = None,
name: str | None = None,
) -> Stats:
"""
Extracts various statistics from a passed iterable/callable, e.g.:
@ -153,8 +148,8 @@ def test_stat() -> None:
#
def get_stats(module_name: str, *, guess: bool = False) -> Optional[StatsFun]:
stats: Optional[StatsFun] = None
def get_stats(module_name: str, *, guess: bool = False) -> StatsFun | None:
stats: StatsFun | None = None
try:
module = importlib.import_module(module_name)
except Exception:
@ -167,7 +162,7 @@ def get_stats(module_name: str, *, guess: bool = False) -> Optional[StatsFun]:
# TODO maybe could be enough to annotate OUTPUTS or something like that?
# then stats could just use them as hints?
def guess_stats(module: ModuleType) -> Optional[StatsFun]:
def guess_stats(module: ModuleType) -> StatsFun | None:
"""
If the module doesn't have explicitly defined 'stat' function,
this is used to try to guess what could be included in stats automatically
@ -206,7 +201,7 @@ def test_guess_stats() -> None:
}
def _guess_data_providers(module: ModuleType) -> Dict[str, Callable]:
def _guess_data_providers(module: ModuleType) -> dict[str, Callable]:
mfunctions = inspect.getmembers(module, inspect.isfunction)
return {k: v for k, v in mfunctions if is_data_provider(v)}
@ -263,7 +258,7 @@ def test_is_data_provider() -> None:
lam = lambda: [1, 2]
assert not idp(lam)
def has_extra_args(count) -> List[int]:
def has_extra_args(count) -> list[int]:
return list(range(count))
assert not idp(has_extra_args)
@ -340,10 +335,10 @@ def test_type_is_iterable() -> None:
assert not fun(None)
assert not fun(int)
assert not fun(Any)
assert not fun(Dict[int, int])
assert not fun(dict[int, int])
assert fun(List[int])
assert fun(Sequence[Dict[str, str]])
assert fun(list[int])
assert fun(Sequence[dict[str, str]])
assert fun(Iterable[Any])
@ -434,7 +429,7 @@ def test_stat_iterable() -> None:
# experimental, not sure about it..
def _guess_datetime(x: Any) -> Optional[datetime]:
def _guess_datetime(x: Any) -> datetime | None:
# todo hmm implement without exception..
try:
d = asdict(x)

View file

@ -5,9 +5,10 @@ import sys
import tarfile
import tempfile
import zipfile
from collections.abc import Generator, Sequence
from contextlib import contextmanager
from pathlib import Path
from typing import Generator, List, Sequence, Tuple, Union
from typing import List, Tuple, Union
from .logging import make_logger

View file

@ -2,11 +2,11 @@
Helper 'module' for test_guess_stats
"""
from collections.abc import Iterable, Iterator, Sequence
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
from typing import Iterable, Iterator, Sequence
@dataclass

View file

@ -1,6 +1,8 @@
from __future__ import annotations
import os
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Iterator, Optional
import pytest
@ -15,7 +17,7 @@ skip_if_uses_optional_deps = pytest.mark.skipif(
# TODO maybe move to hpi core?
@contextmanager
def tmp_environ_set(key: str, value: Optional[str]) -> Iterator[None]:
def tmp_environ_set(key: str, value: str | None) -> Iterator[None]:
prev_value = os.environ.get(key)
if value is None:
os.environ.pop(key, None)

View file

@ -1,8 +1,9 @@
import json
import warnings
from collections.abc import Iterator
from datetime import datetime
from pathlib import Path
from typing import Iterator, NamedTuple
from typing import NamedTuple
from ..denylist import DenyList

View file

@ -1,6 +1,6 @@
from .common import skip_if_uses_optional_deps as pytestmark
from __future__ import annotations
from typing import List
from .common import skip_if_uses_optional_deps as pytestmark
# TODO ugh, this is very messy.. need to sort out config overriding here
@ -16,7 +16,7 @@ def test_cachew() -> None:
# TODO ugh. need doublewrap or something to avoid having to pass parens
@mcachew()
def cf() -> List[int]:
def cf() -> list[int]:
nonlocal called
called += 1
return [1, 2, 3]
@ -43,7 +43,7 @@ def test_cachew_dir_none() -> None:
called = 0
@mcachew(cache_path=cache_dir() / 'ctest')
def cf() -> List[int]:
def cf() -> list[int]:
nonlocal called
called += 1
return [called, called, called]

View file

@ -2,8 +2,8 @@
Various tests that are checking behaviour of user config wrt to various things
"""
import sys
import os
import sys
from pathlib import Path
import pytest

View file

@ -1,5 +1,6 @@
from functools import lru_cache
from typing import Dict, Sequence
from collections.abc import Sequence
from functools import cache, lru_cache
from typing import Dict
import pytz
@ -43,7 +44,7 @@ def _abbr_to_timezone_map() -> Dict[str, pytz.BaseTzInfo]:
return res
@lru_cache(maxsize=None)
@cache
def abbr_to_timezone(abbr: str) -> pytz.BaseTzInfo:
return _abbr_to_timezone_map()[abbr]

View file

@ -1,10 +1,11 @@
import os
import pkgutil
import sys
from collections.abc import Iterable
from itertools import chain
from pathlib import Path
from types import ModuleType
from typing import Iterable, List, Optional
from typing import List, Optional
from .discovery_pure import HPIModule, _is_not_module_src, has_stats, ignored

View file

@ -1,6 +1,8 @@
from __future__ import annotations
import sys
from concurrent.futures import Executor, Future
from typing import Any, Callable, Optional, TypeVar
from typing import Any, Callable, TypeVar
from ..compat import ParamSpec
@ -15,7 +17,7 @@ class DummyExecutor(Executor):
but also want to provide an option to run the code serially (e.g. for debugging)
"""
def __init__(self, max_workers: Optional[int] = 1) -> None:
def __init__(self, max_workers: int | None = 1) -> None:
self._shutdown = False
self._max_workers = max_workers

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import importlib
import importlib.util
import sys
@ -5,23 +7,22 @@ from pathlib import Path
from types import ModuleType
from typing import Optional
from ..common import PathIsh
# TODO only used in tests? not sure if useful at all.
def import_file(p: PathIsh, name: Optional[str] = None) -> ModuleType:
def import_file(p: Path | str, name: str | None = None) -> ModuleType:
p = Path(p)
if name is None:
name = p.stem
spec = importlib.util.spec_from_file_location(name, p)
assert spec is not None, f"Fatal error; Could not create module spec from {name} {p}"
foo = importlib.util.module_from_spec(spec)
loader = spec.loader; assert loader is not None
loader = spec.loader
assert loader is not None
loader.exec_module(foo)
return foo
def import_from(path: PathIsh, name: str) -> ModuleType:
def import_from(path: Path | str, name: str) -> ModuleType:
path = str(path)
sys.path.append(path)
try:
@ -30,7 +31,7 @@ def import_from(path: PathIsh, name: str) -> ModuleType:
sys.path.remove(path)
def import_dir(path: PathIsh, extra: str = '') -> ModuleType:
def import_dir(path: Path | str, extra: str = '') -> ModuleType:
p = Path(path)
if p.parts[0] == '~':
p = p.expanduser() # TODO eh. not sure about this..

View file

@ -4,17 +4,13 @@ Various helpers/transforms of iterators
Ideally this should be as small as possible and we should rely on stdlib itertools or more_itertools
"""
from __future__ import annotations
import warnings
from collections.abc import Hashable
from collections.abc import Hashable, Iterable, Iterator, Sized
from typing import (
TYPE_CHECKING,
Callable,
Dict,
Iterable,
Iterator,
List,
Optional,
Sized,
TypeVar,
Union,
cast,
@ -23,9 +19,8 @@ from typing import (
import more_itertools
from decorator import decorator
from ..compat import ParamSpec
from .. import warnings as core_warnings
from ..compat import ParamSpec
T = TypeVar('T')
K = TypeVar('K')
@ -39,7 +34,7 @@ def _identity(v: T) -> V: # type: ignore[type-var]
# ugh. nothing in more_itertools?
# perhaps duplicates_everseen? but it doesn't yield non-unique elements?
def ensure_unique(it: Iterable[T], *, key: Callable[[T], K]) -> Iterable[T]:
key2item: Dict[K, T] = {}
key2item: dict[K, T] = {}
for i in it:
k = key(i)
pi = key2item.get(k, None)
@ -72,10 +67,10 @@ def make_dict(
key: Callable[[T], K],
# TODO make value optional instead? but then will need a typing override for it?
value: Callable[[T], V] = _identity,
) -> Dict[K, V]:
) -> dict[K, V]:
with_keys = ((key(i), i) for i in it)
uniques = ensure_unique(with_keys, key=lambda p: p[0])
res: Dict[K, V] = {}
res: dict[K, V] = {}
for k, i in uniques:
res[k] = i if value is None else value(i)
return res
@ -93,8 +88,8 @@ def test_make_dict() -> None:
d = make_dict(it, key=lambda i: i % 2, value=lambda i: i)
# check type inference
d2: Dict[str, int] = make_dict(it, key=lambda i: str(i))
d3: Dict[str, bool] = make_dict(it, key=lambda i: str(i), value=lambda i: i % 2 == 0)
d2: dict[str, int] = make_dict(it, key=lambda i: str(i))
d3: dict[str, bool] = make_dict(it, key=lambda i: str(i), value=lambda i: i % 2 == 0)
LFP = ParamSpec('LFP')
@ -102,7 +97,7 @@ LV = TypeVar('LV')
@decorator
def _listify(func: Callable[LFP, Iterable[LV]], *args: LFP.args, **kwargs: LFP.kwargs) -> List[LV]:
def _listify(func: Callable[LFP, Iterable[LV]], *args: LFP.args, **kwargs: LFP.kwargs) -> list[LV]:
"""
Wraps a function's return value in wrapper (e.g. list)
Useful when an algorithm can be expressed more cleanly as a generator
@ -115,7 +110,7 @@ def _listify(func: Callable[LFP, Iterable[LV]], *args: LFP.args, **kwargs: LFP.k
# so seems easiest to just use specialize instantiations of decorator instead
if TYPE_CHECKING:
def listify(func: Callable[LFP, Iterable[LV]]) -> Callable[LFP, List[LV]]: ... # noqa: ARG001
def listify(func: Callable[LFP, Iterable[LV]]) -> Callable[LFP, list[LV]]: ... # noqa: ARG001
else:
listify = _listify
@ -130,7 +125,7 @@ def test_listify() -> None:
yield 2
res = it()
assert_type(res, List[int])
assert_type(res, list[int])
assert res == [1, 2]
@ -201,24 +196,24 @@ def test_warn_if_empty_list() -> None:
ll = [1, 2, 3]
@warn_if_empty
def nonempty() -> List[int]:
def nonempty() -> list[int]:
return ll
with warnings.catch_warnings(record=True) as w:
res1 = nonempty()
assert len(w) == 0
assert_type(res1, List[int])
assert_type(res1, list[int])
assert isinstance(res1, list)
assert res1 is ll # object should be unchanged!
@warn_if_empty
def empty() -> List[str]:
def empty() -> list[str]:
return []
with warnings.catch_warnings(record=True) as w:
res2 = empty()
assert len(w) == 1
assert_type(res2, List[str])
assert_type(res2, list[str])
assert isinstance(res2, list)
assert res2 == []
@ -242,7 +237,7 @@ def check_if_hashable(iterable: Iterable[_HT]) -> Iterable[_HT]:
"""
NOTE: Despite Hashable bound, typing annotation doesn't guarantee runtime safety
Consider hashable type X, and Y that inherits from X, but not hashable
Then l: List[X] = [Y(...)] is a valid expression, and type checks against Hashable,
Then l: list[X] = [Y(...)] is a valid expression, and type checks against Hashable,
but isn't runtime hashable
"""
# Sadly this doesn't work 100% correctly with dataclasses atm...
@ -268,28 +263,27 @@ def check_if_hashable(iterable: Iterable[_HT]) -> Iterable[_HT]:
# TODO different policies -- error/warn/ignore?
def test_check_if_hashable() -> None:
from dataclasses import dataclass
from typing import Set, Tuple
import pytest
from ..compat import assert_type
x1: List[int] = [1, 2]
x1: list[int] = [1, 2]
r1 = check_if_hashable(x1)
assert_type(r1, Iterable[int])
assert r1 is x1
x2: Iterator[Union[int, str]] = iter((123, 'aba'))
x2: Iterator[int | str] = iter((123, 'aba'))
r2 = check_if_hashable(x2)
assert_type(r2, Iterable[Union[int, str]])
assert list(r2) == [123, 'aba']
x3: Tuple[object, ...] = (789, 'aba')
x3: tuple[object, ...] = (789, 'aba')
r3 = check_if_hashable(x3)
assert_type(r3, Iterable[object])
assert r3 is x3 # object should be unchanged
x4: List[Set[int]] = [{1, 2, 3}, {4, 5, 6}]
x4: list[set[int]] = [{1, 2, 3}, {4, 5, 6}]
with pytest.raises(Exception):
# should be rejected by mypy sice set isn't Hashable, but also throw at runtime
r4 = check_if_hashable(x4) # type: ignore[type-var]
@ -307,7 +301,7 @@ def test_check_if_hashable() -> None:
class X:
a: int
x6: List[X] = [X(a=123)]
x6: list[X] = [X(a=123)]
r6 = check_if_hashable(x6)
assert x6 is r6
@ -316,7 +310,7 @@ def test_check_if_hashable() -> None:
class Y(X):
b: str
x7: List[Y] = [Y(a=123, b='aba')]
x7: list[Y] = [Y(a=123, b='aba')]
with pytest.raises(Exception):
# ideally that would also be rejected by mypy, but currently there is a bug
# which treats all dataclasses as hashable: https://github.com/python/mypy/issues/11463
@ -331,11 +325,8 @@ _UEU = TypeVar('_UEU')
# instead of just iterator
# TODO maybe deprecated Callable support? not sure
def unique_everseen(
fun: Union[
Callable[[], Iterable[_UET]],
Iterable[_UET]
],
key: Optional[Callable[[_UET], _UEU]] = None,
fun: Callable[[], Iterable[_UET]] | Iterable[_UET],
key: Callable[[_UET], _UEU] | None = None,
) -> Iterator[_UET]:
import os

View file

@ -4,6 +4,7 @@ TODO ideally would be great to replace with some existing solution, or find a be
since who looks at the terminal output?
E.g. would be nice to propagate the warnings in the UI (it's even a subclass of Exception!)
'''
from __future__ import annotations
import sys
import warnings