From 25a6bcd966d4a006e13c14c934680db418a2e92a Mon Sep 17 00:00:00 2001 From: Dima Gerasimov Date: Sat, 19 Oct 2024 18:27:35 +0100 Subject: [PATCH] general: python3.9 reached EOL, switch min version also enable 3.13 on CI --- .github/workflows/main.yml | 11 +++++---- my/core/__main__.py | 4 +--- my/core/_deprecated/kompress.py | 2 +- my/core/compat.py | 24 ++++++++------------ my/core/pandas.py | 2 +- my/core/utils/concurrent.py | 40 ++++++++++++--------------------- my/youtube/takeout.py | 6 ++--- setup.py | 2 +- 8 files changed, 37 insertions(+), 54 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 53d8e536..111d0e9e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,19 +21,20 @@ on: jobs: build: strategy: + fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] exclude: [ # windows runners are pretty scarce, so let's only run lowest and highest python version - {platform: windows-latest, python-version: '3.9' }, {platform: windows-latest, python-version: '3.10'}, {platform: windows-latest, python-version: '3.11'}, + {platform: windows-latest, python-version: '3.12'}, # same, macos is a bit too slow and ubuntu covers python quirks well - {platform: macos-latest , python-version: '3.9' }, {platform: macos-latest , python-version: '3.10' }, {platform: macos-latest , python-version: '3.11' }, + {platform: macos-latest , python-version: '3.12' }, ] runs-on: ${{ matrix.platform }} @@ -63,11 +64,13 @@ jobs: - if: matrix.platform == 'ubuntu-latest' # no need to compute coverage for other platforms uses: actions/upload-artifact@v4 with: + include-hidden-files: true name: .coverage.mypy-misc_${{ matrix.platform }}_${{ matrix.python-version }} path: .coverage.mypy-misc/ - if: matrix.platform == 'ubuntu-latest' # no need to compute coverage for other platforms uses: actions/upload-artifact@v4 with: + include-hidden-files: true name: .coverage.mypy-core_${{ matrix.platform }}_${{ matrix.python-version }} path: .coverage.mypy-core/ @@ -81,7 +84,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '3.10' - uses: actions/checkout@v4 with: diff --git a/my/core/__main__.py b/my/core/__main__.py index a80aa521..27770085 100644 --- a/my/core/__main__.py +++ b/my/core/__main__.py @@ -171,8 +171,6 @@ def config_ok() -> bool: # use a temporary directory, useful because # - compileall ignores -B, so always craps with .pyc files (annoyng on RO filesystems) # - compileall isn't following symlinks, just silently ignores them - # note: ugh, annoying that copytree requires a non-existing dir before 3.8. - # once we have min version 3.8, can use dirs_exist_ok=True param tdir = Path(td) / 'cfg' # NOTE: compileall still returns code 0 if the path doesn't exist.. # but in our case hopefully it's not an issue @@ -181,7 +179,7 @@ def config_ok() -> bool: try: # this will resolve symlinks when copying # should be under try/catch since might fail if some symlinks are missing - shutil.copytree(cfg_path, tdir) + shutil.copytree(cfg_path, tdir, dirs_exist_ok=True) check_call(cmd) info('syntax check: ' + ' '.join(cmd)) except Exception as e: diff --git a/my/core/_deprecated/kompress.py b/my/core/_deprecated/kompress.py index b08f04bc..cd27a7f8 100644 --- a/my/core/_deprecated/kompress.py +++ b/my/core/_deprecated/kompress.py @@ -210,7 +210,7 @@ def __truediv__(self, key) -> ZipPath: def iterdir(self) -> Iterator[ZipPath]: for s in self._as_dir().iterdir(): - yield ZipPath(s.root, s.at) # type: ignore[attr-defined] + yield ZipPath(s.root, s.at) @property def stem(self) -> str: diff --git a/my/core/compat.py b/my/core/compat.py index 29d4f04d..3273ff42 100644 --- a/my/core/compat.py +++ b/my/core/compat.py @@ -21,25 +21,19 @@ def sqlite_backup(*, source: sqlite3.Connection, dest: sqlite3.Connection, **kwa # TODO warn here? source.backup(dest, **kwargs) + # keeping for runtime backwards compatibility (added in 3.9) + @deprecated('use .removeprefix method on string directly instead') + def removeprefix(text: str, prefix: str) -> str: + return text.removeprefix(prefix) -## can remove after python3.9 (although need to keep the method itself for bwd compat) -def removeprefix(text: str, prefix: str) -> str: - if text.startswith(prefix): - return text[len(prefix) :] - return text + @deprecated('use .removesuffix method on string directly instead') + def removesuffix(text: str, suffix: str) -> str: + return text.removesuffix(suffix) + ## -def removesuffix(text: str, suffix: str) -> str: - if text.endswith(suffix): - return text[:-len(suffix)] - return text -## - -## used to have compat function before 3.8 for these, keeping for runtime back compatibility -if not TYPE_CHECKING: + ## used to have compat function before 3.8 for these, keeping for runtime back compatibility from functools import cached_property from typing import Literal, Protocol, TypedDict -else: - from typing_extensions import Literal, Protocol, TypedDict ## diff --git a/my/core/pandas.py b/my/core/pandas.py index d38465ae..8f5fd29f 100644 --- a/my/core/pandas.py +++ b/my/core/pandas.py @@ -181,7 +181,7 @@ def _to_jsons(it: Iterable[Res[Any]]) -> Iterable[Json]: def _as_columns(s: Schema) -> Dict[str, Type]: # todo would be nice to extract properties; add tests for this as well if dataclasses.is_dataclass(s): - return {f.name: f.type for f in dataclasses.fields(s)} + return {f.name: f.type for f in dataclasses.fields(s)} # type: ignore[misc] # ugh, why mypy thinks f.type can return str?? # else must be NamedTuple?? # todo assert my.core.common.is_namedtuple? return getattr(s, '_field_types') diff --git a/my/core/utils/concurrent.py b/my/core/utils/concurrent.py index 146861b9..73944ec1 100644 --- a/my/core/utils/concurrent.py +++ b/my/core/utils/concurrent.py @@ -1,6 +1,6 @@ import sys from concurrent.futures import Executor, Future -from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar +from typing import Any, Callable, Optional, TypeVar from ..compat import ParamSpec @@ -19,33 +19,21 @@ def __init__(self, max_workers: Optional[int] = 1) -> None: self._shutdown = False self._max_workers = max_workers - if TYPE_CHECKING: - if sys.version_info[:2] <= (3, 8): - # 3.8 doesn't support ParamSpec as Callable arg :( - # and any attempt to type results in incompatible supertype.. so whatever - def submit(self, fn, *args, **kwargs): ... - + def submit(self, fn: Callable[_P, _T], /, *args: _P.args, **kwargs: _P.kwargs) -> Future[_T]: + if self._shutdown: + raise RuntimeError('cannot schedule new futures after shutdown') + + f: Future[Any] = Future() + try: + result = fn(*args, **kwargs) + except KeyboardInterrupt: + raise + except BaseException as e: + f.set_exception(e) else: + f.set_result(result) - def submit(self, fn: Callable[_P, _T], /, *args: _P.args, **kwargs: _P.kwargs) -> Future[_T]: ... - - else: - - def submit(self, fn, *args, **kwargs): - if self._shutdown: - raise RuntimeError('cannot schedule new futures after shutdown') - - f: Future[Any] = Future() - try: - result = fn(*args, **kwargs) - except KeyboardInterrupt: - raise - except BaseException as e: - f.set_exception(e) - else: - f.set_result(result) - - return f + return f def shutdown(self, wait: bool = True, **kwargs) -> None: # noqa: FBT001,FBT002,ARG002 self._shutdown = True diff --git a/my/youtube/takeout.py b/my/youtube/takeout.py index bbce46a4..f29b2e31 100644 --- a/my/youtube/takeout.py +++ b/my/youtube/takeout.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Iterable, Iterator from my.core import Res, Stats, datetime_aware, make_logger, stat, warnings -from my.core.compat import deprecated, removeprefix, removesuffix +from my.core.compat import deprecated logger = make_logger(__name__) @@ -117,10 +117,10 @@ def _watched() -> Iterator[Res[Watched]]: # all titles contain it, so pointless to include 'Watched ' # also compatible with legacy titles - title = removeprefix(title, 'Watched ') + title = title.removeprefix('Watched ') # watches originating from some activity end with this, remove it for consistency - title = removesuffix(title, ' - YouTube') + title = title.removesuffix(' - YouTube') if YOUTUBE_VIDEO_LINK not in url: if 'youtube.com/post/' in url: diff --git a/setup.py b/setup.py index e49eee02..385c810e 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ def main() -> None: author_email='karlicoss@gmail.com', description='A Python interface to my life', - python_requires='>=3.8', + python_requires='>=3.9', install_requires=INSTALL_REQUIRES, extras_require={ 'testing': [