Skip to content

Commit

Permalink
Drop support for python 3.7 (#1173)
Browse files Browse the repository at this point in the history
* test: drop support for python 3.7

* test: test under Pillow version 6.2.x rather than 6.0

Pillow 6.2.x is the first release for which PyPI contains wheels for
python 3.8.

* chore: typing.TypedDict is now always available

* chore: no longer require importlib_metadata

* chore: remove the now unused lektor.compat.FixedTemporaryDirectory

* compat: provide disused bits of lektor.compat

For plugins that might use them, provide
`lektor.compat.TemporaryDirectory` and `lektor.compat.importlib_metadata`
(along with a DeprecationWarning).
  • Loading branch information
dairiki committed Nov 2, 2023
1 parent ffa0f49 commit 2ca065c
Show file tree
Hide file tree
Showing 12 changed files with 73 additions and 117 deletions.
6 changes: 1 addition & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,14 @@ jobs:
fail-fast: false
matrix:
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
python: ["3.8", "3.9", "3.10", "3.11", "3.12"]
exclude:
- os: "macos-latest"
python: "3.8"
- os: "macos-latest"
python: "3.9"
- os: "macos-latest"
python: "3.10"
- os: "macos-latest"
python: "3.11"
- os: "windows-latest"
python: "3.8"
- os: "windows-latest"
python: "3.9"
- os: "windows-latest"
Expand Down
2 changes: 1 addition & 1 deletion lektor/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import sys
import warnings
from importlib import metadata
from itertools import chain

import click
Expand All @@ -13,7 +14,6 @@
from lektor.cli_utils import pruneflag
from lektor.cli_utils import ResolvedPath
from lektor.cli_utils import validate_language
from lektor.compat import importlib_metadata as metadata
from lektor.project import Project
from lektor.utils import secure_url

Expand Down
78 changes: 28 additions & 50 deletions lektor/compat.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,46 @@
from __future__ import annotations

import os
import stat
import sys
import importlib.metadata
import tempfile
import urllib.parse
from functools import partial
from itertools import chain
from typing import Any
from urllib.parse import urlsplit
from warnings import warn

from werkzeug import urls as werkzeug_urls
from werkzeug.datastructures import MultiDict

__all__ = ["TemporaryDirectory", "importlib_metadata", "werkzeug_urls_URL"]
from lektor.utils import DeprecatedWarning

__all__ = ["werkzeug_urls_URL"]

def _ensure_tree_writeable(path: str) -> None:
"""Attempt to ensure that all files in the tree rooted at path are writeable."""
dirscans = []

def fix_mode(path, statfunc):
try:
# paranoia regarding symlink attacks
current_mode = statfunc(follow_symlinks=False).st_mode
if not stat.S_ISLNK(current_mode):
isdir = stat.S_ISDIR(current_mode)
fixed_mode = current_mode | (0o700 if isdir else 0o200)
if current_mode != fixed_mode:
os.chmod(path, fixed_mode)
if isdir:
dirscans.append(os.scandir(path))
except FileNotFoundError:
pass

fix_mode(path, partial(os.stat, path))
for entry in chain.from_iterable(dirscans):
fix_mode(entry.path, entry.stat)


class FixedTemporaryDirectory(tempfile.TemporaryDirectory):
"""A version of tempfile.TemporaryDirectory that works if dir contains read-only files.
On python < 3.8 under Windows, if any read-only files are created
in a TemporaryDirectory, TemporaryDirectory will throw an
exception when it tries to remove them on cleanup. See
https://bugs.python.org/issue26660
This can create issues, e.g., with temporary git repositories since
git creates read-only files in its object store.
"""
_DEPRECATED_ATTRS = {
"TemporaryDirectory": tempfile.TemporaryDirectory,
"importlib_metadata": importlib.metadata,
}

def cleanup(self) -> None:
_ensure_tree_writeable(self.name)
super().cleanup()


if sys.version_info >= (3, 8):
TemporaryDirectory = tempfile.TemporaryDirectory
from importlib import metadata as importlib_metadata
else:
TemporaryDirectory = FixedTemporaryDirectory
import importlib_metadata
def __getattr__(name):
try:
value = _DEPRECATED_ATTRS.get(name)
except KeyError:
# pylint: disable=raise-missing-from
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

if hasattr(value, "__module__"):
replacement = f"{value.__module__}.{value.__name__}"
else:
replacement = f"{value.__name__}"
warn(
DeprecatedWarning(
name=f"lektor.compat.{name}",
reason=f"use {replacement} instead",
version="3.4.0",
),
stacklevel=2,
)
return value


class _CompatURL(urllib.parse.SplitResult):
Expand Down
2 changes: 1 addition & 1 deletion lektor/markdown/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sys
import warnings
from importlib import metadata
from typing import Any
from typing import Dict
from typing import Hashable
Expand All @@ -10,7 +11,6 @@

from markupsafe import Markup

from lektor.compat import importlib_metadata as metadata
from lektor.markdown.controller import ControllerCache
from lektor.markdown.controller import FieldOptions
from lektor.markdown.controller import MarkdownController
Expand Down
18 changes: 5 additions & 13 deletions lektor/markdown/mistune2.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
"""MarkdownController implementation for mistune 2.x"""
from __future__ import annotations

import sys
from importlib import import_module
from typing import Any
from typing import Callable
from typing import ClassVar
from typing import Dict
from typing import List
from typing import Optional
from typing import Sequence
from typing import TypedDict

import mistune.util

Expand Down Expand Up @@ -46,16 +44,10 @@ def image(self, src: str, alt: str = "", title: Optional[str] = None) -> str:
return super().image(src, alt, title)


if sys.version_info < (3, 8):
# No typing.TypedDict → punt
ParserConfigDict = Dict[str, Any]
else:
from typing import TypedDict

class ParserConfigDict(TypedDict, total=False):
block: mistune.BlockParser
inline: mistune.InlineParser
plugins: Sequence[Callable[[mistune.Markdown], None]]
class ParserConfigDict(TypedDict, total=False):
block: mistune.BlockParser
inline: mistune.InlineParser
plugins: Sequence[Callable[[mistune.Markdown], None]]


MistunePlugin = Callable[[mistune.Markdown], None]
Expand Down
2 changes: 1 addition & 1 deletion lektor/pluginsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
import os
import sys
import warnings
from importlib import metadata
from pathlib import Path
from typing import Type
from weakref import ref as weakref

from inifile import IniFile

from lektor.compat import importlib_metadata as metadata
from lektor.context import get_ctx
from lektor.utils import process_extra_flags

Expand Down
2 changes: 1 addition & 1 deletion lektor/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from subprocess import DEVNULL
from subprocess import PIPE
from subprocess import STDOUT
from tempfile import TemporaryDirectory
from typing import Any
from typing import Callable
from typing import ContextManager
Expand All @@ -35,7 +36,6 @@

from werkzeug.datastructures import MultiDict

from lektor.compat import TemporaryDirectory
from lektor.compat import werkzeug_urls_URL
from lektor.exception import LektorException
from lektor.utils import bool_from_string
Expand Down
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
Expand All @@ -33,14 +32,13 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]

requires-python = ">=3.7"
requires-python = ">=3.8"
dependencies = [
"Babel",
"click>=6.0",
"EXIFRead",
"filetype>=1.0.7",
"Flask",
"importlib_metadata; python_version<'3.8'",
"inifile>=0.4.1",
"Jinja2>=3.0",
"MarkupSafe",
Expand Down
52 changes: 25 additions & 27 deletions tests/test_compat.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,36 @@
import os
from pathlib import Path
import importlib
import tempfile
from urllib.parse import urlsplit

import pytest
from werkzeug import urls as werkzeug_urls

from lektor.compat import _CompatURL
from lektor.compat import _ensure_tree_writeable
from lektor.compat import FixedTemporaryDirectory
from lektor.compat import TemporaryDirectory


def test_ensure_tree_writeable(tmp_path):
topdir = tmp_path / "topdir"
subdir = topdir / "subdir"
regfile = subdir / "regfile"
subdir.mkdir(parents=True)
regfile.touch(mode=0)
subdir.chmod(0)
topdir.chmod(0)

_ensure_tree_writeable(topdir)

for p in topdir, subdir, regfile:
assert os.access(p, os.W_OK)


@pytest.mark.parametrize("tmpdir_class", [FixedTemporaryDirectory, TemporaryDirectory])
def test_TemporaryDirectory(tmpdir_class):
with tmpdir_class() as tmpdir:
file = Path(tmpdir, "test-file")
file.touch(mode=0)
os.chmod(tmpdir, 0)
assert not os.path.exists(tmpdir)
@pytest.mark.parametrize(
"name, expected_value, replacement",
[
(
"TemporaryDirectory",
tempfile.TemporaryDirectory,
"tempfile.TemporaryDirectory",
),
("importlib_metadata", importlib.metadata, "importlib.metadata"),
],
)
def test_deprecated_attr(name, expected_value, replacement):
lektor_compat = importlib.import_module("lektor.compat")
with pytest.deprecated_call(match=f"use {replacement} instead") as warnings:
value = getattr(lektor_compat, name)
assert value is expected_value
assert warnings[0].filename == __file__


def test_missing_attr():
lektor_compat = importlib.import_module("lektor.compat")
with pytest.raises(AttributeError):
lektor_compat.MISSING # pylint: disable=pointless-statement


def make_CompatURL(url: str) -> _CompatURL:
Expand Down
11 changes: 3 additions & 8 deletions tests/test_conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import sys
from contextlib import suppress
from importlib import import_module

from lektor.compat import importlib_metadata
from importlib import metadata

# pylint: disable-next=wrong-import-order
from conftest import restore_import_state # noreorder
Expand All @@ -25,15 +24,11 @@ def test_restore_import_state_restores_unneutered_PathFinder():
#
# This tests that restore_import_state manages to unneuter
# this find.
distributions_pre = [
dist.metadata["name"] for dist in importlib_metadata.distributions()
]
distributions_pre = [dist.metadata["name"] for dist in metadata.distributions()]

with restore_import_state(), suppress(ModuleNotFoundError):
import_module("importlib_metadata")

distributions_post = [
dist.metadata["name"] for dist in importlib_metadata.distributions()
]
distributions_post = [dist.metadata["name"] for dist in metadata.distributions()]
assert len(distributions_pre) == len(distributions_post)
assert set(distributions_pre) == set(distributions_post)
2 changes: 1 addition & 1 deletion tests/test_pluginsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import inspect
import sys
from importlib import metadata
from importlib.abc import Loader
from importlib.machinery import ModuleSpec
from pathlib import Path
Expand All @@ -12,7 +13,6 @@
import pytest

from lektor.cli import cli
from lektor.compat import importlib_metadata as metadata
from lektor.context import Context
from lektor.packages import add_package_to_project
from lektor.pluginsystem import _check_dist_name
Expand Down
11 changes: 5 additions & 6 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
minversion = 4.1
envlist =
lint
{py37,py38,py39,py310,py311,py312}{,-mistune0}
{py38,py39,py310,py311,py312}{,-mistune0}
py311-noutils
py311-pytz
py311-tzdata
Expand All @@ -13,7 +13,6 @@ isolated_build = true

[gh-actions]
python =
3.7: py37, cover
3.8: py38, cover
3.9: py39, cover
3.10: py310, cover
Expand All @@ -40,11 +39,11 @@ deps =
mistune0: mistune<2
pytz: pytz
tzdata: tzdata
pillow6: pillow<6.1.0
pillow6: pillow<7.0
pillow7: pillow<7.1.0
depends =
py{37,38,39,310,311,312}: cover-clean
cover-report: py{37,38,39,310,311,312}{,-mistune0,-noutils,-pytz,-tzdata,-pillow6,-pillow7}
py{38,39,310,311,312}: cover-clean
cover-report: py{38,39,310,311,312}{,-mistune0,-noutils,-pytz,-tzdata,-pillow6,-pillow7}
# XXX: I've been experiencing sporadic failures when running tox in parallel mode.
# The crux of the error messages when this happens appears to be something like:
#
Expand All @@ -64,7 +63,7 @@ depends =
# Hopefully, at some point this can be removed.
download = true

[testenv:py{37,38,39,310,311,312}-noutils]
[testenv:py{38,39,310,311,312}-noutils]
# To test in environment without external utitilities like ffmpeg and git installed,
# break PATH in noutils environment(s).
allowlist_externals = env
Expand Down

0 comments on commit 2ca065c

Please sign in to comment.