@wendellresearch-ai/benchwarmers 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +147 -147
- package/BENCHMARKS.md +262 -262
- package/GATEWAY.md +272 -272
- package/NOTICE +28 -28
- package/PRIVACY.md +46 -46
- package/README.md +368 -314
- package/SECURITY.md +83 -83
- package/TERMS.md +22 -22
- package/USAGE.md +170 -163
- package/VERSION +1 -1
- package/bin/benchwarmers.js +16 -74
- package/cli.py +666 -0
- package/examples/config/gateway.json +5 -5
- package/examples/config/model-roles.json +11 -11
- package/gateway.py +755 -755
- package/launcher.py +120 -0
- package/mcp_server.py +266 -266
- package/metrics/alerts.py +116 -116
- package/metrics/aliases.py +41 -41
- package/metrics/bench_collections.py +141 -141
- package/metrics/catalog.py +295 -295
- package/metrics/client.py +394 -394
- package/metrics/cliproxy.py +191 -191
- package/metrics/connect.py +691 -691
- package/metrics/cost.py +152 -152
- package/metrics/decide.py +227 -227
- package/metrics/discover.py +318 -318
- package/metrics/endpoints.py +436 -436
- package/metrics/guard.py +617 -592
- package/metrics/learn.py +221 -221
- package/metrics/litellm_hub.py +206 -206
- package/metrics/model_roles.py +84 -84
- package/metrics/offload.py +166 -166
- package/metrics/owui.py +205 -205
- package/metrics/paths.py +60 -60
- package/metrics/route.py +579 -579
- package/metrics/scheduler.py +275 -275
- package/metrics/sources.py +167 -167
- package/metrics/wrong.py +144 -144
- package/package.json +68 -65
- package/perf/README.md +89 -89
- package/perf/run.py +261 -261
- package/scripts/auto_retire.py +367 -367
- package/scripts/check.py +108 -106
- package/scripts/check_deprecations.py +378 -378
- package/scripts/check_themes.py +103 -103
- package/scripts/init.py +124 -124
- package/scripts/list_served.py +78 -78
- package/scripts/notice.py +78 -78
- package/scripts/onboard.py +401 -331
- package/scripts/provider_tags.py +129 -129
- package/scripts/reprefix.py +136 -136
- package/scripts/sync_config.py +105 -105
- package/scripts/test_auth.py +255 -255
- package/scripts/test_cli.py +189 -0
- package/scripts/test_client.py +233 -233
- package/scripts/test_connect.py +222 -222
- package/scripts/test_first_run.py +180 -180
- package/scripts/test_hubs.py +328 -328
- package/scripts/test_launcher.py +67 -0
- package/scripts/test_llm_log.py +85 -81
- package/scripts/test_mcp.py +178 -178
- package/scripts/test_merged.py +214 -214
- package/scripts/test_no_owui.py +291 -291
- package/scripts/test_owui.py +90 -90
- package/scripts/test_serve_edit.py +448 -438
- package/scripts/test_verdict.py +98 -98
- package/scripts/test_vision.py +228 -228
- package/serve.py +3904 -3764
- package/suite/mine.py +154 -154
- package/suite/pace.py +145 -145
- package/suite/run.py +1046 -1042
- package/suite/seed.py +421 -421
- package/suite/tasks/claim-alias-unbound/fixtures/onboard.py +286 -286
- package/suite/tasks/claim-error-text-string/fixtures/decide.py +187 -187
- package/suite/tasks/claim-list-body/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-member-nominated-twice/fixtures/gateway.py +284 -284
- package/suite/tasks/claim-nominee-uses-slot/fixtures/gateway.py +284 -284
- package/suite/tasks/claim-silent-exit/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-sse-hang/fixtures/gateway.py +235 -235
- package/suite/tasks/claim-unknown-nominee-crash/fixtures/gateway.py +284 -284
- package/suite/tasks/rebench-matchms-809-bug/task.json +23 -23
- package/suite/tasks/rebench-matchms-809-control/task.json +27 -27
- package/suite/tasks/rebench-more-itertools-1028-bug/task.json +23 -23
- package/suite/tasks/rebench-more-itertools-1028-control/task.json +27 -27
- package/suite/tasks/rebench-narwhals-2864-bug/task.json +23 -23
- package/suite/tasks/rebench-narwhals-2864-control/task.json +27 -27
- package/suite/tasks/rebench-quantecon-py-769-bug/task.json +23 -23
- package/suite/tasks/rebench-quantecon-py-769-control/task.json +27 -27
- package/suite/tasks/rebench-scrapy-6867-bug/task.json +23 -23
- package/suite/tasks/rebench-scrapy-6867-control/task.json +27 -27
- package/ui/bench.css +430 -425
- package/ui/bench.js +1756 -1576
- package/ui/themes/dark.css +35 -35
- package/ui/themes/hardwood.css +52 -52
- package/ui/themes/rainforest.css +32 -32
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"shape": "rebench",
|
|
3
|
-
"source": "narwhals-dev__narwhals-2864",
|
|
4
|
-
"upstream": "narwhals-dev/narwhals",
|
|
5
|
-
"source_url": "https://github.com/narwhals-dev/narwhals/pull/2864",
|
|
6
|
-
"created": "2025-07-21 08:49:06",
|
|
7
|
-
"license": "MIT",
|
|
8
|
-
"score_in": "first_line",
|
|
9
|
-
"min_answer_chars": 8,
|
|
10
|
-
"id": "rebench-narwhals-2864-control",
|
|
11
|
-
"title": "Real-bug control: say NO FAULT",
|
|
12
|
-
"why": "narwhals/_arrow/series.py after the fix in https://github.com/narwhals-dev/narwhals/pull/2864; the paired bug task is the file before it. The answer is NO FAULT.",
|
|
13
|
-
"prompt": "The Python file below is from a public project. Either it contains a bug that a later commit to the project fixed, or it is the version after that fix. Lines are numbered as in the file (an excerpt: lines 271-668).\n\nYour first line must be exactly `FAULT: line <n>`, naming a line of the bug (if the fix only added code, the line just before or after where it belongs), or `NO FAULT` if this is the fixed version. Then explain in at most three sentences. Plain ASCII.\n\n271| return self._with_native(pc.power(other, ser))\n272| \n273| def __floordiv__(self, other: Any) -> Self:\n274| ser, other = extract_native(self, other)\n275| return self._with_native(floordiv_compat(ser, other))\n276| \n277| def __rfloordiv__(self, other: Any) -> Self:\n278| ser, other = extract_native(self, other)\n279| return self._with_native(floordiv_compat(other, ser))\n280| \n281| def __truediv__(self, other: Any) -> Self:\n282| ser, other = extract_native(self, other)\n283| return self._with_native(pc.divide(*cast_for_truediv(ser, other))) # type: ignore[type-var]\n284| \n285| def __rtruediv__(self, other: Any) -> Self:\n286| ser, other = extract_native(self, other)\n287| return self._with_native(pc.divide(*cast_for_truediv(other, ser))) # type: ignore[type-var]\n288| \n289| def __mod__(self, other: Any) -> Self:\n290| floor_div = (self // other).native\n291| ser, other = extract_native(self, other)\n292| res = pc.subtract(ser, pc.multiply(floor_div, other))\n293| return self._with_native(res)\n294| \n295| def __rmod__(self, other: Any) -> Self:\n296| floor_div = (other // self).native\n297| ser, other = extract_native(self, other)\n298| res = pc.subtract(other, pc.multiply(floor_div, ser))\n299| return self._with_native(res)\n300| \n301| def __invert__(self) -> Self:\n302| return self._with_native(pc.invert(self.native))\n303| \n304| @property\n305| def _type(self) -> pa.DataType:\n306| return self.native.type\n307| \n308| def len(self, *, _return_py_scalar: bool = True) -> int:\n309| return maybe_extract_py_scalar(len(self.native), _return_py_scalar)\n310| \n311| def filter(self, predicate: ArrowSeries | list[bool | None]) -> Self:\n312| other_native: Any\n313| if not is_list_of(predicate, bool):\n314| _, other_native = extract_native(self, predicate)\n315| else:\n316| other_native = predicate\n317| return self._with_native(self.native.filter(other_native))\n318| \n319| def mean(self, *, _return_py_scalar: bool = True) -> float:\n320| return maybe_extract_py_scalar(pc.mean(self.native), _return_py_scalar)\n321| \n322| def median(self, *, _return_py_scalar: bool = True) -> float:\n323| from narwhals.exceptions import InvalidOperationError\n324| \n325| if not self.dtype.is_numeric():\n326| msg = \"`median` operation not supported for non-numeric input type.\"\n327| raise InvalidOperationError(msg)\n328| \n329| return maybe_extract_py_scalar(\n330| pc.approximate_median(self.native), _return_py_scalar\n331| )\n332| \n333| def min(self, *, _return_py_scalar: bool = True) -> Any:\n334| return maybe_extract_py_scalar(pc.min(self.native), _return_py_scalar)\n335| \n336| def max(self, *, _return_py_scalar: bool = True) -> Any:\n337| return maybe_extract_py_scalar(pc.max(self.native), _return_py_scalar)\n338| \n339| def arg_min(self, *, _return_py_scalar: bool = True) -> int:\n340| index_min = pc.index(self.native, pc.min(self.native))\n341| return maybe_extract_py_scalar(index_min, _return_py_scalar)\n342| \n343| def arg_max(self, *, _return_py_scalar: bool = True) -> int:\n344| index_max = pc.index(self.native, pc.max(self.native))\n345| return maybe_extract_py_scalar(index_max, _return_py_scalar)\n346| \n347| def sum(self, *, _return_py_scalar: bool = True) -> float:\n348| return maybe_extract_py_scalar(\n349| pc.sum(self.native, min_count=0), _return_py_scalar\n350| )\n351| \n352| def drop_nulls(self) -> Self:\n353| return self._with_native(self.native.drop_null())\n354| \n355| def shift(self, n: int) -> Self:\n356| if n > 0:\n357| arrays = [nulls_like(n, self), *self.native[:-n].chunks]\n358| elif n < 0:\n359| arrays = [*self.native[-n:].chunks, nulls_like(-n, self)]\n360| else:\n361| return self._with_native(self.native)\n362| return self._with_native(pa.concat_arrays(arrays))\n363| \n364| def std(self, ddof: int, *, _return_py_scalar: bool = True) -> float:\n365| return maybe_extract_py_scalar(\n366| pc.stddev(self.native, ddof=ddof), _return_py_scalar\n367| )\n368| \n369| def var(self, ddof: int, *, _return_py_scalar: bool = True) -> float:\n370| return maybe_extract_py_scalar(\n371| pc.variance(self.native, ddof=ddof), _return_py_scalar\n372| )\n373| \n374| def skew(self, *, _return_py_scalar: bool = True) -> float | None:\n375| ser_not_null = self.native.drop_null()\n376| if len(ser_not_null) == 0:\n377| return None\n378| elif len(ser_not_null) == 1:\n379| return float(\"nan\")\n380| elif len(ser_not_null) == 2:\n381| return 0.0\n382| else:\n383| m = pc.subtract(ser_not_null, pc.mean(ser_not_null))\n384| m2 = pc.mean(pc.power(m, lit(2)))\n385| m3 = pc.mean(pc.power(m, lit(3)))\n386| biased_population_skewness = pc.divide(m3, pc.power(m2, lit(1.5)))\n387| return maybe_extract_py_scalar(biased_population_skewness, _return_py_scalar)\n388| \n389| def kurtosis(self, *, _return_py_scalar: bool = True) -> float | None:\n390| ser_not_null = self.native.drop_null()\n391| if len(ser_not_null) == 0:\n392| return None\n393| elif len(ser_not_null) == 1:\n394| return float(\"nan\")\n395| else:\n396| m = pc.subtract(ser_not_null, pc.mean(ser_not_null))\n397| m2 = pc.mean(pc.power(m, lit(2)))\n398| m4 = pc.mean(pc.power(m, lit(4)))\n399| k = pc.subtract(pc.divide(m4, pc.power(m2, lit(2))), lit(3))\n400| return maybe_extract_py_scalar(k, _return_py_scalar)\n401| \n402| def count(self, *, _return_py_scalar: bool = True) -> int:\n403| return maybe_extract_py_scalar(pc.count(self.native), _return_py_scalar)\n404| \n405| def n_unique(self, *, _return_py_scalar: bool = True) -> int:\n406| return maybe_extract_py_scalar(\n407| pc.count(self.native.unique(), mode=\"all\"), _return_py_scalar\n408| )\n409| \n410| def __native_namespace__(self) -> ModuleType:\n411| if self._implementation is Implementation.PYARROW:\n412| return self._implementation.to_native_namespace()\n413| \n414| msg = f\"Expected pyarrow, got: {type(self._implementation)}\" # pragma: no cover\n415| raise AssertionError(msg)\n416| \n417| @property\n418| def name(self) -> str:\n419| return self._name\n420| \n421| def _gather(self, rows: SizedMultiIndexSelector[ChunkedArrayAny]) -> Self:\n422| if len(rows) == 0:\n423| return self._with_native(self.native.slice(0, 0))\n424| if self._backend_version < (18,) and isinstance(rows, tuple):\n425| rows = list(rows)\n426| return self._with_native(self.native.take(rows))\n427| \n428| def _gather_slice(self, rows: _SliceIndex | range) -> Self:\n429| start = rows.start or 0\n430| stop = rows.stop if rows.stop is not None else len(self.native)\n431| if start < 0:\n432| start = len(self.native) + start\n433| if stop < 0:\n434| stop = len(self.native) + stop\n435| if rows.step is not None and rows.step != 1:\n436| msg = \"Slicing with step is not supported on PyArrow tables\"\n437| raise NotImplementedError(msg)\n438| return self._with_native(self.native.slice(start, stop - start))\n439| \n440| def scatter(self, indices: int | Sequence[int], values: Any) -> Self:\n441| import numpy as np # ignore-banned-import\n442| \n443| values_native: ArrayAny\n444| if isinstance(indices, int):\n445| indices_native = pa.array([indices])\n446| values_native = pa.array([values])\n447| else:\n448| # TODO(unassigned): we may also want to let `indices` be a Series.\n449| # https://github.com/narwhals-dev/narwhals/issues/2155\n450| indices_native = pa.array(indices)\n451| if isinstance(values, self.__class__):\n452| values_native = values.native.combine_chunks()\n453| else:\n454| # NOTE: Requires fixes in https://github.com/zen-xu/pyarrow-stubs/pull/209\n455| pa_array: Incomplete = pa.array\n456| values_native = pa_array(values)\n457| \n458| sorting_indices = pc.sort_indices(indices_native)\n459| indices_native = indices_native.take(sorting_indices)\n460| values_native = values_native.take(sorting_indices)\n461| \n462| mask: _1DArray = np.zeros(self.len(), dtype=bool)\n463| mask[indices_native] = True\n464| # NOTE: Multiple issues\n465| # - Missing `values` type\n466| # - `mask` accepts a `np.ndarray`, but not mentioned in stubs\n467| # - Missing `replacements` type\n468| # - Missing return type\n469| pc_replace_with_mask: Incomplete = pc.replace_with_mask\n470| return self._with_native(pc_replace_with_mask(self.native, mask, values_native))\n471| \n472| def to_list(self) -> list[Any]:\n473| return self.native.to_pylist()\n474| \n475| def __array__(self, dtype: Any = None, *, copy: bool | None = None) -> _1DArray:\n476| return self.native.__array__(dtype=dtype, copy=copy)\n477| \n478| def to_numpy(self, dtype: Any = None, *, copy: bool | None = None) -> _1DArray:\n479| return self.native.to_numpy()\n480| \n481| def alias(self, name: str) -> Self:\n482| result = self.__class__(self.native, name=name, version=self._version)\n483| result._broadcast = self._broadcast\n484| return result\n485| \n486| @property\n487| def dtype(self) -> DType:\n488| return native_to_narwhals_dtype(self.native.type, self._version)\n489| \n490| def abs(self) -> Self:\n491| return self._with_native(pc.abs(self.native))\n492| \n493| def cum_sum(self, *, reverse: bool) -> Self:\n494| cum_sum = pc.cumulative_sum\n495| result = (\n496| cum_sum(self.native, skip_nulls=True)\n497| if not reverse\n498| else cum_sum(self.native[::-1], skip_nulls=True)[::-1]\n499| )\n500| return self._with_native(result)\n501| \n502| def round(self, decimals: int) -> Self:\n503| return self._with_native(\n504| pc.round(self.native, decimals, round_mode=\"half_towards_infinity\")\n505| )\n506| \n507| def diff(self) -> Self:\n508| return self._with_native(pc.pairwise_diff(self.native.combine_chunks()))\n509| \n510| def any(self, *, _return_py_scalar: bool = True) -> bool:\n511| return maybe_extract_py_scalar(\n512| pc.any(self.native, min_count=0), _return_py_scalar\n513| )\n514| \n515| def all(self, *, _return_py_scalar: bool = True) -> bool:\n516| return maybe_extract_py_scalar(\n517| pc.all(self.native, min_count=0), _return_py_scalar\n518| )\n519| \n520| def is_between(\n521| self, lower_bound: Any, upper_bound: Any, closed: ClosedInterval\n522| ) -> Self:\n523| _, lower_bound = extract_native(self, lower_bound)\n524| _, upper_bound = extract_native(self, upper_bound)\n525| if closed == \"left\":\n526| ge = pc.greater_equal(self.native, lower_bound)\n527| lt = pc.less(self.native, upper_bound)\n528| res = pc.and_kleene(ge, lt)\n529| elif closed == \"right\":\n530| gt = pc.greater(self.native, lower_bound)\n531| le = pc.less_equal(self.native, upper_bound)\n532| res = pc.and_kleene(gt, le)\n533| elif closed == \"none\":\n534| gt = pc.greater(self.native, lower_bound)\n535| lt = pc.less(self.native, upper_bound)\n536| res = pc.and_kleene(gt, lt)\n537| elif closed == \"both\":\n538| ge = pc.greater_equal(self.native, lower_bound)\n539| le = pc.less_equal(self.native, upper_bound)\n540| res = pc.and_kleene(ge, le)\n541| else:\n542| assert_never(closed)\n543| return self._with_native(res)\n544| \n545| def is_null(self) -> Self:\n546| return self._with_native(self.native.is_null(), preserve_broadcast=True)\n547| \n548| def is_nan(self) -> Self:\n549| return self._with_native(pc.is_nan(self.native), preserve_broadcast=True)\n550| \n551| def cast(self, dtype: IntoDType) -> Self:\n552| data_type = narwhals_to_native_dtype(dtype, self._version)\n553| return self._with_native(pc.cast(self.native, data_type), preserve_broadcast=True)\n554| \n555| def null_count(self, *, _return_py_scalar: bool = True) -> int:\n556| return maybe_extract_py_scalar(self.native.null_count, _return_py_scalar)\n557| \n558| def head(self, n: int) -> Self:\n559| if n >= 0:\n560| return self._with_native(self.native.slice(0, n))\n561| else:\n562| num_rows = len(self)\n563| return self._with_native(self.native.slice(0, max(0, num_rows + n)))\n564| \n565| def tail(self, n: int) -> Self:\n566| if n >= 0:\n567| num_rows = len(self)\n568| return self._with_native(self.native.slice(max(0, num_rows - n)))\n569| else:\n570| return self._with_native(self.native.slice(abs(n)))\n571| \n572| def is_in(self, other: Any) -> Self:\n573| if self._is_native(other):\n574| value_set: ArrayOrChunkedArray = other\n575| else:\n576| value_set = pa.array(other)\n577| return self._with_native(pc.is_in(self.native, value_set=value_set))\n578| \n579| def arg_true(self) -> Self:\n580| import numpy as np # ignore-banned-import\n581| \n582| res = np.flatnonzero(self.native)\n583| return self.from_iterable(res, name=self.name, context=self)\n584| \n585| def item(self, index: int | None = None) -> Any:\n586| if index is None:\n587| if len(self) != 1:\n588| msg = (\n589| \"can only call '.item()' if the Series is of length 1,\"\n590| f\" or an explicit index is provided (Series is of length {len(self)})\"\n591| )\n592| raise ValueError(msg)\n593| return maybe_extract_py_scalar(self.native[0], return_py_scalar=True)\n594| return maybe_extract_py_scalar(self.native[index], return_py_scalar=True)\n595| \n596| def value_counts(\n597| self, *, sort: bool, parallel: bool, name: str | None, normalize: bool\n598| ) -> ArrowDataFrame:\n599| \"\"\"Parallel is unused, exists for compatibility.\"\"\"\n600| from narwhals._arrow.dataframe import ArrowDataFrame\n601| \n602| index_name_ = \"index\" if self._name is None else self._name\n603| value_name_ = name or (\"proportion\" if normalize else \"count\")\n604| \n605| val_counts = pc.value_counts(self.native)\n606| values = val_counts.field(\"values\")\n607| counts = cast(\"ChunkedArrayAny\", val_counts.field(\"counts\"))\n608| \n609| if normalize:\n610| arrays = [values, pc.divide(*cast_for_truediv(counts, pc.sum(counts)))]\n611| else:\n612| arrays = [values, counts]\n613| \n614| val_count = pa.Table.from_arrays(arrays, names=[index_name_, value_name_])\n615| \n616| if sort:\n617| val_count = val_count.sort_by([(value_name_, \"descending\")])\n618| \n619| return ArrowDataFrame(\n620| val_count, version=self._version, validate_column_names=True\n621| )\n622| \n623| def zip_with(self, mask: Self, other: Self) -> Self:\n624| cond = mask.native.combine_chunks()\n625| return self._with_native(pc.if_else(cond, self.native, other.native))\n626| \n627| def sample(\n628| self,\n629| n: int | None,\n630| *,\n631| fraction: float | None,\n632| with_replacement: bool,\n633| seed: int | None,\n634| ) -> Self:\n635| import numpy as np # ignore-banned-import\n636| \n637| num_rows = len(self)\n638| if n is None and fraction is not None:\n639| n = int(num_rows * fraction)\n640| \n641| rng = np.random.default_rng(seed=seed)\n642| idx = np.arange(num_rows)\n643| mask = rng.choice(idx, size=n, replace=with_replacement)\n644| return self._with_native(self.native.take(mask))\n645| \n646| def fill_null(\n647| self,\n648| value: Self | NonNestedLiteral,\n649| strategy: FillNullStrategy | None,\n650| limit: int | None,\n651| ) -> Self:\n652| import numpy as np # ignore-banned-import\n653| \n654| def fill_aux(\n655| arr: ChunkedArrayAny, limit: int, direction: FillNullStrategy | None\n656| ) -> ArrayAny:\n657| # this algorithm first finds the indices of the valid values to fill all the null value positions\n658| # then it calculates the distance of each new index and the original index\n659| # if the distance is equal to or less than the limit and the original value is null, it is replaced\n660| valid_mask = pc.is_valid(arr)\n661| indices = pa.array(np.arange(len(arr)), type=pa.int64())\n662| if direction == \"forward\":\n663| valid_index = np.maximum.accumulate(np.where(valid_mask, indices, -1))\n664| distance = indices - valid_index\n665| else:\n666| valid_index = np.minimum.accumulate(\n667| np.where(valid_mask[::-1], indices[::-1], len(arr))\n668| )[::-1]",
|
|
14
|
-
"must_find": [
|
|
15
|
-
{
|
|
16
|
-
"any_of": [
|
|
17
|
-
"no fault"
|
|
18
|
-
]
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"must_not_claim": [
|
|
22
|
-
{
|
|
23
|
-
"regex": "^\\W*fault:\\s*line",
|
|
24
|
-
"label": "fault: line"
|
|
25
|
-
}
|
|
26
|
-
]
|
|
27
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"shape": "rebench",
|
|
3
|
+
"source": "narwhals-dev__narwhals-2864",
|
|
4
|
+
"upstream": "narwhals-dev/narwhals",
|
|
5
|
+
"source_url": "https://github.com/narwhals-dev/narwhals/pull/2864",
|
|
6
|
+
"created": "2025-07-21 08:49:06",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"score_in": "first_line",
|
|
9
|
+
"min_answer_chars": 8,
|
|
10
|
+
"id": "rebench-narwhals-2864-control",
|
|
11
|
+
"title": "Real-bug control: say NO FAULT",
|
|
12
|
+
"why": "narwhals/_arrow/series.py after the fix in https://github.com/narwhals-dev/narwhals/pull/2864; the paired bug task is the file before it. The answer is NO FAULT.",
|
|
13
|
+
"prompt": "The Python file below is from a public project. Either it contains a bug that a later commit to the project fixed, or it is the version after that fix. Lines are numbered as in the file (an excerpt: lines 271-668).\n\nYour first line must be exactly `FAULT: line <n>`, naming a line of the bug (if the fix only added code, the line just before or after where it belongs), or `NO FAULT` if this is the fixed version. Then explain in at most three sentences. Plain ASCII.\n\n271| return self._with_native(pc.power(other, ser))\n272| \n273| def __floordiv__(self, other: Any) -> Self:\n274| ser, other = extract_native(self, other)\n275| return self._with_native(floordiv_compat(ser, other))\n276| \n277| def __rfloordiv__(self, other: Any) -> Self:\n278| ser, other = extract_native(self, other)\n279| return self._with_native(floordiv_compat(other, ser))\n280| \n281| def __truediv__(self, other: Any) -> Self:\n282| ser, other = extract_native(self, other)\n283| return self._with_native(pc.divide(*cast_for_truediv(ser, other))) # type: ignore[type-var]\n284| \n285| def __rtruediv__(self, other: Any) -> Self:\n286| ser, other = extract_native(self, other)\n287| return self._with_native(pc.divide(*cast_for_truediv(other, ser))) # type: ignore[type-var]\n288| \n289| def __mod__(self, other: Any) -> Self:\n290| floor_div = (self // other).native\n291| ser, other = extract_native(self, other)\n292| res = pc.subtract(ser, pc.multiply(floor_div, other))\n293| return self._with_native(res)\n294| \n295| def __rmod__(self, other: Any) -> Self:\n296| floor_div = (other // self).native\n297| ser, other = extract_native(self, other)\n298| res = pc.subtract(other, pc.multiply(floor_div, ser))\n299| return self._with_native(res)\n300| \n301| def __invert__(self) -> Self:\n302| return self._with_native(pc.invert(self.native))\n303| \n304| @property\n305| def _type(self) -> pa.DataType:\n306| return self.native.type\n307| \n308| def len(self, *, _return_py_scalar: bool = True) -> int:\n309| return maybe_extract_py_scalar(len(self.native), _return_py_scalar)\n310| \n311| def filter(self, predicate: ArrowSeries | list[bool | None]) -> Self:\n312| other_native: Any\n313| if not is_list_of(predicate, bool):\n314| _, other_native = extract_native(self, predicate)\n315| else:\n316| other_native = predicate\n317| return self._with_native(self.native.filter(other_native))\n318| \n319| def mean(self, *, _return_py_scalar: bool = True) -> float:\n320| return maybe_extract_py_scalar(pc.mean(self.native), _return_py_scalar)\n321| \n322| def median(self, *, _return_py_scalar: bool = True) -> float:\n323| from narwhals.exceptions import InvalidOperationError\n324| \n325| if not self.dtype.is_numeric():\n326| msg = \"`median` operation not supported for non-numeric input type.\"\n327| raise InvalidOperationError(msg)\n328| \n329| return maybe_extract_py_scalar(\n330| pc.approximate_median(self.native), _return_py_scalar\n331| )\n332| \n333| def min(self, *, _return_py_scalar: bool = True) -> Any:\n334| return maybe_extract_py_scalar(pc.min(self.native), _return_py_scalar)\n335| \n336| def max(self, *, _return_py_scalar: bool = True) -> Any:\n337| return maybe_extract_py_scalar(pc.max(self.native), _return_py_scalar)\n338| \n339| def arg_min(self, *, _return_py_scalar: bool = True) -> int:\n340| index_min = pc.index(self.native, pc.min(self.native))\n341| return maybe_extract_py_scalar(index_min, _return_py_scalar)\n342| \n343| def arg_max(self, *, _return_py_scalar: bool = True) -> int:\n344| index_max = pc.index(self.native, pc.max(self.native))\n345| return maybe_extract_py_scalar(index_max, _return_py_scalar)\n346| \n347| def sum(self, *, _return_py_scalar: bool = True) -> float:\n348| return maybe_extract_py_scalar(\n349| pc.sum(self.native, min_count=0), _return_py_scalar\n350| )\n351| \n352| def drop_nulls(self) -> Self:\n353| return self._with_native(self.native.drop_null())\n354| \n355| def shift(self, n: int) -> Self:\n356| if n > 0:\n357| arrays = [nulls_like(n, self), *self.native[:-n].chunks]\n358| elif n < 0:\n359| arrays = [*self.native[-n:].chunks, nulls_like(-n, self)]\n360| else:\n361| return self._with_native(self.native)\n362| return self._with_native(pa.concat_arrays(arrays))\n363| \n364| def std(self, ddof: int, *, _return_py_scalar: bool = True) -> float:\n365| return maybe_extract_py_scalar(\n366| pc.stddev(self.native, ddof=ddof), _return_py_scalar\n367| )\n368| \n369| def var(self, ddof: int, *, _return_py_scalar: bool = True) -> float:\n370| return maybe_extract_py_scalar(\n371| pc.variance(self.native, ddof=ddof), _return_py_scalar\n372| )\n373| \n374| def skew(self, *, _return_py_scalar: bool = True) -> float | None:\n375| ser_not_null = self.native.drop_null()\n376| if len(ser_not_null) == 0:\n377| return None\n378| elif len(ser_not_null) == 1:\n379| return float(\"nan\")\n380| elif len(ser_not_null) == 2:\n381| return 0.0\n382| else:\n383| m = pc.subtract(ser_not_null, pc.mean(ser_not_null))\n384| m2 = pc.mean(pc.power(m, lit(2)))\n385| m3 = pc.mean(pc.power(m, lit(3)))\n386| biased_population_skewness = pc.divide(m3, pc.power(m2, lit(1.5)))\n387| return maybe_extract_py_scalar(biased_population_skewness, _return_py_scalar)\n388| \n389| def kurtosis(self, *, _return_py_scalar: bool = True) -> float | None:\n390| ser_not_null = self.native.drop_null()\n391| if len(ser_not_null) == 0:\n392| return None\n393| elif len(ser_not_null) == 1:\n394| return float(\"nan\")\n395| else:\n396| m = pc.subtract(ser_not_null, pc.mean(ser_not_null))\n397| m2 = pc.mean(pc.power(m, lit(2)))\n398| m4 = pc.mean(pc.power(m, lit(4)))\n399| k = pc.subtract(pc.divide(m4, pc.power(m2, lit(2))), lit(3))\n400| return maybe_extract_py_scalar(k, _return_py_scalar)\n401| \n402| def count(self, *, _return_py_scalar: bool = True) -> int:\n403| return maybe_extract_py_scalar(pc.count(self.native), _return_py_scalar)\n404| \n405| def n_unique(self, *, _return_py_scalar: bool = True) -> int:\n406| return maybe_extract_py_scalar(\n407| pc.count(self.native.unique(), mode=\"all\"), _return_py_scalar\n408| )\n409| \n410| def __native_namespace__(self) -> ModuleType:\n411| if self._implementation is Implementation.PYARROW:\n412| return self._implementation.to_native_namespace()\n413| \n414| msg = f\"Expected pyarrow, got: {type(self._implementation)}\" # pragma: no cover\n415| raise AssertionError(msg)\n416| \n417| @property\n418| def name(self) -> str:\n419| return self._name\n420| \n421| def _gather(self, rows: SizedMultiIndexSelector[ChunkedArrayAny]) -> Self:\n422| if len(rows) == 0:\n423| return self._with_native(self.native.slice(0, 0))\n424| if self._backend_version < (18,) and isinstance(rows, tuple):\n425| rows = list(rows)\n426| return self._with_native(self.native.take(rows))\n427| \n428| def _gather_slice(self, rows: _SliceIndex | range) -> Self:\n429| start = rows.start or 0\n430| stop = rows.stop if rows.stop is not None else len(self.native)\n431| if start < 0:\n432| start = len(self.native) + start\n433| if stop < 0:\n434| stop = len(self.native) + stop\n435| if rows.step is not None and rows.step != 1:\n436| msg = \"Slicing with step is not supported on PyArrow tables\"\n437| raise NotImplementedError(msg)\n438| return self._with_native(self.native.slice(start, stop - start))\n439| \n440| def scatter(self, indices: int | Sequence[int], values: Any) -> Self:\n441| import numpy as np # ignore-banned-import\n442| \n443| values_native: ArrayAny\n444| if isinstance(indices, int):\n445| indices_native = pa.array([indices])\n446| values_native = pa.array([values])\n447| else:\n448| # TODO(unassigned): we may also want to let `indices` be a Series.\n449| # https://github.com/narwhals-dev/narwhals/issues/2155\n450| indices_native = pa.array(indices)\n451| if isinstance(values, self.__class__):\n452| values_native = values.native.combine_chunks()\n453| else:\n454| # NOTE: Requires fixes in https://github.com/zen-xu/pyarrow-stubs/pull/209\n455| pa_array: Incomplete = pa.array\n456| values_native = pa_array(values)\n457| \n458| sorting_indices = pc.sort_indices(indices_native)\n459| indices_native = indices_native.take(sorting_indices)\n460| values_native = values_native.take(sorting_indices)\n461| \n462| mask: _1DArray = np.zeros(self.len(), dtype=bool)\n463| mask[indices_native] = True\n464| # NOTE: Multiple issues\n465| # - Missing `values` type\n466| # - `mask` accepts a `np.ndarray`, but not mentioned in stubs\n467| # - Missing `replacements` type\n468| # - Missing return type\n469| pc_replace_with_mask: Incomplete = pc.replace_with_mask\n470| return self._with_native(pc_replace_with_mask(self.native, mask, values_native))\n471| \n472| def to_list(self) -> list[Any]:\n473| return self.native.to_pylist()\n474| \n475| def __array__(self, dtype: Any = None, *, copy: bool | None = None) -> _1DArray:\n476| return self.native.__array__(dtype=dtype, copy=copy)\n477| \n478| def to_numpy(self, dtype: Any = None, *, copy: bool | None = None) -> _1DArray:\n479| return self.native.to_numpy()\n480| \n481| def alias(self, name: str) -> Self:\n482| result = self.__class__(self.native, name=name, version=self._version)\n483| result._broadcast = self._broadcast\n484| return result\n485| \n486| @property\n487| def dtype(self) -> DType:\n488| return native_to_narwhals_dtype(self.native.type, self._version)\n489| \n490| def abs(self) -> Self:\n491| return self._with_native(pc.abs(self.native))\n492| \n493| def cum_sum(self, *, reverse: bool) -> Self:\n494| cum_sum = pc.cumulative_sum\n495| result = (\n496| cum_sum(self.native, skip_nulls=True)\n497| if not reverse\n498| else cum_sum(self.native[::-1], skip_nulls=True)[::-1]\n499| )\n500| return self._with_native(result)\n501| \n502| def round(self, decimals: int) -> Self:\n503| return self._with_native(\n504| pc.round(self.native, decimals, round_mode=\"half_towards_infinity\")\n505| )\n506| \n507| def diff(self) -> Self:\n508| return self._with_native(pc.pairwise_diff(self.native.combine_chunks()))\n509| \n510| def any(self, *, _return_py_scalar: bool = True) -> bool:\n511| return maybe_extract_py_scalar(\n512| pc.any(self.native, min_count=0), _return_py_scalar\n513| )\n514| \n515| def all(self, *, _return_py_scalar: bool = True) -> bool:\n516| return maybe_extract_py_scalar(\n517| pc.all(self.native, min_count=0), _return_py_scalar\n518| )\n519| \n520| def is_between(\n521| self, lower_bound: Any, upper_bound: Any, closed: ClosedInterval\n522| ) -> Self:\n523| _, lower_bound = extract_native(self, lower_bound)\n524| _, upper_bound = extract_native(self, upper_bound)\n525| if closed == \"left\":\n526| ge = pc.greater_equal(self.native, lower_bound)\n527| lt = pc.less(self.native, upper_bound)\n528| res = pc.and_kleene(ge, lt)\n529| elif closed == \"right\":\n530| gt = pc.greater(self.native, lower_bound)\n531| le = pc.less_equal(self.native, upper_bound)\n532| res = pc.and_kleene(gt, le)\n533| elif closed == \"none\":\n534| gt = pc.greater(self.native, lower_bound)\n535| lt = pc.less(self.native, upper_bound)\n536| res = pc.and_kleene(gt, lt)\n537| elif closed == \"both\":\n538| ge = pc.greater_equal(self.native, lower_bound)\n539| le = pc.less_equal(self.native, upper_bound)\n540| res = pc.and_kleene(ge, le)\n541| else:\n542| assert_never(closed)\n543| return self._with_native(res)\n544| \n545| def is_null(self) -> Self:\n546| return self._with_native(self.native.is_null(), preserve_broadcast=True)\n547| \n548| def is_nan(self) -> Self:\n549| return self._with_native(pc.is_nan(self.native), preserve_broadcast=True)\n550| \n551| def cast(self, dtype: IntoDType) -> Self:\n552| data_type = narwhals_to_native_dtype(dtype, self._version)\n553| return self._with_native(pc.cast(self.native, data_type), preserve_broadcast=True)\n554| \n555| def null_count(self, *, _return_py_scalar: bool = True) -> int:\n556| return maybe_extract_py_scalar(self.native.null_count, _return_py_scalar)\n557| \n558| def head(self, n: int) -> Self:\n559| if n >= 0:\n560| return self._with_native(self.native.slice(0, n))\n561| else:\n562| num_rows = len(self)\n563| return self._with_native(self.native.slice(0, max(0, num_rows + n)))\n564| \n565| def tail(self, n: int) -> Self:\n566| if n >= 0:\n567| num_rows = len(self)\n568| return self._with_native(self.native.slice(max(0, num_rows - n)))\n569| else:\n570| return self._with_native(self.native.slice(abs(n)))\n571| \n572| def is_in(self, other: Any) -> Self:\n573| if self._is_native(other):\n574| value_set: ArrayOrChunkedArray = other\n575| else:\n576| value_set = pa.array(other)\n577| return self._with_native(pc.is_in(self.native, value_set=value_set))\n578| \n579| def arg_true(self) -> Self:\n580| import numpy as np # ignore-banned-import\n581| \n582| res = np.flatnonzero(self.native)\n583| return self.from_iterable(res, name=self.name, context=self)\n584| \n585| def item(self, index: int | None = None) -> Any:\n586| if index is None:\n587| if len(self) != 1:\n588| msg = (\n589| \"can only call '.item()' if the Series is of length 1,\"\n590| f\" or an explicit index is provided (Series is of length {len(self)})\"\n591| )\n592| raise ValueError(msg)\n593| return maybe_extract_py_scalar(self.native[0], return_py_scalar=True)\n594| return maybe_extract_py_scalar(self.native[index], return_py_scalar=True)\n595| \n596| def value_counts(\n597| self, *, sort: bool, parallel: bool, name: str | None, normalize: bool\n598| ) -> ArrowDataFrame:\n599| \"\"\"Parallel is unused, exists for compatibility.\"\"\"\n600| from narwhals._arrow.dataframe import ArrowDataFrame\n601| \n602| index_name_ = \"index\" if self._name is None else self._name\n603| value_name_ = name or (\"proportion\" if normalize else \"count\")\n604| \n605| val_counts = pc.value_counts(self.native)\n606| values = val_counts.field(\"values\")\n607| counts = cast(\"ChunkedArrayAny\", val_counts.field(\"counts\"))\n608| \n609| if normalize:\n610| arrays = [values, pc.divide(*cast_for_truediv(counts, pc.sum(counts)))]\n611| else:\n612| arrays = [values, counts]\n613| \n614| val_count = pa.Table.from_arrays(arrays, names=[index_name_, value_name_])\n615| \n616| if sort:\n617| val_count = val_count.sort_by([(value_name_, \"descending\")])\n618| \n619| return ArrowDataFrame(\n620| val_count, version=self._version, validate_column_names=True\n621| )\n622| \n623| def zip_with(self, mask: Self, other: Self) -> Self:\n624| cond = mask.native.combine_chunks()\n625| return self._with_native(pc.if_else(cond, self.native, other.native))\n626| \n627| def sample(\n628| self,\n629| n: int | None,\n630| *,\n631| fraction: float | None,\n632| with_replacement: bool,\n633| seed: int | None,\n634| ) -> Self:\n635| import numpy as np # ignore-banned-import\n636| \n637| num_rows = len(self)\n638| if n is None and fraction is not None:\n639| n = int(num_rows * fraction)\n640| \n641| rng = np.random.default_rng(seed=seed)\n642| idx = np.arange(num_rows)\n643| mask = rng.choice(idx, size=n, replace=with_replacement)\n644| return self._with_native(self.native.take(mask))\n645| \n646| def fill_null(\n647| self,\n648| value: Self | NonNestedLiteral,\n649| strategy: FillNullStrategy | None,\n650| limit: int | None,\n651| ) -> Self:\n652| import numpy as np # ignore-banned-import\n653| \n654| def fill_aux(\n655| arr: ChunkedArrayAny, limit: int, direction: FillNullStrategy | None\n656| ) -> ArrayAny:\n657| # this algorithm first finds the indices of the valid values to fill all the null value positions\n658| # then it calculates the distance of each new index and the original index\n659| # if the distance is equal to or less than the limit and the original value is null, it is replaced\n660| valid_mask = pc.is_valid(arr)\n661| indices = pa.array(np.arange(len(arr)), type=pa.int64())\n662| if direction == \"forward\":\n663| valid_index = np.maximum.accumulate(np.where(valid_mask, indices, -1))\n664| distance = indices - valid_index\n665| else:\n666| valid_index = np.minimum.accumulate(\n667| np.where(valid_mask[::-1], indices[::-1], len(arr))\n668| )[::-1]",
|
|
14
|
+
"must_find": [
|
|
15
|
+
{
|
|
16
|
+
"any_of": [
|
|
17
|
+
"no fault"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"must_not_claim": [
|
|
22
|
+
{
|
|
23
|
+
"regex": "^\\W*fault:\\s*line",
|
|
24
|
+
"label": "fault: line"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
{
|
|
2
|
-
"shape": "rebench",
|
|
3
|
-
"source": "quantecon__quantecon.py-769",
|
|
4
|
-
"upstream": "QuantEcon/QuantEcon.py",
|
|
5
|
-
"source_url": "https://github.com/QuantEcon/QuantEcon.py/pull/769",
|
|
6
|
-
"created": "2025-06-22 13:14:57",
|
|
7
|
-
"license": "MIT",
|
|
8
|
-
"score_in": "first_line",
|
|
9
|
-
"min_answer_chars": 8,
|
|
10
|
-
"id": "rebench-quantecon-py-769-bug",
|
|
11
|
-
"title": "Real bug: name the line a later fix changed",
|
|
12
|
-
"why": "QuantEcon/QuantEcon.py PR #769, created 2025-06-22 13:14:57: https://github.com/QuantEcon/QuantEcon.py/pull/769\nThe fix in quantecon/optimize/lcp_lemke.py (base 34f4a47a46) changed line(s) 153:\n - ratio = ratio_min\n + ratio_min = ratio\nProblem: BUG: lcp_lemke Bug found by an instance in some benchmark script generated by ChatGPT: ```python import numpy as np from quantecon.optimize import lcp_lemke n = 10 seed = 0 rng = np.random.RandomState(seed) A = rng.standard_normal((n, n)) M = A.T @ A + np.eye(n) q = rng.standard_normal(n) res = lcp_\nCheck the fix is the bug, and visible from this file alone, before removing draft.",
|
|
13
|
-
"prompt": "The Python file below is from a public project. Either it contains a bug that a later commit to the project fixed, or it is the version after that fix. Lines are numbered as in the file.\n\nYour first line must be exactly `FAULT: line <n>`, naming a line of the bug (if the fix only added code, the line just before or after where it belongs), or `NO FAULT` if this is the fixed version. Then explain in at most three sentences. Plain ASCII.\n\n 1| \"\"\"\n 2| Contain a Numba-jitted linear complementarity problem (LCP) solver based\n 3| on Lemke's algorithm.\n 4| \n 5| \"\"\"\n 6| from collections import namedtuple\n 7| import numpy as np\n 8| from numba import jit\n 9| from .pivoting import _pivoting, _lex_min_ratio_test\n 10| from .linprog_simplex import PivOptions, FEA_TOL, TOL_PIV, TOL_RATIO_DIFF\n 11| \n 12| \n 13| LCPResult = namedtuple(\n 14| 'LCPResult', ['z', 'success', 'status', 'num_iter']\n 15| )\n 16| LCPResult.__doc__ = \\\n 17| 'namedtuple containing the result from `lcp_lemke`.'\n 18| \n 19| \n 20| # Delete useless docstring for fields of namedtuple\n 21| for field in LCPResult._fields:\n 22| getattr(LCPResult, field).__doc__ = ''\n 23| \n 24| \n 25| @jit(nopython=True, cache=True)\n 26| def lcp_lemke(M, q, d=None, max_iter=10**6, piv_options=PivOptions(),\n 27| tableau=None, basis=None, z=None):\n 28| \"\"\"\n 29| Solve the linear complementarity problem::\n 30| \n 31| z >= 0\n 32| M @ z + q >= 0\n 33| z @ (M @ z + q) = 0\n 34| \n 35| by Lemke's algorithm (with the lexicographic pivoting rule).\n 36| \n 37| Parameters\n 38| ----------\n 39| M : ndarray(float, ndim=2)\n 40| ndarray of shape (n, n).\n 41| \n 42| q : ndarray(float, ndim=1)\n 43| ndarray of shape (n,).\n 44| \n 45| d : ndarray(float, ndim=1), optional\n 46| Covering vector, ndarray of shape (n,). Must be strictly\n 47| positive. If None, default to vector of ones.\n 48| \n 49| max_iter : int, optional(default=10**6)\n 50| Maximum number of iteration to perform.\n 51| \n 52| piv_options : PivOptions, optional\n 53| PivOptions namedtuple to set the following tolerance values:\n 54| \n 55| fea_tol : float\n 56| Feasibility tolerance (default={FEA_TOL}). (Not used in\n 57| this function.)\n 58| \n 59| tol_piv : float\n 60| Pivot tolerance (default={TOL_PIV}).\n 61| \n 62| tol_ratio_diff : float\n 63| Tolerance used in the the lexicographic pivoting\n 64| (default={TOL_RATIO_DIFF}).\n 65| \n 66| tableau : ndarray(float, ndim=2), optional\n 67| Temporary ndarray of shape (n, 2*n+2) to store the tableau.\n 68| Modified in place.\n 69| \n 70| basis : ndarray(int, ndim=1), optional\n 71| Temporary ndarray of shape (n,) to store the basic variables.\n 72| Modified in place.\n 73| \n 74| z : ndarray(float, ndim=1), optional\n 75| Output ndarray of shape (n,) to store the solution.\n 76| \n 77| Returns\n 78| -------\n 79| res : LCPResult\n 80| namedtuple consisting of the fields:\n 81| \n 82| z : ndarray(float, ndim=1)\n 83| ndarray of shape (n,) containing the solution.\n 84| \n 85| success : bool\n 86| True if the algorithm succeeded in finding a solution.\n 87| \n 88| status : int\n 89| An integer representing the exit status of the result::\n 90| \n 91| 0 : Solution found successfully\n 92| 1 : Iteration limit reached\n 93| 2 : Secondary ray termination\n 94| \n 95| num_iter : int\n 96| The number of iterations performed.\n 97| \n 98| Examples\n 99| --------\n100| >>> M = np.array([[1, 0, 0], [2, 1, 0], [2, 2, 1]])\n101| >>> q = np.array([-8, -12, -14])\n102| >>> res = lcp_lemke(M, q)\n103| >>> res.success\n104| True\n105| >>> res.z\n106| array([8., 0., 0.])\n107| >>> w = M @ res.z + q\n108| >>> w\n109| array([0., 4., 2.])\n110| >>> res.z @ w\n111| 0.0\n112| \n113| References\n114| ----------\n115| * K. G. Murty, Linear Complementarity, Linear and Nonlinear\n116| Programming, 1988.\n117| \n118| \"\"\"\n119| n = M.shape[0]\n120| \n121| success = False\n122| status = 1\n123| num_iter = 0\n124| \n125| if z is None:\n126| z = np.empty(n)\n127| \n128| if (q >= 0).all(): # Trivial case\n129| z[:] = 0\n130| success = True\n131| status = 0\n132| return LCPResult(z, success, status, num_iter)\n133| \n134| if d is None:\n135| d = np.ones(n)\n136| if tableau is None:\n137| tableau = np.empty((n, 2*n+2))\n138| if basis is None:\n139| basis = np.empty(n, dtype=np.int_)\n140| \n141| _initialize_tableau(M, q, d, tableau, basis)\n142| \n143| art_var = 2*n # Artificial variable\n144| pivcol = art_var\n145| \n146| # Equivalent to lex_min_ratio_test\n147| pivrow = 0\n148| ratio_min = q[0] / d[0]\n149| for i in range(1, n):\n150| ratio = q[i] / d[i]\n151| if ratio <= ratio_min + piv_options.tol_ratio_diff:\n152| pivrow = i\n153| ratio = ratio_min\n154| \n155| _pivoting(tableau, pivcol, pivrow)\n156| basis[pivrow], pivcol = pivcol, pivrow + n\n157| num_iter += 1\n158| \n159| # Array to store row indices in lex_min_ratio_test\n160| argmins = np.empty(n, dtype=np.int_)\n161| \n162| while num_iter < max_iter:\n163| pivrow_found, pivrow = _lex_min_ratio_test(\n164| tableau, pivcol, 0, argmins,\n165| piv_options.tol_piv, piv_options.tol_ratio_diff\n166| )\n167| \n168| if not pivrow_found: # Ray termination\n169| success = False\n170| status = 2\n171| break\n172| \n173| _pivoting(tableau, pivcol, pivrow)\n174| basis[pivrow], leaving_var = pivcol, basis[pivrow]\n175| num_iter += 1\n176| \n177| if leaving_var == art_var: # Solution found\n178| success = True\n179| status = 0\n180| break\n181| elif leaving_var < n:\n182| pivcol = leaving_var + n\n183| else:\n184| pivcol = leaving_var - n\n185| \n186| _get_solution(tableau, basis, z)\n187| \n188| return LCPResult(z, success, status, num_iter)\n189| \n190| \n191| lcp_lemke.__doc__ = lcp_lemke.__doc__.format(\n192| FEA_TOL=FEA_TOL, TOL_PIV=TOL_PIV, TOL_RATIO_DIFF=TOL_RATIO_DIFF\n193| )\n194| \n195| \n196| @jit(nopython=True, cache=True)\n197| def _initialize_tableau(M, q, d, tableau, basis):\n198| \"\"\"\n199| Initialize the `tableau` and `basis` arrays in place.\n200| \n201| With covering vector d and artificial variable z0, the LCP is\n202| written as\n203| \n204| q = w - M z - d z0\n205| \n206| where the variables are ordered as (w, z, z0). Thus,\n207| `tableaus[:, :n]` stores I, `tableaus[:, n:2*n]` stores -M,\n208| `tableaus[:, 2*n]` stores -d, and `tableaus[:, -1]` stores q, while\n209| `basis` stores 0, ..., n-1 (variables w).\n210| \n211| Parameters\n212| ----------\n213| M : ndarray(float, ndim=2)\n214| ndarray of shape (n, n).\n215| \n216| q : ndarray(float, ndim=1)\n217| ndarray of shape (n,).\n218| \n219| d : ndarray(float, ndim=1)\n220| ndarray of shape (n,).\n221| \n222| tableau : ndarray(float, ndim=2)\n223| Empty ndarray of shape (n, 2*n+2) to store the tableau.\n224| Modified in place.\n225| \n226| basis : ndarray(int, ndim=1)\n227| Empty ndarray of shape (n,) to store the basic variables.\n228| Modified in place.\n229| \n230| Returns\n231| -------\n232| tableau : ndarray(float, ndim=2)\n233| View to `tableau`.\n234| \n235| basis : ndarray(int, ndim=1)\n236| View to `basis`.\n237| \n238| \"\"\"\n239| n = M.shape[0]\n240| \n241| tableau[:n, :n] = 0\n242| for i in range(n):\n243| tableau[i, i] = 1\n244| \n245| for i in range(n):\n246| for j in range(n):\n247| tableau[i, n+j] = -M[i, j]\n248| \n249| for i in range(n):\n250| tableau[i, 2*n] = -d[i]\n251| \n252| for i in range(n):\n253| tableau[i, -1] = q[i]\n254| \n255| for i in range(n):\n256| basis[i] = i\n257| \n258| return tableau, basis\n259| \n260| \n261| @jit(nopython=True, cache=True)\n262| def _get_solution(tableau, basis, z):\n263| \"\"\"\n264| Fetch the solution from `tableau` and `basis`.\n265| \n266| Parameters\n267| ----------\n268| tableau : ndarray(float, ndim=2)\n269| ndarray of shape (n, 2*n+2) containing the terminal tableau.\n270| \n271| basis : ndarray(int, ndim=1)\n272| ndarray of shape (n,) containing the terminal basis.\n273| \n274| z : ndarray(float, ndim=1)\n275| Empty ndarray of shape (n,) to store the solution. Modified in\n276| place.\n277| \n278| Returns\n279| -------\n280| z : ndarray(float, ndim=1)\n281| View to `z`.\n282| \n283| \"\"\"\n284| n = z.size\n285| \n286| z[:] = 0\n287| for i in range(n):\n288| if n <= basis[i] < 2*n:\n289| z[basis[i]-n] = tableau[i, -1]\n290| \n291| return z",
|
|
14
|
-
"must_find": [
|
|
15
|
-
{
|
|
16
|
-
"regex": "^\\W*fault:\\s*line\\s*(?:153)\\b",
|
|
17
|
-
"label": "fault: line 153"
|
|
18
|
-
}
|
|
19
|
-
],
|
|
20
|
-
"must_not_claim": [
|
|
21
|
-
"no fault"
|
|
22
|
-
]
|
|
23
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"shape": "rebench",
|
|
3
|
+
"source": "quantecon__quantecon.py-769",
|
|
4
|
+
"upstream": "QuantEcon/QuantEcon.py",
|
|
5
|
+
"source_url": "https://github.com/QuantEcon/QuantEcon.py/pull/769",
|
|
6
|
+
"created": "2025-06-22 13:14:57",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"score_in": "first_line",
|
|
9
|
+
"min_answer_chars": 8,
|
|
10
|
+
"id": "rebench-quantecon-py-769-bug",
|
|
11
|
+
"title": "Real bug: name the line a later fix changed",
|
|
12
|
+
"why": "QuantEcon/QuantEcon.py PR #769, created 2025-06-22 13:14:57: https://github.com/QuantEcon/QuantEcon.py/pull/769\nThe fix in quantecon/optimize/lcp_lemke.py (base 34f4a47a46) changed line(s) 153:\n - ratio = ratio_min\n + ratio_min = ratio\nProblem: BUG: lcp_lemke Bug found by an instance in some benchmark script generated by ChatGPT: ```python import numpy as np from quantecon.optimize import lcp_lemke n = 10 seed = 0 rng = np.random.RandomState(seed) A = rng.standard_normal((n, n)) M = A.T @ A + np.eye(n) q = rng.standard_normal(n) res = lcp_\nCheck the fix is the bug, and visible from this file alone, before removing draft.",
|
|
13
|
+
"prompt": "The Python file below is from a public project. Either it contains a bug that a later commit to the project fixed, or it is the version after that fix. Lines are numbered as in the file.\n\nYour first line must be exactly `FAULT: line <n>`, naming a line of the bug (if the fix only added code, the line just before or after where it belongs), or `NO FAULT` if this is the fixed version. Then explain in at most three sentences. Plain ASCII.\n\n 1| \"\"\"\n 2| Contain a Numba-jitted linear complementarity problem (LCP) solver based\n 3| on Lemke's algorithm.\n 4| \n 5| \"\"\"\n 6| from collections import namedtuple\n 7| import numpy as np\n 8| from numba import jit\n 9| from .pivoting import _pivoting, _lex_min_ratio_test\n 10| from .linprog_simplex import PivOptions, FEA_TOL, TOL_PIV, TOL_RATIO_DIFF\n 11| \n 12| \n 13| LCPResult = namedtuple(\n 14| 'LCPResult', ['z', 'success', 'status', 'num_iter']\n 15| )\n 16| LCPResult.__doc__ = \\\n 17| 'namedtuple containing the result from `lcp_lemke`.'\n 18| \n 19| \n 20| # Delete useless docstring for fields of namedtuple\n 21| for field in LCPResult._fields:\n 22| getattr(LCPResult, field).__doc__ = ''\n 23| \n 24| \n 25| @jit(nopython=True, cache=True)\n 26| def lcp_lemke(M, q, d=None, max_iter=10**6, piv_options=PivOptions(),\n 27| tableau=None, basis=None, z=None):\n 28| \"\"\"\n 29| Solve the linear complementarity problem::\n 30| \n 31| z >= 0\n 32| M @ z + q >= 0\n 33| z @ (M @ z + q) = 0\n 34| \n 35| by Lemke's algorithm (with the lexicographic pivoting rule).\n 36| \n 37| Parameters\n 38| ----------\n 39| M : ndarray(float, ndim=2)\n 40| ndarray of shape (n, n).\n 41| \n 42| q : ndarray(float, ndim=1)\n 43| ndarray of shape (n,).\n 44| \n 45| d : ndarray(float, ndim=1), optional\n 46| Covering vector, ndarray of shape (n,). Must be strictly\n 47| positive. If None, default to vector of ones.\n 48| \n 49| max_iter : int, optional(default=10**6)\n 50| Maximum number of iteration to perform.\n 51| \n 52| piv_options : PivOptions, optional\n 53| PivOptions namedtuple to set the following tolerance values:\n 54| \n 55| fea_tol : float\n 56| Feasibility tolerance (default={FEA_TOL}). (Not used in\n 57| this function.)\n 58| \n 59| tol_piv : float\n 60| Pivot tolerance (default={TOL_PIV}).\n 61| \n 62| tol_ratio_diff : float\n 63| Tolerance used in the the lexicographic pivoting\n 64| (default={TOL_RATIO_DIFF}).\n 65| \n 66| tableau : ndarray(float, ndim=2), optional\n 67| Temporary ndarray of shape (n, 2*n+2) to store the tableau.\n 68| Modified in place.\n 69| \n 70| basis : ndarray(int, ndim=1), optional\n 71| Temporary ndarray of shape (n,) to store the basic variables.\n 72| Modified in place.\n 73| \n 74| z : ndarray(float, ndim=1), optional\n 75| Output ndarray of shape (n,) to store the solution.\n 76| \n 77| Returns\n 78| -------\n 79| res : LCPResult\n 80| namedtuple consisting of the fields:\n 81| \n 82| z : ndarray(float, ndim=1)\n 83| ndarray of shape (n,) containing the solution.\n 84| \n 85| success : bool\n 86| True if the algorithm succeeded in finding a solution.\n 87| \n 88| status : int\n 89| An integer representing the exit status of the result::\n 90| \n 91| 0 : Solution found successfully\n 92| 1 : Iteration limit reached\n 93| 2 : Secondary ray termination\n 94| \n 95| num_iter : int\n 96| The number of iterations performed.\n 97| \n 98| Examples\n 99| --------\n100| >>> M = np.array([[1, 0, 0], [2, 1, 0], [2, 2, 1]])\n101| >>> q = np.array([-8, -12, -14])\n102| >>> res = lcp_lemke(M, q)\n103| >>> res.success\n104| True\n105| >>> res.z\n106| array([8., 0., 0.])\n107| >>> w = M @ res.z + q\n108| >>> w\n109| array([0., 4., 2.])\n110| >>> res.z @ w\n111| 0.0\n112| \n113| References\n114| ----------\n115| * K. G. Murty, Linear Complementarity, Linear and Nonlinear\n116| Programming, 1988.\n117| \n118| \"\"\"\n119| n = M.shape[0]\n120| \n121| success = False\n122| status = 1\n123| num_iter = 0\n124| \n125| if z is None:\n126| z = np.empty(n)\n127| \n128| if (q >= 0).all(): # Trivial case\n129| z[:] = 0\n130| success = True\n131| status = 0\n132| return LCPResult(z, success, status, num_iter)\n133| \n134| if d is None:\n135| d = np.ones(n)\n136| if tableau is None:\n137| tableau = np.empty((n, 2*n+2))\n138| if basis is None:\n139| basis = np.empty(n, dtype=np.int_)\n140| \n141| _initialize_tableau(M, q, d, tableau, basis)\n142| \n143| art_var = 2*n # Artificial variable\n144| pivcol = art_var\n145| \n146| # Equivalent to lex_min_ratio_test\n147| pivrow = 0\n148| ratio_min = q[0] / d[0]\n149| for i in range(1, n):\n150| ratio = q[i] / d[i]\n151| if ratio <= ratio_min + piv_options.tol_ratio_diff:\n152| pivrow = i\n153| ratio = ratio_min\n154| \n155| _pivoting(tableau, pivcol, pivrow)\n156| basis[pivrow], pivcol = pivcol, pivrow + n\n157| num_iter += 1\n158| \n159| # Array to store row indices in lex_min_ratio_test\n160| argmins = np.empty(n, dtype=np.int_)\n161| \n162| while num_iter < max_iter:\n163| pivrow_found, pivrow = _lex_min_ratio_test(\n164| tableau, pivcol, 0, argmins,\n165| piv_options.tol_piv, piv_options.tol_ratio_diff\n166| )\n167| \n168| if not pivrow_found: # Ray termination\n169| success = False\n170| status = 2\n171| break\n172| \n173| _pivoting(tableau, pivcol, pivrow)\n174| basis[pivrow], leaving_var = pivcol, basis[pivrow]\n175| num_iter += 1\n176| \n177| if leaving_var == art_var: # Solution found\n178| success = True\n179| status = 0\n180| break\n181| elif leaving_var < n:\n182| pivcol = leaving_var + n\n183| else:\n184| pivcol = leaving_var - n\n185| \n186| _get_solution(tableau, basis, z)\n187| \n188| return LCPResult(z, success, status, num_iter)\n189| \n190| \n191| lcp_lemke.__doc__ = lcp_lemke.__doc__.format(\n192| FEA_TOL=FEA_TOL, TOL_PIV=TOL_PIV, TOL_RATIO_DIFF=TOL_RATIO_DIFF\n193| )\n194| \n195| \n196| @jit(nopython=True, cache=True)\n197| def _initialize_tableau(M, q, d, tableau, basis):\n198| \"\"\"\n199| Initialize the `tableau` and `basis` arrays in place.\n200| \n201| With covering vector d and artificial variable z0, the LCP is\n202| written as\n203| \n204| q = w - M z - d z0\n205| \n206| where the variables are ordered as (w, z, z0). Thus,\n207| `tableaus[:, :n]` stores I, `tableaus[:, n:2*n]` stores -M,\n208| `tableaus[:, 2*n]` stores -d, and `tableaus[:, -1]` stores q, while\n209| `basis` stores 0, ..., n-1 (variables w).\n210| \n211| Parameters\n212| ----------\n213| M : ndarray(float, ndim=2)\n214| ndarray of shape (n, n).\n215| \n216| q : ndarray(float, ndim=1)\n217| ndarray of shape (n,).\n218| \n219| d : ndarray(float, ndim=1)\n220| ndarray of shape (n,).\n221| \n222| tableau : ndarray(float, ndim=2)\n223| Empty ndarray of shape (n, 2*n+2) to store the tableau.\n224| Modified in place.\n225| \n226| basis : ndarray(int, ndim=1)\n227| Empty ndarray of shape (n,) to store the basic variables.\n228| Modified in place.\n229| \n230| Returns\n231| -------\n232| tableau : ndarray(float, ndim=2)\n233| View to `tableau`.\n234| \n235| basis : ndarray(int, ndim=1)\n236| View to `basis`.\n237| \n238| \"\"\"\n239| n = M.shape[0]\n240| \n241| tableau[:n, :n] = 0\n242| for i in range(n):\n243| tableau[i, i] = 1\n244| \n245| for i in range(n):\n246| for j in range(n):\n247| tableau[i, n+j] = -M[i, j]\n248| \n249| for i in range(n):\n250| tableau[i, 2*n] = -d[i]\n251| \n252| for i in range(n):\n253| tableau[i, -1] = q[i]\n254| \n255| for i in range(n):\n256| basis[i] = i\n257| \n258| return tableau, basis\n259| \n260| \n261| @jit(nopython=True, cache=True)\n262| def _get_solution(tableau, basis, z):\n263| \"\"\"\n264| Fetch the solution from `tableau` and `basis`.\n265| \n266| Parameters\n267| ----------\n268| tableau : ndarray(float, ndim=2)\n269| ndarray of shape (n, 2*n+2) containing the terminal tableau.\n270| \n271| basis : ndarray(int, ndim=1)\n272| ndarray of shape (n,) containing the terminal basis.\n273| \n274| z : ndarray(float, ndim=1)\n275| Empty ndarray of shape (n,) to store the solution. Modified in\n276| place.\n277| \n278| Returns\n279| -------\n280| z : ndarray(float, ndim=1)\n281| View to `z`.\n282| \n283| \"\"\"\n284| n = z.size\n285| \n286| z[:] = 0\n287| for i in range(n):\n288| if n <= basis[i] < 2*n:\n289| z[basis[i]-n] = tableau[i, -1]\n290| \n291| return z",
|
|
14
|
+
"must_find": [
|
|
15
|
+
{
|
|
16
|
+
"regex": "^\\W*fault:\\s*line\\s*(?:153)\\b",
|
|
17
|
+
"label": "fault: line 153"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"must_not_claim": [
|
|
21
|
+
"no fault"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"shape": "rebench",
|
|
3
|
-
"source": "quantecon__quantecon.py-769",
|
|
4
|
-
"upstream": "QuantEcon/QuantEcon.py",
|
|
5
|
-
"source_url": "https://github.com/QuantEcon/QuantEcon.py/pull/769",
|
|
6
|
-
"created": "2025-06-22 13:14:57",
|
|
7
|
-
"license": "MIT",
|
|
8
|
-
"score_in": "first_line",
|
|
9
|
-
"min_answer_chars": 8,
|
|
10
|
-
"id": "rebench-quantecon-py-769-control",
|
|
11
|
-
"title": "Real-bug control: say NO FAULT",
|
|
12
|
-
"why": "quantecon/optimize/lcp_lemke.py after the fix in https://github.com/QuantEcon/QuantEcon.py/pull/769; the paired bug task is the file before it. The answer is NO FAULT.",
|
|
13
|
-
"prompt": "The Python file below is from a public project. Either it contains a bug that a later commit to the project fixed, or it is the version after that fix. Lines are numbered as in the file.\n\nYour first line must be exactly `FAULT: line <n>`, naming a line of the bug (if the fix only added code, the line just before or after where it belongs), or `NO FAULT` if this is the fixed version. Then explain in at most three sentences. Plain ASCII.\n\n 1| \"\"\"\n 2| Contain a Numba-jitted linear complementarity problem (LCP) solver based\n 3| on Lemke's algorithm.\n 4| \n 5| \"\"\"\n 6| from collections import namedtuple\n 7| import numpy as np\n 8| from numba import jit\n 9| from .pivoting import _pivoting, _lex_min_ratio_test\n 10| from .linprog_simplex import PivOptions, FEA_TOL, TOL_PIV, TOL_RATIO_DIFF\n 11| \n 12| \n 13| LCPResult = namedtuple(\n 14| 'LCPResult', ['z', 'success', 'status', 'num_iter']\n 15| )\n 16| LCPResult.__doc__ = \\\n 17| 'namedtuple containing the result from `lcp_lemke`.'\n 18| \n 19| \n 20| # Delete useless docstring for fields of namedtuple\n 21| for field in LCPResult._fields:\n 22| getattr(LCPResult, field).__doc__ = ''\n 23| \n 24| \n 25| @jit(nopython=True, cache=True)\n 26| def lcp_lemke(M, q, d=None, max_iter=10**6, piv_options=PivOptions(),\n 27| tableau=None, basis=None, z=None):\n 28| \"\"\"\n 29| Solve the linear complementarity problem::\n 30| \n 31| z >= 0\n 32| M @ z + q >= 0\n 33| z @ (M @ z + q) = 0\n 34| \n 35| by Lemke's algorithm (with the lexicographic pivoting rule).\n 36| \n 37| Parameters\n 38| ----------\n 39| M : ndarray(float, ndim=2)\n 40| ndarray of shape (n, n).\n 41| \n 42| q : ndarray(float, ndim=1)\n 43| ndarray of shape (n,).\n 44| \n 45| d : ndarray(float, ndim=1), optional\n 46| Covering vector, ndarray of shape (n,). Must be strictly\n 47| positive. If None, default to vector of ones.\n 48| \n 49| max_iter : int, optional(default=10**6)\n 50| Maximum number of iteration to perform.\n 51| \n 52| piv_options : PivOptions, optional\n 53| PivOptions namedtuple to set the following tolerance values:\n 54| \n 55| fea_tol : float\n 56| Feasibility tolerance (default={FEA_TOL}). (Not used in\n 57| this function.)\n 58| \n 59| tol_piv : float\n 60| Pivot tolerance (default={TOL_PIV}).\n 61| \n 62| tol_ratio_diff : float\n 63| Tolerance used in the the lexicographic pivoting\n 64| (default={TOL_RATIO_DIFF}).\n 65| \n 66| tableau : ndarray(float, ndim=2), optional\n 67| Temporary ndarray of shape (n, 2*n+2) to store the tableau.\n 68| Modified in place.\n 69| \n 70| basis : ndarray(int, ndim=1), optional\n 71| Temporary ndarray of shape (n,) to store the basic variables.\n 72| Modified in place.\n 73| \n 74| z : ndarray(float, ndim=1), optional\n 75| Output ndarray of shape (n,) to store the solution.\n 76| \n 77| Returns\n 78| -------\n 79| res : LCPResult\n 80| namedtuple consisting of the fields:\n 81| \n 82| z : ndarray(float, ndim=1)\n 83| ndarray of shape (n,) containing the solution.\n 84| \n 85| success : bool\n 86| True if the algorithm succeeded in finding a solution.\n 87| \n 88| status : int\n 89| An integer representing the exit status of the result::\n 90| \n 91| 0 : Solution found successfully\n 92| 1 : Iteration limit reached\n 93| 2 : Secondary ray termination\n 94| \n 95| num_iter : int\n 96| The number of iterations performed.\n 97| \n 98| Examples\n 99| --------\n100| >>> M = np.array([[1, 0, 0], [2, 1, 0], [2, 2, 1]])\n101| >>> q = np.array([-8, -12, -14])\n102| >>> res = lcp_lemke(M, q)\n103| >>> res.success\n104| True\n105| >>> res.z\n106| array([8., 0., 0.])\n107| >>> w = M @ res.z + q\n108| >>> w\n109| array([0., 4., 2.])\n110| >>> res.z @ w\n111| 0.0\n112| \n113| References\n114| ----------\n115| * K. G. Murty, Linear Complementarity, Linear and Nonlinear\n116| Programming, 1988.\n117| \n118| \"\"\"\n119| n = M.shape[0]\n120| \n121| success = False\n122| status = 1\n123| num_iter = 0\n124| \n125| if z is None:\n126| z = np.empty(n)\n127| \n128| if (q >= 0).all(): # Trivial case\n129| z[:] = 0\n130| success = True\n131| status = 0\n132| return LCPResult(z, success, status, num_iter)\n133| \n134| if d is None:\n135| d = np.ones(n)\n136| if tableau is None:\n137| tableau = np.empty((n, 2*n+2))\n138| if basis is None:\n139| basis = np.empty(n, dtype=np.int_)\n140| \n141| _initialize_tableau(M, q, d, tableau, basis)\n142| \n143| art_var = 2*n # Artificial variable\n144| pivcol = art_var\n145| \n146| # Equivalent to lex_min_ratio_test\n147| pivrow = 0\n148| ratio_min = q[0] / d[0]\n149| for i in range(1, n):\n150| ratio = q[i] / d[i]\n151| if ratio <= ratio_min + piv_options.tol_ratio_diff:\n152| pivrow = i\n153| ratio_min = ratio\n154| \n155| _pivoting(tableau, pivcol, pivrow)\n156| basis[pivrow], pivcol = pivcol, pivrow + n\n157| num_iter += 1\n158| \n159| # Array to store row indices in lex_min_ratio_test\n160| argmins = np.empty(n, dtype=np.int_)\n161| \n162| while num_iter < max_iter:\n163| pivrow_found, pivrow = _lex_min_ratio_test(\n164| tableau, pivcol, 0, argmins,\n165| piv_options.tol_piv, piv_options.tol_ratio_diff\n166| )\n167| \n168| if not pivrow_found: # Ray termination\n169| success = False\n170| status = 2\n171| break\n172| \n173| _pivoting(tableau, pivcol, pivrow)\n174| basis[pivrow], leaving_var = pivcol, basis[pivrow]\n175| num_iter += 1\n176| \n177| if leaving_var == art_var: # Solution found\n178| success = True\n179| status = 0\n180| break\n181| elif leaving_var < n:\n182| pivcol = leaving_var + n\n183| else:\n184| pivcol = leaving_var - n\n185| \n186| _get_solution(tableau, basis, z)\n187| \n188| return LCPResult(z, success, status, num_iter)\n189| \n190| \n191| lcp_lemke.__doc__ = lcp_lemke.__doc__.format(\n192| FEA_TOL=FEA_TOL, TOL_PIV=TOL_PIV, TOL_RATIO_DIFF=TOL_RATIO_DIFF\n193| )\n194| \n195| \n196| @jit(nopython=True, cache=True)\n197| def _initialize_tableau(M, q, d, tableau, basis):\n198| \"\"\"\n199| Initialize the `tableau` and `basis` arrays in place.\n200| \n201| With covering vector d and artificial variable z0, the LCP is\n202| written as\n203| \n204| q = w - M z - d z0\n205| \n206| where the variables are ordered as (w, z, z0). Thus,\n207| `tableaus[:, :n]` stores I, `tableaus[:, n:2*n]` stores -M,\n208| `tableaus[:, 2*n]` stores -d, and `tableaus[:, -1]` stores q, while\n209| `basis` stores 0, ..., n-1 (variables w).\n210| \n211| Parameters\n212| ----------\n213| M : ndarray(float, ndim=2)\n214| ndarray of shape (n, n).\n215| \n216| q : ndarray(float, ndim=1)\n217| ndarray of shape (n,).\n218| \n219| d : ndarray(float, ndim=1)\n220| ndarray of shape (n,).\n221| \n222| tableau : ndarray(float, ndim=2)\n223| Empty ndarray of shape (n, 2*n+2) to store the tableau.\n224| Modified in place.\n225| \n226| basis : ndarray(int, ndim=1)\n227| Empty ndarray of shape (n,) to store the basic variables.\n228| Modified in place.\n229| \n230| Returns\n231| -------\n232| tableau : ndarray(float, ndim=2)\n233| View to `tableau`.\n234| \n235| basis : ndarray(int, ndim=1)\n236| View to `basis`.\n237| \n238| \"\"\"\n239| n = M.shape[0]\n240| \n241| tableau[:n, :n] = 0\n242| for i in range(n):\n243| tableau[i, i] = 1\n244| \n245| for i in range(n):\n246| for j in range(n):\n247| tableau[i, n+j] = -M[i, j]\n248| \n249| for i in range(n):\n250| tableau[i, 2*n] = -d[i]\n251| \n252| for i in range(n):\n253| tableau[i, -1] = q[i]\n254| \n255| for i in range(n):\n256| basis[i] = i\n257| \n258| return tableau, basis\n259| \n260| \n261| @jit(nopython=True, cache=True)\n262| def _get_solution(tableau, basis, z):\n263| \"\"\"\n264| Fetch the solution from `tableau` and `basis`.\n265| \n266| Parameters\n267| ----------\n268| tableau : ndarray(float, ndim=2)\n269| ndarray of shape (n, 2*n+2) containing the terminal tableau.\n270| \n271| basis : ndarray(int, ndim=1)\n272| ndarray of shape (n,) containing the terminal basis.\n273| \n274| z : ndarray(float, ndim=1)\n275| Empty ndarray of shape (n,) to store the solution. Modified in\n276| place.\n277| \n278| Returns\n279| -------\n280| z : ndarray(float, ndim=1)\n281| View to `z`.\n282| \n283| \"\"\"\n284| n = z.size\n285| \n286| z[:] = 0\n287| for i in range(n):\n288| if n <= basis[i] < 2*n:\n289| z[basis[i]-n] = tableau[i, -1]\n290| \n291| return z",
|
|
14
|
-
"must_find": [
|
|
15
|
-
{
|
|
16
|
-
"any_of": [
|
|
17
|
-
"no fault"
|
|
18
|
-
]
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"must_not_claim": [
|
|
22
|
-
{
|
|
23
|
-
"regex": "^\\W*fault:\\s*line",
|
|
24
|
-
"label": "fault: line"
|
|
25
|
-
}
|
|
26
|
-
]
|
|
27
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"shape": "rebench",
|
|
3
|
+
"source": "quantecon__quantecon.py-769",
|
|
4
|
+
"upstream": "QuantEcon/QuantEcon.py",
|
|
5
|
+
"source_url": "https://github.com/QuantEcon/QuantEcon.py/pull/769",
|
|
6
|
+
"created": "2025-06-22 13:14:57",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"score_in": "first_line",
|
|
9
|
+
"min_answer_chars": 8,
|
|
10
|
+
"id": "rebench-quantecon-py-769-control",
|
|
11
|
+
"title": "Real-bug control: say NO FAULT",
|
|
12
|
+
"why": "quantecon/optimize/lcp_lemke.py after the fix in https://github.com/QuantEcon/QuantEcon.py/pull/769; the paired bug task is the file before it. The answer is NO FAULT.",
|
|
13
|
+
"prompt": "The Python file below is from a public project. Either it contains a bug that a later commit to the project fixed, or it is the version after that fix. Lines are numbered as in the file.\n\nYour first line must be exactly `FAULT: line <n>`, naming a line of the bug (if the fix only added code, the line just before or after where it belongs), or `NO FAULT` if this is the fixed version. Then explain in at most three sentences. Plain ASCII.\n\n 1| \"\"\"\n 2| Contain a Numba-jitted linear complementarity problem (LCP) solver based\n 3| on Lemke's algorithm.\n 4| \n 5| \"\"\"\n 6| from collections import namedtuple\n 7| import numpy as np\n 8| from numba import jit\n 9| from .pivoting import _pivoting, _lex_min_ratio_test\n 10| from .linprog_simplex import PivOptions, FEA_TOL, TOL_PIV, TOL_RATIO_DIFF\n 11| \n 12| \n 13| LCPResult = namedtuple(\n 14| 'LCPResult', ['z', 'success', 'status', 'num_iter']\n 15| )\n 16| LCPResult.__doc__ = \\\n 17| 'namedtuple containing the result from `lcp_lemke`.'\n 18| \n 19| \n 20| # Delete useless docstring for fields of namedtuple\n 21| for field in LCPResult._fields:\n 22| getattr(LCPResult, field).__doc__ = ''\n 23| \n 24| \n 25| @jit(nopython=True, cache=True)\n 26| def lcp_lemke(M, q, d=None, max_iter=10**6, piv_options=PivOptions(),\n 27| tableau=None, basis=None, z=None):\n 28| \"\"\"\n 29| Solve the linear complementarity problem::\n 30| \n 31| z >= 0\n 32| M @ z + q >= 0\n 33| z @ (M @ z + q) = 0\n 34| \n 35| by Lemke's algorithm (with the lexicographic pivoting rule).\n 36| \n 37| Parameters\n 38| ----------\n 39| M : ndarray(float, ndim=2)\n 40| ndarray of shape (n, n).\n 41| \n 42| q : ndarray(float, ndim=1)\n 43| ndarray of shape (n,).\n 44| \n 45| d : ndarray(float, ndim=1), optional\n 46| Covering vector, ndarray of shape (n,). Must be strictly\n 47| positive. If None, default to vector of ones.\n 48| \n 49| max_iter : int, optional(default=10**6)\n 50| Maximum number of iteration to perform.\n 51| \n 52| piv_options : PivOptions, optional\n 53| PivOptions namedtuple to set the following tolerance values:\n 54| \n 55| fea_tol : float\n 56| Feasibility tolerance (default={FEA_TOL}). (Not used in\n 57| this function.)\n 58| \n 59| tol_piv : float\n 60| Pivot tolerance (default={TOL_PIV}).\n 61| \n 62| tol_ratio_diff : float\n 63| Tolerance used in the the lexicographic pivoting\n 64| (default={TOL_RATIO_DIFF}).\n 65| \n 66| tableau : ndarray(float, ndim=2), optional\n 67| Temporary ndarray of shape (n, 2*n+2) to store the tableau.\n 68| Modified in place.\n 69| \n 70| basis : ndarray(int, ndim=1), optional\n 71| Temporary ndarray of shape (n,) to store the basic variables.\n 72| Modified in place.\n 73| \n 74| z : ndarray(float, ndim=1), optional\n 75| Output ndarray of shape (n,) to store the solution.\n 76| \n 77| Returns\n 78| -------\n 79| res : LCPResult\n 80| namedtuple consisting of the fields:\n 81| \n 82| z : ndarray(float, ndim=1)\n 83| ndarray of shape (n,) containing the solution.\n 84| \n 85| success : bool\n 86| True if the algorithm succeeded in finding a solution.\n 87| \n 88| status : int\n 89| An integer representing the exit status of the result::\n 90| \n 91| 0 : Solution found successfully\n 92| 1 : Iteration limit reached\n 93| 2 : Secondary ray termination\n 94| \n 95| num_iter : int\n 96| The number of iterations performed.\n 97| \n 98| Examples\n 99| --------\n100| >>> M = np.array([[1, 0, 0], [2, 1, 0], [2, 2, 1]])\n101| >>> q = np.array([-8, -12, -14])\n102| >>> res = lcp_lemke(M, q)\n103| >>> res.success\n104| True\n105| >>> res.z\n106| array([8., 0., 0.])\n107| >>> w = M @ res.z + q\n108| >>> w\n109| array([0., 4., 2.])\n110| >>> res.z @ w\n111| 0.0\n112| \n113| References\n114| ----------\n115| * K. G. Murty, Linear Complementarity, Linear and Nonlinear\n116| Programming, 1988.\n117| \n118| \"\"\"\n119| n = M.shape[0]\n120| \n121| success = False\n122| status = 1\n123| num_iter = 0\n124| \n125| if z is None:\n126| z = np.empty(n)\n127| \n128| if (q >= 0).all(): # Trivial case\n129| z[:] = 0\n130| success = True\n131| status = 0\n132| return LCPResult(z, success, status, num_iter)\n133| \n134| if d is None:\n135| d = np.ones(n)\n136| if tableau is None:\n137| tableau = np.empty((n, 2*n+2))\n138| if basis is None:\n139| basis = np.empty(n, dtype=np.int_)\n140| \n141| _initialize_tableau(M, q, d, tableau, basis)\n142| \n143| art_var = 2*n # Artificial variable\n144| pivcol = art_var\n145| \n146| # Equivalent to lex_min_ratio_test\n147| pivrow = 0\n148| ratio_min = q[0] / d[0]\n149| for i in range(1, n):\n150| ratio = q[i] / d[i]\n151| if ratio <= ratio_min + piv_options.tol_ratio_diff:\n152| pivrow = i\n153| ratio_min = ratio\n154| \n155| _pivoting(tableau, pivcol, pivrow)\n156| basis[pivrow], pivcol = pivcol, pivrow + n\n157| num_iter += 1\n158| \n159| # Array to store row indices in lex_min_ratio_test\n160| argmins = np.empty(n, dtype=np.int_)\n161| \n162| while num_iter < max_iter:\n163| pivrow_found, pivrow = _lex_min_ratio_test(\n164| tableau, pivcol, 0, argmins,\n165| piv_options.tol_piv, piv_options.tol_ratio_diff\n166| )\n167| \n168| if not pivrow_found: # Ray termination\n169| success = False\n170| status = 2\n171| break\n172| \n173| _pivoting(tableau, pivcol, pivrow)\n174| basis[pivrow], leaving_var = pivcol, basis[pivrow]\n175| num_iter += 1\n176| \n177| if leaving_var == art_var: # Solution found\n178| success = True\n179| status = 0\n180| break\n181| elif leaving_var < n:\n182| pivcol = leaving_var + n\n183| else:\n184| pivcol = leaving_var - n\n185| \n186| _get_solution(tableau, basis, z)\n187| \n188| return LCPResult(z, success, status, num_iter)\n189| \n190| \n191| lcp_lemke.__doc__ = lcp_lemke.__doc__.format(\n192| FEA_TOL=FEA_TOL, TOL_PIV=TOL_PIV, TOL_RATIO_DIFF=TOL_RATIO_DIFF\n193| )\n194| \n195| \n196| @jit(nopython=True, cache=True)\n197| def _initialize_tableau(M, q, d, tableau, basis):\n198| \"\"\"\n199| Initialize the `tableau` and `basis` arrays in place.\n200| \n201| With covering vector d and artificial variable z0, the LCP is\n202| written as\n203| \n204| q = w - M z - d z0\n205| \n206| where the variables are ordered as (w, z, z0). Thus,\n207| `tableaus[:, :n]` stores I, `tableaus[:, n:2*n]` stores -M,\n208| `tableaus[:, 2*n]` stores -d, and `tableaus[:, -1]` stores q, while\n209| `basis` stores 0, ..., n-1 (variables w).\n210| \n211| Parameters\n212| ----------\n213| M : ndarray(float, ndim=2)\n214| ndarray of shape (n, n).\n215| \n216| q : ndarray(float, ndim=1)\n217| ndarray of shape (n,).\n218| \n219| d : ndarray(float, ndim=1)\n220| ndarray of shape (n,).\n221| \n222| tableau : ndarray(float, ndim=2)\n223| Empty ndarray of shape (n, 2*n+2) to store the tableau.\n224| Modified in place.\n225| \n226| basis : ndarray(int, ndim=1)\n227| Empty ndarray of shape (n,) to store the basic variables.\n228| Modified in place.\n229| \n230| Returns\n231| -------\n232| tableau : ndarray(float, ndim=2)\n233| View to `tableau`.\n234| \n235| basis : ndarray(int, ndim=1)\n236| View to `basis`.\n237| \n238| \"\"\"\n239| n = M.shape[0]\n240| \n241| tableau[:n, :n] = 0\n242| for i in range(n):\n243| tableau[i, i] = 1\n244| \n245| for i in range(n):\n246| for j in range(n):\n247| tableau[i, n+j] = -M[i, j]\n248| \n249| for i in range(n):\n250| tableau[i, 2*n] = -d[i]\n251| \n252| for i in range(n):\n253| tableau[i, -1] = q[i]\n254| \n255| for i in range(n):\n256| basis[i] = i\n257| \n258| return tableau, basis\n259| \n260| \n261| @jit(nopython=True, cache=True)\n262| def _get_solution(tableau, basis, z):\n263| \"\"\"\n264| Fetch the solution from `tableau` and `basis`.\n265| \n266| Parameters\n267| ----------\n268| tableau : ndarray(float, ndim=2)\n269| ndarray of shape (n, 2*n+2) containing the terminal tableau.\n270| \n271| basis : ndarray(int, ndim=1)\n272| ndarray of shape (n,) containing the terminal basis.\n273| \n274| z : ndarray(float, ndim=1)\n275| Empty ndarray of shape (n,) to store the solution. Modified in\n276| place.\n277| \n278| Returns\n279| -------\n280| z : ndarray(float, ndim=1)\n281| View to `z`.\n282| \n283| \"\"\"\n284| n = z.size\n285| \n286| z[:] = 0\n287| for i in range(n):\n288| if n <= basis[i] < 2*n:\n289| z[basis[i]-n] = tableau[i, -1]\n290| \n291| return z",
|
|
14
|
+
"must_find": [
|
|
15
|
+
{
|
|
16
|
+
"any_of": [
|
|
17
|
+
"no fault"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"must_not_claim": [
|
|
22
|
+
{
|
|
23
|
+
"regex": "^\\W*fault:\\s*line",
|
|
24
|
+
"label": "fault: line"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
{
|
|
2
|
-
"shape": "rebench",
|
|
3
|
-
"source": "scrapy__scrapy-6867",
|
|
4
|
-
"upstream": "scrapy/scrapy",
|
|
5
|
-
"source_url": "https://github.com/scrapy/scrapy/pull/6867",
|
|
6
|
-
"created": "2025-06-06 10:19:55",
|
|
7
|
-
"license": "BSD-3-Clause",
|
|
8
|
-
"score_in": "first_line",
|
|
9
|
-
"min_answer_chars": 8,
|
|
10
|
-
"id": "rebench-scrapy-6867-bug",
|
|
11
|
-
"title": "Real bug: name the line a later fix changed",
|
|
12
|
-
"why": "scrapy/scrapy PR #6867, created 2025-06-06 10:19:55: https://github.com/scrapy/scrapy/pull/6867\nThe fix in scrapy/core/engine.py (base d602f13e8c) changed line(s) 177:\n - self.downloader.close()\n + if hasattr(self, \"downloader\"):\n + self.downloader.close()\nProblem: ExecuteEngine.close() assumes self.downloader has been defined https://github.com/scrapy/scrapy/blob/8fb8d2c6b8a83181ef6564fbc9afa9cfeea2bd05/scrapy/core/engine.py#L177 However, `self.close()` is called in the `finally` block where `self.downloader` is set, so that may not be always the case. Found \nCheck the fix is the bug, and visible from this file alone, before removing draft.",
|
|
13
|
-
"prompt": "The Python file below is from a public project. Either it contains a bug that a later commit to the project fixed, or it is the version after that fix. Lines are numbered as in the file (an excerpt: lines 1-400).\n\nYour first line must be exactly `FAULT: line <n>`, naming a line of the bug (if the fix only added code, the line just before or after where it belongs), or `NO FAULT` if this is the fixed version. Then explain in at most three sentences. Plain ASCII.\n\n 1| \"\"\"\n 2| This is the Scrapy engine which controls the Scheduler, Downloader and Spider.\n 3| \n 4| For more information see docs/topics/architecture.rst\n 5| \n 6| \"\"\"\n 7| \n 8| from __future__ import annotations\n 9| \n 10| import logging\n 11| from time import time\n 12| from traceback import format_exc\n 13| from typing import TYPE_CHECKING, Any, cast\n 14| \n 15| from twisted.internet.defer import Deferred, inlineCallbacks, succeed\n 16| from twisted.python.failure import Failure\n 17| \n 18| from scrapy import signals\n 19| from scrapy.core.scraper import Scraper\n 20| from scrapy.exceptions import CloseSpider, DontCloseSpider, IgnoreRequest\n 21| from scrapy.http import Request, Response\n 22| from scrapy.utils.asyncio import (\n 23| AsyncioLoopingCall,\n 24| create_looping_call,\n 25| )\n 26| from scrapy.utils.defer import (\n 27| deferred_f_from_coro_f,\n 28| deferred_from_coro,\n 29| maybe_deferred_to_future,\n 30| )\n 31| from scrapy.utils.log import failure_to_exc_info, logformatter_adapter\n 32| from scrapy.utils.misc import build_from_crawler, load_object\n 33| from scrapy.utils.reactor import CallLaterOnce\n 34| \n 35| if TYPE_CHECKING:\n 36| from collections.abc import AsyncIterator, Callable, Generator\n 37| \n 38| from twisted.internet.task import LoopingCall\n 39| \n 40| from scrapy.core.downloader import Downloader\n 41| from scrapy.core.scheduler import BaseScheduler\n 42| from scrapy.crawler import Crawler\n 43| from scrapy.logformatter import LogFormatter\n 44| from scrapy.settings import BaseSettings, Settings\n 45| from scrapy.signalmanager import SignalManager\n 46| from scrapy.spiders import Spider\n 47| \n 48| \n 49| logger = logging.getLogger(__name__)\n 50| \n 51| \n 52| class _Slot:\n 53| def __init__(\n 54| self,\n 55| close_if_idle: bool,\n 56| nextcall: CallLaterOnce[None],\n 57| scheduler: BaseScheduler,\n 58| ) -> None:\n 59| self.closing: Deferred[None] | None = None\n 60| self.inprogress: set[Request] = set()\n 61| self.close_if_idle: bool = close_if_idle\n 62| self.nextcall: CallLaterOnce[None] = nextcall\n 63| self.scheduler: BaseScheduler = scheduler\n 64| self.heartbeat: AsyncioLoopingCall | LoopingCall = create_looping_call(\n 65| nextcall.schedule\n 66| )\n 67| \n 68| def add_request(self, request: Request) -> None:\n 69| self.inprogress.add(request)\n 70| \n 71| def remove_request(self, request: Request) -> None:\n 72| self.inprogress.remove(request)\n 73| self._maybe_fire_closing()\n 74| \n 75| def close(self) -> Deferred[None]:\n 76| self.closing = Deferred()\n 77| self._maybe_fire_closing()\n 78| return self.closing\n 79| \n 80| def _maybe_fire_closing(self) -> None:\n 81| if self.closing is not None and not self.inprogress:\n 82| if self.nextcall:\n 83| self.nextcall.cancel()\n 84| if self.heartbeat.running:\n 85| self.heartbeat.stop()\n 86| self.closing.callback(None)\n 87| \n 88| \n 89| class ExecutionEngine:\n 90| _SLOT_HEARTBEAT_INTERVAL: float = 5.0\n 91| \n 92| def __init__(\n 93| self,\n 94| crawler: Crawler,\n 95| spider_closed_callback: Callable[[Spider], Deferred[None] | None],\n 96| ) -> None:\n 97| self.crawler: Crawler = crawler\n 98| self.settings: Settings = crawler.settings\n 99| self.signals: SignalManager = crawler.signals\n100| assert crawler.logformatter\n101| self.logformatter: LogFormatter = crawler.logformatter\n102| self._slot: _Slot | None = None\n103| self.spider: Spider | None = None\n104| self.running: bool = False\n105| self.paused: bool = False\n106| self._spider_closed_callback: Callable[[Spider], Deferred[None] | None] = (\n107| spider_closed_callback\n108| )\n109| self.start_time: float | None = None\n110| self._start: AsyncIterator[Any] | None = None\n111| downloader_cls: type[Downloader] = load_object(self.settings[\"DOWNLOADER\"])\n112| try:\n113| self.scheduler_cls: type[BaseScheduler] = self._get_scheduler_class(\n114| crawler.settings\n115| )\n116| self.downloader: Downloader = downloader_cls(crawler)\n117| self.scraper: Scraper = Scraper(crawler)\n118| except Exception:\n119| self.close()\n120| raise\n121| \n122| def _get_scheduler_class(self, settings: BaseSettings) -> type[BaseScheduler]:\n123| from scrapy.core.scheduler import BaseScheduler\n124| \n125| scheduler_cls: type[BaseScheduler] = load_object(settings[\"SCHEDULER\"])\n126| if not issubclass(scheduler_cls, BaseScheduler):\n127| raise TypeError(\n128| f\"The provided scheduler class ({settings['SCHEDULER']})\"\n129| \" does not fully implement the scheduler interface\"\n130| )\n131| return scheduler_cls\n132| \n133| def start(self, _start_request_processing=True) -> Deferred[None]:\n134| return deferred_from_coro(self.start_async(_start_request_processing))\n135| \n136| async def start_async(self, _start_request_processing=True) -> None:\n137| if self.running:\n138| raise RuntimeError(\"Engine already running\")\n139| self.start_time = time()\n140| await self.signals.send_catch_log_async(signal=signals.engine_started)\n141| self.running = True\n142| self._closewait: Deferred[None] = Deferred()\n143| if _start_request_processing:\n144| self._start_request_processing()\n145| await maybe_deferred_to_future(self._closewait)\n146| \n147| def stop(self) -> Deferred[None]:\n148| \"\"\"Gracefully stop the execution engine\"\"\"\n149| \n150| @deferred_f_from_coro_f\n151| async def _finish_stopping_engine(_: Any) -> None:\n152| await self.signals.send_catch_log_async(signal=signals.engine_stopped)\n153| self._closewait.callback(None)\n154| \n155| if not self.running:\n156| raise RuntimeError(\"Engine not running\")\n157| \n158| self.running = False\n159| dfd = (\n160| self.close_spider(self.spider, reason=\"shutdown\")\n161| if self.spider is not None\n162| else succeed(None)\n163| )\n164| return dfd.addBoth(_finish_stopping_engine)\n165| \n166| def close(self) -> Deferred[None]:\n167| \"\"\"\n168| Gracefully close the execution engine.\n169| If it has already been started, stop it. In all cases, close the spider and the downloader.\n170| \"\"\"\n171| if self.running:\n172| return self.stop() # will also close spider and downloader\n173| if self.spider is not None:\n174| return self.close_spider(\n175| self.spider, reason=\"shutdown\"\n176| ) # will also close downloader\n177| self.downloader.close()\n178| return succeed(None)\n179| \n180| def pause(self) -> None:\n181| self.paused = True\n182| \n183| def unpause(self) -> None:\n184| self.paused = False\n185| \n186| async def _process_start_next(self):\n187| \"\"\"Processes the next item or request from Spider.start().\n188| \n189| If a request, it is scheduled. If an item, it is sent to item\n190| pipelines.\n191| \"\"\"\n192| try:\n193| item_or_request = await self._start.__anext__()\n194| except StopAsyncIteration:\n195| self._start = None\n196| except Exception as exception:\n197| self._start = None\n198| exception_traceback = format_exc()\n199| logger.error(\n200| f\"Error while reading start items and requests: {exception}.\\n{exception_traceback}\",\n201| exc_info=True,\n202| )\n203| else:\n204| if not self.spider:\n205| return # spider already closed\n206| if isinstance(item_or_request, Request):\n207| self.crawl(item_or_request)\n208| else:\n209| self.scraper.start_itemproc(item_or_request, response=None)\n210| self._slot.nextcall.schedule()\n211| \n212| @deferred_f_from_coro_f\n213| async def _start_request_processing(self) -> None:\n214| \"\"\"Starts consuming Spider.start() output and sending scheduled\n215| requests.\"\"\"\n216| # Starts the processing of scheduled requests, as well as a periodic\n217| # call to that processing method for scenarios where the scheduler\n218| # reports having pending requests but returns none.\n219| assert self._slot is not None # typing\n220| self._slot.nextcall.schedule()\n221| self._slot.heartbeat.start(self._SLOT_HEARTBEAT_INTERVAL)\n222| \n223| while self._start and self.spider:\n224| await self._process_start_next()\n225| if not self.needs_backout():\n226| # Give room for the outcome of self._process_start_next() to be\n227| # processed before continuing with the next iteration.\n228| self._slot.nextcall.schedule()\n229| await self._slot.nextcall.wait()\n230| \n231| def _start_scheduled_requests(self) -> None:\n232| if self._slot is None or self._slot.closing is not None or self.paused:\n233| return\n234| \n235| while not self.needs_backout():\n236| if not self._start_scheduled_request():\n237| break\n238| \n239| if self.spider_is_idle() and self._slot.close_if_idle:\n240| self._spider_idle()\n241| \n242| def needs_backout(self) -> bool:\n243| \"\"\"Returns ``True`` if no more requests can be sent at the moment, or\n244| ``False`` otherwise.\n245| \n246| See :ref:`start-requests-lazy` for an example.\n247| \"\"\"\n248| assert self._slot is not None # typing\n249| assert self.scraper.slot is not None # typing\n250| return (\n251| not self.running\n252| or bool(self._slot.closing)\n253| or self.downloader.needs_backout()\n254| or self.scraper.slot.needs_backout()\n255| )\n256| \n257| def _start_scheduled_request(self) -> bool:\n258| assert self._slot is not None # typing\n259| assert self.spider is not None # typing\n260| \n261| request = self._slot.scheduler.next_request()\n262| if request is None:\n263| self.signals.send_catch_log(signals.scheduler_empty)\n264| return False\n265| \n266| d: Deferred[Response | Request] = self._download(request)\n267| d.addBoth(self._handle_downloader_output, request)\n268| d.addErrback(\n269| lambda f: logger.info(\n270| \"Error while handling downloader output\",\n271| exc_info=failure_to_exc_info(f),\n272| extra={\"spider\": self.spider},\n273| )\n274| )\n275| \n276| def _remove_request(_: Any) -> None:\n277| assert self._slot\n278| self._slot.remove_request(request)\n279| \n280| d2: Deferred[None] = d.addBoth(_remove_request)\n281| d2.addErrback(\n282| lambda f: logger.info(\n283| \"Error while removing request from slot\",\n284| exc_info=failure_to_exc_info(f),\n285| extra={\"spider\": self.spider},\n286| )\n287| )\n288| slot = self._slot\n289| d2.addBoth(lambda _: slot.nextcall.schedule())\n290| d2.addErrback(\n291| lambda f: logger.info(\n292| \"Error while scheduling new request\",\n293| exc_info=failure_to_exc_info(f),\n294| extra={\"spider\": self.spider},\n295| )\n296| )\n297| return True\n298| \n299| @inlineCallbacks\n300| def _handle_downloader_output(\n301| self, result: Request | Response | Failure, request: Request\n302| ) -> Generator[Deferred[Any], Any, None]:\n303| if not isinstance(result, (Request, Response, Failure)):\n304| raise TypeError(\n305| f\"Incorrect type: expected Request, Response or Failure, got {type(result)}: {result!r}\"\n306| )\n307| \n308| # downloader middleware can return requests (for example, redirects)\n309| if isinstance(result, Request):\n310| self.crawl(result)\n311| return\n312| \n313| try:\n314| yield self.scraper.enqueue_scrape(result, request)\n315| except Exception:\n316| assert self.spider is not None\n317| logger.error(\n318| \"Error while enqueuing scrape\",\n319| exc_info=True,\n320| extra={\"spider\": self.spider},\n321| )\n322| \n323| def spider_is_idle(self) -> bool:\n324| if self._slot is None:\n325| raise RuntimeError(\"Engine slot not assigned\")\n326| if not self.scraper.slot.is_idle(): # type: ignore[union-attr]\n327| return False\n328| if self.downloader.active: # downloader has pending requests\n329| return False\n330| if self._start is not None: # not all start requests are handled\n331| return False\n332| return not self._slot.scheduler.has_pending_requests()\n333| \n334| def crawl(self, request: Request) -> None:\n335| \"\"\"Inject the request into the spider <-> downloader pipeline\"\"\"\n336| if self.spider is None:\n337| raise RuntimeError(f\"No open spider to crawl: {request}\")\n338| self._schedule_request(request)\n339| self._slot.nextcall.schedule() # type: ignore[union-attr]\n340| \n341| def _schedule_request(self, request: Request) -> None:\n342| request_scheduled_result = self.signals.send_catch_log(\n343| signals.request_scheduled,\n344| request=request,\n345| spider=self.spider,\n346| dont_log=IgnoreRequest,\n347| )\n348| for handler, result in request_scheduled_result:\n349| if isinstance(result, Failure) and isinstance(result.value, IgnoreRequest):\n350| return\n351| if not self._slot.scheduler.enqueue_request(request): # type: ignore[union-attr]\n352| self.signals.send_catch_log(\n353| signals.request_dropped, request=request, spider=self.spider\n354| )\n355| \n356| @inlineCallbacks\n357| def download(self, request: Request) -> Generator[Deferred[Any], Any, Response]:\n358| \"\"\"Return a Deferred which fires with a Response as result, only downloader middlewares are applied\"\"\"\n359| if self.spider is None:\n360| raise RuntimeError(f\"No open spider to crawl: {request}\")\n361| try:\n362| response_or_request = yield self._download(request)\n363| finally:\n364| assert self._slot is not None\n365| self._slot.remove_request(request)\n366| if isinstance(response_or_request, Request):\n367| return (yield self.download(response_or_request))\n368| return response_or_request\n369| \n370| @inlineCallbacks\n371| def _download(\n372| self, request: Request\n373| ) -> Generator[Deferred[Any], Any, Response | Request]:\n374| assert self._slot is not None # typing\n375| assert self.spider is not None\n376| \n377| self._slot.add_request(request)\n378| try:\n379| result: Response | Request = yield self.downloader.fetch(\n380| request, self.spider\n381| )\n382| if not isinstance(result, (Response, Request)):\n383| raise TypeError(\n384| f\"Incorrect type: expected Response or Request, got {type(result)}: {result!r}\"\n385| )\n386| if isinstance(result, Response):\n387| if result.request is None:\n388| result.request = request\n389| assert self.spider is not None\n390| logkws = self.logformatter.crawled(result.request, result, self.spider)\n391| if logkws is not None:\n392| logger.log(\n393| *logformatter_adapter(logkws), extra={\"spider\": self.spider}\n394| )\n395| self.signals.send_catch_log(\n396| signal=signals.response_received,\n397| response=result,\n398| request=result.request,\n399| spider=self.spider,\n400| )",
|
|
14
|
-
"must_find": [
|
|
15
|
-
{
|
|
16
|
-
"regex": "^\\W*fault:\\s*line\\s*(?:177)\\b",
|
|
17
|
-
"label": "fault: line 177"
|
|
18
|
-
}
|
|
19
|
-
],
|
|
20
|
-
"must_not_claim": [
|
|
21
|
-
"no fault"
|
|
22
|
-
]
|
|
23
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"shape": "rebench",
|
|
3
|
+
"source": "scrapy__scrapy-6867",
|
|
4
|
+
"upstream": "scrapy/scrapy",
|
|
5
|
+
"source_url": "https://github.com/scrapy/scrapy/pull/6867",
|
|
6
|
+
"created": "2025-06-06 10:19:55",
|
|
7
|
+
"license": "BSD-3-Clause",
|
|
8
|
+
"score_in": "first_line",
|
|
9
|
+
"min_answer_chars": 8,
|
|
10
|
+
"id": "rebench-scrapy-6867-bug",
|
|
11
|
+
"title": "Real bug: name the line a later fix changed",
|
|
12
|
+
"why": "scrapy/scrapy PR #6867, created 2025-06-06 10:19:55: https://github.com/scrapy/scrapy/pull/6867\nThe fix in scrapy/core/engine.py (base d602f13e8c) changed line(s) 177:\n - self.downloader.close()\n + if hasattr(self, \"downloader\"):\n + self.downloader.close()\nProblem: ExecuteEngine.close() assumes self.downloader has been defined https://github.com/scrapy/scrapy/blob/8fb8d2c6b8a83181ef6564fbc9afa9cfeea2bd05/scrapy/core/engine.py#L177 However, `self.close()` is called in the `finally` block where `self.downloader` is set, so that may not be always the case. Found \nCheck the fix is the bug, and visible from this file alone, before removing draft.",
|
|
13
|
+
"prompt": "The Python file below is from a public project. Either it contains a bug that a later commit to the project fixed, or it is the version after that fix. Lines are numbered as in the file (an excerpt: lines 1-400).\n\nYour first line must be exactly `FAULT: line <n>`, naming a line of the bug (if the fix only added code, the line just before or after where it belongs), or `NO FAULT` if this is the fixed version. Then explain in at most three sentences. Plain ASCII.\n\n 1| \"\"\"\n 2| This is the Scrapy engine which controls the Scheduler, Downloader and Spider.\n 3| \n 4| For more information see docs/topics/architecture.rst\n 5| \n 6| \"\"\"\n 7| \n 8| from __future__ import annotations\n 9| \n 10| import logging\n 11| from time import time\n 12| from traceback import format_exc\n 13| from typing import TYPE_CHECKING, Any, cast\n 14| \n 15| from twisted.internet.defer import Deferred, inlineCallbacks, succeed\n 16| from twisted.python.failure import Failure\n 17| \n 18| from scrapy import signals\n 19| from scrapy.core.scraper import Scraper\n 20| from scrapy.exceptions import CloseSpider, DontCloseSpider, IgnoreRequest\n 21| from scrapy.http import Request, Response\n 22| from scrapy.utils.asyncio import (\n 23| AsyncioLoopingCall,\n 24| create_looping_call,\n 25| )\n 26| from scrapy.utils.defer import (\n 27| deferred_f_from_coro_f,\n 28| deferred_from_coro,\n 29| maybe_deferred_to_future,\n 30| )\n 31| from scrapy.utils.log import failure_to_exc_info, logformatter_adapter\n 32| from scrapy.utils.misc import build_from_crawler, load_object\n 33| from scrapy.utils.reactor import CallLaterOnce\n 34| \n 35| if TYPE_CHECKING:\n 36| from collections.abc import AsyncIterator, Callable, Generator\n 37| \n 38| from twisted.internet.task import LoopingCall\n 39| \n 40| from scrapy.core.downloader import Downloader\n 41| from scrapy.core.scheduler import BaseScheduler\n 42| from scrapy.crawler import Crawler\n 43| from scrapy.logformatter import LogFormatter\n 44| from scrapy.settings import BaseSettings, Settings\n 45| from scrapy.signalmanager import SignalManager\n 46| from scrapy.spiders import Spider\n 47| \n 48| \n 49| logger = logging.getLogger(__name__)\n 50| \n 51| \n 52| class _Slot:\n 53| def __init__(\n 54| self,\n 55| close_if_idle: bool,\n 56| nextcall: CallLaterOnce[None],\n 57| scheduler: BaseScheduler,\n 58| ) -> None:\n 59| self.closing: Deferred[None] | None = None\n 60| self.inprogress: set[Request] = set()\n 61| self.close_if_idle: bool = close_if_idle\n 62| self.nextcall: CallLaterOnce[None] = nextcall\n 63| self.scheduler: BaseScheduler = scheduler\n 64| self.heartbeat: AsyncioLoopingCall | LoopingCall = create_looping_call(\n 65| nextcall.schedule\n 66| )\n 67| \n 68| def add_request(self, request: Request) -> None:\n 69| self.inprogress.add(request)\n 70| \n 71| def remove_request(self, request: Request) -> None:\n 72| self.inprogress.remove(request)\n 73| self._maybe_fire_closing()\n 74| \n 75| def close(self) -> Deferred[None]:\n 76| self.closing = Deferred()\n 77| self._maybe_fire_closing()\n 78| return self.closing\n 79| \n 80| def _maybe_fire_closing(self) -> None:\n 81| if self.closing is not None and not self.inprogress:\n 82| if self.nextcall:\n 83| self.nextcall.cancel()\n 84| if self.heartbeat.running:\n 85| self.heartbeat.stop()\n 86| self.closing.callback(None)\n 87| \n 88| \n 89| class ExecutionEngine:\n 90| _SLOT_HEARTBEAT_INTERVAL: float = 5.0\n 91| \n 92| def __init__(\n 93| self,\n 94| crawler: Crawler,\n 95| spider_closed_callback: Callable[[Spider], Deferred[None] | None],\n 96| ) -> None:\n 97| self.crawler: Crawler = crawler\n 98| self.settings: Settings = crawler.settings\n 99| self.signals: SignalManager = crawler.signals\n100| assert crawler.logformatter\n101| self.logformatter: LogFormatter = crawler.logformatter\n102| self._slot: _Slot | None = None\n103| self.spider: Spider | None = None\n104| self.running: bool = False\n105| self.paused: bool = False\n106| self._spider_closed_callback: Callable[[Spider], Deferred[None] | None] = (\n107| spider_closed_callback\n108| )\n109| self.start_time: float | None = None\n110| self._start: AsyncIterator[Any] | None = None\n111| downloader_cls: type[Downloader] = load_object(self.settings[\"DOWNLOADER\"])\n112| try:\n113| self.scheduler_cls: type[BaseScheduler] = self._get_scheduler_class(\n114| crawler.settings\n115| )\n116| self.downloader: Downloader = downloader_cls(crawler)\n117| self.scraper: Scraper = Scraper(crawler)\n118| except Exception:\n119| self.close()\n120| raise\n121| \n122| def _get_scheduler_class(self, settings: BaseSettings) -> type[BaseScheduler]:\n123| from scrapy.core.scheduler import BaseScheduler\n124| \n125| scheduler_cls: type[BaseScheduler] = load_object(settings[\"SCHEDULER\"])\n126| if not issubclass(scheduler_cls, BaseScheduler):\n127| raise TypeError(\n128| f\"The provided scheduler class ({settings['SCHEDULER']})\"\n129| \" does not fully implement the scheduler interface\"\n130| )\n131| return scheduler_cls\n132| \n133| def start(self, _start_request_processing=True) -> Deferred[None]:\n134| return deferred_from_coro(self.start_async(_start_request_processing))\n135| \n136| async def start_async(self, _start_request_processing=True) -> None:\n137| if self.running:\n138| raise RuntimeError(\"Engine already running\")\n139| self.start_time = time()\n140| await self.signals.send_catch_log_async(signal=signals.engine_started)\n141| self.running = True\n142| self._closewait: Deferred[None] = Deferred()\n143| if _start_request_processing:\n144| self._start_request_processing()\n145| await maybe_deferred_to_future(self._closewait)\n146| \n147| def stop(self) -> Deferred[None]:\n148| \"\"\"Gracefully stop the execution engine\"\"\"\n149| \n150| @deferred_f_from_coro_f\n151| async def _finish_stopping_engine(_: Any) -> None:\n152| await self.signals.send_catch_log_async(signal=signals.engine_stopped)\n153| self._closewait.callback(None)\n154| \n155| if not self.running:\n156| raise RuntimeError(\"Engine not running\")\n157| \n158| self.running = False\n159| dfd = (\n160| self.close_spider(self.spider, reason=\"shutdown\")\n161| if self.spider is not None\n162| else succeed(None)\n163| )\n164| return dfd.addBoth(_finish_stopping_engine)\n165| \n166| def close(self) -> Deferred[None]:\n167| \"\"\"\n168| Gracefully close the execution engine.\n169| If it has already been started, stop it. In all cases, close the spider and the downloader.\n170| \"\"\"\n171| if self.running:\n172| return self.stop() # will also close spider and downloader\n173| if self.spider is not None:\n174| return self.close_spider(\n175| self.spider, reason=\"shutdown\"\n176| ) # will also close downloader\n177| self.downloader.close()\n178| return succeed(None)\n179| \n180| def pause(self) -> None:\n181| self.paused = True\n182| \n183| def unpause(self) -> None:\n184| self.paused = False\n185| \n186| async def _process_start_next(self):\n187| \"\"\"Processes the next item or request from Spider.start().\n188| \n189| If a request, it is scheduled. If an item, it is sent to item\n190| pipelines.\n191| \"\"\"\n192| try:\n193| item_or_request = await self._start.__anext__()\n194| except StopAsyncIteration:\n195| self._start = None\n196| except Exception as exception:\n197| self._start = None\n198| exception_traceback = format_exc()\n199| logger.error(\n200| f\"Error while reading start items and requests: {exception}.\\n{exception_traceback}\",\n201| exc_info=True,\n202| )\n203| else:\n204| if not self.spider:\n205| return # spider already closed\n206| if isinstance(item_or_request, Request):\n207| self.crawl(item_or_request)\n208| else:\n209| self.scraper.start_itemproc(item_or_request, response=None)\n210| self._slot.nextcall.schedule()\n211| \n212| @deferred_f_from_coro_f\n213| async def _start_request_processing(self) -> None:\n214| \"\"\"Starts consuming Spider.start() output and sending scheduled\n215| requests.\"\"\"\n216| # Starts the processing of scheduled requests, as well as a periodic\n217| # call to that processing method for scenarios where the scheduler\n218| # reports having pending requests but returns none.\n219| assert self._slot is not None # typing\n220| self._slot.nextcall.schedule()\n221| self._slot.heartbeat.start(self._SLOT_HEARTBEAT_INTERVAL)\n222| \n223| while self._start and self.spider:\n224| await self._process_start_next()\n225| if not self.needs_backout():\n226| # Give room for the outcome of self._process_start_next() to be\n227| # processed before continuing with the next iteration.\n228| self._slot.nextcall.schedule()\n229| await self._slot.nextcall.wait()\n230| \n231| def _start_scheduled_requests(self) -> None:\n232| if self._slot is None or self._slot.closing is not None or self.paused:\n233| return\n234| \n235| while not self.needs_backout():\n236| if not self._start_scheduled_request():\n237| break\n238| \n239| if self.spider_is_idle() and self._slot.close_if_idle:\n240| self._spider_idle()\n241| \n242| def needs_backout(self) -> bool:\n243| \"\"\"Returns ``True`` if no more requests can be sent at the moment, or\n244| ``False`` otherwise.\n245| \n246| See :ref:`start-requests-lazy` for an example.\n247| \"\"\"\n248| assert self._slot is not None # typing\n249| assert self.scraper.slot is not None # typing\n250| return (\n251| not self.running\n252| or bool(self._slot.closing)\n253| or self.downloader.needs_backout()\n254| or self.scraper.slot.needs_backout()\n255| )\n256| \n257| def _start_scheduled_request(self) -> bool:\n258| assert self._slot is not None # typing\n259| assert self.spider is not None # typing\n260| \n261| request = self._slot.scheduler.next_request()\n262| if request is None:\n263| self.signals.send_catch_log(signals.scheduler_empty)\n264| return False\n265| \n266| d: Deferred[Response | Request] = self._download(request)\n267| d.addBoth(self._handle_downloader_output, request)\n268| d.addErrback(\n269| lambda f: logger.info(\n270| \"Error while handling downloader output\",\n271| exc_info=failure_to_exc_info(f),\n272| extra={\"spider\": self.spider},\n273| )\n274| )\n275| \n276| def _remove_request(_: Any) -> None:\n277| assert self._slot\n278| self._slot.remove_request(request)\n279| \n280| d2: Deferred[None] = d.addBoth(_remove_request)\n281| d2.addErrback(\n282| lambda f: logger.info(\n283| \"Error while removing request from slot\",\n284| exc_info=failure_to_exc_info(f),\n285| extra={\"spider\": self.spider},\n286| )\n287| )\n288| slot = self._slot\n289| d2.addBoth(lambda _: slot.nextcall.schedule())\n290| d2.addErrback(\n291| lambda f: logger.info(\n292| \"Error while scheduling new request\",\n293| exc_info=failure_to_exc_info(f),\n294| extra={\"spider\": self.spider},\n295| )\n296| )\n297| return True\n298| \n299| @inlineCallbacks\n300| def _handle_downloader_output(\n301| self, result: Request | Response | Failure, request: Request\n302| ) -> Generator[Deferred[Any], Any, None]:\n303| if not isinstance(result, (Request, Response, Failure)):\n304| raise TypeError(\n305| f\"Incorrect type: expected Request, Response or Failure, got {type(result)}: {result!r}\"\n306| )\n307| \n308| # downloader middleware can return requests (for example, redirects)\n309| if isinstance(result, Request):\n310| self.crawl(result)\n311| return\n312| \n313| try:\n314| yield self.scraper.enqueue_scrape(result, request)\n315| except Exception:\n316| assert self.spider is not None\n317| logger.error(\n318| \"Error while enqueuing scrape\",\n319| exc_info=True,\n320| extra={\"spider\": self.spider},\n321| )\n322| \n323| def spider_is_idle(self) -> bool:\n324| if self._slot is None:\n325| raise RuntimeError(\"Engine slot not assigned\")\n326| if not self.scraper.slot.is_idle(): # type: ignore[union-attr]\n327| return False\n328| if self.downloader.active: # downloader has pending requests\n329| return False\n330| if self._start is not None: # not all start requests are handled\n331| return False\n332| return not self._slot.scheduler.has_pending_requests()\n333| \n334| def crawl(self, request: Request) -> None:\n335| \"\"\"Inject the request into the spider <-> downloader pipeline\"\"\"\n336| if self.spider is None:\n337| raise RuntimeError(f\"No open spider to crawl: {request}\")\n338| self._schedule_request(request)\n339| self._slot.nextcall.schedule() # type: ignore[union-attr]\n340| \n341| def _schedule_request(self, request: Request) -> None:\n342| request_scheduled_result = self.signals.send_catch_log(\n343| signals.request_scheduled,\n344| request=request,\n345| spider=self.spider,\n346| dont_log=IgnoreRequest,\n347| )\n348| for handler, result in request_scheduled_result:\n349| if isinstance(result, Failure) and isinstance(result.value, IgnoreRequest):\n350| return\n351| if not self._slot.scheduler.enqueue_request(request): # type: ignore[union-attr]\n352| self.signals.send_catch_log(\n353| signals.request_dropped, request=request, spider=self.spider\n354| )\n355| \n356| @inlineCallbacks\n357| def download(self, request: Request) -> Generator[Deferred[Any], Any, Response]:\n358| \"\"\"Return a Deferred which fires with a Response as result, only downloader middlewares are applied\"\"\"\n359| if self.spider is None:\n360| raise RuntimeError(f\"No open spider to crawl: {request}\")\n361| try:\n362| response_or_request = yield self._download(request)\n363| finally:\n364| assert self._slot is not None\n365| self._slot.remove_request(request)\n366| if isinstance(response_or_request, Request):\n367| return (yield self.download(response_or_request))\n368| return response_or_request\n369| \n370| @inlineCallbacks\n371| def _download(\n372| self, request: Request\n373| ) -> Generator[Deferred[Any], Any, Response | Request]:\n374| assert self._slot is not None # typing\n375| assert self.spider is not None\n376| \n377| self._slot.add_request(request)\n378| try:\n379| result: Response | Request = yield self.downloader.fetch(\n380| request, self.spider\n381| )\n382| if not isinstance(result, (Response, Request)):\n383| raise TypeError(\n384| f\"Incorrect type: expected Response or Request, got {type(result)}: {result!r}\"\n385| )\n386| if isinstance(result, Response):\n387| if result.request is None:\n388| result.request = request\n389| assert self.spider is not None\n390| logkws = self.logformatter.crawled(result.request, result, self.spider)\n391| if logkws is not None:\n392| logger.log(\n393| *logformatter_adapter(logkws), extra={\"spider\": self.spider}\n394| )\n395| self.signals.send_catch_log(\n396| signal=signals.response_received,\n397| response=result,\n398| request=result.request,\n399| spider=self.spider,\n400| )",
|
|
14
|
+
"must_find": [
|
|
15
|
+
{
|
|
16
|
+
"regex": "^\\W*fault:\\s*line\\s*(?:177)\\b",
|
|
17
|
+
"label": "fault: line 177"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"must_not_claim": [
|
|
21
|
+
"no fault"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"shape": "rebench",
|
|
3
|
-
"source": "scrapy__scrapy-6867",
|
|
4
|
-
"upstream": "scrapy/scrapy",
|
|
5
|
-
"source_url": "https://github.com/scrapy/scrapy/pull/6867",
|
|
6
|
-
"created": "2025-06-06 10:19:55",
|
|
7
|
-
"license": "BSD-3-Clause",
|
|
8
|
-
"score_in": "first_line",
|
|
9
|
-
"min_answer_chars": 8,
|
|
10
|
-
"id": "rebench-scrapy-6867-control",
|
|
11
|
-
"title": "Real-bug control: say NO FAULT",
|
|
12
|
-
"why": "scrapy/core/engine.py after the fix in https://github.com/scrapy/scrapy/pull/6867; the paired bug task is the file before it. The answer is NO FAULT.",
|
|
13
|
-
"prompt": "The Python file below is from a public project. Either it contains a bug that a later commit to the project fixed, or it is the version after that fix. Lines are numbered as in the file (an excerpt: lines 1-401).\n\nYour first line must be exactly `FAULT: line <n>`, naming a line of the bug (if the fix only added code, the line just before or after where it belongs), or `NO FAULT` if this is the fixed version. Then explain in at most three sentences. Plain ASCII.\n\n 1| \"\"\"\n 2| This is the Scrapy engine which controls the Scheduler, Downloader and Spider.\n 3| \n 4| For more information see docs/topics/architecture.rst\n 5| \n 6| \"\"\"\n 7| \n 8| from __future__ import annotations\n 9| \n 10| import logging\n 11| from time import time\n 12| from traceback import format_exc\n 13| from typing import TYPE_CHECKING, Any, cast\n 14| \n 15| from twisted.internet.defer import Deferred, inlineCallbacks, succeed\n 16| from twisted.python.failure import Failure\n 17| \n 18| from scrapy import signals\n 19| from scrapy.core.scraper import Scraper\n 20| from scrapy.exceptions import CloseSpider, DontCloseSpider, IgnoreRequest\n 21| from scrapy.http import Request, Response\n 22| from scrapy.utils.asyncio import (\n 23| AsyncioLoopingCall,\n 24| create_looping_call,\n 25| )\n 26| from scrapy.utils.defer import (\n 27| deferred_f_from_coro_f,\n 28| deferred_from_coro,\n 29| maybe_deferred_to_future,\n 30| )\n 31| from scrapy.utils.log import failure_to_exc_info, logformatter_adapter\n 32| from scrapy.utils.misc import build_from_crawler, load_object\n 33| from scrapy.utils.reactor import CallLaterOnce\n 34| \n 35| if TYPE_CHECKING:\n 36| from collections.abc import AsyncIterator, Callable, Generator\n 37| \n 38| from twisted.internet.task import LoopingCall\n 39| \n 40| from scrapy.core.downloader import Downloader\n 41| from scrapy.core.scheduler import BaseScheduler\n 42| from scrapy.crawler import Crawler\n 43| from scrapy.logformatter import LogFormatter\n 44| from scrapy.settings import BaseSettings, Settings\n 45| from scrapy.signalmanager import SignalManager\n 46| from scrapy.spiders import Spider\n 47| \n 48| \n 49| logger = logging.getLogger(__name__)\n 50| \n 51| \n 52| class _Slot:\n 53| def __init__(\n 54| self,\n 55| close_if_idle: bool,\n 56| nextcall: CallLaterOnce[None],\n 57| scheduler: BaseScheduler,\n 58| ) -> None:\n 59| self.closing: Deferred[None] | None = None\n 60| self.inprogress: set[Request] = set()\n 61| self.close_if_idle: bool = close_if_idle\n 62| self.nextcall: CallLaterOnce[None] = nextcall\n 63| self.scheduler: BaseScheduler = scheduler\n 64| self.heartbeat: AsyncioLoopingCall | LoopingCall = create_looping_call(\n 65| nextcall.schedule\n 66| )\n 67| \n 68| def add_request(self, request: Request) -> None:\n 69| self.inprogress.add(request)\n 70| \n 71| def remove_request(self, request: Request) -> None:\n 72| self.inprogress.remove(request)\n 73| self._maybe_fire_closing()\n 74| \n 75| def close(self) -> Deferred[None]:\n 76| self.closing = Deferred()\n 77| self._maybe_fire_closing()\n 78| return self.closing\n 79| \n 80| def _maybe_fire_closing(self) -> None:\n 81| if self.closing is not None and not self.inprogress:\n 82| if self.nextcall:\n 83| self.nextcall.cancel()\n 84| if self.heartbeat.running:\n 85| self.heartbeat.stop()\n 86| self.closing.callback(None)\n 87| \n 88| \n 89| class ExecutionEngine:\n 90| _SLOT_HEARTBEAT_INTERVAL: float = 5.0\n 91| \n 92| def __init__(\n 93| self,\n 94| crawler: Crawler,\n 95| spider_closed_callback: Callable[[Spider], Deferred[None] | None],\n 96| ) -> None:\n 97| self.crawler: Crawler = crawler\n 98| self.settings: Settings = crawler.settings\n 99| self.signals: SignalManager = crawler.signals\n100| assert crawler.logformatter\n101| self.logformatter: LogFormatter = crawler.logformatter\n102| self._slot: _Slot | None = None\n103| self.spider: Spider | None = None\n104| self.running: bool = False\n105| self.paused: bool = False\n106| self._spider_closed_callback: Callable[[Spider], Deferred[None] | None] = (\n107| spider_closed_callback\n108| )\n109| self.start_time: float | None = None\n110| self._start: AsyncIterator[Any] | None = None\n111| downloader_cls: type[Downloader] = load_object(self.settings[\"DOWNLOADER\"])\n112| try:\n113| self.scheduler_cls: type[BaseScheduler] = self._get_scheduler_class(\n114| crawler.settings\n115| )\n116| self.downloader: Downloader = downloader_cls(crawler)\n117| self.scraper: Scraper = Scraper(crawler)\n118| except Exception:\n119| self.close()\n120| raise\n121| \n122| def _get_scheduler_class(self, settings: BaseSettings) -> type[BaseScheduler]:\n123| from scrapy.core.scheduler import BaseScheduler\n124| \n125| scheduler_cls: type[BaseScheduler] = load_object(settings[\"SCHEDULER\"])\n126| if not issubclass(scheduler_cls, BaseScheduler):\n127| raise TypeError(\n128| f\"The provided scheduler class ({settings['SCHEDULER']})\"\n129| \" does not fully implement the scheduler interface\"\n130| )\n131| return scheduler_cls\n132| \n133| def start(self, _start_request_processing=True) -> Deferred[None]:\n134| return deferred_from_coro(self.start_async(_start_request_processing))\n135| \n136| async def start_async(self, _start_request_processing=True) -> None:\n137| if self.running:\n138| raise RuntimeError(\"Engine already running\")\n139| self.start_time = time()\n140| await self.signals.send_catch_log_async(signal=signals.engine_started)\n141| self.running = True\n142| self._closewait: Deferred[None] = Deferred()\n143| if _start_request_processing:\n144| self._start_request_processing()\n145| await maybe_deferred_to_future(self._closewait)\n146| \n147| def stop(self) -> Deferred[None]:\n148| \"\"\"Gracefully stop the execution engine\"\"\"\n149| \n150| @deferred_f_from_coro_f\n151| async def _finish_stopping_engine(_: Any) -> None:\n152| await self.signals.send_catch_log_async(signal=signals.engine_stopped)\n153| self._closewait.callback(None)\n154| \n155| if not self.running:\n156| raise RuntimeError(\"Engine not running\")\n157| \n158| self.running = False\n159| dfd = (\n160| self.close_spider(self.spider, reason=\"shutdown\")\n161| if self.spider is not None\n162| else succeed(None)\n163| )\n164| return dfd.addBoth(_finish_stopping_engine)\n165| \n166| def close(self) -> Deferred[None]:\n167| \"\"\"\n168| Gracefully close the execution engine.\n169| If it has already been started, stop it. In all cases, close the spider and the downloader.\n170| \"\"\"\n171| if self.running:\n172| return self.stop() # will also close spider and downloader\n173| if self.spider is not None:\n174| return self.close_spider(\n175| self.spider, reason=\"shutdown\"\n176| ) # will also close downloader\n177| if hasattr(self, \"downloader\"):\n178| self.downloader.close()\n179| return succeed(None)\n180| \n181| def pause(self) -> None:\n182| self.paused = True\n183| \n184| def unpause(self) -> None:\n185| self.paused = False\n186| \n187| async def _process_start_next(self):\n188| \"\"\"Processes the next item or request from Spider.start().\n189| \n190| If a request, it is scheduled. If an item, it is sent to item\n191| pipelines.\n192| \"\"\"\n193| try:\n194| item_or_request = await self._start.__anext__()\n195| except StopAsyncIteration:\n196| self._start = None\n197| except Exception as exception:\n198| self._start = None\n199| exception_traceback = format_exc()\n200| logger.error(\n201| f\"Error while reading start items and requests: {exception}.\\n{exception_traceback}\",\n202| exc_info=True,\n203| )\n204| else:\n205| if not self.spider:\n206| return # spider already closed\n207| if isinstance(item_or_request, Request):\n208| self.crawl(item_or_request)\n209| else:\n210| self.scraper.start_itemproc(item_or_request, response=None)\n211| self._slot.nextcall.schedule()\n212| \n213| @deferred_f_from_coro_f\n214| async def _start_request_processing(self) -> None:\n215| \"\"\"Starts consuming Spider.start() output and sending scheduled\n216| requests.\"\"\"\n217| # Starts the processing of scheduled requests, as well as a periodic\n218| # call to that processing method for scenarios where the scheduler\n219| # reports having pending requests but returns none.\n220| assert self._slot is not None # typing\n221| self._slot.nextcall.schedule()\n222| self._slot.heartbeat.start(self._SLOT_HEARTBEAT_INTERVAL)\n223| \n224| while self._start and self.spider:\n225| await self._process_start_next()\n226| if not self.needs_backout():\n227| # Give room for the outcome of self._process_start_next() to be\n228| # processed before continuing with the next iteration.\n229| self._slot.nextcall.schedule()\n230| await self._slot.nextcall.wait()\n231| \n232| def _start_scheduled_requests(self) -> None:\n233| if self._slot is None or self._slot.closing is not None or self.paused:\n234| return\n235| \n236| while not self.needs_backout():\n237| if not self._start_scheduled_request():\n238| break\n239| \n240| if self.spider_is_idle() and self._slot.close_if_idle:\n241| self._spider_idle()\n242| \n243| def needs_backout(self) -> bool:\n244| \"\"\"Returns ``True`` if no more requests can be sent at the moment, or\n245| ``False`` otherwise.\n246| \n247| See :ref:`start-requests-lazy` for an example.\n248| \"\"\"\n249| assert self._slot is not None # typing\n250| assert self.scraper.slot is not None # typing\n251| return (\n252| not self.running\n253| or bool(self._slot.closing)\n254| or self.downloader.needs_backout()\n255| or self.scraper.slot.needs_backout()\n256| )\n257| \n258| def _start_scheduled_request(self) -> bool:\n259| assert self._slot is not None # typing\n260| assert self.spider is not None # typing\n261| \n262| request = self._slot.scheduler.next_request()\n263| if request is None:\n264| self.signals.send_catch_log(signals.scheduler_empty)\n265| return False\n266| \n267| d: Deferred[Response | Request] = self._download(request)\n268| d.addBoth(self._handle_downloader_output, request)\n269| d.addErrback(\n270| lambda f: logger.info(\n271| \"Error while handling downloader output\",\n272| exc_info=failure_to_exc_info(f),\n273| extra={\"spider\": self.spider},\n274| )\n275| )\n276| \n277| def _remove_request(_: Any) -> None:\n278| assert self._slot\n279| self._slot.remove_request(request)\n280| \n281| d2: Deferred[None] = d.addBoth(_remove_request)\n282| d2.addErrback(\n283| lambda f: logger.info(\n284| \"Error while removing request from slot\",\n285| exc_info=failure_to_exc_info(f),\n286| extra={\"spider\": self.spider},\n287| )\n288| )\n289| slot = self._slot\n290| d2.addBoth(lambda _: slot.nextcall.schedule())\n291| d2.addErrback(\n292| lambda f: logger.info(\n293| \"Error while scheduling new request\",\n294| exc_info=failure_to_exc_info(f),\n295| extra={\"spider\": self.spider},\n296| )\n297| )\n298| return True\n299| \n300| @inlineCallbacks\n301| def _handle_downloader_output(\n302| self, result: Request | Response | Failure, request: Request\n303| ) -> Generator[Deferred[Any], Any, None]:\n304| if not isinstance(result, (Request, Response, Failure)):\n305| raise TypeError(\n306| f\"Incorrect type: expected Request, Response or Failure, got {type(result)}: {result!r}\"\n307| )\n308| \n309| # downloader middleware can return requests (for example, redirects)\n310| if isinstance(result, Request):\n311| self.crawl(result)\n312| return\n313| \n314| try:\n315| yield self.scraper.enqueue_scrape(result, request)\n316| except Exception:\n317| assert self.spider is not None\n318| logger.error(\n319| \"Error while enqueuing scrape\",\n320| exc_info=True,\n321| extra={\"spider\": self.spider},\n322| )\n323| \n324| def spider_is_idle(self) -> bool:\n325| if self._slot is None:\n326| raise RuntimeError(\"Engine slot not assigned\")\n327| if not self.scraper.slot.is_idle(): # type: ignore[union-attr]\n328| return False\n329| if self.downloader.active: # downloader has pending requests\n330| return False\n331| if self._start is not None: # not all start requests are handled\n332| return False\n333| return not self._slot.scheduler.has_pending_requests()\n334| \n335| def crawl(self, request: Request) -> None:\n336| \"\"\"Inject the request into the spider <-> downloader pipeline\"\"\"\n337| if self.spider is None:\n338| raise RuntimeError(f\"No open spider to crawl: {request}\")\n339| self._schedule_request(request)\n340| self._slot.nextcall.schedule() # type: ignore[union-attr]\n341| \n342| def _schedule_request(self, request: Request) -> None:\n343| request_scheduled_result = self.signals.send_catch_log(\n344| signals.request_scheduled,\n345| request=request,\n346| spider=self.spider,\n347| dont_log=IgnoreRequest,\n348| )\n349| for handler, result in request_scheduled_result:\n350| if isinstance(result, Failure) and isinstance(result.value, IgnoreRequest):\n351| return\n352| if not self._slot.scheduler.enqueue_request(request): # type: ignore[union-attr]\n353| self.signals.send_catch_log(\n354| signals.request_dropped, request=request, spider=self.spider\n355| )\n356| \n357| @inlineCallbacks\n358| def download(self, request: Request) -> Generator[Deferred[Any], Any, Response]:\n359| \"\"\"Return a Deferred which fires with a Response as result, only downloader middlewares are applied\"\"\"\n360| if self.spider is None:\n361| raise RuntimeError(f\"No open spider to crawl: {request}\")\n362| try:\n363| response_or_request = yield self._download(request)\n364| finally:\n365| assert self._slot is not None\n366| self._slot.remove_request(request)\n367| if isinstance(response_or_request, Request):\n368| return (yield self.download(response_or_request))\n369| return response_or_request\n370| \n371| @inlineCallbacks\n372| def _download(\n373| self, request: Request\n374| ) -> Generator[Deferred[Any], Any, Response | Request]:\n375| assert self._slot is not None # typing\n376| assert self.spider is not None\n377| \n378| self._slot.add_request(request)\n379| try:\n380| result: Response | Request = yield self.downloader.fetch(\n381| request, self.spider\n382| )\n383| if not isinstance(result, (Response, Request)):\n384| raise TypeError(\n385| f\"Incorrect type: expected Response or Request, got {type(result)}: {result!r}\"\n386| )\n387| if isinstance(result, Response):\n388| if result.request is None:\n389| result.request = request\n390| assert self.spider is not None\n391| logkws = self.logformatter.crawled(result.request, result, self.spider)\n392| if logkws is not None:\n393| logger.log(\n394| *logformatter_adapter(logkws), extra={\"spider\": self.spider}\n395| )\n396| self.signals.send_catch_log(\n397| signal=signals.response_received,\n398| response=result,\n399| request=result.request,\n400| spider=self.spider,\n401| )",
|
|
14
|
-
"must_find": [
|
|
15
|
-
{
|
|
16
|
-
"any_of": [
|
|
17
|
-
"no fault"
|
|
18
|
-
]
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"must_not_claim": [
|
|
22
|
-
{
|
|
23
|
-
"regex": "^\\W*fault:\\s*line",
|
|
24
|
-
"label": "fault: line"
|
|
25
|
-
}
|
|
26
|
-
]
|
|
27
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"shape": "rebench",
|
|
3
|
+
"source": "scrapy__scrapy-6867",
|
|
4
|
+
"upstream": "scrapy/scrapy",
|
|
5
|
+
"source_url": "https://github.com/scrapy/scrapy/pull/6867",
|
|
6
|
+
"created": "2025-06-06 10:19:55",
|
|
7
|
+
"license": "BSD-3-Clause",
|
|
8
|
+
"score_in": "first_line",
|
|
9
|
+
"min_answer_chars": 8,
|
|
10
|
+
"id": "rebench-scrapy-6867-control",
|
|
11
|
+
"title": "Real-bug control: say NO FAULT",
|
|
12
|
+
"why": "scrapy/core/engine.py after the fix in https://github.com/scrapy/scrapy/pull/6867; the paired bug task is the file before it. The answer is NO FAULT.",
|
|
13
|
+
"prompt": "The Python file below is from a public project. Either it contains a bug that a later commit to the project fixed, or it is the version after that fix. Lines are numbered as in the file (an excerpt: lines 1-401).\n\nYour first line must be exactly `FAULT: line <n>`, naming a line of the bug (if the fix only added code, the line just before or after where it belongs), or `NO FAULT` if this is the fixed version. Then explain in at most three sentences. Plain ASCII.\n\n 1| \"\"\"\n 2| This is the Scrapy engine which controls the Scheduler, Downloader and Spider.\n 3| \n 4| For more information see docs/topics/architecture.rst\n 5| \n 6| \"\"\"\n 7| \n 8| from __future__ import annotations\n 9| \n 10| import logging\n 11| from time import time\n 12| from traceback import format_exc\n 13| from typing import TYPE_CHECKING, Any, cast\n 14| \n 15| from twisted.internet.defer import Deferred, inlineCallbacks, succeed\n 16| from twisted.python.failure import Failure\n 17| \n 18| from scrapy import signals\n 19| from scrapy.core.scraper import Scraper\n 20| from scrapy.exceptions import CloseSpider, DontCloseSpider, IgnoreRequest\n 21| from scrapy.http import Request, Response\n 22| from scrapy.utils.asyncio import (\n 23| AsyncioLoopingCall,\n 24| create_looping_call,\n 25| )\n 26| from scrapy.utils.defer import (\n 27| deferred_f_from_coro_f,\n 28| deferred_from_coro,\n 29| maybe_deferred_to_future,\n 30| )\n 31| from scrapy.utils.log import failure_to_exc_info, logformatter_adapter\n 32| from scrapy.utils.misc import build_from_crawler, load_object\n 33| from scrapy.utils.reactor import CallLaterOnce\n 34| \n 35| if TYPE_CHECKING:\n 36| from collections.abc import AsyncIterator, Callable, Generator\n 37| \n 38| from twisted.internet.task import LoopingCall\n 39| \n 40| from scrapy.core.downloader import Downloader\n 41| from scrapy.core.scheduler import BaseScheduler\n 42| from scrapy.crawler import Crawler\n 43| from scrapy.logformatter import LogFormatter\n 44| from scrapy.settings import BaseSettings, Settings\n 45| from scrapy.signalmanager import SignalManager\n 46| from scrapy.spiders import Spider\n 47| \n 48| \n 49| logger = logging.getLogger(__name__)\n 50| \n 51| \n 52| class _Slot:\n 53| def __init__(\n 54| self,\n 55| close_if_idle: bool,\n 56| nextcall: CallLaterOnce[None],\n 57| scheduler: BaseScheduler,\n 58| ) -> None:\n 59| self.closing: Deferred[None] | None = None\n 60| self.inprogress: set[Request] = set()\n 61| self.close_if_idle: bool = close_if_idle\n 62| self.nextcall: CallLaterOnce[None] = nextcall\n 63| self.scheduler: BaseScheduler = scheduler\n 64| self.heartbeat: AsyncioLoopingCall | LoopingCall = create_looping_call(\n 65| nextcall.schedule\n 66| )\n 67| \n 68| def add_request(self, request: Request) -> None:\n 69| self.inprogress.add(request)\n 70| \n 71| def remove_request(self, request: Request) -> None:\n 72| self.inprogress.remove(request)\n 73| self._maybe_fire_closing()\n 74| \n 75| def close(self) -> Deferred[None]:\n 76| self.closing = Deferred()\n 77| self._maybe_fire_closing()\n 78| return self.closing\n 79| \n 80| def _maybe_fire_closing(self) -> None:\n 81| if self.closing is not None and not self.inprogress:\n 82| if self.nextcall:\n 83| self.nextcall.cancel()\n 84| if self.heartbeat.running:\n 85| self.heartbeat.stop()\n 86| self.closing.callback(None)\n 87| \n 88| \n 89| class ExecutionEngine:\n 90| _SLOT_HEARTBEAT_INTERVAL: float = 5.0\n 91| \n 92| def __init__(\n 93| self,\n 94| crawler: Crawler,\n 95| spider_closed_callback: Callable[[Spider], Deferred[None] | None],\n 96| ) -> None:\n 97| self.crawler: Crawler = crawler\n 98| self.settings: Settings = crawler.settings\n 99| self.signals: SignalManager = crawler.signals\n100| assert crawler.logformatter\n101| self.logformatter: LogFormatter = crawler.logformatter\n102| self._slot: _Slot | None = None\n103| self.spider: Spider | None = None\n104| self.running: bool = False\n105| self.paused: bool = False\n106| self._spider_closed_callback: Callable[[Spider], Deferred[None] | None] = (\n107| spider_closed_callback\n108| )\n109| self.start_time: float | None = None\n110| self._start: AsyncIterator[Any] | None = None\n111| downloader_cls: type[Downloader] = load_object(self.settings[\"DOWNLOADER\"])\n112| try:\n113| self.scheduler_cls: type[BaseScheduler] = self._get_scheduler_class(\n114| crawler.settings\n115| )\n116| self.downloader: Downloader = downloader_cls(crawler)\n117| self.scraper: Scraper = Scraper(crawler)\n118| except Exception:\n119| self.close()\n120| raise\n121| \n122| def _get_scheduler_class(self, settings: BaseSettings) -> type[BaseScheduler]:\n123| from scrapy.core.scheduler import BaseScheduler\n124| \n125| scheduler_cls: type[BaseScheduler] = load_object(settings[\"SCHEDULER\"])\n126| if not issubclass(scheduler_cls, BaseScheduler):\n127| raise TypeError(\n128| f\"The provided scheduler class ({settings['SCHEDULER']})\"\n129| \" does not fully implement the scheduler interface\"\n130| )\n131| return scheduler_cls\n132| \n133| def start(self, _start_request_processing=True) -> Deferred[None]:\n134| return deferred_from_coro(self.start_async(_start_request_processing))\n135| \n136| async def start_async(self, _start_request_processing=True) -> None:\n137| if self.running:\n138| raise RuntimeError(\"Engine already running\")\n139| self.start_time = time()\n140| await self.signals.send_catch_log_async(signal=signals.engine_started)\n141| self.running = True\n142| self._closewait: Deferred[None] = Deferred()\n143| if _start_request_processing:\n144| self._start_request_processing()\n145| await maybe_deferred_to_future(self._closewait)\n146| \n147| def stop(self) -> Deferred[None]:\n148| \"\"\"Gracefully stop the execution engine\"\"\"\n149| \n150| @deferred_f_from_coro_f\n151| async def _finish_stopping_engine(_: Any) -> None:\n152| await self.signals.send_catch_log_async(signal=signals.engine_stopped)\n153| self._closewait.callback(None)\n154| \n155| if not self.running:\n156| raise RuntimeError(\"Engine not running\")\n157| \n158| self.running = False\n159| dfd = (\n160| self.close_spider(self.spider, reason=\"shutdown\")\n161| if self.spider is not None\n162| else succeed(None)\n163| )\n164| return dfd.addBoth(_finish_stopping_engine)\n165| \n166| def close(self) -> Deferred[None]:\n167| \"\"\"\n168| Gracefully close the execution engine.\n169| If it has already been started, stop it. In all cases, close the spider and the downloader.\n170| \"\"\"\n171| if self.running:\n172| return self.stop() # will also close spider and downloader\n173| if self.spider is not None:\n174| return self.close_spider(\n175| self.spider, reason=\"shutdown\"\n176| ) # will also close downloader\n177| if hasattr(self, \"downloader\"):\n178| self.downloader.close()\n179| return succeed(None)\n180| \n181| def pause(self) -> None:\n182| self.paused = True\n183| \n184| def unpause(self) -> None:\n185| self.paused = False\n186| \n187| async def _process_start_next(self):\n188| \"\"\"Processes the next item or request from Spider.start().\n189| \n190| If a request, it is scheduled. If an item, it is sent to item\n191| pipelines.\n192| \"\"\"\n193| try:\n194| item_or_request = await self._start.__anext__()\n195| except StopAsyncIteration:\n196| self._start = None\n197| except Exception as exception:\n198| self._start = None\n199| exception_traceback = format_exc()\n200| logger.error(\n201| f\"Error while reading start items and requests: {exception}.\\n{exception_traceback}\",\n202| exc_info=True,\n203| )\n204| else:\n205| if not self.spider:\n206| return # spider already closed\n207| if isinstance(item_or_request, Request):\n208| self.crawl(item_or_request)\n209| else:\n210| self.scraper.start_itemproc(item_or_request, response=None)\n211| self._slot.nextcall.schedule()\n212| \n213| @deferred_f_from_coro_f\n214| async def _start_request_processing(self) -> None:\n215| \"\"\"Starts consuming Spider.start() output and sending scheduled\n216| requests.\"\"\"\n217| # Starts the processing of scheduled requests, as well as a periodic\n218| # call to that processing method for scenarios where the scheduler\n219| # reports having pending requests but returns none.\n220| assert self._slot is not None # typing\n221| self._slot.nextcall.schedule()\n222| self._slot.heartbeat.start(self._SLOT_HEARTBEAT_INTERVAL)\n223| \n224| while self._start and self.spider:\n225| await self._process_start_next()\n226| if not self.needs_backout():\n227| # Give room for the outcome of self._process_start_next() to be\n228| # processed before continuing with the next iteration.\n229| self._slot.nextcall.schedule()\n230| await self._slot.nextcall.wait()\n231| \n232| def _start_scheduled_requests(self) -> None:\n233| if self._slot is None or self._slot.closing is not None or self.paused:\n234| return\n235| \n236| while not self.needs_backout():\n237| if not self._start_scheduled_request():\n238| break\n239| \n240| if self.spider_is_idle() and self._slot.close_if_idle:\n241| self._spider_idle()\n242| \n243| def needs_backout(self) -> bool:\n244| \"\"\"Returns ``True`` if no more requests can be sent at the moment, or\n245| ``False`` otherwise.\n246| \n247| See :ref:`start-requests-lazy` for an example.\n248| \"\"\"\n249| assert self._slot is not None # typing\n250| assert self.scraper.slot is not None # typing\n251| return (\n252| not self.running\n253| or bool(self._slot.closing)\n254| or self.downloader.needs_backout()\n255| or self.scraper.slot.needs_backout()\n256| )\n257| \n258| def _start_scheduled_request(self) -> bool:\n259| assert self._slot is not None # typing\n260| assert self.spider is not None # typing\n261| \n262| request = self._slot.scheduler.next_request()\n263| if request is None:\n264| self.signals.send_catch_log(signals.scheduler_empty)\n265| return False\n266| \n267| d: Deferred[Response | Request] = self._download(request)\n268| d.addBoth(self._handle_downloader_output, request)\n269| d.addErrback(\n270| lambda f: logger.info(\n271| \"Error while handling downloader output\",\n272| exc_info=failure_to_exc_info(f),\n273| extra={\"spider\": self.spider},\n274| )\n275| )\n276| \n277| def _remove_request(_: Any) -> None:\n278| assert self._slot\n279| self._slot.remove_request(request)\n280| \n281| d2: Deferred[None] = d.addBoth(_remove_request)\n282| d2.addErrback(\n283| lambda f: logger.info(\n284| \"Error while removing request from slot\",\n285| exc_info=failure_to_exc_info(f),\n286| extra={\"spider\": self.spider},\n287| )\n288| )\n289| slot = self._slot\n290| d2.addBoth(lambda _: slot.nextcall.schedule())\n291| d2.addErrback(\n292| lambda f: logger.info(\n293| \"Error while scheduling new request\",\n294| exc_info=failure_to_exc_info(f),\n295| extra={\"spider\": self.spider},\n296| )\n297| )\n298| return True\n299| \n300| @inlineCallbacks\n301| def _handle_downloader_output(\n302| self, result: Request | Response | Failure, request: Request\n303| ) -> Generator[Deferred[Any], Any, None]:\n304| if not isinstance(result, (Request, Response, Failure)):\n305| raise TypeError(\n306| f\"Incorrect type: expected Request, Response or Failure, got {type(result)}: {result!r}\"\n307| )\n308| \n309| # downloader middleware can return requests (for example, redirects)\n310| if isinstance(result, Request):\n311| self.crawl(result)\n312| return\n313| \n314| try:\n315| yield self.scraper.enqueue_scrape(result, request)\n316| except Exception:\n317| assert self.spider is not None\n318| logger.error(\n319| \"Error while enqueuing scrape\",\n320| exc_info=True,\n321| extra={\"spider\": self.spider},\n322| )\n323| \n324| def spider_is_idle(self) -> bool:\n325| if self._slot is None:\n326| raise RuntimeError(\"Engine slot not assigned\")\n327| if not self.scraper.slot.is_idle(): # type: ignore[union-attr]\n328| return False\n329| if self.downloader.active: # downloader has pending requests\n330| return False\n331| if self._start is not None: # not all start requests are handled\n332| return False\n333| return not self._slot.scheduler.has_pending_requests()\n334| \n335| def crawl(self, request: Request) -> None:\n336| \"\"\"Inject the request into the spider <-> downloader pipeline\"\"\"\n337| if self.spider is None:\n338| raise RuntimeError(f\"No open spider to crawl: {request}\")\n339| self._schedule_request(request)\n340| self._slot.nextcall.schedule() # type: ignore[union-attr]\n341| \n342| def _schedule_request(self, request: Request) -> None:\n343| request_scheduled_result = self.signals.send_catch_log(\n344| signals.request_scheduled,\n345| request=request,\n346| spider=self.spider,\n347| dont_log=IgnoreRequest,\n348| )\n349| for handler, result in request_scheduled_result:\n350| if isinstance(result, Failure) and isinstance(result.value, IgnoreRequest):\n351| return\n352| if not self._slot.scheduler.enqueue_request(request): # type: ignore[union-attr]\n353| self.signals.send_catch_log(\n354| signals.request_dropped, request=request, spider=self.spider\n355| )\n356| \n357| @inlineCallbacks\n358| def download(self, request: Request) -> Generator[Deferred[Any], Any, Response]:\n359| \"\"\"Return a Deferred which fires with a Response as result, only downloader middlewares are applied\"\"\"\n360| if self.spider is None:\n361| raise RuntimeError(f\"No open spider to crawl: {request}\")\n362| try:\n363| response_or_request = yield self._download(request)\n364| finally:\n365| assert self._slot is not None\n366| self._slot.remove_request(request)\n367| if isinstance(response_or_request, Request):\n368| return (yield self.download(response_or_request))\n369| return response_or_request\n370| \n371| @inlineCallbacks\n372| def _download(\n373| self, request: Request\n374| ) -> Generator[Deferred[Any], Any, Response | Request]:\n375| assert self._slot is not None # typing\n376| assert self.spider is not None\n377| \n378| self._slot.add_request(request)\n379| try:\n380| result: Response | Request = yield self.downloader.fetch(\n381| request, self.spider\n382| )\n383| if not isinstance(result, (Response, Request)):\n384| raise TypeError(\n385| f\"Incorrect type: expected Response or Request, got {type(result)}: {result!r}\"\n386| )\n387| if isinstance(result, Response):\n388| if result.request is None:\n389| result.request = request\n390| assert self.spider is not None\n391| logkws = self.logformatter.crawled(result.request, result, self.spider)\n392| if logkws is not None:\n393| logger.log(\n394| *logformatter_adapter(logkws), extra={\"spider\": self.spider}\n395| )\n396| self.signals.send_catch_log(\n397| signal=signals.response_received,\n398| response=result,\n399| request=result.request,\n400| spider=self.spider,\n401| )",
|
|
14
|
+
"must_find": [
|
|
15
|
+
{
|
|
16
|
+
"any_of": [
|
|
17
|
+
"no fault"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"must_not_claim": [
|
|
22
|
+
{
|
|
23
|
+
"regex": "^\\W*fault:\\s*line",
|
|
24
|
+
"label": "fault: line"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|