Skip to content

Commit 9716aa9

Browse files
feat: delay import of requests, drop python 3.8, add python 3.13 (#27)
* feat: delay requests import * style(pre-commit.ci): auto fixes [...] * fix lint --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent e674bb1 commit 9716aa9

File tree

8 files changed

+82
-79
lines changed

8 files changed

+82
-79
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ jobs:
2626
strategy:
2727
fail-fast: false
2828
matrix:
29-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
29+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
3030
os: [ubuntu-latest]
3131
include:
32-
- python-version: "3.8"
32+
- python-version: "3.9"
3333
os: windows-latest
34-
- python-version: "3.8"
34+
- python-version: "3.9"
3535
os: macos-latest
3636

3737
test-qt:

pyproject.toml

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@ name = "pyconify"
1212
dynamic = ["version"]
1313
description = "iconify for python. Universal icon framework"
1414
readme = "README.md"
15-
requires-python = ">=3.8"
15+
requires-python = ">=3.9"
1616
license = { text = "BSD-3-Clause" }
1717
authors = [{ name = "Talley Lambert", email = "[email protected]" }]
1818
classifiers = [
19-
"Development Status :: 3 - Alpha",
19+
"Development Status :: 4 - Beta",
2020
"Programming Language :: Python :: 3",
21-
"Programming Language :: Python :: 3.8",
2221
"Programming Language :: Python :: 3.9",
2322
"Programming Language :: Python :: 3.10",
2423
"Programming Language :: Python :: 3.11",
2524
"Programming Language :: Python :: 3.12",
25+
"Programming Language :: Python :: 3.13",
2626
"License :: OSI Approved :: BSD License",
2727
"Typing :: Typed",
2828
]
@@ -36,44 +36,38 @@ dev = ["black", "ipython", "mypy", "pdbpp", "rich", "ruff", "types-requests"]
3636
homepage = "https://github.com/pyapp-kit/pyconify"
3737
repository = "https://github.com/pyapp-kit/pyconify"
3838

39-
# https://github.com/charliermarsh/ruff
39+
# https://beta.ruff.rs/docs/rules/
4040
[tool.ruff]
4141
line-length = 88
42-
target-version = "py38"
43-
# https://beta.ruff.rs/docs/rules/
42+
target-version = "py39"
43+
src = ["src", "tests"]
44+
45+
[tool.ruff.lint]
46+
pydocstyle = { convention = "numpy" }
4447
select = [
45-
"E", # style errors
4648
"W", # style warnings
49+
"E", # style errors
4750
"F", # flakes
4851
"D", # pydocstyle
4952
"I", # isort
5053
"UP", # pyupgrade
51-
"C", # flake8-comprehensions
52-
"B", # flake8-bugbear
53-
"A001", # flake8-builtins
54+
"S", # bandit
55+
"C4", # comprehensions
56+
"B", # bugbear
57+
"A001", # Variable shadowing a python builtin
58+
"TC", # flake8-type-checking
59+
"TID", # flake8-tidy-imports
5460
"RUF", # ruff-specific rules
55-
"TCH",
56-
"TID",
61+
"PERF", # performance
62+
"SLF", # private access
5763
]
58-
59-
# I do this to get numpy-style docstrings AND retain
60-
# D417 (Missing argument descriptions in the docstring)
61-
# otherwise, see:
62-
# https://beta.ruff.rs/docs/faq/#does-ruff-support-numpy-or-google-style-docstrings
63-
# https://github.com/charliermarsh/ruff/issues/2606
6464
ignore = [
6565
"D100", # Missing docstring in public module
66-
"D107", # Missing docstring in __init__
67-
"D203", # 1 blank line required before class docstring
68-
"D212", # Multi-line docstring summary should start at the first line
69-
"D213", # Multi-line docstring summary should start at the second line
70-
"D401", # First line should be in imperative mood
71-
"D413", # Missing blank line after last section
72-
"D416", # Section name should end with a colon
66+
"D401", # First line should be in imperative mood (remove to opt in)
7367
]
7468

75-
[tool.ruff.per-file-ignores]
76-
"tests/*.py" = ["D"]
69+
[tool.ruff.lint.per-file-ignores]
70+
"tests/*.py" = ["D", "S101", "E501", "SLF"]
7771

7872
# https://mypy.readthedocs.io/en/stable/config_file.html
7973
[tool.mypy]

src/pyconify/_cache.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from __future__ import annotations
22

33
import os
4+
from collections.abc import Iterator, MutableMapping
45
from contextlib import suppress
56
from pathlib import Path
6-
from typing import Iterator, MutableMapping
77

88
_SVG_CACHE: MutableMapping[str, bytes] | None = None
99
PYCONIFY_CACHE: str = os.environ.get("PYCONIFY_CACHE", "")
@@ -36,7 +36,7 @@ def clear_cache() -> None:
3636
global _SVG_CACHE
3737
_SVG_CACHE = None
3838
with suppress(AttributeError):
39-
svg_path.cache_clear() # type: ignore
39+
svg_path.cache_clear()
4040

4141

4242
def get_cache_directory(app_name: str = "pyconify") -> Path:

src/pyconify/api.py

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,23 @@
33
from __future__ import annotations
44

55
import atexit
6+
import functools
67
import os
78
import re
89
import tempfile
910
import warnings
1011
from contextlib import suppress
1112
from pathlib import Path
12-
from typing import TYPE_CHECKING, Iterable, Literal, overload
13-
14-
import requests
13+
from typing import TYPE_CHECKING, Literal, overload
1514

1615
from ._cache import CACHE_DISABLED, _SVGCache, cache_key, svg_cache
1716

1817
if TYPE_CHECKING:
18+
from collections.abc import Iterable
1919
from typing import Callable, TypeVar
2020

21+
import requests
22+
2123
F = TypeVar("F", bound=Callable)
2224

2325
from .iconify_types import (
@@ -30,16 +32,21 @@
3032
Rotation,
3133
)
3234

33-
def lru_cache(maxsize: int | None = None) -> Callable[[F], F]:
34-
"""Dummy lru_cache decorator for type checking."""
35-
36-
else:
37-
from functools import lru_cache
3835

3936
ROOT = "https://api.iconify.design"
4037

4138

42-
@lru_cache(maxsize=None)
39+
@functools.cache
40+
def _session() -> requests.Session:
41+
"""Return a requests session."""
42+
import requests
43+
44+
session = requests.Session()
45+
session.headers.update({"User-Agent": "pyconify"})
46+
return session
47+
48+
49+
@functools.cache
4350
def collections(*prefixes: str) -> dict[str, IconifyInfo]:
4451
"""Return collections where key is icon set prefix, value is IconifyInfo object.
4552
@@ -55,12 +62,12 @@ def collections(*prefixes: str) -> dict[str, IconifyInfo]:
5562
end with "-", such as "mdi-" matches "mdi-light".
5663
"""
5764
query_params = {"prefixes": ",".join(prefixes)}
58-
resp = requests.get(f"{ROOT}/collections", params=query_params)
65+
resp = _session().get(f"{ROOT}/collections", params=query_params, timeout=2)
5966
resp.raise_for_status()
6067
return resp.json() # type: ignore
6168

6269

63-
@lru_cache(maxsize=None)
70+
@functools.cache
6471
def collection(
6572
prefix: str,
6673
info: bool = False,
@@ -86,18 +93,19 @@ def collection(
8693
query_params["chars"] = 1
8794
if info:
8895
query_params["info"] = 1
89-
resp = requests.get(f"{ROOT}/collection?prefix={prefix}", params=query_params)
90-
resp.raise_for_status()
91-
if (content := resp.json()) == 404:
92-
raise requests.HTTPError(
96+
resp = _session().get(
97+
f"{ROOT}/collection?prefix={prefix}", params=query_params, timeout=2
98+
)
99+
if 400 <= resp.status_code < 500:
100+
raise OSError(
93101
f"Icon set {prefix!r} not found. "
94102
"Search for icons at https://icon-sets.iconify.design",
95-
response=resp,
96103
)
97-
return content # type: ignore
104+
resp.raise_for_status()
105+
return resp.json() # type: ignore
98106

99107

100-
@lru_cache(maxsize=None)
108+
@functools.cache
101109
def last_modified(*prefixes: str) -> dict[str, int]:
102110
"""Return last modified date for icon sets.
103111
@@ -120,7 +128,7 @@ def last_modified(*prefixes: str) -> dict[str, int]:
120128
UTC integer timestamp.
121129
"""
122130
query_params = {"prefixes": ",".join(prefixes)}
123-
resp = requests.get(f"{ROOT}/last-modified", params=query_params)
131+
resp = _session().get(f"{ROOT}/last-modified", params=query_params, timeout=2)
124132
resp.raise_for_status()
125133
if "lastModified" not in (content := resp.json()): # pragma: no cover
126134
raise ValueError(
@@ -200,14 +208,13 @@ def svg(
200208
}
201209
if box:
202210
query_params["box"] = 1
203-
resp = requests.get(f"{ROOT}/{prefix}/{name}.svg", params=query_params)
204-
resp.raise_for_status()
205-
if resp.content == b"404":
206-
raise requests.HTTPError(
211+
resp = _session().get(f"{ROOT}/{prefix}/{name}.svg", params=query_params, timeout=2)
212+
if 400 <= resp.status_code < 500:
213+
raise OSError(
207214
f"Icon '{prefix}:{name}' not found. "
208215
f"Search for icons at https://icon-sets.iconify.design?query={name}",
209-
response=resp,
210216
)
217+
resp.raise_for_status()
211218

212219
# cache response and return
213220
cache[svg_cache_key] = resp.content
@@ -253,7 +260,7 @@ def _cached_svg_path(svg_cache_key: str) -> Path | None:
253260
return None # pragma: no cover
254261

255262

256-
@lru_cache(maxsize=None)
263+
@functools.cache
257264
def svg_path(
258265
*key: str,
259266
color: str | None = None,
@@ -320,7 +327,7 @@ def _remove_tmp_svg() -> None:
320327
return Path(tmp_name)
321328

322329

323-
@lru_cache(maxsize=None)
330+
@functools.cache
324331
def css(
325332
*keys: str,
326333
selector: str | None = None,
@@ -389,14 +396,15 @@ def css(
389396
if square:
390397
params["square"] = 1
391398

392-
resp = requests.get(f"{ROOT}/{prefix}.css?icons={','.join(icons)}", params=params)
393-
resp.raise_for_status()
394-
if resp.text == "404":
395-
raise requests.HTTPError(
399+
resp = _session().get(
400+
f"{ROOT}/{prefix}.css?icons={','.join(icons)}", params=params, timeout=2
401+
)
402+
if 400 <= resp.status_code < 500:
403+
raise OSError(
396404
f"Icon set {prefix!r} not found. "
397405
"Search for icons at https://icon-sets.iconify.design",
398-
response=resp,
399406
)
407+
resp.raise_for_status()
400408
if missing := set(re.findall(r"Could not find icon: ([^\s]*) ", resp.text)):
401409
warnings.warn(
402410
f"Icon(s) {sorted(missing)} not found. "
@@ -425,14 +433,13 @@ def icon_data(*keys: str) -> IconifyJSON:
425433
Icon name(s).
426434
"""
427435
prefix, names = _split_prefix_name(keys, allow_many=True)
428-
resp = requests.get(f"{ROOT}/{prefix}.json?icons={','.join(names)}")
429-
resp.raise_for_status()
436+
resp = _session().get(f"{ROOT}/{prefix}.json?icons={','.join(names)}", timeout=2)
430437
if (content := resp.json()) == 404:
431-
raise requests.HTTPError(
438+
raise OSError(
432439
f"Icon set {prefix!r} not found. "
433440
"Search for icons at https://icon-sets.iconify.design",
434-
response=resp,
435441
)
442+
resp.raise_for_status()
436443
return content # type: ignore
437444

438445

@@ -499,7 +506,7 @@ def search(
499506
params["prefixes"] = ",".join(prefixes)
500507
if category is not None:
501508
params["category"] = category
502-
resp = requests.get(f"{ROOT}/search?query={query}", params=params)
509+
resp = _session().get(f"{ROOT}/search?query={query}", params=params, timeout=2)
503510
resp.raise_for_status()
504511
return resp.json() # type: ignore
505512

@@ -536,12 +543,12 @@ def keywords(
536543
params = {"keyword": keyword}
537544
else:
538545
params = {}
539-
resp = requests.get(f"{ROOT}/keywords", params=params)
546+
resp = _session().get(f"{ROOT}/keywords", params=params, timeout=2)
540547
resp.raise_for_status()
541548
return resp.json() # type: ignore
542549

543550

544-
@lru_cache(maxsize=None)
551+
@functools.cache
545552
def iconify_version() -> str:
546553
"""Return version of iconify API.
547554
@@ -556,7 +563,7 @@ def iconify_version() -> str:
556563
>>> iconify_version()
557564
'Iconify API version 3.0.0-beta.1'
558565
"""
559-
resp = requests.get(f"{ROOT}/version")
566+
resp = _session().get(f"{ROOT}/version", timeout=2)
560567
resp.raise_for_status()
561568
return resp.text
562569

src/pyconify/freedesktop.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
import atexit
44
import shutil
5+
from collections.abc import Mapping
56
from pathlib import Path
67
from tempfile import mkdtemp
7-
from typing import TYPE_CHECKING, Any, Mapping
8+
from typing import TYPE_CHECKING, Any
89

910
from pyconify.api import svg
1011

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import shutil
2+
from collections.abc import Iterator
23
from pathlib import Path
3-
from typing import Iterator
44
from unittest.mock import patch
55

66
import pytest

tests/test_cache.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
from collections.abc import Iterator
12
from contextlib import contextmanager
23
from pathlib import Path
3-
from typing import Iterator
44
from unittest.mock import patch
55

66
import pytest
77
import requests
88

99
import pyconify
10-
from pyconify import _cache
10+
from pyconify import _cache, api
1111
from pyconify._cache import _SVGCache, clear_cache, get_cache_directory
1212

1313

@@ -62,8 +62,8 @@ def test_tmp_svg_with_fixture() -> None:
6262
@contextmanager
6363
def internet_offline() -> Iterator[None]:
6464
"""Simulate an offline internet connection."""
65-
66-
with patch.object(requests, "get") as mock:
65+
session = api._session()
66+
with patch.object(session, "get") as mock:
6767
mock.side_effect = requests.ConnectionError("No internet connection.")
6868
# clear functools caches...
6969
for val in vars(pyconify).values():

tests/test_pyconify.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,15 @@ def test_keywords() -> None:
9898
with pytest.warns(UserWarning, match="Cannot specify both prefix and keyword"):
9999
assert isinstance(pyconify.keywords("home", keyword="home"), dict)
100100

101-
assert pyconify.keywords()
101+
with pytest.raises(OSError):
102+
pyconify.keywords()
102103

103104

104105
def test_search() -> None:
105106
result = pyconify.search("arrow", prefixes={"bi"}, limit=10, start=2)
106107
assert result["collections"]
107108

108-
result = pyconify.search("arrow", prefixes="bi", category="General")
109+
result = pyconify.search("home", prefixes="material-symbols", category="Material")
109110
assert result["collections"]
110111

111112

0 commit comments

Comments
 (0)