Skip to content

Conversation

@codeflash-ai
Copy link

@codeflash-ai codeflash-ai bot commented Nov 22, 2025

📄 5% (0.05x) speedup for deselect_by_mark in src/_pytest/mark/__init__.py

⏱️ Runtime : 8.22 milliseconds 7.81 milliseconds (best of 147 runs)

📝 Explanation and details

The optimization achieves a 5% speedup through two key micro-optimizations that reduce overhead in tight loops:

What optimizations were applied:

  1. Method lookup caching in deselect_by_mark: Extracted remaining.append and deselected.append to local variables before the main loop, avoiding repeated attribute lookups on each iteration.

  2. Constant hoisting in Expression.evaluate: Moved the empty {"__builtins__": {}} dictionary to a module-level constant _EMPTY_BUILTINS, eliminating dictionary creation on every evaluation.

Why these optimizations work:

  • Attribute lookup reduction: Python's attribute access (obj.method) involves dictionary lookups that are slower than local variable access. In the main loop that processes thousands of items, this overhead accumulates significantly.

  • Object allocation elimination: Creating the builtins dictionary on every eval() call adds memory allocation overhead. Since the dictionary is always identical and immutable, reusing a constant is more efficient.

Performance impact based on test results:

The optimization shows strongest gains on large-scale workloads (6-8% faster on tests with 1000 items) where the loop overhead dominates. Small test cases show minimal or slightly negative impact due to the added variable assignments, but these are negligible in real-world usage.

Context significance:

Given that deselect_by_mark is called from pytest_collection_modifyitems during test collection, this optimization directly benefits pytest's test discovery phase. Since test collection happens before every test run and can involve hundreds or thousands of test items, even small per-item optimizations provide meaningful cumulative speedups for developer workflow.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 39 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
🌀 Generated Regression Tests and Runtime
# --- Minimal stubs/mocks for pytest internals, since we can't import real pytest internals ---
from typing import Iterator
from typing import Optional

from _pytest.mark.__init__ import deselect_by_mark

# imports
import pytest


# Simulate a minimal Mark object
class Mark:
    def __init__(self, name):
        self.name = name


# Simulate a minimal Item object as used by pytest
class Item:
    def __init__(self, name, marks=None):
        self.name = name
        self._marks = marks or []

    def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]:
        for mark in self._marks:
            if name is None or mark.name == name:
                yield mark

    def __repr__(self):
        return f"<Item {self.name}>"


# Simulate pytest Config and hook
class DummyHook:
    def __init__(self):
        self.deselected = []

    def pytest_deselected(self, items):
        self.deselected.extend(items)


class DummyOption:
    def __init__(self, markexpr=None):
        self.markexpr = markexpr


class DummyConfig:
    def __init__(self, markexpr=None):
        self.option = DummyOption(markexpr)
        self.hook = DummyHook()


# ------------------ UNIT TESTS ------------------


# Helper to create items with given mark names
def make_items(names_and_marks):
    return [Item(name, [Mark(m) for m in marks]) for name, marks in names_and_marks]


# --- 1. BASIC TEST CASES ---


def test_no_markexpr_keeps_all():
    # No markexpr: all items remain
    items = make_items([("a", ["foo"]), ("b", ["bar"]), ("c", [])])
    config = DummyConfig(markexpr=None)
    deselect_by_mark(items, config)  # 460ns -> 508ns (9.45% slower)


def test_single_markexpr_selects_matching():
    # Only items with 'foo' mark remain
    items = make_items(
        [("a", ["foo"]), ("b", ["bar"]), ("c", ["foo", "bar"]), ("d", [])]
    )
    config = DummyConfig(markexpr="foo")
    deselect_by_mark(items, config)  # 44.0μs -> 43.7μs (0.843% faster)


def test_markexpr_and_operator():
    # Only items with both 'foo' and 'bar'
    items = make_items(
        [("a", ["foo"]), ("b", ["bar"]), ("c", ["foo", "bar"]), ("d", [])]
    )
    config = DummyConfig(markexpr="foo and bar")
    deselect_by_mark(items, config)  # 54.9μs -> 55.4μs (0.820% slower)


def test_markexpr_or_operator():
    # Items with 'foo' or 'bar'
    items = make_items(
        [("a", ["foo"]), ("b", ["bar"]), ("c", ["foo", "bar"]), ("d", [])]
    )
    config = DummyConfig(markexpr="foo or bar")
    deselect_by_mark(items, config)  # 52.5μs -> 52.4μs (0.244% faster)


def test_markexpr_not_operator():
    # Items without 'foo'
    items = make_items(
        [("a", ["foo"]), ("b", ["bar"]), ("c", []), ("d", ["foo", "bar"])]
    )
    config = DummyConfig(markexpr="not foo")
    deselect_by_mark(items, config)  # 43.2μs -> 44.0μs (1.78% slower)


def test_markexpr_parentheses():
    # (foo or bar) and not baz
    items = make_items(
        [
            ("a", ["foo"]),
            ("b", ["bar"]),
            ("c", ["baz"]),
            ("d", ["foo", "baz"]),
            ("e", ["bar", "baz"]),
            ("f", ["foo", "bar"]),
            ("g", []),
        ]
    )
    config = DummyConfig(markexpr="(foo or bar) and not baz")
    deselect_by_mark(items, config)  # 76.3μs -> 74.6μs (2.30% faster)


def test_markexpr_true_false_literals():
    # True: all items remain; False: all items deselected
    items = make_items([("a", ["foo"]), ("b", []), ("c", ["bar"])])
    config_true = DummyConfig(markexpr="True")
    items_true = items.copy()
    deselect_by_mark(items_true, config_true)  # 33.0μs -> 33.7μs (2.11% slower)

    config_false = DummyConfig(markexpr="False")
    items_false = items.copy()
    deselect_by_mark(items_false, config_false)  # 19.7μs -> 20.2μs (2.42% slower)


# --- 2. EDGE TEST CASES ---


def test_empty_items_list():
    # No items: nothing happens, no error
    items = []
    config = DummyConfig(markexpr="foo")
    deselect_by_mark(items, config)  # 24.0μs -> 24.6μs (2.56% slower)


def test_items_with_no_marks():
    # All items have no marks, markexpr selects none
    items = make_items([("a", []), ("b", []), ("c", [])])
    config = DummyConfig(markexpr="foo")
    deselect_by_mark(items, config)  # 30.9μs -> 32.0μs (3.40% slower)


def test_items_with_multiple_same_mark():
    # Items with duplicate marks (shouldn't affect selection)
    items = [
        Item("a", [Mark("foo"), Mark("foo")]),
        Item("b", [Mark("bar"), Mark("bar")]),
        Item("c", [Mark("foo"), Mark("bar")]),
    ]
    config = DummyConfig(markexpr="foo and bar")
    deselect_by_mark(items, config)  # 49.9μs -> 51.2μs (2.59% slower)


def test_invalid_markexpr_raises():
    # Invalid expression raises error
    items = make_items([("a", ["foo"])])
    config = DummyConfig(markexpr="foo and or")
    with pytest.raises(Exception) as excinfo:
        deselect_by_mark(items, config)  # 23.7μs -> 24.0μs (1.26% slower)


def test_mark_names_with_true_false():
    # Mark names 'True' and 'False'
    items = make_items([("a", ["True"]), ("b", ["False"]), ("c", ["foo"])])
    config = DummyConfig(markexpr="True or False")
    deselect_by_mark(items, config)  # 63.7μs -> 64.0μs (0.486% slower)


def test_markexpr_with_spaces_and_case():
    # Markexpr with extra spaces and different case
    items = make_items([("a", ["foo"]), ("b", ["bar"]), ("c", ["baz"])])
    config = DummyConfig(markexpr="  foo   or   bar ")
    deselect_by_mark(items, config)  # 52.6μs -> 53.2μs (1.29% slower)


def test_items_with_extra_markers_ignored():
    # Items with extra marks not mentioned in markexpr
    items = make_items([("a", ["foo", "bar", "baz"]), ("b", ["baz"]), ("c", ["foo"])])
    config = DummyConfig(markexpr="foo")
    deselect_by_mark(items, config)  # 34.5μs -> 34.5μs (0.142% slower)


# --- 3. LARGE SCALE TEST CASES ---


def test_large_number_of_items_and_marks():
    # 1000 items, marks cycle through 'foo', 'bar', 'baz'
    names_and_marks = []
    for i in range(1000):
        marks = []
        if i % 3 == 0:
            marks.append("foo")
        if i % 5 == 0:
            marks.append("bar")
        if i % 7 == 0:
            marks.append("baz")
        names_and_marks.append((f"item{i}", marks))
    items = make_items(names_and_marks)
    config = DummyConfig(markexpr="foo or baz")
    deselect_by_mark(items, config)  # 1.10ms -> 1.04ms (6.12% faster)
    # All items with 'foo' or 'baz' remain
    expected = set()
    for i in range(1000):
        if (i % 3 == 0) or (i % 7 == 0):
            expected.add(f"item{i}")
    # All others are deselected
    deselected = set(f"item{i}" for i in range(1000)) - expected


def test_large_items_all_selected():
    # All items have 'foo', all should remain
    items = make_items([(f"item{i}", ["foo"]) for i in range(1000)])
    config = DummyConfig(markexpr="foo")
    deselect_by_mark(items, config)  # 951μs -> 899μs (5.84% faster)


def test_large_items_none_selected():
    # None have 'foo', all should be deselected
    items = make_items([(f"item{i}", ["bar"]) for i in range(1000)])
    config = DummyConfig(markexpr="foo")
    deselect_by_mark(items, config)  # 960μs -> 885μs (8.50% faster)


def test_large_items_half_selected():
    # Half have 'foo', half have 'bar'
    items = make_items(
        [(f"item{i}", ["foo"] if i % 2 == 0 else ["bar"]) for i in range(1000)]
    )
    config = DummyConfig(markexpr="foo")
    deselect_by_mark(items, config)  # 979μs -> 920μs (6.44% faster)


# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
import ast

from _pytest.mark.__init__ import deselect_by_mark


# imports


# --- Minimal stubs and helpers for pytest internals ---


class DummyMark:
    """A minimal stand-in for pytest.mark.structures.Mark."""

    def __init__(self, name):
        self.name = name


class DummyItem:
    """A minimal stand-in for pytest.Item."""

    def __init__(self, name, marks=None):
        self.name = name
        self._marks = marks or []

    def iter_markers(self, name=None):
        """Yield markers, optionally filtered by name."""
        if name is None:
            for m in self._marks:
                yield m
        else:
            for m in self._marks:
                if m.name == name:
                    yield m

    def __repr__(self):
        return f"<DummyItem {self.name}>"


class DummyHook:
    """Dummy hook to capture deselected items."""

    def __init__(self):
        self.deselected_items = []

    def pytest_deselected(self, items):
        self.deselected_items.extend(items)


class DummyConfig:
    """Minimal config object with option.markexpr and hook."""

    def __init__(self, markexpr=None):
        class Option:
            pass

        self.option = Option()
        self.option.markexpr = markexpr
        self.hook = DummyHook()


# --- Expression evaluation mimicking pytest's -m expressions ---


class SimpleExpression:
    """Very simple boolean expression parser for marker names."""

    def __init__(self, expr):
        self.expr = expr
        # Only support 'and', 'or', 'not', parentheses, and marker names
        # Marker names must be valid Python identifiers
        self.ast = ast.parse(expr, mode="eval")

    def evaluate(self, matcher):
        # matcher: callable(str) -> bool
        # Evaluate the expression AST using matcher for names
        def eval_node(node):
            if isinstance(node, ast.Expression):
                return eval_node(node.body)
            elif isinstance(node, ast.BoolOp):
                if isinstance(node.op, ast.And):
                    return all(eval_node(v) for v in node.values)
                elif isinstance(node.op, ast.Or):
                    return any(eval_node(v) for v in node.values)
            elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
                return not eval_node(node.operand)
            elif isinstance(node, ast.Name):
                return matcher(node.id)
            elif isinstance(node, ast.Constant):
                # Allow True/False/None as marker names
                return matcher(str(node.value))
            else:
                raise ValueError("Unsupported expression node: %r" % node)

        return eval_node(self.ast)


# --- Unit Tests ---

# 1. Basic Test Cases


def test_no_markexpr_keeps_all():
    # No markexpr: all items remain
    items = [DummyItem("a", [DummyMark("fast")]), DummyItem("b", [DummyMark("slow")])]
    config = DummyConfig(markexpr=None)
    deselect_by_mark(items, config)  # 504ns -> 568ns (11.3% slower)


def test_simple_mark_match():
    # Only items with 'fast' marker should remain
    items = [DummyItem("a", [DummyMark("fast")]), DummyItem("b", [DummyMark("slow")])]
    config = DummyConfig(markexpr="fast")
    deselect_by_mark(items, config)  # 41.2μs -> 40.9μs (0.596% faster)


def test_simple_mark_no_match():
    # No items have the marker, all should be deselected
    items = [DummyItem("a", [DummyMark("slow")]), DummyItem("b", [DummyMark("slower")])]
    config = DummyConfig(markexpr="fast")
    deselect_by_mark(items, config)  # 35.7μs -> 35.7μs (0.143% slower)


def test_multiple_markers_and():
    # Only items with both 'fast' and 'db' remain
    items = [
        DummyItem("a", [DummyMark("fast"), DummyMark("db")]),
        DummyItem("b", [DummyMark("fast")]),
        DummyItem("c", [DummyMark("db")]),
    ]
    config = DummyConfig(markexpr="fast and db")
    deselect_by_mark(items, config)  # 53.7μs -> 53.6μs (0.203% faster)


def test_multiple_markers_or():
    # Items with 'fast' or 'db' remain
    items = [
        DummyItem("a", [DummyMark("fast")]),
        DummyItem("b", [DummyMark("db")]),
        DummyItem("c", [DummyMark("other")]),
    ]
    config = DummyConfig(markexpr="fast or db")
    deselect_by_mark(items, config)  # 50.6μs -> 50.0μs (1.27% faster)


def test_not_expression():
    # Only items without 'slow' remain
    items = [
        DummyItem("a", [DummyMark("fast")]),
        DummyItem("b", [DummyMark("slow")]),
        DummyItem("c", [DummyMark("other")]),
    ]
    config = DummyConfig(markexpr="not slow")
    deselect_by_mark(items, config)  # 42.6μs -> 43.9μs (3.03% slower)


def test_parentheses_precedence():
    # (fast or slow) and db
    items = [
        DummyItem("a", [DummyMark("fast"), DummyMark("db")]),
        DummyItem("b", [DummyMark("slow"), DummyMark("db")]),
        DummyItem("c", [DummyMark("fast")]),
        DummyItem("d", [DummyMark("db")]),
    ]
    config = DummyConfig(markexpr="(fast or slow) and db")
    deselect_by_mark(items, config)  # 67.6μs -> 66.5μs (1.68% faster)


def test_multiple_markers_on_item():
    # Item with multiple markers matches 'fast or slow'
    items = [
        DummyItem("a", [DummyMark("fast"), DummyMark("slow")]),
        DummyItem("b", [DummyMark("other")]),
    ]
    config = DummyConfig(markexpr="fast or slow")
    deselect_by_mark(items, config)  # 47.9μs -> 48.6μs (1.37% slower)


# 2. Edge Test Cases


def test_empty_items_list():
    # No items: nothing to do
    items = []
    config = DummyConfig(markexpr="fast")
    deselect_by_mark(items, config)  # 26.0μs -> 26.4μs (1.55% slower)


def test_items_with_no_markers():
    # Items with no markers should not match any markexpr
    items = [DummyItem("a"), DummyItem("b")]
    config = DummyConfig(markexpr="fast")
    deselect_by_mark(items, config)  # 32.6μs -> 32.2μs (1.08% faster)


def test_markexpr_true_false_none():
    # Markers named 'True', 'False', 'None' (legal as marker names)
    items = [
        DummyItem("a", [DummyMark("True")]),
        DummyItem("b", [DummyMark("False")]),
        DummyItem("c", [DummyMark("None")]),
        DummyItem("d", [DummyMark("fast")]),
    ]
    config = DummyConfig(markexpr="True or False or None")
    deselect_by_mark(items, config)  # 63.7μs -> 63.6μs (0.101% faster)


def test_markexpr_with_spaces_and_case():
    # Marker names are case sensitive, and extra spaces are ignored
    items = [DummyItem("a", [DummyMark("fast")]), DummyItem("b", [DummyMark("FAST")])]
    config = DummyConfig(markexpr=" fast ")
    deselect_by_mark(items, config)  # 33.2μs -> 33.7μs (1.49% slower)


def test_markexpr_empty_string():
    # Empty string disables deselection (all items remain)
    items = [DummyItem("a", [DummyMark("fast")]), DummyItem("b", [DummyMark("slow")])]
    config = DummyConfig(markexpr="")
    deselect_by_mark(items, config)  # 636ns -> 659ns (3.49% slower)


def test_item_with_duplicate_markers():
    # Item with duplicate marker names; should still match
    items = [
        DummyItem("a", [DummyMark("fast"), DummyMark("fast")]),
        DummyItem("b", [DummyMark("slow")]),
    ]
    config = DummyConfig(markexpr="fast")
    deselect_by_mark(items, config)  # 45.5μs -> 45.3μs (0.499% faster)


def test_item_with_non_string_marker_name():
    # Marker names should be strings; non-string marker names are ignored
    class WeirdMark:
        def __init__(self, name):
            self.name = name

    items = [DummyItem("a", [WeirdMark(123)]), DummyItem("b", [DummyMark("fast")])]
    config = DummyConfig(markexpr="fast")
    deselect_by_mark(items, config)  # 36.6μs -> 37.0μs (0.979% slower)


# 3. Large Scale Test Cases


def test_large_number_of_items_and_markers():
    # 1000 items, half with 'fast', half with 'slow'
    items = []
    for i in range(500):
        items.append(DummyItem(f"fast_{i}", [DummyMark("fast")]))
    for i in range(500):
        items.append(DummyItem(f"slow_{i}", [DummyMark("slow")]))
    config = DummyConfig(markexpr="fast")
    deselect_by_mark(items, config)  # 956μs -> 899μs (6.39% faster)


def test_large_number_of_markers_per_item():
    # Each item has 100 markers, only one matches
    markers = [DummyMark(f"m{i}") for i in range(99)] + [DummyMark("target")]
    items = [
        DummyItem("a", markers.copy()),
        DummyItem("b", [DummyMark(f"m{i}") for i in range(100)]),
    ]
    config = DummyConfig(markexpr="target")
    deselect_by_mark(items, config)  # 56.5μs -> 57.6μs (1.86% slower)


def test_large_complex_expression():
    # 100 items, alternating 'a' and 'b', use a complex expression
    items = []
    for i in range(100):
        if i % 2 == 0:
            items.append(DummyItem(f"a_{i}", [DummyMark("a")]))
        else:
            items.append(DummyItem(f"b_{i}", [DummyMark("b")]))
    # Expression: (a or b) and not (a and b)
    config = DummyConfig(markexpr="(a or b) and not (a and b)")
    deselect_by_mark(items, config)  # 206μs -> 201μs (2.45% faster)


def test_large_scale_no_matches():
    # 1000 items, none match the markexpr
    items = [DummyItem(f"x_{i}", [DummyMark("x")]) for i in range(1000)]
    config = DummyConfig(markexpr="y")
    deselect_by_mark(items, config)  # 930μs -> 856μs (8.66% faster)


def test_large_scale_all_match():
    # 1000 items, all match the markexpr
    items = [DummyItem(f"z_{i}", [DummyMark("z")]) for i in range(1000)]
    config = DummyConfig(markexpr="z")
    deselect_by_mark(items, config)  # 896μs -> 861μs (4.06% faster)


# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

To edit these changes git checkout codeflash/optimize-deselect_by_mark-mi9x51dg and push.

Codeflash Static Badge

The optimization achieves a **5% speedup** through two key micro-optimizations that reduce overhead in tight loops:

**What optimizations were applied:**

1. **Method lookup caching in `deselect_by_mark`**: Extracted `remaining.append` and `deselected.append` to local variables before the main loop, avoiding repeated attribute lookups on each iteration.

2. **Constant hoisting in `Expression.evaluate`**: Moved the empty `{"__builtins__": {}}` dictionary to a module-level constant `_EMPTY_BUILTINS`, eliminating dictionary creation on every evaluation.

**Why these optimizations work:**

- **Attribute lookup reduction**: Python's attribute access (`obj.method`) involves dictionary lookups that are slower than local variable access. In the main loop that processes thousands of items, this overhead accumulates significantly.

- **Object allocation elimination**: Creating the builtins dictionary on every `eval()` call adds memory allocation overhead. Since the dictionary is always identical and immutable, reusing a constant is more efficient.

**Performance impact based on test results:**

The optimization shows **strongest gains on large-scale workloads** (6-8% faster on tests with 1000 items) where the loop overhead dominates. Small test cases show minimal or slightly negative impact due to the added variable assignments, but these are negligible in real-world usage.

**Context significance:**

Given that `deselect_by_mark` is called from `pytest_collection_modifyitems` during test collection, this optimization directly benefits pytest's test discovery phase. Since test collection happens before every test run and can involve hundreds or thousands of test items, even small per-item optimizations provide meaningful cumulative speedups for developer workflow.
@codeflash-ai codeflash-ai bot requested a review from mashraf-222 November 22, 2025 06:39
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Nov 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant