Skip to content
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

Add support for Python 3.13 #730

Merged
merged 12 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/python-version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12.0
3.13-dev
8 changes: 5 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ jobs:
matrix:
include:
- os: windows-latest
py: 3.12.0
py: 3.13-dev
args: --cov-fail-under=100
- os: macos-latest
py: 3.12.0
py: 3.13-dev
args: --cov-fail-under=100
- os: ubuntu-latest
py: 3.12.0
py: 3.13-dev
args: --cov-fail-under=100
- os: ubuntu-latest
py: 3.12.4
- os: ubuntu-latest
py: 3.11.5
- os: ubuntu-latest
Expand Down
16 changes: 8 additions & 8 deletions pdoc/doc_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,11 @@ def _parse_class(source: str) -> ast.ClassDef:
Returns an empty ast.ClassDef if source is empty.
"""
tree = _parse(source)
assert len(tree.body) <= 1
if tree.body:
if tree.body and len(tree.body) == 1:
t = tree.body[0]
assert isinstance(t, ast.ClassDef)
return t
return ast.ClassDef(body=[], decorator_list=[]) # type: ignore
if isinstance(t, ast.ClassDef):
return t
return ast.ClassDef(name="PdocStub", body=[], decorator_list=[]) # type: ignore


@cache
Expand All @@ -256,16 +255,17 @@ def _parse_function(source: str) -> ast.FunctionDef | ast.AsyncFunctionDef:
Returns an empty ast.FunctionDef if source is empty.
"""
tree = _parse(source)
assert len(tree.body) <= 1
if tree.body:
if tree.body and len(tree.body) == 1:
t = tree.body[0]
if isinstance(t, (ast.FunctionDef, ast.AsyncFunctionDef)):
return t
else:
# we have a lambda function,
# to simplify the API return the ast.FunctionDef stub.
pass
return ast.FunctionDef(body=[], decorator_list=[]) # type: ignore
return ast.FunctionDef(
name="pdoc_stub", args=ast.arguments(), body=[], decorator_list=[]
) # type: ignore


def _parse(
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Typing :: Typed",
]

Expand Down
15 changes: 8 additions & 7 deletions test/test_doc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import builtins
import dataclasses
from pathlib import Path
import sys
import types
from unittest.mock import patch

Expand Down Expand Up @@ -120,13 +119,15 @@ def test_builtin_source():
assert m.source_lines is None


@pytest.mark.skipif(sys.version_info < (3, 9), reason="3.9+ only")
def test_raising_getdoc():
class Foo:
@classmethod
@property
def __doc__(self):
raise RuntimeError
class FooMeta(type):
def __getattribute__(cls, name):
if name == "__doc__":
raise RuntimeError
return super().__getattribute__(name)

class Foo(metaclass=FooMeta):
pass

x = Class(Foo.__module__, Foo.__qualname__, Foo, (Foo.__module__, Foo.__qualname__))
with pytest.warns(UserWarning, match="inspect.getdoc(.+) raised an exception"):
Expand Down
4 changes: 3 additions & 1 deletion test/test_doc_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ def test_eval_fail_import_nonexistent(monkeypatch):
lambda _: "import typing\nif typing.TYPE_CHECKING:\n\timport nonexistent_module",
)
a = types.ModuleType("a")
with pytest.warns(UserWarning, match="No module named 'nonexistent_module'"):
with pytest.warns(
UserWarning, match="No module named 'nonexistent_module'|Import of xyz failed"
):
assert safe_eval_type("xyz", a.__dict__, None, a, "a") == "xyz"


Expand Down
15 changes: 5 additions & 10 deletions test/test_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def outfile(self, format: str) -> Path:
Snapshot("ast_parsing"),
Snapshot("collections_abc", min_version=(3, 9)),
Snapshot("demo", min_version=(3, 9)),
Snapshot("enums", min_version=(3, 12)),
Snapshot("enums", min_version=(3, 13)),
Snapshot("flavors_google"),
Snapshot("flavors_numpy"),
Snapshot("flavors_rst"),
Expand Down Expand Up @@ -136,16 +136,10 @@ def outfile(self, format: str) -> Path:
with_output_directory=True,
),
Snapshot("misc"),
Snapshot(
"misc_py39",
min_version=(3, 9),
),
Snapshot("misc_py39", min_version=(3, 9)),
Snapshot("misc_py310", min_version=(3, 10)),
Snapshot("misc_py311", min_version=(3, 11)),
Snapshot(
"misc_py312",
min_version=(3, 12),
),
Snapshot("misc_py312", min_version=(3, 12)),
Snapshot("misc_py313", min_version=(3, 13)),
Snapshot("math_demo", render_options={"math": True}),
Snapshot("math_misc", render_options={"math": True}),
Snapshot("mermaid_demo", render_options={"mermaid": True}, min_version=(3, 9)),
Expand All @@ -165,6 +159,7 @@ def outfile(self, format: str) -> Path:
Snapshot("pyo3_sample_library", specs=["pdoc_pyo3_sample_library"]),
Snapshot("top_level_reimports", ["top_level_reimports"]),
Snapshot("type_checking_imports", ["type_checking_imports.main"]),
Snapshot("typed_dict", min_version=(3, 13)),
Snapshot("type_stubs", ["type_stubs"], min_version=(3, 10)),
Snapshot(
"visibility",
Expand Down
22 changes: 11 additions & 11 deletions test/testdata/enums.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<var BAR = <IntEnum.BAR: 2>>
<var name # inherited from enum.Enum.name, The name of the Enum…>
<var value # inherited from enum.Enum.value, The value of the Enu…>
<method def conjugate(unknown): ... # inherited from builtins.int.conjugate, Returns self, the co…>
<method def conjugate(self, /): ... # inherited from builtins.int.conjugate, Returns self, the co…>
<method def bit_length(self, /): ... # inherited from builtins.int.bit_length, Number of bits neces…>
<method def bit_count(self, /): ... # inherited from builtins.int.bit_count, Number of ones in th…>
<method def to_bytes(self, /, length=1, byteorder='big', *, signed=False): ... # inherited from builtins.int.to_bytes, Return an array of b…>
Expand All @@ -35,24 +35,24 @@
<var name # inherited from enum.Enum.name, The name of the Enum…>
<var value # inherited from enum.Enum.value, The value of the Enu…>
<method def encode(self, /, encoding='utf-8', errors='strict'): ... # inherited from builtins.str.encode, Encode the string us…>
<method def replace(self, old, new, count=-1, /): ... # inherited from builtins.str.replace, Return a copy with a…>
<method def replace(self, old, new, /, count=-1): ... # inherited from builtins.str.replace, Return a copy with a…>
<method def split(self, /, sep=None, maxsplit=-1): ... # inherited from builtins.str.split, Return a list of the…>
<method def rsplit(self, /, sep=None, maxsplit=-1): ... # inherited from builtins.str.rsplit, Return a list of the…>
<method def join(self, iterable, /): ... # inherited from builtins.str.join, Concatenate any numb…>
<method def capitalize(self, /): ... # inherited from builtins.str.capitalize, Return a capitalized…>
<method def casefold(self, /): ... # inherited from builtins.str.casefold, Return a version of …>
<method def title(self, /): ... # inherited from builtins.str.title, Return a version of …>
<method def center(self, width, fillchar=' ', /): ... # inherited from builtins.str.center, Return a centered st…>
<method def count(unknown): ... # inherited from builtins.str.count, S.count(sub[, start[…>
<method def count(unknown): ... # inherited from builtins.str.count, Return the number of…>
<method def expandtabs(self, /, tabsize=8): ... # inherited from builtins.str.expandtabs, Return a copy where …>
<method def find(unknown): ... # inherited from builtins.str.find, S.find(sub[, start[,…>
<method def find(unknown): ... # inherited from builtins.str.find, Return the lowest in…>
<method def partition(self, sep, /): ... # inherited from builtins.str.partition, Partition the string…>
<method def index(unknown): ... # inherited from builtins.str.index, S.index(sub[, start[…>
<method def index(unknown): ... # inherited from builtins.str.index, Return the lowest in…>
<method def ljust(self, width, fillchar=' ', /): ... # inherited from builtins.str.ljust, Return a left-justif…>
<method def lower(self, /): ... # inherited from builtins.str.lower, Return a copy of the…>
<method def lstrip(self, chars=None, /): ... # inherited from builtins.str.lstrip, Return a copy of the…>
<method def rfind(unknown): ... # inherited from builtins.str.rfind, S.rfind(sub[, start[…>
<method def rindex(unknown): ... # inherited from builtins.str.rindex, S.rindex(sub[, start…>
<method def rfind(unknown): ... # inherited from builtins.str.rfind, Return the highest i…>
<method def rindex(unknown): ... # inherited from builtins.str.rindex, Return the highest i…>
<method def rjust(self, width, fillchar=' ', /): ... # inherited from builtins.str.rjust, Return a right-justi…>
<method def rstrip(self, chars=None, /): ... # inherited from builtins.str.rstrip, Return a copy of the…>
<method def rpartition(self, sep, /): ... # inherited from builtins.str.rpartition, Partition the string…>
Expand All @@ -61,8 +61,8 @@
<method def swapcase(self, /): ... # inherited from builtins.str.swapcase, Convert uppercase ch…>
<method def translate(self, table, /): ... # inherited from builtins.str.translate, Replace each charact…>
<method def upper(self, /): ... # inherited from builtins.str.upper, Return a copy of the…>
<method def startswith(unknown): ... # inherited from builtins.str.startswith, S.startswith(prefix[…>
<method def endswith(unknown): ... # inherited from builtins.str.endswith, S.endswith(suffix[, …>
<method def startswith(unknown): ... # inherited from builtins.str.startswith, Return True if the s…>
<method def endswith(unknown): ... # inherited from builtins.str.endswith, Return True if the s…>
<method def removeprefix(self, prefix, /): ... # inherited from builtins.str.removeprefix, Return a str with th…>
<method def removesuffix(self, suffix, /): ... # inherited from builtins.str.removesuffix, Return a str with th…>
<method def isascii(self, /): ... # inherited from builtins.str.isascii, Return True if all c…>
Expand All @@ -78,8 +78,8 @@
<method def isidentifier(self, /): ... # inherited from builtins.str.isidentifier, Return True if the s…>
<method def isprintable(self, /): ... # inherited from builtins.str.isprintable, Return True if the s…>
<method def zfill(self, width, /): ... # inherited from builtins.str.zfill, Pad a numeric string…>
<method def format(unknown): ... # inherited from builtins.str.format, S.format(*args, **kw…>
<method def format_map(unknown): ... # inherited from builtins.str.format_map, S.format_map(mapping…>
<method def format(self, /, *args, **kwargs): ... # inherited from builtins.str.format, Return a formatted v…>
<method def format_map(self, mapping, /): ... # inherited from builtins.str.format_map, Return a formatted v…>
<static def maketrans(unknown): ... # inherited from builtins.str.maketrans, Return a translation…>
>
>
Loading