Skip to content

Commit

Permalink
Merge pull request #246 from joerick/test-3.12
Browse files Browse the repository at this point in the history
Add support for Python 3.12
  • Loading branch information
joerick committed Aug 21, 2023
2 parents c227d4e + 9bcdfae commit 59d59b5
Show file tree
Hide file tree
Showing 15 changed files with 239 additions and 60 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"]
fail-fast: false

steps:
- uses: actions/checkout@v2
Expand All @@ -38,13 +39,12 @@ jobs:

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
python -m pip install --upgrade pip setuptools wheel
pip install -e '.[test]'
- name: Test with pytest
run: |
pytest
pytest --only-ipython-magic
pytest && pytest --only-ipython-magic
test-aarch64:
name: "test (aarch64, ${{ matrix.pyver }})"
Expand All @@ -69,7 +69,7 @@ jobs:
bash -exc '${{ env.py }} -m venv .env && \
source .env/bin/activate && \
pip install --upgrade pip && \
pip install -r requirements-dev.txt && \
pip install -e ".[test]" && \
pytest && \
pytest --only-ipython-magic && \
deactivate'
2 changes: 1 addition & 1 deletion .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
python-version: '3.8'

- name: Build wheels
uses: joerick/cibuildwheel@v2.14.1
uses: joerick/cibuildwheel@v2.15.0
env:
CIBW_SKIP: pp*
CIBW_ARCHS: ${{matrix.archs}}
Expand Down
41 changes: 26 additions & 15 deletions pyinstrument/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,18 @@ def dash_m_callback(option: str, opt: str, value: str, parser: optparse.OptionPa
if options.from_path and sys.platform == "win32":
parser.error("--from-path is not supported on Windows")

renderer_class = get_renderer_class(options)

# open the output file

if options.outfile:
f = open(options.outfile, "w", encoding="utf-8", errors="surrogateescape")
f = open(
options.outfile,
"w",
encoding="utf-8",
errors="surrogateescape",
newline="" if renderer_class.output_is_binary else None,
)
should_close_f_after_writing = True
else:
f = sys.stdout
Expand All @@ -271,7 +279,7 @@ def dash_m_callback(option: str, opt: str, value: str, parser: optparse.OptionPa
# create the renderer

try:
renderer = create_renderer(options, output_file=f)
renderer = create_renderer(renderer_class, options, output_file=f)
except OptionsParseError as e:
parser.error(e.args[0])
exit(1)
Expand Down Expand Up @@ -342,7 +350,7 @@ def dash_m_callback(option: str, opt: str, value: str, parser: optparse.OptionPa
if should_close_f_after_writing:
f.close()

if options.renderer == "text":
if isinstance(renderer, renderers.ConsoleRenderer) and not options.outfile:
_, report_identifier = save_report_to_temp_storage(session)
print("To view this report with different options, run:")
print(" pyinstrument --load-prev %s [options]" % report_identifier)
Expand Down Expand Up @@ -425,17 +433,9 @@ class OptionsParseError(Exception):
pass


def create_renderer(options: CommandLineOptions, output_file: TextIO) -> renderers.Renderer:
if options.output_html:
options.renderer = "html"

if options.renderer is None and options.outfile:
options.renderer = guess_renderer_from_outfile(options.outfile)

if options.renderer is None:
options.renderer = "text"

renderer_class = get_renderer_class(options.renderer)
def create_renderer(
renderer_class: type[renderers.Renderer], options: CommandLineOptions, output_file: TextIO
) -> renderers.Renderer:
render_options = compute_render_options(
options, renderer_class=renderer_class, output_file=output_file
)
Expand All @@ -449,7 +449,18 @@ def create_renderer(options: CommandLineOptions, output_file: TextIO) -> rendere
)


def get_renderer_class(renderer: str) -> type[renderers.Renderer]:
def get_renderer_class(options: CommandLineOptions) -> type[renderers.Renderer]:
renderer = options.renderer

if options.output_html:
renderer = "html"

if renderer is None and options.outfile:
renderer = guess_renderer_from_outfile(options.outfile)

if renderer is None:
renderer = "text"

if renderer == "text":
return renderers.ConsoleRenderer
elif renderer == "html":
Expand Down
9 changes: 7 additions & 2 deletions pyinstrument/renderers/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import contextlib
from typing import Any, List

from pyinstrument import processors
Expand Down Expand Up @@ -93,9 +94,13 @@ def __init__(
# processors.remove_first_pyinstrument_frames_processor,
# (still hide the outer pyinstrument calling frames)
):
self.processors.remove(p)
with contextlib.suppress(ValueError):
# don't care if the processor isn't in the list
self.processors.remove(p)

if timeline:
self.processors.remove(processors.aggregate_repeated_calls)
with contextlib.suppress(ValueError):
self.processors.remove(processors.aggregate_repeated_calls)

def default_processors(self) -> ProcessorList:
"""
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.black]
line-length = 100

Expand Down
20 changes: 1 addition & 19 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,19 +1 @@
-e .
pytest
flaky
trio
click
django # used by middleware
sphinx==4.2.0
myst-parser==0.15.1
furo==2021.6.18b36
sphinxcontrib-programoutput==0.17
pytest-asyncio==0.12.0 # pinned to an older version due to an incompatibility with flaky
greenlet>=1.1.3
nox
typing_extensions
https://github.com/nyurik/py-ascii-graph/archive/refs/heads/fix-python310.zip # used by a metric
ipython
numpy # used by an example
pre-commit # used to run pre-commit hooks
sphinx-autobuild==2021.3.14
-e .[test,bin,docs,examples,types]
30 changes: 29 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,35 @@
url="https://github.com/joerick/pyinstrument",
keywords=["profiling", "profile", "profiler", "cpu", "time", "sampling"],
install_requires=[],
extras_require={"jupyter": ["ipython"]},
extras_require={
"test": [
"pytest",
"flaky",
"trio",
"greenlet>=3.0.0a1",
"pytest-asyncio==0.12.0", # pinned to an older version due to an incompatibility with flaky
"sphinx-autobuild==2021.3.14",
"ipython",
],
"bin": [
"click",
"nox",
],
"docs": [
"sphinx==4.2.0",
"myst-parser==0.15.1",
"furo==2021.6.18b36",
"sphinxcontrib-programoutput==0.17",
],
"examples": [
"numpy",
"django",
"ascii_graph @ https://github.com/nyurik/py-ascii-graph/archive/refs/heads/fix-python310.zip",
],
"types": [
"typing_extensions",
],
},
include_package_data=True,
python_requires=">=3.7",
entry_points={"console_scripts": ["pyinstrument = pyinstrument.__main__:main"]},
Expand Down
10 changes: 7 additions & 3 deletions test/fake_time_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import contextlib
import functools
import random
from typing import TYPE_CHECKING
from unittest import mock

from trio.testing import MockClock

from pyinstrument import stack_sampler

if TYPE_CHECKING:
from trio.testing import MockClock


class FakeClock:
def __init__(self) -> None:
Expand Down Expand Up @@ -71,7 +73,7 @@ def fake_time_asyncio(loop=None):


class FakeClockTrio:
def __init__(self, clock: MockClock) -> None:
def __init__(self, clock: "MockClock") -> None:
self.trio_clock = clock

def get_time(self):
Expand All @@ -83,6 +85,8 @@ def sleep(self, duration):

@contextlib.contextmanager
def fake_time_trio():
from trio.testing import MockClock

trio_clock = MockClock(autojump_threshold=0)
fake_clock = FakeClockTrio(trio_clock)

Expand Down
24 changes: 24 additions & 0 deletions test/test_cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,27 @@ def test_invocation_machinery_is_trimmed(self, pyinstrument_invocation, tmp_path

assert function_name == "<module>"
assert "busy_wait.py" in location

def test_binary_output(self, pyinstrument_invocation, tmp_path: Path):
busy_wait_py = tmp_path / "busy_wait.py"
busy_wait_py.write_text(BUSY_WAIT_SCRIPT)

output_file = tmp_path / "output.pstats"

subprocess.check_call(
[
*pyinstrument_invocation,
"--renderer=pstats",
f"--outfile={output_file}",
str(busy_wait_py),
],
universal_newlines=True,
)

assert output_file.exists()

# check it can be loaded
import pstats

stats = pstats.Stats(str(output_file))
assert stats
25 changes: 21 additions & 4 deletions test/test_processors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import sys
from test.util import calculate_frame_tree_times

Expand All @@ -21,6 +22,20 @@ def self_time_frame(time):
return Frame(SELF_TIME_FRAME_IDENTIFIER, time=time)


def fixup_windows_paths(frame: Frame):
"""
Deeply fixes windows paths within a frame tree. These tests are written with forward-slashes, but windows uses backslashes
"""
identifier_parts = frame._identifier_parts
if len(identifier_parts) > 1:
identifier_parts[1] = os.path.normpath(identifier_parts[1])
frame._identifier_parts = identifier_parts
frame.identifier = "\x00".join(identifier_parts)

for child in frame.children:
fixup_windows_paths(child)


def test_frame_passthrough_none():
for processor in ALL_PROCESSORS:
assert processor(None, options={}) is None
Expand Down Expand Up @@ -296,7 +311,8 @@ def test_remove_unnecessary_self_time_nodes():
assert strip_newlines_frame.time == 0.2


def test_group_library_frames_processor():
def test_group_library_frames_processor(monkeypatch):
monkeypatch.syspath_prepend("env/lib/python3.6")
frame = Frame(
identifier_or_frame_info="<module>\x00cibuildwheel/__init__.py\x0012",
children=[
Expand Down Expand Up @@ -330,6 +346,10 @@ def test_group_library_frames_processor():
),
],
)

if sys.platform.startswith("win"):
fixup_windows_paths(frame)

calculate_frame_tree_times(frame)
frame.self_check()

Expand All @@ -355,7 +375,4 @@ def test_group_library_frames_processor():
assert group_root.children[0].children[0] in group.exit_frames
assert group_root.children[0].children[0].children[0] not in group.frames

old_sys_path = sys.path[:]
sys.path.append("env/lib/python3.6")
assert group.libraries == ["django"]
sys.path[:] = old_sys_path
1 change: 0 additions & 1 deletion test/test_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from typing import Generator, Optional

import pytest
import trio

from pyinstrument import Profiler, renderers
from pyinstrument.frame import Frame
Expand Down
8 changes: 5 additions & 3 deletions test/test_profiler_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@

import greenlet
import pytest
import trio
import trio._core._run
import trio.lowlevel

from pyinstrument import processors, stack_sampler
from pyinstrument.frame import AWAIT_FRAME_IDENTIFIER, OUT_OF_CONTEXT_FRAME_IDENTIFIER, Frame
Expand Down Expand Up @@ -61,6 +58,8 @@ async def test_sleep():


def test_sleep_trio():
import trio

async def run():
profiler = Profiler()
profiler.start()
Expand Down Expand Up @@ -102,6 +101,8 @@ async def async_wait(sync_time, async_time, profile=False, engine="asyncio"):
if engine == "asyncio":
await asyncio.sleep(async_time)
else:
import trio

await trio.sleep(async_time)

time.sleep(sync_time / 2)
Expand All @@ -124,6 +125,7 @@ async def async_wait(sync_time, async_time, profile=False, engine="asyncio"):

profiler_session = profile_task.result()
elif engine == "trio":
import trio

async def async_wait_and_capture(**kwargs):
nonlocal profiler_session
Expand Down
Loading

0 comments on commit 59d59b5

Please sign in to comment.